Compare commits

..

2 Commits

Author SHA1 Message Date
Aid Thompson b8494eb83a signpost - unfinished just parking changes 2021-11-22 15:08:46 +00:00
Aid Thompson cb549dfe25 1st commit 2021-11-17 11:55:06 +00:00
95 changed files with 18708 additions and 10984 deletions
-1
View File
@@ -24,7 +24,6 @@ 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
Generated
+20 -164
View File
@@ -420,10 +420,7 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [
"lazy_static",
"memchr",
"regex-automata",
"serde",
]
[[package]]
@@ -507,15 +504,6 @@ 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"
@@ -679,11 +667,30 @@ 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"
@@ -979,42 +986,6 @@ 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"
@@ -1154,28 +1125,6 @@ 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"
@@ -1791,7 +1740,6 @@ dependencies = [
"az",
"bytemuck",
"half",
"serde",
"typenum",
]
@@ -3516,7 +3464,6 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"network-defaults",
"nymsphinx",
"pemstore",
"pretty_env_logger",
@@ -3666,7 +3613,6 @@ dependencies = [
"gateway-client",
"gateway-requests",
"log",
"network-defaults",
"nymsphinx",
"ordered-buffer",
"pemstore",
@@ -3724,27 +3670,6 @@ 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"
@@ -3909,12 +3834,6 @@ 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"
@@ -4345,34 +4264,6 @@ 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"
@@ -4919,12 +4810,6 @@ 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"
@@ -5206,15 +5091,6 @@ 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"
@@ -5454,16 +5330,6 @@ 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"
@@ -6742,16 +6608,6 @@ 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"
-1
View File
@@ -30,7 +30,6 @@ members = [
"common/mixnode-common",
"common/network-defaults",
"common/nonexhaustive-delayqueue",
"common/nymcoconut",
"common/nymsphinx",
"common/nymsphinx/acknowledgements",
"common/nymsphinx/addressing",
+1 -2
View File
@@ -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,7 +44,6 @@ 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"]
+3 -3
View File
@@ -1,6 +1,8 @@
// 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,
@@ -22,7 +24,6 @@ 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,8 +35,7 @@ use nymsphinx::anonymous_replies::ReplySurb;
use nymsphinx::receiver::ReconstructedMessage;
use tokio::runtime::Runtime;
use crate::client::config::{Config, SocketType};
use crate::websocket;
use gateway_client::bandwidth::BandwidthController;
pub(crate) mod config;
+5 -46
View File
@@ -1,23 +1,15 @@
// 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;
@@ -29,9 +21,6 @@ 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!")
@@ -47,9 +36,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")
@@ -82,36 +71,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
app
}
// this behaviour should definitely be changed, we shouldn't
// need to get bandwidth credential for registration
#[cfg(feature = "coconut")]
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
let verification_key = obtain_aggregate_verification_key(validators)
.await
.expect("could not obtain aggregate verification key of validators");
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let 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(&params, &bandwidth_credential_attributes, validators)
.await
.expect("could not obtain bandwidth credential");
prepare_for_spending(
raw_identity,
&bandwidth_credential,
&bandwidth_credential_attributes,
&verification_key,
)
.expect("could not prepare out bandwidth credential for spending")
}
async fn register_with_gateway(
gateway: &gateway::Node,
our_identity: Arc<identity::KeyPair>,
+3 -3
View File
@@ -1,8 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
pub(crate) use handler::Handler;
pub(crate) use listener::Listener;
pub(crate) mod handler;
pub(crate) mod listener;
pub(crate) use handler::Handler;
pub(crate) use listener::Listener;
+1 -2
View File
@@ -32,14 +32,13 @@ 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"]
+6 -6
View File
@@ -1,6 +1,11 @@
// 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,
@@ -20,7 +25,6 @@ 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,
@@ -30,11 +34,7 @@ use nymsphinx::addressing::clients::Recipient;
use nymsphinx::addressing::nodes::NodeIdentity;
use tokio::runtime::Runtime;
use crate::client::config::Config;
use crate::socks::{
authentication::{AuthenticationMethods, Authenticator, User},
server::SphinxSocksServer,
};
use gateway_client::bandwidth::BandwidthController;
pub(crate) mod config;
+5 -46
View File
@@ -1,23 +1,15 @@
// 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};
@@ -27,9 +19,6 @@ 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!")
@@ -51,9 +40,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")
@@ -82,36 +71,6 @@ pub fn command_args<'a, 'b>() -> clap::App<'a, 'b> {
app
}
// this behaviour should definitely be changed, we shouldn't
// need to get bandwidth credential for registration
#[cfg(feature = "coconut")]
async fn _prepare_temporary_credential(validators: &[Url], raw_identity: &[u8]) -> Credential {
let verification_key = obtain_aggregate_verification_key(validators)
.await
.expect("could not obtain aggregate verification key of validators");
let params = Parameters::new(TOTAL_ATTRIBUTES).unwrap();
let 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(&params, &bandwidth_credential_attributes, validators)
.await
.expect("could not obtain bandwidth credential");
prepare_for_spending(
raw_identity,
&bandwidth_credential,
&bandwidth_credential_attributes,
&verification_key,
)
.expect("could not prepare out bandwidth credential for spending")
}
async fn register_with_gateway(
gateway: &gateway::Node,
our_identity: Arc<identity::KeyPair>,
+17 -29
View File
@@ -3,24 +3,21 @@
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,
serial_number: Attribute,
binding_number: Attribute,
voucher_value: Attribute,
voucher_info: Attribute,
public_attributes_bytes: Vec<Vec<u8>>,
public_attributes: Vec<Attribute>,
private_attributes: Vec<Attribute>,
aggregated_verification_key: Option<VerificationKey>,
}
@@ -40,10 +37,9 @@ impl State {
signatures: Vec::new(),
n_attributes,
params,
serial_number: private_attributes[0],
binding_number: private_attributes[1],
voucher_value: public_attributes[0],
voucher_info: public_attributes[1],
public_attributes_bytes,
public_attributes,
private_attributes,
aggregated_verification_key: None,
}
}
@@ -67,8 +63,8 @@ async fn randomise_credential(
) -> Result<Vec<Signature>, String> {
let mut state = state.write().await;
let signature = state.signatures.remove(idx);
let (new_signature, _) = signature.randomise(&state.params);
state.signatures.insert(idx, new_signature);
let new = signature.randomise(&state.params);
state.signatures.insert(idx, new);
Ok(state.signatures.clone())
}
@@ -121,15 +117,14 @@ async fn prove_credential(
let state = state.read().await;
if let Some(signature) = state.signatures.get(idx) {
match coconut_interface::prove_bandwidth_credential(
match coconut_interface::prove_credential(
&state.params,
&verification_key,
signature,
state.serial_number,
state.binding_number,
&state.private_attributes,
) {
Ok(theta) => Ok(theta),
Err(e) => Err(format!("{:?}", e)),
Err(e) => Err(format!("{}", e)),
}
} else {
Err("Got invalid Signature idx".to_string())
@@ -149,15 +144,10 @@ 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,
public_attributes_bytes,
state.public_attributes_bytes.clone(),
state
.signatures
.get(idx)
@@ -174,13 +164,11 @@ 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,
&public_attributes,
&private_attributes,
&guard.public_attributes,
&guard.private_attributes,
&parsed_urls,
)
.await
+5600 -45
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -7,6 +7,7 @@
"types": "dist/index.d.ts",
"scripts": {
"build": "tsc",
"run_cli": "clear && ts-node src/cli.ts",
"test": "ts-mocha tests/**/*.test.ts",
"coverage": "nyc npm test",
"lint": "eslint \"**/*.ts\"",
@@ -22,6 +23,7 @@
"devDependencies": {
"@types/chai": "^4.2.15",
"@types/expect": "^24.3.0",
"@types/inquirer": "^8.1.3",
"@types/mocha": "^8.2.1",
"@typescript-eslint/eslint-plugin": "^4.14.0",
"@typescript-eslint/parser": "^4.14.0",
@@ -35,10 +37,11 @@
"typescript": "^4.1.3"
},
"dependencies": {
"axios": "^0.21.1",
"@cosmjs/cosmwasm-stargate": "^0.25.5",
"@cosmjs/stargate": "^0.25.5",
"@cosmjs/math": "^0.25.5",
"@cosmjs/proto-signing": "^0.25.5"
"@cosmjs/proto-signing": "^0.25.5",
"@cosmjs/stargate": "^0.25.5",
"axios": "^0.21.1",
"inquirer": "^8.2.0"
}
}
}
+48 -44
View File
@@ -1,7 +1,7 @@
import {MixNodeBond, PagedMixnodeResponse} from "../types";
import { INetClient } from "../net-client"
import {IQueryClient} from "../query-client";
import {VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT} from "../index";
import { MixNodeBond, PagedMixnodeResponse } from "../types";
import { INetClient } from "../net-client";
import { IQueryClient } from "../query-client";
import { VALIDATOR_API_MIXNODES, VALIDATOR_API_PORT } from "../index";
import axios from "axios";
export { MixnodesCache };
@@ -13,48 +13,52 @@ export { MixnodesCache };
* available for querying.
* */
export default class MixnodesCache {
mixNodes: MixNodeBond[]
client: INetClient | IQueryClient
perPage: number
mixNodes: MixNodeBond[];
client: INetClient | IQueryClient;
perPage: number;
constructor(client: INetClient | IQueryClient, perPage: number) {
this.client = client;
this.mixNodes = [];
this.perPage = perPage;
constructor(client: INetClient | IQueryClient, perPage: number) {
this.client = client;
this.mixNodes = [];
this.perPage = perPage;
}
/// Makes repeated requests to assemble a full list of nodes.
/// Requests continue to be make as long as `shouldMakeAnotherRequest()`
// returns true.
async refreshMixNodes(contractAddress: string): Promise<MixNodeBond[]> {
let newMixnodes: MixNodeBond[] = [];
let response: PagedMixnodeResponse;
let next: string | undefined = undefined;
for (;;) {
response = await this.client.getMixNodes(
contractAddress,
this.perPage,
next
);
newMixnodes = newMixnodes.concat(response.nodes);
next = response.start_next_after;
// if `start_next_after` is not set, we're done
if (!next) {
break;
}
}
/// Makes repeated requests to assemble a full list of nodes.
/// Requests continue to be make as long as `shouldMakeAnotherRequest()`
// returns true.
async refreshMixNodes(contractAddress: string): Promise<MixNodeBond[]> {
let newMixnodes: MixNodeBond[] = [];
let response: PagedMixnodeResponse;
let next: string | undefined = undefined;
for (;;) {
response = await this.client.getMixNodes(contractAddress, this.perPage, next);
newMixnodes = newMixnodes.concat(response.nodes)
next = response.start_next_after;
// if `start_next_after` is not set, we're done
if (!next) {
break
}
}
this.mixNodes = newMixnodes;
return this.mixNodes;
}
this.mixNodes = newMixnodes
return this.mixNodes;
/// Makes requests to assemble a full list of mixnodes from validator-api
async refreshValidatorAPIMixNodes(urls: string[]): Promise<MixNodeBond[]> {
for (const url of urls) {
const validator_api_url = new URL(url);
validator_api_url.port = VALIDATOR_API_PORT;
validator_api_url.pathname += VALIDATOR_API_MIXNODES;
const response = await axios.get(validator_api_url.toString());
if (response.status == 200) {
return response.data;
}
}
/// Makes requests to assemble a full list of mixnodes from validator-api
async refreshValidatorAPIMixNodes(urls: string[]): Promise<MixNodeBond[]> {
for (const url of urls) {
const validator_api_url = new URL(url);
validator_api_url.port = VALIDATOR_API_PORT;
validator_api_url.pathname += VALIDATOR_API_MIXNODES;
const response = await axios.get(validator_api_url.toString());
if (response.status == 200) {
return response.data;
}
}
throw new Error("None of the provided validators seem to be alive")
}
}
throw new Error("None of the provided validators seem to be alive");
}
}
+317
View File
@@ -0,0 +1,317 @@
import ValidatorClient from "./index";
import inquirer from "inquirer";
// This script runs a CLI to consume the Validator and provide mixnet information to the user
const VALIDATOR_URLS: string[] = [
"https://testnet-milhon-validator1.nymtech.net",
// "https://testnet-milhon-validator2.nymtech.net", // <-- val 2 doesnt work apparently.
];
const DENOM = "punk";
const MOCK_MNEMONIC =
"vault risk throw flat garlic pretty clay senior birth correct panic floor around pen horror mail entry arrest zoo devote message evoke street total";
// ^^ addr: punk10dxwmqjy72s9nkm9x9pluyn6pyx0gkptjhs4k9
// curr balance: 899999747
// const MOCK_MNEMONIC =
// "oil once motion cute crawl patch happy wave donkey zoo retreat matrix emerge adult very universe aware error snap credit actress couple upset engine";
// ^^ addr: punk1yzr7gtmtlfd0s7s9wpexhteeu05y4xlcvh65eh
// curr balance: 5045 UPUNK
// const MOCK_MNEMONIC =
// "sample menu edit midnight guard review call record horn antenna stairs awkward fringe document during amazing twelve wise wide escape matter betray staff someone";
// ^^ addr: punk1wn8lwxe5hvdtx60c6p7ekskmu75agwfrslf0qs
// curr balance:
type AccountType = {
addr: string;
client: any;
mnemonic?: string;
};
function validatorCli() {
// define funcs to be used in CLI switch-case
let state: AccountType = {
addr: "",
client: null,
mnemonic: "",
};
function restartApp() {
setTimeout(() => {
validatorCli();
}, 300);
}
function generateNewAccount() {
const mnemonic = ValidatorClient.randomMnemonic();
ValidatorClient.mnemonicToAddress(mnemonic, "punk")
.then((address) => {
console.log("Your address is: ", address);
console.log("Your mnemonic is: ", mnemonic);
return address;
})
.catch((err) => {
console.log("err", err);
});
restartApp();
}
function sendFundsMenu() {
inquirer
.prompt([
{
name: "recipient",
type: "input",
message: "please enter the receipient:",
},
{
name: "amount",
type: "input",
message: "please enter the amount (UPUNK):",
},
])
.then(async ({ recipient, amount }) => {
const { addr, client } = state;
console.log(
`🔥 Hold Tight - Sending ${amount}UPUNK to ${recipient} 🚀`
);
const res = await client.send(addr, recipient, [
{
denom: "upunk",
amount: amount,
},
]);
console.log("Funds Transfer Response:", res);
restartApp();
});
}
async function delegateGateway() {
console.log(
"unfortunately - gateway delegation is switched off at the moment."
);
startTransactionMenu();
// const id = "punk1yzr7gtmtlfd0s7s9wpexhteeu05y4xlcvh65eh";
// const gatewayID = "EQhjPpUuy4i1u87nfQMW21WiBT5mJk4dcq4ju7Vct7cB";
// const coin = {
// denom: "upunk",
// amount: "101",
// };
// const res = await state.client.delegateToMixnode(gatewayID, coin);
// console.log("delegateMixnode ==> ", res);
}
async function delegateMixnode() {
const mixNodeID = "2cFpCe7yP79CcuRpf6JBRdJaSp7JF5YcA5SHi8JVm1d2";
// const mixNodeID = "2Vrr7s2peGiWsPh6xY3ZFEMDRmMNv8xLBUtV5XMyQLSB";
const coin = {
denom: "upunk",
amount: "1001",
};
const res = await state.client.delegateToMixnode(mixNodeID, coin);
console.log("delegate to mixnode response: ", res);
}
async function findMinimumMixnodeBond() {
const res = await state.client.minimumMixnodeBond();
console.log("res is back ", res);
}
async function bondMixnode() {
state.client.bondMixnode();
}
async function checkOwnsMixnodes() {
const res = await state.client.ownsMixNode();
console.log("owns mixnode? ", res);
}
function startTransactionMenu() {
inquirer
.prompt([
{
type: "list",
name: "task",
message: "What now?",
choices: [
"send_funds",
"get_mixnodes",
"refresh_mixnodes",
"refresh_val_api_mixnodes",
"min_mixn_bond",
"bond_mixnode",
"delegate_mixnode",
"delegate_gateway",
"check_owns_mixnode",
],
},
])
.then(({ task }) => {
switch (task) {
case "send_funds":
sendFundsMenu();
break;
case "get_mixnodes":
getMixnodes();
break;
case "refresh_mixnodes":
refreshMixnodes();
break;
case "refresh_val_api_mixnodes":
refreshValApiMixnodes();
break;
case "min_mixn_bond":
findMinimumMixnodeBond();
break;
case "bond_mixnode":
bondMixnode();
break;
case "delegate_gateway":
delegateGateway();
break;
case "delegate_mixnode":
delegateMixnode();
break;
case "check_owns_mixnode":
checkOwnsMixnodes();
break;
default:
return null;
}
});
}
function queryUserAccount() {
inquirer
.prompt([
{
type: "input",
name: "query_user",
message: "Please enter the public address of user you wish to query",
},
])
.then(async ({ query_user }) => {
let response = "";
try {
const client = await ValidatorClient.connectForQuery(
query_user,
VALIDATOR_URLS,
DENOM
);
const balance = await client.getBalance(query_user);
response = `User ${query_user} has a balance of ${balance?.amount}${balance?.denom}`;
console.log(response);
return validatorCli();
} catch (error) {
console.log("error back ", error);
return validatorCli();
}
});
}
async function refreshMixnodes() {
const res = await state.client.refreshMixNodes(
"punk1yksauczytk60x5cejaras8w6nwf7r772n3kwkp"
);
console.log("done:", res);
}
function connectAccount() {
inquirer
.prompt([
{
name: "user_mnemonic",
type: "input",
message: "please enter your mnemonic:",
},
])
.then(async ({ user_mnemonic }) => {
console.log("Connecting...");
const addr = await ValidatorClient.mnemonicToAddress(
MOCK_MNEMONIC,
// user_mnemonic,
"punk"
);
const client = await ValidatorClient.connect(
addr,
MOCK_MNEMONIC,
VALIDATOR_URLS,
DENOM
);
state = {
addr,
mnemonic: MOCK_MNEMONIC,
client,
};
const balance = await client.getBalance(addr);
console.log(`connected to validator, our address is ${client.address}`);
console.log("connected to validator", client.urls[0]);
console.log(
`💰 Your balance is ${balance?.amount}${balance?.denom.toUpperCase()}`
);
startTransactionMenu();
})
.catch((err) => {
console.log("error: ", err);
});
}
function buildAWallet() {
inquirer
.prompt([
{
message: "enter your mnemonic to build wallet:",
type: "input",
name: "mnemonic",
},
])
.then(async ({ mnemonic }) => {
const res = await ValidatorClient.buildWallet(mnemonic, DENOM);
console.log("Build_Wallet Response: ", res);
});
}
async function refreshValApiMixnodes() {
const res = await state.client.refreshValidatorAPIMixNodes();
console.log("res is back: ", res);
}
function getMixnodes() {
const res = state.client.mixNodesCache;
console.log("Mixnodes", res);
}
// app provides a list of possible tasks
inquirer
.prompt([
{
type: "list",
name: "task",
message: "Yo, What would you like to do today?",
choices: [
"create_account",
"connect_account",
"build_wallet",
"query_user",
],
},
])
.then(({ task }) => {
switch (task) {
case "create_account":
generateNewAccount();
break;
case "connect_account":
connectAccount();
break;
case "build_wallet":
buildAWallet();
break;
case "query_user":
queryUserAccount();
break;
default:
return null;
}
});
}
validatorCli();
File diff suppressed because it is too large Load Diff
+2517 -2259
View File
File diff suppressed because it is too large Load Diff
+3 -2
View File
@@ -3,7 +3,6 @@
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;
@@ -18,6 +17,8 @@ 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);
@@ -249,7 +250,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,11 +1,10 @@
// 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, BandwidthVoucherAttributes, TOTAL_ATTRIBUTES,
},
bandwidth::{obtain_signature, prepare_for_spending},
utils::obtain_aggregate_verification_key,
};
#[cfg(not(feature = "coconut"))]
@@ -13,11 +12,10 @@ 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, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS, ETH_MIN_BLOCK_DEPTH,
TOKENS_TO_BURN,
eth_contract::ETH_JSON_ABI, BANDWIDTH_VALUE, ETH_BURN_FUNCTION_NAME, ETH_CONTRACT_ADDRESS,
ETH_MIN_BLOCK_DEPTH, TOKENS_TO_BURN,
};
#[cfg(not(feature = "coconut"))]
use rand::rngs::OsRng;
@@ -35,8 +33,6 @@ use web3::{
Web3,
};
use crate::error::GatewayClientError;
#[cfg(not(feature = "coconut"))]
pub fn eth_contract(web3: Web3<Http>) -> Contract<Http> {
Contract::from_json(
@@ -112,31 +108,15 @@ 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();
// 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(
&params,
&bandwidth_credential_attributes,
&self.validator_endpoints,
)
.await?;
let bandwidth_credential =
obtain_signature(&self.identity.to_bytes(), &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,
)?)
}
@@ -223,9 +203,8 @@ impl BandwidthController {
#[cfg(not(feature = "coconut"))]
#[cfg(test)]
mod tests {
use network_defaults::ETH_EVENT_NAME;
use super::*;
use network_defaults::ETH_EVENT_NAME;
#[test]
fn parse_contract() {
@@ -10,7 +10,7 @@ use mixnet_contract::StateParams;
use crate::{validator_api, ValidatorClientError};
use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse};
use mixnet_contract::{GatewayBond, MixNodeBond, MixnodeRewardingStatusResponse};
use mixnet_contract::{GatewayBond, MixNodeBond};
#[cfg(feature = "nymd-client")]
use mixnet_contract::{RawDelegationData, RewardingIntervalResponse};
use url::Url;
@@ -181,20 +181,6 @@ 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,
@@ -14,10 +14,10 @@ use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl};
use cosmwasm_std::{Coin, Uint128};
use mixnet_contract::{
Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey,
LayerDistribution, MixNode, MixOwnershipResponse, MixnodeRewardingStatusResponse,
PagedAllDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse,
PagedMixnodeResponse, PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData,
RewardingIntervalResponse, StateParams,
LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse,
PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse,
PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, RewardingIntervalResponse,
StateParams,
};
use serde::Serialize;
use std::collections::HashMap;
@@ -230,23 +230,6 @@ 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,
+1 -1
View File
@@ -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"
nymcoconut = {path = "../nymcoconut" }
coconut-rs = { git = "https://github.com/nymtech/coconut.git", branch = "0.5.0" }
+6 -6
View File
@@ -4,7 +4,7 @@
use getset::{CopyGetters, Getters};
use serde::{Deserialize, Serialize};
pub use nymcoconut::*;
pub use coconut_rs::*;
#[derive(Serialize, Deserialize, Getters, CopyGetters, Clone)]
pub struct Credential {
@@ -42,7 +42,7 @@ impl Credential {
.iter()
.map(hash_to_scalar)
.collect::<Vec<Attribute>>();
nymcoconut::verify_credential(&params, verification_key, &self.theta, &public_attributes)
coconut_rs::verify_credential(&params, 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: nymcoconut::PublicKey,
public_key: coconut_rs::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: &nymcoconut::PublicKey,
blind_sign_request: BlindSignRequest,
public_key: &coconut_rs::PublicKey,
public_attributes: &[Attribute],
total_params: u32,
) -> BlindSignRequestBody {
BlindSignRequestBody {
blind_sign_request: blind_sign_request.clone(),
blind_sign_request,
public_key: public_key.clone(),
public_attributes: public_attributes
.iter()
+14 -44
View File
@@ -6,71 +6,41 @@
// 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 crate::error::Error;
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 = 2;
pub const PRIVATE_ATTRIBUTES: u32 = 2;
pub const PUBLIC_ATTRIBUTES: u32 = 1;
pub const PRIVATE_ATTRIBUTES: u32 = 1;
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(
params: &Parameters,
attributes: &BandwidthVoucherAttributes,
validators: &[Url],
) -> Result<Signature, Error> {
let public_attributes = attributes.get_public_attributes();
let private_attributes = attributes.get_private_attributes();
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)];
obtain_aggregate_signature(params, &public_attributes, &private_attributes, validators).await
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
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![
raw_identity.to_vec(),
BANDWIDTH_VALUE.to_be_bytes().to_vec(),
];
let public_attributes = vec![BANDWIDTH_VALUE.to_be_bytes().to_vec()];
let private_attributes = vec![raw_identity.to_vec()];
let params = Parameters::new(TOTAL_ATTRIBUTES)?;
prepare_credential_for_spending(
&params,
public_attributes,
attributes.serial_number,
attributes.binding_number,
private_attributes,
signature,
verification_key,
)
+19 -68
View File
@@ -1,16 +1,14 @@
// 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, prepare_blind_sign,
prove_bandwidth_credential, Attribute, BlindSignRequestBody, Credential, Parameters, Signature,
aggregate_signature_shares, aggregate_verification_keys, hash_to_scalar, prepare_blind_sign,
prove_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
@@ -65,18 +63,17 @@ 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,
elgamal_keypair.public_key(),
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,
@@ -86,17 +83,7 @@ async fn obtain_partial_credential(
.blind_sign(&blind_sign_request_body)
.await?
.blinded_signature;
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)
Ok(blinded_signature.unblind(elgamal_keypair.private_key()))
}
pub async fn obtain_aggregate_signature(
@@ -110,76 +97,40 @@ 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 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?;
let first =
obtain_partial_credential(params, public_attributes, private_attributes, &client).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 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 signature =
obtain_partial_credential(params, public_attributes, private_attributes, &client)
.await?;
let share = SignatureShare::new(signature, id as u64);
shares.push(share)
}
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,
)?)
Ok(aggregate_signature_shares(&shares)?)
}
// TODO: better type flow
pub fn prepare_credential_for_spending(
params: &Parameters,
public_attributes: Vec<Vec<u8>>,
serial_number: Attribute,
binding_number: Attribute,
private_attributes: Vec<Vec<u8>>,
signature: &Signature,
verification_key: &VerificationKey,
) -> Result<Credential, Error> {
let theta = prove_bandwidth_credential(
params,
verification_key,
signature,
serial_number,
binding_number,
)?;
let private_attributes = private_attributes
.iter()
.map(hash_to_scalar)
.collect::<Vec<Attribute>>();
let theta = prove_credential(params, verification_key, signature, &private_attributes)?;
Ok(Credential::new(
(public_attributes.len() + PRIVATE_ATTRIBUTES as usize) as u32,
(public_attributes.len() + private_attributes.len()) as u32,
theta,
public_attributes,
signature,
+1 -1
View File
@@ -17,7 +17,7 @@ schemars = "0.8"
ts-rs = { version = "5.1", optional = true }
thiserror = "1.0"
network-defaults = { path = "../network-defaults" }
fixed = { version = "1.1", features = ["serde"] }
fixed = "1.1"
az = "1.1"
log = "0.4.14"
+4 -3
View File
@@ -8,8 +8,6 @@ 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,
@@ -18,4 +16,7 @@ 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::*;
pub use types::{
IdentityKey, IdentityKeyRef, LayerDistribution, RewardingIntervalResponse, SphinxKey,
StateParams,
};
+23 -94
View File
@@ -14,10 +14,6 @@ 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 {
@@ -114,7 +110,7 @@ impl NodeRewardParams {
}
pub fn one_over_k(&self) -> U128 {
ONE / U128::from_num(self.k.u128())
U128::from_num(1) / U128::from_num(self.k.u128())
}
pub fn alpha(&self) -> U128 {
@@ -122,90 +118,6 @@ 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,
@@ -316,14 +228,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 = ONE;
let omega_k = U128::from_num(1u128);
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())
/ (ONE + params.alpha());
/ (U128::from_num(1) + params.alpha());
NodeRewardResult {
reward,
@@ -349,7 +261,7 @@ impl MixNodeBond {
};
let operator_base_reward = reward.reward.min(params.operator_cost());
let operator_reward = (self.profit_margin()
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
+ (U128::from_num(1) - self.profit_margin()) * reward.lambda / reward.sigma)
* profit;
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0));
@@ -376,8 +288,25 @@ impl MixNodeBond {
}
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 {
let reward_params = DelegatorRewardParams::new(self, *params);
reward_params.determine_delegation_reward(delegation_amount)
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
}
}
}
+9 -10
View File
@@ -37,6 +37,15 @@ 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,
@@ -50,12 +59,6 @@ pub enum ExecuteMsg {
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
RewardNextMixDelegators {
mix_identity: IdentityKey,
// nonce of the current rewarding interval
rewarding_interval_nonce: u32,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -100,10 +103,6 @@ pub enum QueryMsg {
GetCirculatingSupply {},
GetEpochRewardPercent {},
GetSybilResistancePercent {},
GetRewardingStatus {
mix_identity: IdentityKey,
rewarding_interval_nonce: u32,
},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+17 -33
View File
@@ -1,9 +1,8 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::DelegatorRewardParams;
use crate::Layer;
use cosmwasm_std::Uint128;
use cosmwasm_std::{Decimal, Uint128};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::fmt::{self, Display, Formatter};
@@ -36,13 +35,14 @@ pub struct RewardingIntervalResponse {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StateParams {
// 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 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,
@@ -55,8 +55,19 @@ pub struct StateParams {
impl Display for StateParams {
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: {}",
@@ -70,33 +81,6 @@ 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;
-43
View File
@@ -1,43 +0,0 @@
[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"] }
-322
View File
@@ -1,322 +0,0 @@
// 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(&params);
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(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, ephemeral_key) = keypair.public_key.encrypt(&params, &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(&params);
let r = params.random_scalar();
let h = params.gen1() * r;
let m = params.random_scalar();
let (ciphertext, _) = keypair.public_key.encrypt(&params, &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())
}
}
-53
View File
@@ -1,53 +0,0 @@
// 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,
},
}
-15
View File
@@ -1,15 +0,0 @@
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);
-2
View File
@@ -1,2 +0,0 @@
mod clone;
mod serde;
-56
View File
@@ -1,56 +0,0 @@
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);
-57
View File
@@ -1,57 +0,0 @@
// 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 {}
-663
View File
@@ -1,663 +0,0 @@
// 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(&params);
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(
&params,
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(&params, &keypair.verification_key(), &private_attributes, r);
let zeta = compute_zeta(&params, 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);
}
}
-377
View File
@@ -1,377 +0,0 @@
// 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(
&params,
&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(
&params,
&aggr_vk_1,
&attributes,
&sigs[2..],
Some(&[3, 4, 5]),
)
.unwrap();
assert_eq!(aggr_sig1, aggr_sig2);
// verify credential for good measure
assert!(verify(&params, &aggr_vk_1, &attributes, &aggr_sig1));
assert!(verify(&params, &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(
&params,
&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(
&params,
&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(
&params,
&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(
&params,
&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(&params, &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(&params, &aggr_vk_all, &attributes, &signatures, Some(&[]))
.is_err()
);
assert!(aggregate_signatures(
&params,
&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(
&params,
&aggr_vk_all,
&attributes,
&signatures,
Some(&[1, 1]),
)
.is_err());
}
// TODO: test for aggregating non-threshold keys
}
-382
View File
@@ -1,382 +0,0 @@
// 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(&params);
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
);
}
}
-539
View File
@@ -1,539 +0,0 @@
// 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
);
}
}
-661
View File
@@ -1,661 +0,0 @@
// 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(
&params,
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(
&params,
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(
&params,
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(
&params,
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(
&params,
&keypair1.verification_key(),
&theta1,
&[],
));
assert!(verify_credential(
&params,
&keypair2.verification_key(),
&theta2,
&[],
));
assert!(!verify_credential(
&params,
&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(
&params,
&keypair1.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
&keypair2.verification_key(),
&attributes,
&sig1,
));
assert!(!verify(
&params,
&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(
&params,
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(
&params,
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(
&params,
&keypair1.verification_key(),
&theta1,
&public_attributes,
));
assert!(verify_credential(
&params,
&keypair2.verification_key(),
&theta2,
&public_attributes,
));
assert!(!verify_credential(
&params,
&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(&params);
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(
&params,
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(&params, &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(
&params,
&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(&params, &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(
&params,
&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())
}
}
-90
View File
@@ -1,90 +0,0 @@
// 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()),
}
}
}
@@ -1,296 +0,0 @@
// 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);
}
}
-106
View File
@@ -1,106 +0,0 @@
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(&params);
// generate commitment and encryption
let blind_sign_request = prepare_blind_sign(
&params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)?;
// generate_keys
let coconut_keypairs = ttp_keygen(&params, 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(
&params,
&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(
&params,
&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(&params, &verification_key, &attributes, &signature_shares)?;
// Generate cryptographic material to verify them
let theta = prove_bandwidth_credential(
&params,
&verification_key,
&signature,
serial_number,
binding_number,
)?;
// Verify credentials
assert!(verify_credential(
&params,
&verification_key,
&theta,
&public_attributes,
));
Ok(())
}
-1
View File
@@ -1 +0,0 @@
mod e2e;
-22
View File
@@ -1,22 +0,0 @@
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()
}
}
-386
View File
@@ -1,386 +0,0 @@
// 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));
}
}
+681 -6
View File
@@ -2,6 +2,45 @@
# 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"
@@ -14,6 +53,12 @@ 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"
@@ -44,6 +89,12 @@ 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"
@@ -62,6 +113,12 @@ 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"
@@ -190,6 +247,41 @@ dependencies = [
"zeroize",
]
[[package]]
name = "darling"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d706e75d87e35569db781a9b5e2416cff1236a47ed380831f959382ccd5f858"
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",
]
[[package]]
name = "der"
version = "0.4.4"
@@ -217,6 +309,45 @@ 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"
@@ -249,6 +380,12 @@ 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"
@@ -265,6 +402,18 @@ 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"
@@ -290,10 +439,15 @@ 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"
@@ -304,6 +458,27 @@ 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"
@@ -329,7 +504,7 @@ version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi 0.9.0+wasi-snapshot-preview1",
]
@@ -340,7 +515,7 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi 0.10.2+wasi-snapshot-preview1",
]
@@ -414,6 +589,12 @@ 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"
@@ -425,6 +606,28 @@ 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"
@@ -437,25 +640,40 @@ version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "903ae2481bcdfdb7b68e0a9baa4b7c9aff600b9ae2e8e5bb5833b8c91ab851ea"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"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",
"cfg-if 1.0.0",
]
[[package]]
@@ -470,6 +688,12 @@ 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"
@@ -483,6 +707,7 @@ dependencies = [
"serde",
"serde_repr",
"thiserror",
"ts-rs",
]
[[package]]
@@ -510,6 +735,49 @@ 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"
@@ -522,6 +790,40 @@ 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"
@@ -571,6 +873,25 @@ 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"
@@ -581,6 +902,29 @@ 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"
@@ -605,6 +949,30 @@ 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"
@@ -623,6 +991,50 @@ 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"
@@ -653,6 +1065,18 @@ 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"
@@ -734,7 +1158,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b69f9a4c9740d74c5baa3fd2e547f9525fa8088a8a958e0ca2409a514e33f5fa"
dependencies = [
"block-buffer 0.9.0",
"cfg-if",
"cfg-if 1.0.0",
"cpufeatures",
"digest 0.9.0",
"opaque-debug 0.3.0",
@@ -750,6 +1174,18 @@ 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"
@@ -759,18 +1195,207 @@ 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"
@@ -842,6 +1467,28 @@ 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"
@@ -881,6 +1528,12 @@ 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"
@@ -917,6 +1570,28 @@ 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"
+36 -19
View File
@@ -3,21 +3,29 @@
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 config::defaults::REWARDING_VALIDATOR_ADDRESS;
use cosmwasm_std::{
entry_point, to_binary, Addr, Deps, DepsMut, Env, MessageInfo, QueryResponse, Response, Uint128,
entry_point, to_binary, Addr, Decimal, Deps, DepsMut, Env, MessageInfo, QueryResponse,
Response, Uint128,
};
use mixnet_contract::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg, StateParams};
pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2;
/// Constant specifying minimum of coin required to bond a gateway
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
/// 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_REWARDED_SET_SIZE: u32 = 200;
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
@@ -25,22 +33,36 @@ 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 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
// 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
pub const DEFAULT_COST_PER_EPOCH: u32 = 40_000_000;
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 {
owner,
rewarding_validator_address: Addr::unchecked(REWARDING_VALIDATOR_ADDRESS), // we trust our hardcoded value
params: StateParams {
epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH,
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,
),
}
}
@@ -83,6 +105,18 @@ pub fn execute(
ExecuteMsg::UpdateStateParams(params) => {
transactions::try_update_state_params(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,
@@ -107,15 +141,6 @@ pub fn execute(
ExecuteMsg::FinishMixnodeRewarding {
rewarding_interval_nonce,
} => transactions::try_finish_mixnode_rewarding(deps, info, rewarding_interval_nonce),
ExecuteMsg::RewardNextMixDelegators {
mix_identity,
rewarding_interval_nonce,
} => transactions::try_reward_next_mixnode_delegators_v2(
deps,
info,
mix_identity,
rewarding_interval_nonce,
),
}
}
@@ -174,14 +199,6 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, Cont
QueryMsg::GetCirculatingSupply {} => to_binary(&queries::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(&queries::query_rewarding_status(
deps,
mix_identity,
rewarding_interval_nonce,
)?),
};
Ok(query_res?)
+11 -12
View File
@@ -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,10 +80,15 @@ 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 },
@@ -95,10 +100,4 @@ pub enum ContractError {
#[error("Mixnode {identity} has already been rewarded during the current rewarding interval")]
MixnodeAlreadyRewarded { identity: IdentityKey },
#[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 },
}
+119 -2
View File
@@ -1,16 +1,78 @@
// 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::{Order, StdError, StdResult};
use cosmwasm_std::{Decimal, Order, StdError, StdResult, Uint128};
use cosmwasm_storage::ReadonlyBucket;
use mixnet_contract::{Addr, IdentityKey, PagedAllDelegationsResponse, 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)> {
@@ -99,7 +161,6 @@ 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 {
@@ -152,6 +213,7 @@ mod tests {
use crate::support::tests::helpers;
use cosmwasm_std::testing::mock_dependencies;
use mixnet_contract::RawDelegationData;
use std::str::FromStr;
#[test]
fn delegations_iterator() {
@@ -187,6 +249,61 @@ 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());
+39 -281
View File
@@ -6,24 +6,23 @@ use crate::helpers::get_all_delegations_paged;
use crate::storage::{
all_mix_delegations_read, circulating_supply, config_read, gateways_owners_read, gateways_read,
mix_delegations_read, mixnodes_owners_read, mixnodes_read, read_layer_distribution,
read_state_params, reverse_mix_delegations_read, reward_pool_value, rewarded_mixnodes_read,
total_delegation_read,
read_state_params, reverse_mix_delegations_read, reward_pool_value,
};
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr, Deps, Order, StdResult, Uint128};
use mixnet_contract::{
Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond,
MixOwnershipResponse, MixnodeRewardingStatusResponse, PagedAllDelegationsResponse,
PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse,
PagedReverseMixDelegationsResponse, RawDelegationData, RewardingIntervalResponse, StateParams,
MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayResponse,
PagedMixDelegationsResponse, PagedMixnodeResponse, PagedReverseMixDelegationsResponse,
RawDelegationData, RewardingIntervalResponse, StateParams,
};
const BOND_PAGE_MAX_LIMIT: u32 = 75;
const BOND_PAGE_MAX_LIMIT: u32 = 100;
const BOND_PAGE_DEFAULT_LIMIT: u32 = 50;
// currently the maximum limit before running into memory issue is somewhere between 1150 and 1200
const DELEGATION_PAGE_MAX_LIMIT: u32 = 500;
const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250;
pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 750;
pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 500;
pub fn query_mixnodes_paged(
deps: Deps,
@@ -39,16 +38,7 @@ pub fn query_mixnodes_paged(
.range(start.as_deref(), 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 =
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>>>>()??;
.collect::<StdResult<Vec<MixNodeBond>>>()?;
let start_next_after = nodes.last().map(|node| node.identity().clone());
@@ -236,22 +226,11 @@ pub(crate) fn query_mixnode_delegation(
}
}
pub(crate) fn query_rewarding_status(
deps: Deps,
mix_identity: IdentityKey,
rewarding_interval_nonce: u32,
) -> StdResult<MixnodeRewardingStatusResponse> {
let status = 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::state::State;
use crate::storage::{config, gateways, mix_delegations};
use crate::storage::{config, gateways, mix_delegations, mixnodes};
use crate::support::tests::helpers;
use crate::support::tests::helpers::{
good_gateway_bond, good_mixnode_bond, raw_delegation_fixture,
@@ -271,10 +250,12 @@ pub(crate) mod tests {
#[test]
fn mixnodes_paged_retrieval_obeys_limits() {
let mut deps = helpers::init_contract();
let storage = deps.as_mut().storage;
let limit = 2;
for n in 0..10000 {
let key = format!("bond{}", n);
helpers::add_mixnode(&key, good_mixnode_bond(), deps.as_mut());
let node = helpers::mixnode_bond_fixture();
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
}
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(limit)).unwrap();
@@ -284,9 +265,11 @@ pub(crate) mod tests {
#[test]
fn mixnodes_paged_retrieval_has_default_limit() {
let mut deps = helpers::init_contract();
let storage = deps.as_mut().storage;
for n in 0..100 {
let key = format!("bond{}", n);
helpers::add_mixnode(&key, good_mixnode_bond(), deps.as_mut());
let node = helpers::mixnode_bond_fixture();
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
}
// query without explicitly setting a limit
@@ -299,9 +282,11 @@ pub(crate) mod tests {
#[test]
fn mixnodes_paged_retrieval_has_max_limit() {
let mut deps = helpers::init_contract();
let storage = deps.as_mut().storage;
for n in 0..10000 {
let key = format!("bond{}", n);
helpers::add_mixnode(&key, good_mixnode_bond(), deps.as_mut());
let node = helpers::mixnode_bond_fixture();
mixnodes(storage).save(key.as_bytes(), &node).unwrap();
}
// query with a crazily high limit in an attempt to use too many resources
@@ -309,7 +294,7 @@ pub(crate) mod tests {
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 = BOND_PAGE_MAX_LIMIT;
let expected_limit = 100;
assert_eq!(expected_limit, page1.nodes.len() as u32);
}
@@ -321,7 +306,10 @@ pub(crate) mod tests {
let addr4 = "hal103";
let mut deps = helpers::init_contract();
let _identity1 = helpers::add_mixnode(&addr1, good_mixnode_bond(), deps.as_mut());
let node = helpers::mixnode_bond_fixture();
mixnodes(&mut deps.storage)
.save(addr1.as_bytes(), &node)
.unwrap();
let per_page = 2;
let page1 = query_mixnodes_paged(deps.as_ref(), None, Option::from(per_page)).unwrap();
@@ -330,20 +318,24 @@ pub(crate) mod tests {
assert_eq!(1, page1.nodes.len());
// save another
let identity2 = helpers::add_mixnode(&addr2, good_mixnode_bond(), deps.as_mut());
mixnodes(&mut deps.storage)
.save(addr2.as_bytes(), &node)
.unwrap();
// 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 = helpers::add_mixnode(&addr3, good_mixnode_bond(), deps.as_mut());
mixnodes(&mut deps.storage)
.save(addr3.as_bytes(), &node)
.unwrap();
// 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 start_after = String::from(addr2);
let page2 = query_mixnodes_paged(
deps.as_ref(),
Option::from(start_after),
@@ -354,9 +346,11 @@ pub(crate) mod tests {
assert_eq!(1, page2.nodes.len());
// save another one
helpers::add_mixnode(&addr4, good_mixnode_bond(), deps.as_mut());
mixnodes(&mut deps.storage)
.save(addr4.as_bytes(), &node)
.unwrap();
let start_after = identity2;
let start_after = String::from(addr2);
let page2 = query_mixnodes_paged(
deps.as_ref(),
Option::from(start_after),
@@ -587,14 +581,19 @@ pub(crate) mod tests {
owner: Addr::unchecked("someowner"),
rewarding_validator_address: Addr::unchecked("monitor"),
params: StateParams {
epoch_length: 1,
minimum_mixnode_bond: 123u128.into(),
minimum_gateway_bond: 456u128.into(),
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
mixnode_delegation_reward_rate: "7.89".parse().unwrap(),
mixnode_rewarded_set_size: 1000,
mixnode_active_set_size: 500,
},
rewarding_interval_starting_block: 123,
latest_rewarding_interval_nonce: 0,
rewarding_in_progress: false,
mixnode_epoch_bond_reward: "1.23".parse().unwrap(),
mixnode_epoch_delegation_reward: "7.89".parse().unwrap(),
};
config(deps.as_mut().storage).save(&dummy_state).unwrap();
@@ -1115,245 +1114,4 @@ pub(crate) mod tests {
assert_eq!(2, page2.delegated_nodes.len());
}
}
#[cfg(test)]
mod querying_for_rewarding_status {
use super::*;
use crate::support::tests::helpers::{add_mixnode, node_rewarding_params_fixture};
use crate::transactions::{
try_add_mixnode, try_begin_mixnode_rewarding, try_delegate_to_mixnode,
try_finish_mixnode_rewarding, try_reward_mixnode_v2,
try_reward_next_mixnode_delegators_v2, MINIMUM_BLOCK_AGE_FOR_REWARDING,
};
use mixnet_contract::{RewardingResult, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT};
#[test]
fn returns_empty_status_for_unrewarded_nodes() {
let mut deps = helpers::init_contract();
let env = mock_env();
let current_state = config_read(deps.as_mut().storage).load().unwrap();
let rewarding_validator_address = current_state.rewarding_validator_address;
let node_identity = add_mixnode("bob", 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(),
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 = helpers::init_contract();
let mut env = mock_env();
let current_state = config_read(deps.as_mut().storage).load().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", &good_mixnode_bond()),
MixNode {
identity_key: node_identity.clone(),
..helpers::mix_node_fixture()
},
)
.unwrap();
env.block.height += 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(),
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", &good_mixnode_bond()),
MixNode {
identity_key: node_identity.clone(),
..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 += 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(),
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 = helpers::init_contract();
let mut env = mock_env();
let current_state = config_read(deps.as_mut().storage).load().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", &good_mixnode_bond()),
MixNode {
identity_key: node_identity.clone(),
..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 += 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(),
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!(),
}
}
}
}
+5 -1
View File
@@ -1,7 +1,7 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cosmwasm_std::Addr;
use cosmwasm_std::{Addr, Decimal};
use mixnet_contract::StateParams;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -18,4 +18,8 @@ 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
}
+340 -117
View File
@@ -1,21 +1,22 @@
// 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 crate::state::State;
use config::defaults::{DENOM, TOTAL_SUPPLY};
use cosmwasm_std::{Coin, StdResult, Storage, Uint128};
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, MixNode, MixNodeBond,
RawDelegationData, RewardingStatus, StateParams,
Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond,
RawDelegationData, StateParams,
};
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
use serde::Serialize;
// storage prefixes
// all of them must be unique and presumably not be a prefix of a different one
@@ -28,7 +29,7 @@ const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers";
const REWARD_POOL_PREFIX: &[u8] = b"pool";
// buckets
const PREFIX_MIXNODES: &[u8] = b"mn";
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";
@@ -38,71 +39,6 @@ const PREFIX_REVERSE_MIX_DELEGATION: &[u8] = b"dm";
const PREFIX_REWARDED_MIXNODES: &[u8] = b"rm";
const PREFIX_TOTAL_DELEGATION: &[u8] = b"td";
#[derive(Serialize, Deserialize, Debug, PartialEq)]
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
)
}
}
// Contract-level stuff
// TODO Unify bucket and mixnode storage functions
@@ -229,11 +165,11 @@ pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResu
// Mixnode-related stuff
pub(crate) fn mixnodes(storage: &mut dyn Storage) -> Bucket<StoredMixnodeBond> {
pub fn mixnodes(storage: &mut dyn Storage) -> Bucket<MixNodeBond> {
bucket(storage, PREFIX_MIXNODES)
}
pub(crate) fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<StoredMixnodeBond> {
pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<MixNodeBond> {
bucket_read(storage, PREFIX_MIXNODES)
}
@@ -246,21 +182,10 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket<IdentityKey
bucket_read(storage, PREFIX_MIXNODES_OWNERS)
}
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> {
pub fn rewarded_mixnodes(storage: &mut dyn Storage, rewarding_interval_nonce: u32) -> Bucket<u8> {
Bucket::multilevel(
storage,
&[
@@ -270,10 +195,13 @@ pub(crate) fn rewarded_mixnodes(
)
}
pub(crate) fn rewarded_mixnodes_read(
// 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<RewardingStatus> {
) -> ReadonlyBucket<u8> {
ReadonlyBucket::multilevel(
storage,
&[
@@ -284,35 +212,107 @@ pub(crate) fn rewarded_mixnodes_read(
}
// helpers
pub(crate) fn read_mixnode_bond(
storage: &dyn Storage,
pub(crate) fn increase_mix_delegated_stakes(
storage: &mut dyn Storage,
mix_identity: IdentityKeyRef,
) -> StdResult<Option<MixNodeBond>> {
let stored_bond = mixnodes_read(storage).may_load(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,
}))
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_amount(
pub(crate) fn read_mixnode_bond(
storage: &dyn Storage,
identity: &[u8],
) -> StdResult<cosmwasm_std::Uint128> {
@@ -321,6 +321,17 @@ pub(crate) fn read_mixnode_bond_amount(
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> {
@@ -389,7 +400,8 @@ 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, stored_mixnode_bond_fixture,
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};
@@ -399,8 +411,8 @@ mod tests {
#[test]
fn mixnode_single_read_retrieval() {
let mut storage = MockStorage::new();
let bond1 = stored_mixnode_bond_fixture();
let bond2 = stored_mixnode_bond_fixture();
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();
@@ -431,14 +443,15 @@ mod tests {
let node_identity: IdentityKey = "nodeidentity".into();
// produces an error if target mixnode doesn't exist
let res = read_mixnode_bond_amount(&storage, node_owner.as_bytes());
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 = StoredMixnodeBond {
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,
@@ -455,7 +468,7 @@ mod tests {
assert_eq!(
Uint128(bond_value),
read_mixnode_bond_amount(&storage, node_identity.as_bytes()).unwrap()
read_mixnode_bond(&storage, node_identity.as_bytes()).unwrap()
);
}
@@ -526,6 +539,216 @@ mod tests {
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),
)
.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),
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),
)
.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),
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),
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),
)
.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),
)
.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::*;
+9 -32
View File
@@ -1,13 +1,11 @@
#[cfg(test)]
pub mod helpers {
use super::*;
use crate::contract::query;
use crate::contract::{instantiate, INITIAL_MIXNODE_BOND};
use crate::contract::{
query, DEFAULT_SYBIL_RESISTANCE_PERCENT, EPOCH_REWARD_PERCENT, INITIAL_REWARD_POOL,
};
use crate::storage::StoredMixnodeBond;
use crate::transactions::{try_add_gateway, try_add_mixnode};
use config::defaults::{DENOM, TOTAL_SUPPLY};
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;
@@ -18,19 +16,21 @@ pub mod helpers {
use cosmwasm_std::Coin;
use cosmwasm_std::OwnedDeps;
use cosmwasm_std::{coin, Uint128};
use cosmwasm_std::{from_binary, DepsMut};
use cosmwasm_std::{Empty, MemoryStorage};
use mixnet_contract::mixnode::NodeRewardParams;
use mixnet_contract::{
Gateway, GatewayBond, InstantiateMsg, Layer, MixNode, MixNodeBond, PagedGatewayResponse,
PagedMixnodeResponse, QueryMsg, RawDelegationData,
};
pub fn add_mixnode(sender: &str, stake: Vec<Coin>, deps: DepsMut) -> String {
pub fn add_mixnode(
sender: &str,
stake: Vec<Coin>,
deps: &mut OwnedDeps<MockStorage, MockApi, MockQuerier>,
) -> String {
let info = mock_info(sender, &stake);
let key = format!("{}mixnode", sender);
try_add_mixnode(
deps,
deps.as_mut(),
mock_env(),
info,
MixNode {
@@ -137,17 +137,6 @@ pub mod helpers {
)
}
pub(crate) fn stored_mixnode_bond_fixture() -> StoredMixnodeBond {
StoredMixnodeBond::new(
coin(50, DENOM),
Addr::unchecked("foo"),
Layer::One,
12_345,
mix_node_fixture(),
None,
)
}
pub fn gateway_fixture() -> Gateway {
Gateway {
host: "1.1.1.1".to_string(),
@@ -199,16 +188,4 @@ 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,
)
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-2
View File
@@ -48,5 +48,3 @@ If you would like to contribute to the Network Explorer send us a PR or
## Development
Please see [development docs](./docs) here for more information on the structure and design of this app.
@@ -17,8 +17,6 @@ export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
fontWeight: 'bold',
fontSize: 14,
padding: 0,
// border: '1px solid red',
// minWidth: 300,
}}
data-testid={headingTitle}
>
+1 -2
View File
@@ -28,7 +28,7 @@ export interface UniversalTableProps {
function formatCellValues(val: string | number, field: string) {
if (field === 'bond') {
return printableCoin({ amount: val.toString(), denom: 'upunk' });
return printableCoin({ amount: val.toString(), denom: 'unpunk' });
}
return val;
}
@@ -66,7 +66,6 @@ export const DetailTable: React.FC<{
padding: 2,
width: 200,
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(
eachRow[columnsData[index].field],
+39 -70
View File
@@ -1,13 +1,5 @@
import * as React from 'react';
import {
Box,
Card,
CardContent,
IconButton,
Typography,
useMediaQuery,
} from '@mui/material';
import { useTheme } from '@mui/material/styles';
import { Box, Card, CardContent, IconButton, Typography } from '@mui/material';
import ArrowRightAltIcon from '@mui/icons-material/ArrowRightAlt';
interface StatsCardProps {
@@ -23,69 +15,46 @@ export const StatsCard: React.FC<StatsCardProps> = ({
count,
onClick,
errorMsg,
}) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
return (
<Card onClick={onClick} sx={{ height: '100%' }}>
<CardContent
sx={{
padding: 2,
'&:last-child': {
paddingBottom: 2,
},
cursor: 'pointer',
}}
}) => (
<Card onClick={onClick} sx={{ height: '100%' }}>
<CardContent
sx={{
padding: 2,
'&:last-child': {
paddingBottom: 2,
},
cursor: 'pointer',
}}
>
<Box
display="flex"
alignItems="center"
sx={{ color: (theme) => theme.palette.text.primary, fontSize: 18 }}
>
<Box
display="flex"
alignItems="center"
sx={{
color: theme.palette.text.primary,
fontSize: 18,
justifyContent: matches ? 'space-between' : 'flex-start',
maxWidth: matches ? 230 : null,
}}
{icon}
<Typography
ml={3}
mr={0.75}
fontSize="inherit"
data-testid={`${title}-amount`}
>
{icon}
<Box
sx={{
color: theme.palette.text.primary,
display: 'flex',
flexDirection: 'row',
fontSize: 18,
justifyContent: matches ? 'space-between' : 'flex-start',
alignItems: 'center',
width: '100%',
maxWidth: matches ? 230 : null,
}}
>
<Typography
ml={3}
mr={0.75}
fontSize="inherit"
data-testid={`${title}-amount`}
>
{count}
</Typography>
<Typography mr={1} fontSize="inherit" data-testid={title}>
{title}
</Typography>
<IconButton color="inherit">
<ArrowRightAltIcon />
</IconButton>
</Box>
</Box>
{errorMsg && (
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
{errorMsg}
</Typography>
)}
</CardContent>
</Card>
);
};
{count}
</Typography>
<Typography mr={1} fontSize="inherit" data-testid={title}>
{title}
</Typography>
<IconButton color="inherit">
<ArrowRightAltIcon />
</IconButton>
</Box>
{errorMsg && (
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
{errorMsg}
</Typography>
)}
</CardContent>
</Card>
);
StatsCard.defaultProps = {
onClick: undefined,
+4 -3
View File
@@ -25,7 +25,7 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
width: '100%',
marginBottom: 2,
display: 'flex',
flexDirection: matches ? 'column-reverse' : 'row',
flexDirection: matches ? 'column' : 'row',
justifyContent: 'space-between',
}}
>
@@ -35,7 +35,8 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
value={pageSize}
onChange={onChangePageSize}
sx={{
width: matches ? 100 : 200,
width: 200,
marginBottom: matches ? 2 : 0,
}}
>
<MenuItem value={10} data-testid="ten">
@@ -52,7 +53,7 @@ export const TableToolbar: React.FC<TableToolBarProps> = ({
</MenuItem>
</Select>
<TextField
sx={{ width: matches ? '100%' : 350, marginBottom: matches ? 2 : 0 }}
sx={{ width: 350 }}
value={searchTerm}
data-testid="search-box"
placeholder="search"
+38 -21
View File
@@ -2,13 +2,14 @@ import * as React from 'react';
import { makeStyles } from '@mui/styles';
import {
DataGrid,
GridColDef,
GridColumns,
GridRowModel,
GridSortModel,
useGridApiContext,
useGridState,
} from '@mui/x-data-grid';
import Pagination from '@mui/material/Pagination';
import { SxProps } from '@mui/system';
import { LinearProgress } from '@mui/material';
const useStyles = makeStyles({
root: {
@@ -16,6 +17,16 @@ const useStyles = makeStyles({
},
});
type DataGridProps = {
loading?: boolean;
rows: GridRowModel[];
columnsData: GridColumns;
pageSize?: string;
pagination?: boolean;
hideFooter?: boolean;
sortModel?: GridSortModel;
};
export const cellStyles: SxProps = {
width: '100%',
padding: 0,
@@ -39,7 +50,6 @@ function CustomPagination() {
return (
<Pagination
className={classes.root}
sx={{ mt: 2 }}
color="primary"
count={state.pagination.pageCount}
page={state.pagination.page + 1}
@@ -48,46 +58,53 @@ function CustomPagination() {
);
}
type DataGridProps = {
columns: GridColDef[];
pagination?: true | undefined;
pageSize?: string | undefined;
rows: any;
loading?: boolean;
};
export const UniversalDataGrid: React.FC<DataGridProps> = ({
rows,
columns,
loading,
pagination,
rows,
columnsData,
pageSize,
pagination,
hideFooter,
sortModel,
}) => {
if (loading) return <LinearProgress />;
if (!loading)
const [sortModelState, setSortModelState] = React.useState<
GridSortModel | undefined
>(sortModel);
if (columnsData && rows) {
return (
<DataGrid
pagination={pagination}
rows={rows}
pagination
components={{
Pagination: CustomPagination,
}}
columns={columns}
loading={loading}
columns={columnsData}
rows={rows}
pageSize={Number(pageSize)}
rowsPerPageOptions={[5]}
hideFooterPagination={!pagination}
disableColumnFilter
disableColumnMenu
disableSelectionOnClick
columnBuffer={0}
autoHeight
hideFooter={!pagination}
hideFooter={hideFooter}
sortModel={sortModelState}
onSortModelChange={setSortModelState}
style={{
width: '100%',
border: 'none',
}}
/>
);
}
return null;
};
UniversalDataGrid.defaultProps = {
loading: false,
pagination: undefined,
pageSize: '10',
pageSize: undefined,
pagination: false,
hideFooter: true,
sortModel: undefined,
};
+29 -25
View File
@@ -1,18 +1,19 @@
import * as React from 'react';
import { Button, Card, Grid, Typography } from '@mui/material';
import { Button, Grid, Typography } from '@mui/material';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { printableCoin } from '@nymproject/nym-validator-client';
import { SelectChangeEvent } from '@mui/material/Select';
import { useMainContext } from 'src/context/main';
import { gatewayToGridRow } from 'src/utils';
import { GatewayResponse } from 'src/typeDefs/explorer-api';
import { TableToolbar } from 'src/components/TableToolbar';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { Title } from 'src/components/Title';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { useMainContext } from 'src/context/main';
import { gatewayToGridRow } from 'src/utils';
import { GatewayResponse } from 'src/typeDefs/explorer-api';
import { TableToolbar } from 'src/components/TableToolbar';
import { ContentCard } from 'src/components/ContentCard';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { Title } from 'src/components/Title';
export const PageGateways: React.FC = () => {
const { gateways } = useMainContext();
@@ -48,9 +49,8 @@ export const PageGateways: React.FC = () => {
const columns: GridColDef[] = [
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -61,11 +61,10 @@ export const PageGateways: React.FC = () => {
},
{
field: 'identity_key',
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
width: 200,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Typography sx={cellStyles} data-testid="identity-key">
{params.value}
@@ -74,7 +73,7 @@ export const PageGateways: React.FC = () => {
},
{
field: 'bond',
width: 150,
width: 120,
type: 'number',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
headerClassName: 'MuiDataGrid-header-override',
@@ -94,7 +93,7 @@ export const PageGateways: React.FC = () => {
{
field: 'host',
renderHeader: () => <CustomColumnHeading headingTitle="IP:Port" />,
width: 110,
width: 150,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -106,7 +105,7 @@ export const PageGateways: React.FC = () => {
{
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 150,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -129,14 +128,9 @@ export const PageGateways: React.FC = () => {
return (
<>
<Title text="Gateways" />
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
height: '100%',
}}
>
<Grid>
<Grid item>
<ContentCard>
<TableToolbar
onChangeSearch={handleSearch}
onChangePageSize={handlePageSize}
@@ -144,11 +138,21 @@ export const PageGateways: React.FC = () => {
searchTerm={searchTerm}
/>
<UniversalDataGrid
loading={gateways?.isLoading}
columnsData={columns}
rows={gatewayToGridRow(filteredGateways)}
columns={columns}
pageSize={pageSize}
pagination={gateways?.data?.length >= 12}
hideFooter={gateways?.data?.length < 12}
data-testid="gateway-data-grid"
sortModel={[
{
field: 'bond',
sort: 'desc',
},
]}
/>
</Card>
</ContentCard>
</Grid>
</Grid>
</>
+51 -51
View File
@@ -1,20 +1,21 @@
import * as React from 'react';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { printableCoin } from '@nymproject/nym-validator-client';
import { Button, Grid, Link as MuiLink, Card } from '@mui/material';
import { Link as RRDLink } from 'react-router-dom';
import { Button, Grid, Link as MuiLink } from '@mui/material';
import { SelectChangeEvent } from '@mui/material/Select';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { useMainContext } from 'src/context/main';
import { mixnodeToGridRow } from 'src/utils';
import { TableToolbar } from 'src/components/TableToolbar';
import { MixNodeResponse } from 'src/typeDefs/explorer-api';
import { BIG_DIPPER } from 'src/api/constants';
import { ContentCard } from 'src/components/ContentCard';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { Title } from 'src/components/Title';
import {
UniversalDataGrid,
cellStyles,
} from 'src/components/Universal-DataGrid';
export const PageMixnodes: React.FC = () => {
const { mixnodes } = useMainContext();
@@ -50,9 +51,8 @@ export const PageMixnodes: React.FC = () => {
const columns: GridColDef[] = [
{
field: 'owner',
headerName: 'Owner',
renderHeader: () => <CustomColumnHeading headingTitle="Owner" />,
width: 380,
flex: 3,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
@@ -68,11 +68,10 @@ export const PageMixnodes: React.FC = () => {
},
{
field: 'identity_key',
headerName: 'Identity Key',
renderHeader: () => <CustomColumnHeading headingTitle="Identity Key" />,
headerClassName: 'MuiDataGrid-header-override',
width: 380,
flex: 3,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
@@ -87,11 +86,11 @@ export const PageMixnodes: React.FC = () => {
{
field: 'bond',
headerName: 'Bond',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
type: 'number',
headerClassName: 'MuiDataGrid-header-override',
width: 150,
headerAlign: 'left',
flex: 1,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Bond" />,
renderCell: (params: GridRenderCellParams) => {
const bondAsPunk = printableCoin({
amount: params.value as string,
@@ -108,30 +107,14 @@ export const PageMixnodes: React.FC = () => {
);
},
},
{
field: 'location',
headerName: 'Location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
width: 150,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
>
{params.value}
</Button>
),
},
{
field: 'self_percentage',
headerName: 'Self %',
width: 110,
headerAlign: 'left',
type: 'number',
width: 99,
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Self %" />,
type: 'number',
headerAlign: 'left',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
@@ -144,11 +127,10 @@ export const PageMixnodes: React.FC = () => {
},
{
field: 'host',
headerName: 'Host',
renderHeader: () => <CustomColumnHeading headingTitle="Host" />,
headerClassName: 'MuiDataGrid-header-override',
width: 110,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={cellStyles}
@@ -160,12 +142,27 @@ export const PageMixnodes: React.FC = () => {
),
},
{
field: 'layer',
headerName: 'Layer',
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
headerClassName: 'MuiDataGrid-header-override',
width: 110,
field: 'location',
renderHeader: () => <CustomColumnHeading headingTitle="Location" />,
flex: 1,
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderCell: (params: GridRenderCellParams) => (
<Button
onClick={() => handleSearch(params.value as string)}
sx={{ ...cellStyles, justifyContent: 'flex-start' }}
>
{params.value}
</Button>
),
},
{
field: 'layer',
headerAlign: 'left',
headerClassName: 'MuiDataGrid-header-override',
renderHeader: () => <CustomColumnHeading headingTitle="Layer" />,
flex: 1,
type: 'number',
renderCell: (params: GridRenderCellParams) => (
<MuiLink
sx={{ ...cellStyles, textAlign: 'left' }}
@@ -185,14 +182,9 @@ export const PageMixnodes: React.FC = () => {
return (
<>
<Title text="Mixnodes" />
<Grid container>
<Grid item xs={12}>
<Card
sx={{
padding: 2,
height: '100%',
}}
>
<Grid>
<Grid item>
<ContentCard>
<TableToolbar
onChangeSearch={handleSearch}
onChangePageSize={handlePageSize}
@@ -200,12 +192,20 @@ export const PageMixnodes: React.FC = () => {
searchTerm={searchTerm}
/>
<UniversalDataGrid
pagination
loading={mixnodes?.isLoading}
columnsData={columns}
rows={mixnodeToGridRow(filteredMixnodes)}
columns={columns}
pageSize={pageSize}
pagination
hideFooter={false}
sortModel={[
{
field: 'bond',
sort: 'desc',
},
]}
/>
</Card>
</ContentCard>
</Grid>
</Grid>
</>
+11 -11
View File
@@ -7,18 +7,18 @@ import {
SelectChangeEvent,
Typography,
} from '@mui/material';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { ContentCard } from 'src/components/ContentCard';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { TableToolbar } from 'src/components/TableToolbar';
import { Title } from 'src/components/Title';
import {
UniversalDataGrid,
cellStyles,
} from 'src/components/Universal-DataGrid';
import { WorldMap } from 'src/components/WorldMap';
import { useMainContext } from 'src/context/main';
import {
cellStyles,
UniversalDataGrid,
} from 'src/components/Universal-DataGrid';
import { CustomColumnHeading } from 'src/components/CustomColumnHeading';
import { GridColDef, GridRenderCellParams } from '@mui/x-data-grid';
import { TableToolbar } from 'src/components/TableToolbar';
import { CountryDataRowType, countryDataToGridRow } from 'src/utils';
import { Title } from 'src/components/Title';
import { ContentCard } from '../../components/ContentCard';
export const PageMixnodesMap: React.FC = () => {
const { countryData } = useMainContext();
@@ -119,11 +119,11 @@ export const PageMixnodesMap: React.FC = () => {
searchTerm={searchTerm}
/>
<UniversalDataGrid
pagination
loading={countryData?.isLoading}
columns={columns}
columnsData={columns}
rows={formattedCountries}
pageSize={pageSize}
pagination
/>
</ContentCard>
</Grid>
+1 -1
View File
@@ -4,7 +4,7 @@
the theme declaration in index.tsx or the style prop
in <DataGrid /> */
.MuiDataGrid-sortIcon, .MuiDataGrid-menuIcon {
.MuiDataGrid-sortIcon {
display: none !important;
}
+7 -4
View File
@@ -65,7 +65,9 @@ export function countryDataToGridRow(
return sorted;
}
export function mixnodeToGridRow(arrayOfMixnodes: MixNodeResponse): any {
export function mixnodeToGridRow(
arrayOfMixnodes: MixNodeResponse,
): MixnodeRowType[] {
return !arrayOfMixnodes
? []
: arrayOfMixnodes.map((mn) => {
@@ -73,15 +75,16 @@ export function mixnodeToGridRow(arrayOfMixnodes: MixNodeResponse): any {
const delegations = Number(mn.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
const selfPercentage = ((pledge * 100) / totalBond).toFixed(2);
return {
id: mn.owner,
owner: mn.owner,
location: mn?.location?.country_name || '',
identity_key: mn.mix_node.identity_key || '',
bond: totalBond || 0,
location: mn?.location?.country_name || '',
self_percentage: selfPercentage,
host: mn?.mix_node?.host || '',
layer: mn?.layer || '',
host: mn.mix_node.host || '',
layer: mn.layer || '',
};
});
}
+20 -20
View File
@@ -514,11 +514,30 @@ 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 = "com"
version = "0.2.0"
@@ -1298,7 +1317,6 @@ dependencies = [
"az",
"bytemuck",
"half",
"serde",
"typenum",
]
@@ -2711,24 +2729,6 @@ dependencies = [
"validator-client",
]
[[package]]
name = "nymcoconut"
version = "0.5.0"
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 = "nymsphinx-types"
version = "0.1.0"
+2 -3
View File
@@ -9,19 +9,18 @@ Nym is an open-source, decentralized and permissionless privacy system. It provi
The Nym desktop wallet enables you to use the Nym network and take advantage of its key capabilities
## Installation prerequisites - Linux / Mac
## Installation prerequisites Linux / Mac
- `Yarn`
- `NodeJS >= v16.8.0`
- `Rust & cargo >= v1.56`
## Installation prerequisites - Windows
## Installation prerequisites Windows
- When running on Windows you will need to install c++ build tools
- An easy guide to get rust up and running [Installation]("http://kennykerr.ca/2019/11/18/rust-getting-started/")
- When installing NodeJS please use the `current features` version
- Using a package manager like [Chocolatey]("chocolatey.org") is recommended
- The nym wallet requires you to have `Webview2` installed, please head to the [Installer](https://developer.microsoft.com/en-us/microsoft-edge/webview2/#download-section), this will ensure a smooth app launch
## Installation
+43
View File
@@ -0,0 +1,43 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 25.3.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 5389.9 5389.9" style="enable-background:new 0 0 5389.9 5389.9;" xml:space="preserve">
<style type="text/css">
.st0{fill:#121726;}
.st1{fill:url(#SVGID_1_);}
.st2{fill:#FFFFFF;}
</style>
<g>
<g>
<circle class="st0" cx="2695" cy="2695" r="2585"/>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="0" y1="8058.165" x2="5390" y2="8058.165" gradientTransform="matrix(1 0 0 -1 0 10753.165)">
<stop offset="0" style="stop-color:#F77846"/>
<stop offset="1" style="stop-color:#ED3572"/>
</linearGradient>
<path class="st1" d="M2695,5390c-182.8,0-365.5-18.4-543-54.8c-173.1-35.4-343.3-88.3-506-157.1
c-159.7-67.6-313.7-151.2-457.8-248.5c-142.7-96.4-276.8-207.1-398.8-329c-121.9-121.9-232.6-256.1-329-398.8
C363,4057.8,279.4,3903.8,211.8,3744C143,3581.4,90.2,3411.1,54.8,3238C18.4,3060.5,0,2877.8,0,2695c0-182.8,18.4-365.5,54.8-543
c35.4-173.1,88.3-343.3,157.1-506c67.6-159.7,151.2-313.7,248.5-457.8c96.4-142.7,207.1-276.8,329-398.8s256.1-232.6,398.8-329
c144.1-97.3,298.1-180.9,457.8-248.5c162.7-68.8,332.9-121.7,506-157.1C2329.5,18.4,2512.2,0,2695,0c182.8,0,365.5,18.4,543,54.8
c173.1,35.4,343.3,88.3,506,157.1c159.7,67.6,313.7,151.2,457.8,248.5c142.7,96.4,276.8,207.1,398.8,329
c121.9,121.9,232.6,256.1,329,398.8c97.3,144.1,180.9,298.1,248.5,457.8c68.8,162.7,121.7,332.9,157.1,506
c36.3,177.5,54.8,360.2,54.8,543c0,182.8-18.4,365.5-54.8,543c-35.4,173.1-88.3,343.3-157.1,506
c-67.6,159.7-151.2,313.7-248.5,457.8c-96.4,142.7-207.1,276.8-329,398.8c-121.9,121.9-256.1,232.6-398.8,329
c-144.1,97.3-298.1,180.9-457.8,248.5c-162.7,68.8-332.9,121.7-506,157.1C3060.5,5371.6,2877.8,5390,2695,5390z M2695,220
c-168,0-335.9,16.9-498.9,50.3c-158.9,32.5-315.1,81-464.4,144.2c-146.6,62-288.1,138.8-420.4,228.2
c-131.1,88.6-254.3,190.3-366.4,302.3c-112,112-213.7,235.3-302.3,366.4c-89.4,132.3-166.2,273.7-228.2,420.4
c-63.2,149.3-111.7,305.6-144.2,464.4C236.9,2359.1,220,2527,220,2695s16.9,335.9,50.3,498.9c32.5,158.9,81,315.1,144.2,464.4
c62,146.6,138.8,288.1,228.2,420.4c88.6,131.1,190.3,254.3,302.3,366.4c112,112,235.3,213.7,366.4,302.3
c132.3,89.4,273.7,166.2,420.4,228.2c149.3,63.2,305.6,111.7,464.4,144.2c163.1,33.4,330.9,50.3,498.9,50.3s335.9-16.9,498.9-50.3
c158.9-32.5,315.1-81,464.4-144.2c146.6-62,288.1-138.8,420.4-228.2c131.1-88.6,254.3-190.3,366.4-302.3
c112-112,213.7-235.3,302.3-366.4c89.4-132.3,166.2-273.7,228.2-420.4c63.2-149.3,111.7-305.6,144.2-464.4
c33.4-163.1,50.3-330.9,50.3-498.9s-16.9-335.9-50.3-498.9c-32.5-158.9-81-315.1-144.2-464.4c-62-146.6-138.8-288.1-228.2-420.4
c-88.6-131.1-190.3-254.3-302.3-366.4c-112-112-235.3-213.7-366.4-302.3c-132.3-89.4-273.7-166.2-420.4-228.2
c-149.3-63.2-305.6-111.7-464.4-144.2C3030.9,236.9,2863,220,2695,220z"/>
</g>
</g>
<path class="st2" d="M1958.5,3160.4h-269.6l-735.8-725.3v725.3H734.6v-930.9h276.2l735.8,725.1v-725.1h211.9V3160.4z M4378.9,2229.5
l-335.7,330.9l-335.7-330.9h-276.2v930.9h218.4v-725.3l345.4,340.6c26.7,26.3,69.6,26.3,96.3,0l345.4-340.6v725.3h218.4v-930.9
H4378.9z M2589.1,2715.4v445h218.4v-445l502.7-485.9H3034l-335.9,330.9l-335.7-330.9h-276.2L2589.1,2715.4z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 349 KiB

+9
View File
@@ -0,0 +1,9 @@
<!DOCTYPE html>
<html>
<head>
<title>Nym Wallet</title>
<link rel="icon" href="assets/favicon.ico"><script defer src="main.js"></script></head>
<body>
<div id="root"></div>
</body>
</html>
+5778
View File
File diff suppressed because one or more lines are too long
+8 -4
View File
@@ -5,19 +5,17 @@
use mixnet_contract::{Gateway, MixNode};
use std::sync::Arc;
use tauri::{Menu};
use tauri::{Menu, MenuItem};
use tokio::sync::RwLock;
use validator_client::nymd::fee_helpers::Operation;
mod coin;
mod config;
mod error;
mod menu;
mod operations;
mod state;
mod utils;
use crate::menu::AddDefaultSubmenus;
use crate::operations::account::*;
use crate::operations::admin::*;
use crate::operations::bond::*;
@@ -37,6 +35,12 @@ macro_rules! format_err {
};
}
pub fn create_menu_items() -> Menu {
Menu::new()
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Paste)
}
fn main() {
tauri::Builder::default()
.manage(Arc::new(RwLock::new(State::default())))
@@ -60,7 +64,7 @@ fn main() {
update_state_params,
get_reverse_mix_delegations_paged,
])
.menu(Menu::new().add_default_app_submenu_if_macos())
.menu(create_menu_items())
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
-25
View File
@@ -1,25 +0,0 @@
use tauri::{Menu, MenuItem, Submenu};
pub trait AddDefaultSubmenus {
fn add_default_app_submenu_if_macos(self) -> Self;
}
impl AddDefaultSubmenus for Menu {
fn add_default_app_submenu_if_macos(self) -> Menu {
#[cfg(target_os = "macos")]
return self.add_submenu(Submenu::new(
"Menu",
Menu::new()
.add_native_item(MenuItem::Copy)
.add_native_item(MenuItem::Cut)
.add_native_item(MenuItem::Paste)
.add_native_item(MenuItem::Hide)
.add_native_item(MenuItem::HideOthers)
.add_native_item(MenuItem::SelectAll)
.add_native_item(MenuItem::ShowAll)
.add_native_item(MenuItem::Quit),
));
#[cfg(not(target_os = "macos"))]
return self;
}
}
@@ -1,17 +1,22 @@
use crate::format_err;
use crate::state::State;
use cosmwasm_std::Decimal;
use cosmwasm_std::Uint128;
use mixnet_contract::StateParams;
use serde::{Deserialize, Serialize};
use std::convert::{TryFrom, TryInto};
use std::str::FromStr;
use std::sync::Arc;
use tokio::sync::RwLock;
#[cfg_attr(test, derive(ts_rs::TS))]
#[derive(Serialize, Deserialize)]
pub struct TauriStateParams {
epoch_length: u32,
minimum_mixnode_bond: String,
minimum_gateway_bond: String,
mixnode_bond_reward_rate: String,
mixnode_delegation_reward_rate: String,
mixnode_rewarded_set_size: u32,
mixnode_active_set_size: u32,
}
@@ -19,8 +24,11 @@ pub struct TauriStateParams {
impl From<StateParams> for TauriStateParams {
fn from(p: StateParams) -> TauriStateParams {
TauriStateParams {
epoch_length: p.epoch_length,
minimum_mixnode_bond: p.minimum_mixnode_bond.to_string(),
minimum_gateway_bond: p.minimum_gateway_bond.to_string(),
mixnode_bond_reward_rate: p.mixnode_bond_reward_rate.to_string(),
mixnode_delegation_reward_rate: p.mixnode_delegation_reward_rate.to_string(),
mixnode_rewarded_set_size: p.mixnode_rewarded_set_size,
mixnode_active_set_size: p.mixnode_active_set_size,
}
@@ -32,8 +40,11 @@ impl TryFrom<TauriStateParams> for StateParams {
fn try_from(p: TauriStateParams) -> Result<StateParams, Self::Error> {
Ok(StateParams {
epoch_length: p.epoch_length,
minimum_mixnode_bond: Uint128::try_from(p.minimum_mixnode_bond.as_str())?,
minimum_gateway_bond: Uint128::try_from(p.minimum_gateway_bond.as_str())?,
mixnode_bond_reward_rate: Decimal::from_str(p.mixnode_bond_reward_rate.as_str())?,
mixnode_delegation_reward_rate: Decimal::from_str(p.mixnode_delegation_reward_rate.as_str())?,
mixnode_rewarded_set_size: p.mixnode_rewarded_set_size,
mixnode_active_set_size: p.mixnode_active_set_size,
})
+1 -1
View File
@@ -13,7 +13,7 @@
"bundle": {
"active": true,
"targets": "all",
"identifier": "com.nymwallet.nymtech",
"identifier": "com.tauri.dev",
"icon": [
"icons/32x32.png",
"icons/128x128.png",
+3
View File
@@ -1,6 +1,9 @@
export interface TauriStateParams {
epoch_length: number;
minimum_mixnode_bond: string;
minimum_gateway_bond: string;
mixnode_bond_reward_rate: string;
mixnode_delegation_reward_rate: string;
mixnode_rewarded_set_size: number;
mixnode_active_set_size: number;
}
+2
View File
@@ -57,6 +57,8 @@ credentials = { path = "../common/credentials", optional = true }
[features]
coconut = ["coconut-interface", "credentials", "gateway-client/coconut"]
default = ["tokenomics"]
tokenomics = []
[build-dependencies]
tokio = { version = "1.4", features = ["rt-multi-thread", "macros"] }
+3
View File
@@ -16,6 +16,9 @@ local_validator = '{{ base.local_validator }}'
# Address of the validator contract managing the network.
mixnet_contract_address = '{{ base.mixnet_contract_address }}'
# Mnemonic (currently of the network monitor) used for rewarding
mnemonic = '{{ base.mnemonic }}'
##### network monitor config options #####
[network_monitor]
+5 -5
View File
@@ -1,11 +1,6 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use gateway_client::bandwidth::BandwidthController;
use std::sync::Arc;
use crate::cache::ValidatorCache;
use crate::config::Config;
use crate::network_monitor::monitor::preparer::PacketPreparer;
@@ -19,6 +14,11 @@ use crate::network_monitor::monitor::sender::PacketSender;
use crate::network_monitor::monitor::summary_producer::SummaryProducer;
use crate::network_monitor::monitor::Monitor;
use crate::storage::ValidatorApiStorage;
use crypto::asymmetric::{encryption, identity};
use futures::channel::mpsc;
use std::sync::Arc;
use gateway_client::bandwidth::BandwidthController;
pub(crate) mod chunker;
pub(crate) mod gateways_reader;
@@ -297,7 +297,7 @@ impl Monitor {
// wait for validator cache to be ready
self.packet_preparer
.wait_for_validator_cache_initial_values(self.minimum_test_routes)
.wait_for_validator_cache_initial_values(self.test_routes)
.await;
self.packet_sender
@@ -188,11 +188,6 @@ impl PacketPreparer {
let mixnodes = self.validator_cache.rewarded_mixnodes().await;
if gateways.into_inner().len() < minimum_full_routes {
info!(
"Minimal topology is still not offline. Going to check again in {:?}",
initialisation_backoff
);
tokio::time::sleep(initialisation_backoff).await;
continue;
}
+1 -1
View File
@@ -17,7 +17,7 @@ use time::OffsetDateTime;
pub struct InvalidUptime;
// value in range 0-100
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default)]
#[derive(Clone, Copy, Serialize, Deserialize, Debug)]
pub struct Uptime(u8);
impl Uptime {
+1 -1
View File
@@ -103,7 +103,7 @@ impl NodeUptimes {
NodeUptimes {
most_recent: most_recent.try_into().unwrap(),
last_hour: Uptime::from_uptime_sum(last_hour_sum, last_hour_test_runs).unwrap(),
last_day: Uptime::from_uptime_sum(last_day_sum, last_day_test_runs).unwrap(),
last_day: Uptime::from_uptime_sum(last_day_sum, last_hour_test_runs).unwrap(),
}
}
}
+5 -105
View File
@@ -8,18 +8,16 @@ use crate::rewarding::{
};
use config::defaults::DEFAULT_VALIDATOR_API_PORT;
use mixnet_contract::{
Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, MixnodeRewardingStatusResponse,
RewardingIntervalResponse, StateParams, MIXNODE_DELEGATORS_PAGE_LIMIT,
Delegation, ExecuteMsg, GatewayBond, IdentityKey, MixNodeBond, RewardingIntervalResponse,
StateParams,
};
use serde::Serialize;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
use tokio::time::sleep;
use validator_client::nymd::{
hash::{Hash, SHA256_HASH_SIZE},
CosmWasmClient, CosmosCoin, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient,
TendermintTime,
CosmWasmClient, Fee, QueryNymdClient, SigningCosmWasmClient, SigningNymdClient, TendermintTime,
};
use validator_client::ValidatorClientError;
@@ -157,21 +155,6 @@ impl<C> Client<C> {
self.0.read().await.get_current_rewarding_interval().await
}
pub(crate) async fn get_rewarding_status(
&self,
mix_identity: mixnet_contract::IdentityKey,
rewarding_interval_nonce: u32,
) -> Result<MixnodeRewardingStatusResponse, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
self.0
.read()
.await
.get_rewarding_status(mix_identity, rewarding_interval_nonce)
.await
}
/// Obtains the hash of a block specified by the provided height.
/// If the resulting digest is empty, a `None` is returned instead.
///
@@ -252,77 +235,7 @@ impl<C> Client<C> {
Ok(())
}
pub(crate) async fn reward_mixnode_and_all_delegators(
&self,
node: &MixnodeToReward,
rewarding_interval_nonce: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
// determine how many times we are going to have to call the delegator rewarding,
// note that it doesn't include the "base" call to `RewardMixnode` that rewards one page
let further_calls = node.total_delegations / MIXNODE_DELEGATORS_PAGE_LIMIT;
// start with the base call to reward operator and first page of delegators
let fee = self
.estimate_mixnode_reward_fees(1, MIXNODE_DELEGATORS_PAGE_LIMIT)
.await;
let msgs = vec![(
node.to_reward_execute_msg_v2(rewarding_interval_nonce),
vec![],
)];
let memo = format!(
"operator + {} delegators rewarding",
MIXNODE_DELEGATORS_PAGE_LIMIT
);
self.execute_multiple_with_retry(msgs, fee, memo).await?;
// reward rest of delegators
let mut remaining_delegators = node.total_delegations - MIXNODE_DELEGATORS_PAGE_LIMIT;
let delegator_rewarding_msg = (
node.to_next_delegator_reward_execute_msg_v2(rewarding_interval_nonce),
vec![],
);
for _ in 0..further_calls {
let delegators_in_call = remaining_delegators.min(MIXNODE_DELEGATORS_PAGE_LIMIT);
let fee = self
.estimate_mixnode_reward_fees(1, delegators_in_call)
.await;
let msgs = vec![delegator_rewarding_msg.clone()];
let memo = format!("rewarding another {} delegators", delegators_in_call);
self.execute_multiple_with_retry(msgs, fee, memo).await?;
remaining_delegators -= MIXNODE_DELEGATORS_PAGE_LIMIT;
}
Ok(())
}
pub(crate) async fn reward_mix_delegators(
&self,
node: &MixnodeToReward,
rewarding_interval_nonce: u32,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
{
// the fee is a tricky subject here because we don't know exactly how many delegators we missed,
// let's aim for the worst case scenario and assume it was the entire page
let fee = self
.estimate_mixnode_reward_fees(1, MIXNODE_DELEGATORS_PAGE_LIMIT)
.await;
let delegator_rewarding_msg = (
node.to_next_delegator_reward_execute_msg_v2(rewarding_interval_nonce),
vec![],
);
let memo = "rewarding delegators".to_string();
self.execute_multiple_with_retry(vec![delegator_rewarding_msg], fee, memo)
.await
}
pub(crate) async fn reward_mixnodes_with_single_page_of_delegators(
pub(crate) async fn reward_mixnodes(
&self,
nodes: &[MixnodeToReward],
rewarding_interval_nonce: u32,
@@ -336,25 +249,12 @@ impl<C> Client<C> {
.await;
let msgs: Vec<(ExecuteMsg, _)> = nodes
.iter()
.map(|node| node.to_reward_execute_msg_v2(rewarding_interval_nonce))
.map(|node| node.to_execute_msg(rewarding_interval_nonce))
.zip(std::iter::repeat(Vec::new()))
.collect();
let memo = format!("rewarding {} mixnodes", msgs.len());
self.execute_multiple_with_retry(msgs, fee, memo).await
}
async fn execute_multiple_with_retry<M>(
&self,
msgs: Vec<(M, Vec<CosmosCoin>)>,
fee: Fee,
memo: String,
) -> Result<(), RewardingError>
where
C: SigningCosmWasmClient + Sync,
M: Serialize + Clone + Send,
{
let contract = self
.0
.read()
+140 -245
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use crate::cache::ValidatorCache;
use crate::node_status_api::models::{MixnodeStatusReport, Uptime};
use crate::node_status_api::ONE_DAY;
use crate::nymd_client::Client;
use crate::rewarding::epoch::Epoch;
@@ -12,7 +13,8 @@ use crate::storage::models::{
use crate::storage::ValidatorApiStorage;
use log::{error, info};
use mixnet_contract::mixnode::NodeRewardParams;
use mixnet_contract::{ExecuteMsg, IdentityKey, RewardingStatus, MIXNODE_DELEGATORS_PAGE_LIMIT};
use mixnet_contract::{ExecuteMsg, IdentityKey};
use std::collections::HashMap;
use std::convert::TryInto;
use std::time::Duration;
use time::OffsetDateTime;
@@ -38,39 +40,47 @@ pub(crate) const PER_MIXNODE_DELEGATION_GAS_INCREASE: u64 = 2750;
// the calculated total gas limit is going to get multiplied by that value.
pub(crate) const REWARDING_GAS_LIMIT_MULTIPLIER: f64 = 1.05;
pub(crate) const MAX_TO_REWARD_AT_ONCE: usize = 50;
#[derive(Debug, Clone)]
pub(crate) struct MixnodeToReward {
pub(crate) identity: IdentityKey,
pub(crate) uptime: Uptime,
/// Total number of individual addresses that have delegated to this particular node
pub(crate) total_delegations: usize,
/// Node absolute uptime over total active set uptime
pub(crate) params: NodeRewardParams,
params: Option<NodeRewardParams>,
}
impl MixnodeToReward {
/// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We leverage that to Into a different ExecuteMsg variant
fn params(&self) -> NodeRewardParams {
self.params
/// Somewhat clumsy way of feature gatting tokenomics payments. In a tokenomics scenario this will never be None at reward time. We levarage that to Into a different ExecuteMsg variant
// TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics
#[allow(dead_code)]
fn params(&self) -> Option<NodeRewardParams> {
if cfg!(feature = "tokenomics") {
self.params
} else {
None
}
}
}
impl MixnodeToReward {
pub(crate) fn to_reward_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnodeV2 {
pub(crate) fn to_execute_msg(&self, rewarding_interval_nonce: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnode {
identity: self.identity.clone(),
params: self.params(),
uptime: self.uptime.u8() as u32,
rewarding_interval_nonce,
}
}
pub(crate) fn to_next_delegator_reward_execute_msg_v2(
&self,
rewarding_interval_nonce: u32,
) -> ExecuteMsg {
ExecuteMsg::RewardNextMixDelegators {
mix_identity: self.identity.clone(),
// TODO: to re-integrate in another PR that combines rewarded/active sets with tokenomics
#[allow(dead_code)]
pub(crate) fn to_execute_msg_v2(&self, rewarding_interval_nonce: u32) -> ExecuteMsg {
ExecuteMsg::RewardMixnodeV2 {
identity: self.identity.clone(),
params: self.params().unwrap(),
rewarding_interval_nonce,
}
}
@@ -139,18 +149,56 @@ impl Rewarder {
.len())
}
/// Obtain the list of current 'rewarded' set, determine their uptime in the provided epoch
/// and attach information required for rewarding.
/// Queries the smart contract in order to obtain the current list of bonded mixnodes and then
/// for each mixnode determines how many delegators it has.
async fn produce_active_mixnode_delegators_map(
&self,
) -> Result<HashMap<IdentityKey, usize>, RewardingError> {
// Technically we could optimise it by creating a concurrent stream and executing multiple
// queries concurrently.
//
// I've actually tested that approach and for 5300 nodes running it all sequentially was taking around 19s
// while running it with 20 concurrent queries was taking around 4.5s.
// Note that the results were a bit biased as I was testing it against remote validator
// while in real world this would be making only local requests.
// During the test my average ping times to the machine were around 2.6ms.
// So I guess the network latency was 2.6ms * 5300 = 13.78s in total in the sequential case.
//
// HOWEVER, even if the method was taking that long in real world,
// in the grand scheme of things it makes absolutely no difference. If the rewards
// distribution is delayed by 15s, it changes nothing as the process itself is not
// instantaneous.
let mut map = HashMap::new();
let active_bonded_mixnodes = self.validator_cache.active_mixnodes().await.into_inner();
for mix in active_bonded_mixnodes.into_iter() {
let delegator_count = self
.get_mixnode_delegators_count(mix.mix_node.identity_key.clone())
.await?;
map.insert(mix.mix_node.identity_key, delegator_count);
}
Ok(map)
}
/// Given the list of mixnodes that were tested in the last epoch, tries to determine the
/// subset that are eligible for any rewards.
///
/// As of right now, it is a rather straightforward process. It is checked whether the node
/// is currently bonded, has uptime > 0 and is part of the "active" set.
/// Unlike the typescript rewards script, it currently does not look at the verloc data nor
/// whether the non-mixing ports are open.
///
/// The method also obtains the number of delegators towards the node in order to more accurately
/// approximate the required gas fees when distributing the rewards.
///
/// # Arguments
///
/// * `epoch`: current rewarding epoch
/// * `active_mixnodes`: list of the nodes that were tested at least once by the network monitor
/// in the last epoch.
async fn determine_eligible_mixnodes(
&self,
epoch: Epoch,
active_mixnodes: &[MixnodeStatusReport],
) -> Result<Vec<MixnodeToReward>, RewardingError> {
// Currently we don't have as many 'features' as in the typescript reward script,
// such as we don't check ports or verloc data anymore. However, that's fine as
@@ -158,103 +206,75 @@ impl Rewarder {
// and the lack of port data / verloc data will eventually be balanced out anyway
// by people hesitating to delegate to nodes without them and thus those nodes disappearing
// from the active set (once introduced)
let mixnode_delegators = self.produce_active_mixnode_delegators_map().await?;
let state = self.nymd_client.get_state_params().await?;
let reward_pool = self.nymd_client.get_reward_pool().await?;
let circulating_supply = self.nymd_client.get_circulating_supply().await?;
let sybil_resistance_percent = self.nymd_client.get_sybil_resistance_percent().await?;
let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?;
// 1. go through all active mixnodes
// 2. filter out nodes that are currently not in the active set (as `mixnode_delegators` was obtained by
// querying the validator)
// 3. determine uptime and attach delegators count
let mut eligible_nodes: Vec<MixnodeToReward> = active_mixnodes
.iter()
.filter_map(|mix| {
mixnode_delegators
.get(&mix.identity)
.map(|&total_delegations| MixnodeToReward {
identity: mix.identity.clone(),
uptime: mix.last_day,
total_delegations,
params: None,
})
})
.filter(|node| node.uptime.u8() > 0)
.collect();
// TODO: question to @durch: is k active set or 'rewarded' set?
let k = state.mixnode_active_set_size;
let period_reward_pool = (reward_pool / 100) * epoch_reward_percent as u128;
if cfg!(feature = "tokenomics") {
let reward_pool = self.nymd_client.get_reward_pool().await?;
let circulating_supply = self.nymd_client.get_circulating_supply().await?;
let sybil_resistance_percent = self.nymd_client.get_sybil_resistance_percent().await?;
let epoch_reward_percent = self.nymd_client.get_epoch_reward_percent().await?;
let k = state.mixnode_active_set_size;
let period_reward_pool = (reward_pool / 100) * epoch_reward_percent as u128;
info!("Rewarding pool stats");
info!("-- Reward pool: {} unym", reward_pool);
info!("---- Epoch reward pool: {} unym", period_reward_pool);
info!("-- Circulating supply: {} unym", circulating_supply);
info!("Rewarding pool stats");
info!("-- Reward pool: {} unym", reward_pool);
info!("---- Epoch reward pool: {} unym", period_reward_pool);
info!("-- Circulating supply: {} unym", circulating_supply);
// 1. get list of 'rewarded' nodes
// 2. for each of them determine their delegator count
// 3. for each of them determine their uptime for the epoch
let rewarded_nodes = self.validator_cache.rewarded_mixnodes().await.into_inner();
let mut nodes_with_delegations = Vec::with_capacity(rewarded_nodes.len());
for rewarded_node in rewarded_nodes {
let delegator_count = self
.get_mixnode_delegators_count(rewarded_node.mix_node.identity_key.clone())
.await?;
nodes_with_delegations.push((rewarded_node, delegator_count));
}
let mut eligible_nodes = Vec::with_capacity(nodes_with_delegations.len());
for (rewarded_node, total_delegations) in nodes_with_delegations {
let uptime = self
.storage
.get_average_mixnode_uptime_in_interval(
rewarded_node.identity(),
epoch.start_unix_timestamp(),
epoch.end_unix_timestamp(),
)
.await?;
eligible_nodes.push(MixnodeToReward {
identity: rewarded_node.mix_node.identity_key,
total_delegations,
params: NodeRewardParams::new(
for mix in eligible_nodes.iter_mut() {
mix.params = Some(NodeRewardParams::new(
period_reward_pool,
k.into(),
0,
circulating_supply,
uptime.u8().into(),
mix.uptime.u8().into(),
sybil_resistance_percent,
),
})
));
}
} else {
info!("Tokenomics feature is OFF");
}
Ok(eligible_nodes)
}
/// Check whether every node, and their delegators, on the provided list were fully rewarded
/// in the specified interval.
/// Obtains the lists of all mixnodes that were tested at least a single time
/// by the network monitor in the specified epoch.
///
/// It is used to deal with edge cases such that mixnode had exactly full page of delegations and
/// somebody created a new delegation thus causing the "last" delegator to possibly be pushed
/// onto the next page that the validator API was not aware of.
/// # Arguments
///
/// * `eligible_mixnodes`: list of the nodes that were eligible to receive rewards.
/// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval
async fn verify_rewarding_completion(
/// * `epoch`: the specified epoch.
async fn get_active_monitor_mixnodes(
&self,
eligible_mixnodes: &[MixnodeToReward],
current_rewarding_nonce: u32,
) -> (Vec<MixnodeToReward>, Vec<MixnodeToReward>) {
let mut unrewarded = Vec::new();
let mut further_delegators_present = Vec::new();
for mix in eligible_mixnodes {
match self
.nymd_client
.get_rewarding_status(mix.identity.clone(), current_rewarding_nonce)
.await
{
Ok(rewarding_status) => match rewarding_status.status {
// that case is super weird, it implies the node hasn't been rewarded at all!
// maybe the transaction timed out twice or something? In any case, we should attempt
// the reward for the final time!
None => unrewarded.push(mix.clone()),
Some(RewardingStatus::PendingNextDelegatorPage(_)) => {
further_delegators_present.push(mix.clone())
}
Some(RewardingStatus::Complete(_)) => {}
},
Err(err) => {
error!(
"failed to query rewarding status of {} - {}",
mix.identity, err
)
}
}
}
(unrewarded, further_delegators_present)
epoch: Epoch,
) -> Result<Vec<MixnodeStatusReport>, RewardingError> {
Ok(self
.storage
.get_all_active_mixnode_reports_in_interval(
epoch.start_unix_timestamp(),
epoch.end_unix_timestamp(),
)
.await?)
}
/// Using the list of mixnodes eligible for rewards, chunks it into pre-defined sized-chunks
@@ -270,7 +290,7 @@ impl Rewarder {
///
/// # Arguments
///
/// * `eligible_mixnodes`: list of the nodes that are eligible to receive rewards.
/// * `eligible_mixnodes`: list of the nodes that are eligible to receive non-zero rewards.
/// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval
async fn distribute_rewards_to_mixnodes(
&self,
@@ -279,80 +299,10 @@ impl Rewarder {
) -> Option<Vec<FailedMixnodeRewardChunkDetails>> {
let mut failed_chunks = Vec::new();
// construct chunks such that we reward at most MIXNODE_DELEGATORS_PAGE_LIMIT delegators per block
// nodes with > MIXNODE_DELEGATORS_PAGE_LIMIT delegators that have to be treated in a special way,
// because we cannot batch them together
let mut individually_rewarded = Vec::new();
// sets of nodes that together they have < MIXNODE_DELEGATORS_PAGE_LIMIT delegators
let mut batch_rewarded = vec![vec![]];
let mut current_batch_i = 0;
let mut current_batch_total = 0;
// right now put mixes into batches super naively, if it doesn't fit into the current one,
// create a new one.
for mix in eligible_mixnodes {
// if mixnode has uptime of 0, no rewarding will actually happen regardless of number of delegators,
// so we can just batch it with the current batch
if mix.params.uptime() == 0 {
batch_rewarded[current_batch_i].push(mix.clone());
continue;
}
if mix.total_delegations > MIXNODE_DELEGATORS_PAGE_LIMIT {
individually_rewarded.push(mix)
} else if current_batch_total + mix.total_delegations < MIXNODE_DELEGATORS_PAGE_LIMIT {
batch_rewarded[current_batch_i].push(mix.clone());
current_batch_total += mix.total_delegations;
} else {
batch_rewarded.push(vec![mix.clone()]);
current_batch_i += 1;
current_batch_total = 0;
}
}
let mut total_rewarded = 0;
// start rewarding, first the nodes that are dealt with individually, i.e. nodes that
// need to have their own special blocks due to number of delegators
for mix in individually_rewarded {
for (i, mix_chunk) in eligible_mixnodes.chunks(MAX_TO_REWARD_AT_ONCE).enumerate() {
if let Err(err) = self
.nymd_client
.reward_mixnode_and_all_delegators(mix, rewarding_interval_nonce)
.await
{
// this is a super weird edge case that we didn't catch change to sequence and
// resent rewards unnecessarily, but the mempool saved us from executing it again
// however, still we want to wait until we're sure we're into the next block
if !err.is_tendermint_duplicate() {
error!("failed to reward mixnode with all delegators... - {}", err);
failed_chunks.push(FailedMixnodeRewardChunkDetails {
possibly_unrewarded: vec![mix.clone()],
error_message: err.to_string(),
});
}
sleep(Duration::from_secs(11)).await;
}
total_rewarded += 1;
let percentage = total_rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32;
info!(
"Rewarded {} / {} mixnodes\t{:.2}%",
total_rewarded,
eligible_mixnodes.len(),
percentage
);
}
// then we move onto the chunks
for mix_chunk in batch_rewarded {
if let Err(err) = self
.nymd_client
.reward_mixnodes_with_single_page_of_delegators(
&mix_chunk,
rewarding_interval_nonce,
)
.reward_mixnodes(mix_chunk, rewarding_interval_nonce)
.await
{
// this is a super weird edge case that we didn't catch change to sequence and
@@ -367,12 +317,11 @@ impl Rewarder {
}
sleep(Duration::from_secs(11)).await;
}
total_rewarded += mix_chunk.len();
let percentage = total_rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32;
let rewarded = i * MAX_TO_REWARD_AT_ONCE + mix_chunk.len();
let percentage = rewarded as f32 * 100.0 / eligible_mixnodes.len() as f32;
info!(
"Rewarded {} / {} mixnodes\t{:.2}%",
total_rewarded,
rewarded,
eligible_mixnodes.len(),
percentage
);
@@ -385,53 +334,28 @@ impl Rewarder {
}
}
/// For each mixnode on the list, try to "continue" rewarding its delegators.
/// Note: due to the checks inside the smart contract, it's impossible to accidentally
/// reward the same mixnode (or delegator) twice during particular rewarding interval.
///
/// Realistically if this method is ever called, it will be only done once per node, so there's
/// no need to determine the exact number of missed delegators.
///
/// * `nodes`: mixnodes which delegators did not receive all rewards in this epoch.
/// * `rewarding_interval_nonce`: nonce associated with the current rewarding interval.
async fn reward_missed_delegators(
&self,
nodes: &[MixnodeToReward],
rewarding_interval_nonce: u32,
) {
for missed_node in nodes {
if let Err(err) = self
.nymd_client
.reward_mix_delegators(missed_node, rewarding_interval_nonce)
.await
{
warn!(
"failed to attempt to reward missed delegators of node {} - {}",
missed_node.identity, err
)
}
}
}
/// Using the list of active mixnode and gateways, determine which of them are eligible for
/// rewarding and distribute the rewards.
///
/// # Arguments
///
/// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the database.
/// * `epoch`: current rewarding epoch
/// * `epoch_rewarding_id`: id of the current epoch rewarding as stored in the databse.
///
/// * `active_monitor_mixnodes`: list of the nodes that were tested at least once by the network monitor
/// in the last epoch.
async fn distribute_rewards(
&self,
epoch_rewarding_database_id: i64,
epoch: Epoch,
active_monitor_mixnodes: &[MixnodeStatusReport],
) -> Result<(RewardingReport, Option<FailureData>), RewardingError> {
let mut failure_data = FailureData::default();
let eligible_mixnodes = self.determine_eligible_mixnodes(epoch).await?;
let eligible_mixnodes = self
.determine_eligible_mixnodes(active_monitor_mixnodes)
.await?;
if eligible_mixnodes.is_empty() {
return Err(RewardingError::NoMixnodesToReward);
}
let total_eligible = eligible_mixnodes.len();
let current_rewarding_nonce = self
.nymd_client
@@ -445,43 +369,9 @@ impl Rewarder {
.distribute_rewards_to_mixnodes(&eligible_mixnodes, current_rewarding_nonce + 1)
.await;
let mut nodes_to_verify = eligible_mixnodes;
// if there's some underlying networking error or something, don't keep retrying forever
let mut retries_allowed = 5;
loop {
if retries_allowed <= 0 {
break;
}
let (unrewarded, mut pending_delegators) = self
.verify_rewarding_completion(&nodes_to_verify, current_rewarding_nonce + 1)
.await;
if unrewarded.is_empty() && pending_delegators.is_empty() {
// we're all good - everyone got their rewards
break;
}
if !unrewarded.is_empty() {
// no need to save failure data as we already know about those from the very first run
self.distribute_rewards_to_mixnodes(&unrewarded, current_rewarding_nonce + 1)
.await;
}
if !pending_delegators.is_empty() {
self.reward_missed_delegators(&pending_delegators, current_rewarding_nonce + 1)
.await;
}
// no point in verifying EVERYTHING again, just check the nodes that went through retries
nodes_to_verify = unrewarded;
nodes_to_verify.append(&mut pending_delegators);
retries_allowed -= 1;
}
let report = RewardingReport {
epoch_rewarding_id: epoch_rewarding_database_id,
eligible_mixnodes: total_eligible as i64,
eligible_mixnodes: eligible_mixnodes.len() as i64,
possibly_unrewarded_mixnodes: failure_data
.mixnodes
.as_ref()
@@ -536,7 +426,7 @@ impl Rewarder {
.insert_possibly_unrewarded_mixnode(PossiblyUnrewardedMixnode {
chunk_id,
identity: node.identity,
uptime: node.params.uptime() as u8,
uptime: node.uptime.u8(),
})
.await?;
}
@@ -663,6 +553,9 @@ impl Rewarder {
epoch
);
// get nodes that were active during the epoch
let active_monitor_mixnodes = self.get_active_monitor_mixnodes(epoch).await?;
// insert information about beginning the procedure (so that if we crash during it,
// we wouldn't attempt to possibly double reward operators)
let epoch_rewarding_id = self
@@ -670,7 +563,9 @@ impl Rewarder {
.insert_started_epoch_rewarding(epoch.start_unix_timestamp())
.await?;
let (report, failure_data) = self.distribute_rewards(epoch_rewarding_id, epoch).await?;
let (report, failure_data) = self
.distribute_rewards(epoch_rewarding_id, &active_monitor_mixnodes)
.await?;
self.storage
.finish_rewarding_epoch_and_insert_report(report)
+2 -50
View File
@@ -4,7 +4,7 @@
use crate::network_monitor::monitor::summary_producer::NodeResult;
use crate::network_monitor::test_route::TestRoute;
use crate::node_status_api::models::{
GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory, Uptime,
GatewayStatusReport, GatewayUptimeHistory, MixnodeStatusReport, MixnodeUptimeHistory,
ValidatorApiStorageError,
};
use crate::node_status_api::{ONE_DAY, ONE_HOUR};
@@ -264,54 +264,6 @@ impl ValidatorApiStorage {
))
}
/// Based on the data available in the validator API, determines the average uptime of particular
/// mixnode during the specified time interval.
///
/// # Arguments
///
/// * `identity`: base58-encoded identity of the mixnode.
/// * `since`: unix timestamp indicating the lower bound interval of the selection.
/// * `end`: unix timestamp indicating the upper bound interval of the selection.
pub(crate) async fn get_average_mixnode_uptime_in_interval(
&self,
identity: &str,
start: UnixTimestamp,
end: UnixTimestamp,
) -> Result<Uptime, ValidatorApiStorageError> {
let mixnode_database_id = match self
.manager
.get_mixnode_id(identity)
.await
.map_err(|_| ValidatorApiStorageError::InternalDatabaseError)?
{
Some(id) => id,
None => return Ok(Uptime::zero()),
};
let monitor_runs = self.get_monitor_runs_count(start, end).await?;
let mixnode_statuses = self
.manager
.get_mixnode_statuses_by_id(mixnode_database_id, start, end)
.await
.map_err(|_| ValidatorApiStorageError::InternalDatabaseError)?;
let mut total: f32 = 0.0;
for mixnode_status in mixnode_statuses {
total += mixnode_status.reliability as f32;
}
let uptime = match Uptime::from_uptime_sum(total, monitor_runs) {
Ok(uptime) => uptime,
Err(_) => {
// this should really ever happen...
error!("mixnode {} has uptime > 100!", identity);
Uptime::default()
}
};
Ok(uptime)
}
/// Obtain status reports of mixnodes that were active in the specified time interval.
///
/// # Arguments
@@ -681,7 +633,7 @@ impl ValidatorApiStorage {
}
////////////////////////////////////////////////////////////////////////
// TODO: Should all of the below really return a "ValidatorApiStorageError" Errors?
// TODO: Should all of the below really return a "NodeStatusApi" Errors?
////////////////////////////////////////////////////////////////////////
/// Inserts information about starting new epoch rewarding into the database.
@@ -11,9 +11,12 @@ import DelegateForm from './DelegateForm'
import { Coin } from '@cosmjs/launchpad'
import { UDENOM } from '../../pages/_app'
import { theme } from '../../lib/theme'
import { makeBasicStyle } from '../../common/helpers'
import NodeTypeChooser from '../NodeTypeChooser'
import ExecFeeNotice from '../ExecFeeNotice'
const DelegateToNode = () => {
const classes = makeBasicStyle(theme)
const router = useRouter()
const { client } = useContext(ValidatorClientContext)
@@ -76,7 +79,12 @@ const DelegateToNode = () => {
// we haven't clicked delegate button yet
if (isLoading === undefined) {
return <DelegateForm onSubmit={delegateToNode} />
return (
<>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<DelegateForm onSubmit={delegateToNode} />
</>
)
}
// We started delegation
@@ -6,11 +6,13 @@ import { NodeType } from '../../common/node'
import NoClientError from '../NoClientError'
import Confirmation from '../Confirmation'
import { theme } from '../../lib/theme'
import { makeBasicStyle } from '../../common/helpers'
import NodeTypeChooser from '../NodeTypeChooser'
import NodeIdentityForm from '../NodeIdentityForm'
import ExecFeeNotice from '../ExecFeeNotice'
const UndelegateFromNode = () => {
const classes = makeBasicStyle(theme)
const router = useRouter()
const { client } = useContext(ValidatorClientContext)
@@ -64,10 +66,13 @@ const UndelegateFromNode = () => {
// we haven't clicked undelegate button yet
if (isLoading === undefined) {
return (
<NodeIdentityForm
onSubmit={undelegateFromNode}
buttonText={'Remove delegation'}
/>
<>
<NodeTypeChooser nodeType={nodeType} setNodeType={setNodeType} />
<NodeIdentityForm
onSubmit={undelegateFromNode}
buttonText={'Remove delegation'}
/>
</>
)
}
+1 -1
View File
@@ -8,7 +8,7 @@ const DelegateStake = () => {
<>
<MainNav />
<Layout>
<NymCard title="Delegate" subheader="Delegate to Mixnode">
<NymCard title="Delegate" subheader="Delegate to Mixnode or Gateway">
<NodeDelegation />
</NymCard>
</Layout>
+4 -1
View File
@@ -8,7 +8,10 @@ const UndelegateStake = () => {
<>
<MainNav />
<Layout>
<NymCard title="Undelegate" subheader="Undelegate from a Mixnode">
<NymCard
title="Undelegate"
subheader="Undelegate from a Mixnode or Gateway"
>
<NodeUndelegation />
</NymCard>
</Layout>