merge develop

This commit is contained in:
fmtabbara
2022-03-12 22:17:33 +00:00
parent d4286aeb76
commit 51bced8766
336 changed files with 7605 additions and 45396 deletions
+4 -4
View File
@@ -33,13 +33,13 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --all
args: --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features
args: --workspace --all-features
- name: Check formatting
uses: actions-rs/cargo@v1
@@ -63,13 +63,13 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --all --features=coconut
args: --workspace --features=coconut
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --all --features=coconut
args: --workspace --features=coconut
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
+2 -2
View File
@@ -45,7 +45,7 @@ jobs:
RUSTFLAGS: '-C link-arg=-s'
with:
command: build
args: --manifest-path contracts/Cargo.toml --all --target wasm32-unknown-unknown
args: --manifest-path contracts/Cargo.toml --workspace --target wasm32-unknown-unknown
- uses: actions-rs/cargo@v1
with:
@@ -61,4 +61,4 @@ jobs:
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --manifest-path contracts/Cargo.toml --all -- -D warnings
args: --manifest-path contracts/Cargo.toml --workspace -- -D warnings
+4 -3
View File
@@ -16,8 +16,9 @@ jobs:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
node-version: '16'
- name: Setup yarn
run: npm install -g yarn
- name: Run ESLint
# GitHub should automatically annotate the PR
run: npm run lint
run: yarn && yarn lint
+8 -5
View File
@@ -19,14 +19,17 @@ jobs:
- uses: rlespinasse/github-slug-action@v3.x
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
node-version: '16'
- name: Setup yarn
run: npm install -g yarn
continue-on-error: true
- name: Build shared packages
run: cd .. && yarn && yarn build
- name: Set environment from the example
run: cp .env.prod .env
- run: npm run test
continue-on-error: true
- run: npm run build
# - run: yarn test
# continue-on-error: true
- run: yarn && yarn build
continue-on-error: true
- name: Deploy branch to CI www
continue-on-error: true
+6 -6
View File
@@ -42,13 +42,13 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --all
args: --workspace
- name: Run all tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all
args: --workspace
- name: Check formatting
uses: actions-rs/cargo@v1
@@ -73,7 +73,7 @@ jobs:
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: -- -D warnings
args: --workspace --all-targets -- -D warnings
# COCONUT stuff
@@ -81,20 +81,20 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: build
args: --all --features=coconut
args: --workspace --features=coconut
- name: Run all tests with coconut enabled
uses: actions-rs/cargo@v1
with:
command: test
args: --all --features=coconut
args: --workspace --features=coconut
- name: Run clippy with coconut enabled
uses: actions-rs/cargo@v1
if: ${{ matrix.rust != 'nightly' }}
with:
command: clippy
args: --features=coconut -- -D warnings
args: --workspace --all-targets --features=coconut -- -D warnings
notification:
needs: build
runs-on: ubuntu-latest
-32
View File
@@ -1,32 +0,0 @@
name: Generate TS types
on:
push:
paths-ignore:
- "explorer/**"
pull_request:
paths-ignore:
- "explorer/**"
jobs:
nym-wallet-types:
runs-on: [ self-hosted, custom-linux-exoscale ]
# Enable sccache
env:
RUSTC_WRAPPER: /home/ubuntu/.cargo/bin/sccache
if: ${{ github.event_name != 'pull_request' }}
steps:
- name: Prepare
run: sudo apt-get update && sudo apt-get install -y libpango1.0-dev libatk1.0-dev libgdk-pixbuf2.0-dev libsoup2.4-dev librust-gdk-dev libwebkit2gtk-4.0-dev
- uses: actions/checkout@v2
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
- name: Generate TS
run: cd nym-wallet/src-tauri && cargo test
- uses: EndBug/add-and-commit@v7.2.1 # https://github.com/marketplace/actions/add-commit
with:
add: '["nym-wallet"]'
message: "[ci skip] Generate TS types"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Generated
+14 -2
View File
@@ -823,6 +823,7 @@ dependencies = [
name = "credentials"
version = "0.1.0"
dependencies = [
"bls12_381",
"coconut-interface",
"crypto",
"network-defaults",
@@ -1077,6 +1078,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "darling"
version = "0.13.1"
@@ -6046,7 +6058,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"mixnet-contract-common",
"schemars",
"serde",
@@ -6060,7 +6072,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.11.1",
"mixnet-contract-common",
"schemars",
"serde",
+7 -7
View File
@@ -16,16 +16,16 @@ clippy-happy-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml
clippy-all-main:
cargo clippy --all-features -- -D warnings
cargo clippy --workspace --all-features -- -D warnings
clippy-all-contracts:
cargo clippy --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
cargo clippy --workspace --manifest-path contracts/Cargo.toml --all-features --target wasm32-unknown-unknown -- -D warnings
clippy-all-wallet:
cargo clippy --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
cargo clippy --workspace --manifest-path nym-wallet/Cargo.toml --all-features -- -D warnings
test-main:
cargo test --all-features --all
cargo test --all-features --workspace
test-contracts:
cargo test --manifest-path contracts/Cargo.toml --all-features
@@ -34,13 +34,13 @@ test-wallet:
cargo test --manifest-path nym-wallet/Cargo.toml --all-features
build-main:
cargo build --all
cargo build --workspace
build-contracts:
cargo build --manifest-path contracts/Cargo.toml --all
cargo build --manifest-path contracts/Cargo.toml --workspace
build-wallet:
cargo build --manifest-path nym-wallet/Cargo.toml --all
cargo build --manifest-path nym-wallet/Cargo.toml --workspace
fmt-main:
cargo fmt --all
Binary file not shown.

After

Width:  |  Height:  |  Size: 545 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 48 KiB

+5 -8
View File
@@ -1,13 +1,10 @@
<svg width="300" height="300" viewBox="0 0 296 296" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M148 296C229.738 296 296 229.738 296 148C296 66.2619 229.738 0 148 0C66.2619 0 0 66.2619 0 148C0 229.738 66.2619 296 148 296Z" fill="url(#paint0_linear_113_1244)"/>
<path d="M148 285.875C224.147 285.875 285.875 224.146 285.875 148C285.875 71.8536 224.147 10.1248 148 10.1248C71.8538 10.1248 10.125 71.8536 10.125 148C10.125 224.146 71.8538 285.875 148 285.875Z" fill="#121725"/>
<path d="M88.8829 120.143H88.7169V120.281V168.637L68.3289 120.226L68.3012 120.143H68.1905H56.6272H43.653H43.5146V120.281V175.719V175.857H43.653H56.6272H56.7655V175.719V127.28L77.2365 175.774L77.2642 175.857H77.3748H88.8829H101.829H101.968V175.719V120.281V120.143H101.829H88.8829Z" fill="white"/>
<path d="M252.347 120.143H227.616H227.477L227.45 120.253L214.78 168.858L202.082 120.253L202.054 120.143H201.944H177.157H176.991V120.281V175.719V175.857H177.157H190.104H190.242V175.719V127.667L202.774 175.747L202.801 175.857H202.94H226.564H226.675L226.703 175.747L239.234 127.667V175.719V175.857H239.373H252.347H252.485V175.719V120.281V120.143H252.347Z" fill="white"/>
<path d="M155.663 120.143H155.58L155.552 120.198L139.812 147.557L123.988 120.198L123.96 120.143H123.877H108.911H108.635L108.773 120.364L133.145 162.579V175.719V175.857H133.283H146.257H146.396V175.719V162.579L170.767 120.364L170.905 120.143H170.629H155.663Z" fill="white"/>
<svg width="64" height="64" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M40 78.5C61.263 78.5 78.5 61.263 78.5 40C78.5 18.737 61.263 1.5 40 1.5C18.737 1.5 1.5 18.737 1.5 40C1.5 61.263 18.737 78.5 40 78.5Z" fill="#070B15" stroke="url(#paint0_linear_0_1)" stroke-width="3"/>
<path d="M31.4894 27.56L41.8623 56H48.5106H56V24H48.5106V52.4L38.1777 24H31.4894H24V56H31.4894V27.56Z" fill="white"/>
<defs>
<linearGradient id="paint0_linear_113_1244" x1="0" y1="148" x2="296" y2="148" gradientUnits="userSpaceOnUse">
<linearGradient id="paint0_linear_0_1" x1="0.839161" y1="80" x2="80" y2="80" gradientUnits="userSpaceOnUse">
<stop offset="0.09375" stop-color="#FB6E4E"/>
<stop offset="1" stop-color="#FC1D60"/>
<stop offset="1" stop-color="#F51473"/>
</linearGradient>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.6 KiB

After

Width:  |  Height:  |  Size: 672 B

@@ -92,7 +92,7 @@ impl Config {
#[cfg(feature = "nymd-client")]
pub struct Client<C> {
network: network_defaults::all::Network,
pub network: network_defaults::all::Network,
mixnet_contract_address: Option<cosmrs::AccountId>,
vesting_contract_address: Option<cosmrs::AccountId>,
erc20_bridge_contract_address: Option<cosmrs::AccountId>,
@@ -283,6 +283,13 @@ impl<C> Client<C> {
Ok(self.nymd.get_current_interval().await?)
}
pub async fn get_epochs_in_interval(&self) -> Result<u64, ValidatorClientError>
where
C: CosmWasmClient + Sync,
{
Ok(self.nymd.get_epochs_in_interval().await?)
}
pub async fn get_circulating_supply(&self) -> Result<u128, ValidatorClientError>
where
C: CosmWasmClient + Sync,
@@ -21,7 +21,7 @@ where
}
// maybe the wallet could be made into a generic, but for now, let's just have this one implementation
pub fn connect_with_signer<U>(
pub fn connect_with_signer<U: Clone>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
@@ -641,7 +641,7 @@ pub struct Client {
}
impl Client {
pub fn connect_with_signer<U>(
pub fn connect_with_signer<U: Clone>(
endpoint: U,
signer: DirectSecp256k1HdWallet,
gas_price: GasPrice,
@@ -47,9 +47,12 @@ pub enum Operation {
CreatePeriodicVestingAccount,
AdvanceCurrentInterval,
AdvanceCurrentEpoch,
WriteRewardedSet,
ClearRewardedSet,
UpdateMixnetAddress,
CheckpointMixnodes,
ReconcileDelegations,
}
pub(crate) fn calculate_fee(gas_price: &GasPrice, gas_limit: Gas) -> Coin {
@@ -91,6 +94,9 @@ impl fmt::Display for Operation {
Operation::WriteRewardedSet => f.write_str("WriteRewardedSet"),
Operation::ClearRewardedSet => f.write_str("ClearRewardedSet"),
Operation::UpdateMixnetAddress => f.write_str("UpdateMixnetAddress"),
Operation::CheckpointMixnodes => f.write_str("CheckpointMixnodes"),
Operation::ReconcileDelegations => f.write_str("ReconcileDelegations"),
Operation::AdvanceCurrentEpoch => f.write_str("AdvanceCurrentEpoch"),
}
}
}
@@ -132,6 +138,9 @@ impl Operation {
Operation::WriteRewardedSet => 175_000u64.into(),
Operation::ClearRewardedSet => 175_000u64.into(),
Operation::UpdateMixnetAddress => 80_000u64.into(),
Operation::CheckpointMixnodes => 175_000u64.into(),
Operation::ReconcileDelegations => 500_000u64.into(),
Operation::AdvanceCurrentEpoch => 175_000u64.into(),
}
}
@@ -9,7 +9,8 @@ use crate::nymd::cosmwasm_client::types::{
use crate::nymd::error::NymdError;
use crate::nymd::wallet::DirectSecp256k1HdWallet;
use cosmrs::rpc::endpoint::broadcast;
use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl};
use cosmrs::rpc::Error as TendermintRpcError;
use cosmrs::rpc::HttpClientUrl;
use cosmwasm_std::{Coin, Uint128};
pub use fee::gas_price::GasPrice;
use fee::helpers::Operation;
@@ -82,7 +83,7 @@ impl NymdClient<QueryNymdClient> {
impl NymdClient<SigningNymdClient> {
// maybe the wallet could be made into a generic, but for now, let's just have this one implementation
pub fn connect_with_signer<U>(
pub fn connect_with_signer<U: Clone>(
network: config::defaults::all::Network,
endpoint: U,
mixnet_contract_address: Option<AccountId>,
@@ -113,7 +114,7 @@ impl NymdClient<SigningNymdClient> {
})
}
pub fn connect_with_mnemonic<U>(
pub fn connect_with_mnemonic<U: Clone>(
network: config::defaults::all::Network,
endpoint: U,
mixnet_contract_address: Option<AccountId>,
@@ -388,6 +389,16 @@ impl<C> NymdClient<C> {
.await
}
pub async fn get_epochs_in_interval(&self) -> Result<u64, NymdError>
where
C: CosmWasmClient + Sync,
{
let request = QueryMsg::GetEpochsInInterval {};
self.client
.query_contract_smart(self.mixnet_contract_address()?, &request)
.await
}
pub async fn get_circulating_supply(&self) -> Result<Uint128, NymdError>
where
C: CosmWasmClient + Sync,
@@ -496,7 +507,7 @@ impl<C> NymdClient<C> {
pub async fn get_mix_delegations_paged(
&self,
mix_identity: IdentityKey,
start_after: Option<String>,
start_after: Option<(String, u64)>,
page_limit: Option<u32>,
) -> Result<PagedMixDelegationsResponse, NymdError>
where
@@ -515,7 +526,7 @@ impl<C> NymdClient<C> {
/// Gets list of all mixnode delegations on particular page.
pub async fn get_all_network_delegations_paged(
&self,
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, Vec<u8>, u64)>,
page_limit: Option<u32>,
) -> Result<PagedAllDelegationsResponse, NymdError>
where
@@ -1175,6 +1186,25 @@ impl<C> NymdClient<C> {
.await
}
pub async fn advance_current_epoch(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::AdvanceCurrentEpoch);
let req = ExecuteMsg::AdvanceCurrentEpoch {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Advance current epoch",
Vec::new(),
)
.await
}
pub async fn advance_current_interval(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
@@ -1194,6 +1224,44 @@ impl<C> NymdClient<C> {
.await
}
pub async fn reconcile_delegations(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::ReconcileDelegations);
let req = ExecuteMsg::ReconcileDelegations {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Reconciling delegation events",
Vec::new(),
)
.await
}
pub async fn checkpoint_mixnodes(&self) -> Result<ExecuteResult, NymdError>
where
C: SigningCosmWasmClient + Sync,
{
let fee = self.operation_fee(Operation::CheckpointMixnodes);
let req = ExecuteMsg::CheckpointMixnodes {};
self.client
.execute(
self.address(),
self.mixnet_contract_address()?,
&req,
fee,
"Snapshotting mixnodes",
Vec::new(),
)
.await
}
pub async fn write_rewarded_set(
&self,
rewarded_set: Vec<IdentityKey>,
-4
View File
@@ -83,8 +83,6 @@ impl VerifyCredentialBody {
pub struct BlindSignRequestBody {
#[getset(get = "pub")]
blind_sign_request: BlindSignRequest,
#[getset(get = "pub")]
public_key: nymcoconut::PublicKey,
public_attributes: Vec<String>,
#[getset(get = "pub")]
total_params: u32,
@@ -93,13 +91,11 @@ pub struct BlindSignRequestBody {
impl BlindSignRequestBody {
pub fn new(
blind_sign_request: &BlindSignRequest,
public_key: &nymcoconut::PublicKey,
public_attributes: &[Attribute],
total_params: u32,
) -> BlindSignRequestBody {
BlindSignRequestBody {
blind_sign_request: blind_sign_request.clone(),
public_key: public_key.clone(),
public_attributes: public_attributes
.iter()
.map(|attr| attr.to_bs58())
@@ -37,8 +37,12 @@ impl Delegation {
}
// TODO: change that to use .joined_key() and return Vec<u8>
pub fn storage_key(&self) -> (IdentityKey, Addr) {
(self.node_identity(), self.owner())
pub fn storage_key(&self) -> (IdentityKey, Vec<u8>, u64) {
(
self.node_identity(),
self.owner().as_bytes().to_vec(),
self.block_height(),
)
}
pub fn increment_amount(&mut self, amount: Uint128, at_height: Option<u64>) {
@@ -78,14 +82,14 @@ impl Display for Delegation {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedMixDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<String>,
pub start_next_after: Option<(String, u64)>,
}
impl PagedMixDelegationsResponse {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<Addr>) -> Self {
pub fn new(delegations: Vec<Delegation>, start_next_after: Option<(Addr, u64)>) -> Self {
PagedMixDelegationsResponse {
delegations,
start_next_after: start_next_after.map(|s| s.to_string()),
start_next_after: start_next_after.map(|(s, h)| (s.to_string(), h)),
}
}
}
@@ -108,17 +112,17 @@ impl PagedDelegatorDelegationsResponse {
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
pub struct PagedAllDelegationsResponse {
pub delegations: Vec<Delegation>,
pub start_next_after: Option<(IdentityKey, String)>,
pub start_next_after: Option<(IdentityKey, Vec<u8>, u64)>,
}
impl PagedAllDelegationsResponse {
pub fn new(
delegations: Vec<Delegation>,
start_next_after: Option<(IdentityKey, Addr)>,
start_next_after: Option<(IdentityKey, Vec<u8>, u64)>,
) -> Self {
PagedAllDelegationsResponse {
delegations,
start_next_after: start_next_after.map(|(id, addr)| (id, addr.to_string())),
start_next_after: start_next_after.map(|(id, addr, height)| (id, addr, height)),
}
}
}
@@ -6,4 +6,11 @@ pub enum MixnetContractError {
OverflowError(#[from] cosmwasm_std::OverflowError),
#[error("reward_blockstamp field not set, set_reward_blockstamp must be called before attempting to issue rewards")]
BlockstampNotSet,
#[error("{source}")]
TryFromIntError {
#[from]
source: std::num::TryFromIntError,
},
#[error("Error casting from U128")]
CastError,
}
@@ -2,14 +2,16 @@
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::NodeRewardResult;
use crate::{ContractStateParams, Delegation, IdentityKeyRef, Interval, Layer};
use crate::{ContractStateParams, IdentityKeyRef, Interval, Layer};
use cosmwasm_std::{Addr, Coin, Event, Uint128};
pub use contracts_common::events::*;
// FIXME: This should becoma an Enum
// event types
pub const DELEGATION_EVENT_TYPE: &str = "delegation";
pub const PENDING_DELEGATION_EVENT_TYPE: &str = "pending_delegation";
pub const UNDELEGATION_EVENT_TYPE: &str = "undelegation";
pub const PENDING_UNDELEGATION_EVENT_TYPE: &str = "pending_undelegation";
pub const GATEWAY_BONDING_EVENT_TYPE: &str = "gateway_bonding";
pub const GATEWAY_UNBONDING_EVENT_TYPE: &str = "gateway_unbonding";
pub const MIXNODE_BONDING_EVENT_TYPE: &str = "mixnode_bonding";
@@ -19,6 +21,10 @@ pub const OPERATOR_REWARDING_EVENT_TYPE: &str = "mix_rewarding";
pub const MIX_DELEGATORS_REWARDING_EVENT_TYPE: &str = "mix_delegators_rewarding";
pub const CHANGE_REWARDED_SET_EVENT_TYPE: &str = "change_rewarded_set";
pub const ADVANCE_INTERVAL_EVENT_TYPE: &str = "advance_interval";
pub const ADVANCE_EPOCH_EVENT_TYPE: &str = "advance_epoch";
pub const COMPOUND_DELEGATOR_REWARD_EVENT_TYPE: &str = "compound_delegator_reward";
pub const COMPOUND_OPERATOR_REWARD_EVENT_TYPE: &str = "compound_operator_reward";
pub const SNAPSHOT_MIXNODES_EVENT: &str = "snapshot_mixnodes";
// attributes that are used in multiple places
pub const OWNER_KEY: &str = "owner";
@@ -70,6 +76,14 @@ pub const NODES_IN_REWARDED_SET_KEY: &str = "nodes_in_rewarded_set";
pub const CURRENT_INTERVAL_ID_KEY: &str = "current_interval";
pub const NEW_CURRENT_INTERVAL_KEY: &str = "new_current_interval";
pub const NEW_CURRENT_EPOCH_KEY: &str = "new_current_epoch";
pub const BLOCK_HEIGHT_KEY: &str = "block_height";
pub const CHECKPOINT_MIXNODES_EVENT: &str = "checkpoint_mixnodes";
pub fn new_checkpoint_mixnodes_event(block_height: u64) -> Event {
Event::new(CHECKPOINT_MIXNODES_EVENT)
.add_attribute(BLOCK_HEIGHT_KEY, format!("{}", block_height))
}
pub fn new_delegation_event(
delegator: &Addr,
@@ -89,11 +103,55 @@ pub fn new_delegation_event(
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_pending_delegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
amount: &Coin,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(PENDING_DELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_compound_operator_reward_event(owner: &Addr, amount: Uint128) -> Event {
let event = Event::new(COMPOUND_OPERATOR_REWARD_EVENT_TYPE).add_attribute(OWNER_KEY, owner);
event.add_attribute(AMOUNT_KEY, amount.to_string())
}
pub fn new_compound_delegator_reward_event(
delegator: &Addr,
proxy: &Option<Addr>,
amount: Uint128,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(COMPOUND_DELEGATOR_REWARD_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
.add_attribute(DELEGATOR_KEY, delegator)
}
pub fn new_undelegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
old_delegation: &Delegation,
mix_identity: IdentityKeyRef<'_>,
amount: Uint128,
) -> Event {
let mut event = Event::new(UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
@@ -103,14 +161,26 @@ pub fn new_undelegation_event(
// coin implements Display trait and we use that implementation here
event
.add_attribute(AMOUNT_KEY, old_delegation.amount.to_string())
.add_attribute(
DELEGATION_HEIGHT_KEY,
old_delegation.block_height.to_string(),
)
.add_attribute(AMOUNT_KEY, amount.to_string())
.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_pending_undelegation_event(
delegator: &Addr,
proxy: &Option<Addr>,
mix_identity: IdentityKeyRef<'_>,
) -> Event {
let mut event =
Event::new(PENDING_UNDELEGATION_EVENT_TYPE).add_attribute(DELEGATOR_KEY, delegator);
if let Some(proxy) = proxy {
event = event.add_attribute(PROXY_KEY, proxy)
}
// coin implements Display trait and we use that implementation here
event.add_attribute(DELEGATION_TARGET_KEY, mix_identity)
}
pub fn new_gateway_bonding_event(
owner: &Addr,
proxy: &Option<Addr>,
@@ -280,9 +350,6 @@ pub fn new_mix_operator_rewarding_event(
node_reward_result: NodeRewardResult,
node_pledge: Uint128,
node_delegation: Uint128,
operator_reward: Uint128,
delegation_rewards_distributed: Uint128,
further_delegations: bool,
) -> Event {
Event::new(OPERATOR_REWARDING_EVENT_TYPE)
.add_attribute(INTERVAL_ID_KEY, interval_id.to_string())
@@ -295,15 +362,6 @@ pub fn new_mix_operator_rewarding_event(
)
.add_attribute(LAMBDA_KEY, node_reward_result.lambda().to_string())
.add_attribute(SIGMA_KEY, node_reward_result.sigma().to_string())
.add_attribute(OPERATOR_REWARD_KEY, operator_reward)
.add_attribute(
DISTRIBUTED_DELEGATION_REWARDS_KEY,
delegation_rewards_distributed,
)
.add_attribute(
FURTHER_DELEGATIONS_TO_REWARD_KEY,
further_delegations.to_string(),
)
}
pub fn new_mix_delegators_rewarding_event(
@@ -343,3 +401,7 @@ pub fn new_advance_interval_event(interval: Interval) -> Event {
Event::new(ADVANCE_INTERVAL_EVENT_TYPE)
.add_attribute(NEW_CURRENT_INTERVAL_KEY, interval.to_string())
}
pub fn new_advance_epoch_event(interval: Interval) -> Event {
Event::new(ADVANCE_EPOCH_EVENT_TYPE).add_attribute(NEW_CURRENT_EPOCH_KEY, interval.to_string())
}
@@ -72,7 +72,7 @@ impl Interval {
/// Returns the next interval.
#[must_use]
pub fn next_interval(&self) -> Self {
pub fn next(&self) -> Self {
Interval {
id: self.id + 1,
start: self.end(),
@@ -81,7 +81,7 @@ impl Interval {
}
/// Returns the last interval.
pub fn previous_interval(&self) -> Option<Self> {
pub fn previous(&self) -> Option<Self> {
if self.id > 0 {
Some(Interval {
id: self.id - 1,
@@ -125,14 +125,14 @@ impl Interval {
if candidate.contains(now) {
return Some(candidate);
}
candidate = candidate.next_interval();
candidate = candidate.next();
}
} else {
loop {
if candidate.contains(now) {
return Some(candidate);
}
candidate = candidate.previous_interval()?;
candidate = candidate.previous()?;
}
}
}
@@ -151,14 +151,14 @@ impl Interval {
if candidate.contains_timestamp(now_unix) {
return Some(candidate);
}
candidate = candidate.next_interval();
candidate = candidate.next();
}
} else {
loop {
if candidate.contains_timestamp(now_unix) {
return Some(candidate);
}
candidate = candidate.previous_interval()?;
candidate = candidate.previous()?;
}
}
}
@@ -239,7 +239,7 @@ mod tests {
use super::*;
#[test]
fn previous_interval() {
fn previous() {
let interval = Interval {
id: 1,
start: time::macros::datetime!(2021-08-23 12:00 UTC),
@@ -250,18 +250,18 @@ mod tests {
start: time::macros::datetime!(2021-08-22 12:00 UTC),
length: Duration::from_secs(24 * 60 * 60),
};
assert_eq!(expected, interval.previous_interval().unwrap());
assert_eq!(expected, interval.previous().unwrap());
let genesis_interval = Interval {
id: 0,
start: time::macros::datetime!(2021-08-23 12:00 UTC),
length: Duration::from_secs(24 * 60 * 60),
};
assert!(genesis_interval.previous_interval().is_none());
assert!(genesis_interval.previous().is_none());
}
#[test]
fn next_interval() {
fn next() {
let interval = Interval {
id: 0,
start: time::macros::datetime!(2021-08-23 12:00 UTC),
@@ -273,7 +273,7 @@ mod tests {
length: Duration::from_secs(24 * 60 * 60),
};
assert_eq!(expected, interval.next_interval())
assert_eq!(expected, interval.next())
}
#[test]
@@ -291,8 +291,8 @@ mod tests {
let in_the_midle = interval.start + Duration::from_secs(interval.length.as_secs() / 2);
assert!(interval.contains(in_the_midle));
assert!(!interval.contains(interval.next_interval().end()));
assert!(!interval.contains(interval.previous_interval().unwrap().start()));
assert!(!interval.contains(interval.next().end()));
assert!(!interval.contains(interval.previous().unwrap().start()));
}
#[test]
@@ -305,10 +305,7 @@ mod tests {
// interval just before
let fake_now = first_interval.start - Duration::from_secs(123);
assert_eq!(
first_interval.previous_interval(),
first_interval.current(fake_now)
);
assert_eq!(first_interval.previous(), first_interval.current(fake_now));
// this interval (start boundary)
assert_eq!(
@@ -329,7 +326,7 @@ mod tests {
// next interval
let fake_now = first_interval.end() + Duration::from_secs(123);
assert_eq!(
first_interval.next_interval(),
first_interval.next(),
first_interval.current(fake_now).unwrap()
);
@@ -340,11 +337,11 @@ mod tests {
- first_interval.length;
assert_eq!(
first_interval
.previous_interval()
.previous()
.unwrap()
.previous_interval()
.previous()
.unwrap()
.previous_interval()
.previous()
.unwrap(),
first_interval.current(fake_now).unwrap()
);
@@ -355,10 +352,7 @@ mod tests {
+ first_interval.length
+ first_interval.length;
assert_eq!(
first_interval
.next_interval()
.next_interval()
.next_interval(),
first_interval.next().next().next(),
first_interval.current(fake_now).unwrap()
);
}
@@ -8,6 +8,7 @@ mod gateway;
mod interval;
pub mod mixnode;
mod msg;
pub mod reward_params;
mod types;
pub const MIXNODE_DELEGATORS_PAGE_LIMIT: usize = 250;
@@ -24,3 +25,9 @@ pub use mixnode::{
};
pub use msg::*;
pub use types::*;
pub type U128 = fixed::types::U75F53;
fixed::const_fixed_from_int! {
const ONE: U128 = 1;
}
@@ -1,23 +1,19 @@
// due to code generated by JsonSchema
#![allow(clippy::field_reassign_with_default)]
use crate::{IdentityKey, SphinxKey};
use crate::error::MixnetContractError;
use crate::reward_params::RewardParams;
use crate::{Delegation, IdentityKey, SphinxKey};
use crate::{ONE, U128};
use az::CheckedCast;
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use log::error;
use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use serde_repr::{Deserialize_repr, Serialize_repr};
use std::cmp::Ordering;
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(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
@@ -26,7 +22,7 @@ fixed::const_fixed_from_int! {
export_to = "../../../nym-wallet/src/types/rust/rewardedsetnodestatus.ts"
)
)]
#[derive(Clone, Copy, Debug, Deserialize, PartialEq, PartialOrd, Serialize, JsonSchema)]
#[derive(Clone, Copy, Debug, Deserialize, Serialize, JsonSchema, PartialEq)]
pub enum RewardedSetNodeStatus {
Active,
Standby,
@@ -38,6 +34,52 @@ impl RewardedSetNodeStatus {
}
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub enum DelegationEvent {
Delegate(Delegation),
Undelegate(PendingUndelegate),
}
#[derive(Clone, Debug, Deserialize, Serialize, JsonSchema)]
pub struct PendingUndelegate {
mix_identity: IdentityKey,
delegate: Addr,
proxy: Option<Addr>,
block_height: u64,
}
impl PendingUndelegate {
pub fn new(
mix_identity: IdentityKey,
delegate: Addr,
proxy: Option<Addr>,
block_height: u64,
) -> Self {
Self {
mix_identity,
delegate,
proxy,
block_height,
}
}
pub fn mix_identity(&self) -> IdentityKey {
self.mix_identity.clone()
}
pub fn delegate(&self) -> Addr {
self.delegate.clone()
}
pub fn proxy(&self) -> Option<Addr> {
self.proxy.clone()
}
pub fn block_height(&self) -> u64 {
self.block_height
}
}
#[cfg_attr(test, derive(ts_rs::TS))]
#[cfg_attr(
test,
@@ -87,112 +129,6 @@ impl From<Layer> for String {
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeRewardParams {
period_reward_pool: Uint128,
rewarded_set_size: Uint128,
active_set_size: Uint128,
reward_blockstamp: u64,
circulating_supply: Uint128,
uptime: Uint128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
}
impl NodeRewardParams {
#[allow(clippy::too_many_arguments)]
pub fn new(
period_reward_pool: u128,
rewarded_set_size: u128,
active_set_size: u128,
reward_blockstamp: u64,
circulating_supply: u128,
uptime: u128,
sybil_resistance_percent: u8,
in_active_set: bool,
active_set_work_factor: u8,
) -> NodeRewardParams {
NodeRewardParams {
period_reward_pool: Uint128::new(period_reward_pool),
rewarded_set_size: Uint128::new(rewarded_set_size),
active_set_size: Uint128::new(active_set_size),
reward_blockstamp,
circulating_supply: Uint128::new(circulating_supply),
uptime: Uint128::new(uptime),
sybil_resistance_percent,
in_active_set,
active_set_work_factor,
}
}
pub fn omega(&self) -> U128 {
// As per keybase://chat/nymtech#tokeneconomics/1179
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
if self.in_active_set() {
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
self.active_set_work_factor() / denom * self.rewarded_set_size()
} else {
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
ONE / denom * self.rewarded_set_size()
}
}
pub fn idle_nodes(&self) -> Uint128 {
self.rewarded_set_size - self.active_set_size
}
pub fn active_set_work_factor(&self) -> U128 {
U128::from_num(self.active_set_work_factor)
}
pub fn in_active_set(&self) -> bool {
self.in_active_set
}
pub fn performance(&self) -> U128 {
U128::from_num(self.uptime.u128()) / U128::from_num(100)
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.reward_blockstamp = blockstamp;
}
pub fn period_reward_pool(&self) -> u128 {
self.period_reward_pool.u128()
}
pub fn rewarded_set_size(&self) -> u128 {
self.rewarded_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.circulating_supply.u128()
}
pub fn reward_blockstamp(&self) -> u64 {
self.reward_blockstamp
}
pub fn uptime(&self) -> u128 {
self.uptime.u128()
}
pub fn one_over_k(&self) -> U128 {
ONE / U128::from_num(self.rewarded_set_size.u128())
}
pub fn alpha(&self) -> U128 {
U128::from_num(self.sybil_resistance_percent) / U128::from_num(100)
}
}
// cosmwasm's limited serde doesn't work with U128 directly
#[allow(non_snake_case)]
pub mod fixed_U128_as_string {
@@ -226,7 +162,7 @@ pub mod fixed_U128_as_string {
// everything required to reward delegator of given mixnode
#[derive(Debug, Clone, Copy, Serialize, Deserialize, JsonSchema)]
pub struct DelegatorRewardParams {
node_reward_params: NodeRewardParams,
reward_params: RewardParams,
// 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
@@ -242,19 +178,24 @@ pub struct DelegatorRewardParams {
}
impl DelegatorRewardParams {
pub fn new(mixnode_bond: &MixNodeBond, node_reward_params: NodeRewardParams) -> Self {
pub fn new(
sigma: U128,
profit_margin: U128,
node_profit: U128,
reward_params: RewardParams,
) -> 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,
sigma,
profit_margin,
node_profit,
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 circulating_supply = U128::from_num(self.reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward =
@@ -272,8 +213,56 @@ impl DelegatorRewardParams {
}
}
pub fn node_reward_params(&self) -> &NodeRewardParams {
&self.node_reward_params
pub fn node_reward_params(&self) -> RewardParams {
self.reward_params
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct StoredNodeRewardResult {
reward: Uint128,
lambda: Uint128,
sigma: Uint128,
}
impl StoredNodeRewardResult {
pub fn reward(&self) -> Uint128 {
self.reward
}
pub fn lambda(&self) -> Uint128 {
self.lambda
}
pub fn sigma(&self) -> Uint128 {
self.sigma
}
}
impl TryFrom<NodeRewardResult> for StoredNodeRewardResult {
type Error = MixnetContractError;
fn try_from(node_reward_result: NodeRewardResult) -> Result<Self, Self::Error> {
Ok(StoredNodeRewardResult {
reward: Uint128::new(
node_reward_result
.reward()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
lambda: Uint128::new(
node_reward_result
.lambda()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
sigma: Uint128::new(
node_reward_result
.sigma()
.checked_cast()
.ok_or(MixnetContractError::CastError)?,
),
})
}
}
@@ -307,6 +296,7 @@ pub struct MixNodeBond {
pub block_height: u64,
pub mix_node: MixNode,
pub proxy: Option<Addr>,
pub accumulated_rewards: Uint128,
}
impl MixNodeBond {
@@ -326,6 +316,7 @@ impl MixNodeBond {
block_height,
mix_node,
proxy,
accumulated_rewards: Uint128::zero(),
}
}
@@ -349,11 +340,16 @@ impl MixNodeBond {
&self.mix_node
}
// Takes into account accumulated rewards as well as current pledge and delegation amounts
pub fn total_bond(&self) -> Option<u128> {
if self.pledge_amount.denom != self.total_delegation.denom {
None
} else {
Some(self.pledge_amount.amount.u128() + self.total_delegation.amount.u128())
Some(
self.pledge_amount.amount.u128()
+ self.total_delegation.amount.u128()
+ self.accumulated_rewards.u128(),
)
}
}
@@ -366,6 +362,10 @@ impl MixNodeBond {
* U128::from_num(rewarded_set_size)
}
// TODO: There is an effect here when adding accumulted rewards to the total bond, ie accumulated rewards will not
// affect lambda, but will affect sigma, in turn over time, if left unclaimed operator rewards will not compound, but
// behave similarly to delegations.
// The question is should this be taken into account when calculating operator rewards?
pub fn pledge_to_circulating_supply(&self, circulating_supply: u128) -> U128 {
U128::from_num(self.pledge_amount().amount.u128()) / U128::from_num(circulating_supply)
}
@@ -375,26 +375,46 @@ impl MixNodeBond {
/ U128::from_num(circulating_supply)
}
pub fn lambda(&self, params: &NodeRewardParams) -> U128 {
pub fn lambda(&self, params: &RewardParams) -> U128 {
// Ratio of a bond to the token circulating supply
let pledge_to_circulating_supply_ratio =
self.pledge_to_circulating_supply(params.circulating_supply());
pledge_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn sigma(&self, params: &NodeRewardParams) -> U128 {
pub fn sigma(&self, params: &RewardParams) -> U128 {
// Ratio of a delegation to the the token circulating supply
let total_bond_to_circulating_supply_ratio =
self.total_bond_to_circulating_supply(params.circulating_supply());
total_bond_to_circulating_supply_ratio.min(params.one_over_k())
}
pub fn reward(&self, params: &NodeRewardParams) -> NodeRewardResult {
pub fn estimate_reward(
&self,
params: &RewardParams,
) -> Result<(u64, u64, u64), MixnetContractError> {
let total_node_reward = self.reward(params);
let operator_reward = self.operator_reward(params);
// TODO: This overestimates the reward by a lot, it should take a Uint128 and return estiamte for that
let delegators_reward = self.reward_delegation(self.total_delegation().amount, params);
Ok((
total_node_reward
.reward()
.checked_to_num::<u128>()
.unwrap_or_default()
.try_into()?,
operator_reward.try_into()?,
delegators_reward.try_into()?,
))
}
pub fn reward(&self, params: &RewardParams) -> NodeRewardResult {
let lambda = self.lambda(params);
let sigma = self.sigma(params);
let reward = params.performance()
* params.period_reward_pool()
* params.epoch_reward_pool()
* (sigma * params.omega()
+ params.alpha() * lambda * sigma * params.rewarded_set_size())
/ (ONE + params.alpha());
@@ -406,22 +426,22 @@ impl MixNodeBond {
}
}
pub fn node_profit(&self, params: &NodeRewardParams) -> U128 {
if self.reward(params).reward() < params.operator_cost() {
U128::from_num(0)
pub fn node_profit(&self, params: &RewardParams) -> U128 {
if self.reward(params).reward() < params.node.operator_cost() {
U128::from_num(0u128)
} else {
self.reward(params).reward() - params.operator_cost()
self.reward(params).reward() - params.node.operator_cost()
}
}
pub fn operator_reward(&self, params: &NodeRewardParams) -> u128 {
pub fn operator_reward(&self, params: &RewardParams) -> u128 {
let reward = self.reward(params);
let profit = if reward.reward < params.operator_cost() {
U128::from_num(0)
let profit = if reward.reward < params.node.operator_cost() {
U128::from_num(0u128)
} else {
reward.reward - params.operator_cost()
reward.reward - params.node.operator_cost()
};
let operator_base_reward = reward.reward.min(params.operator_cost());
let operator_base_reward = reward.reward.min(params.node.operator_cost());
let operator_reward = (self.profit_margin()
+ (ONE - self.profit_margin()) * reward.lambda / reward.sigma)
* profit;
@@ -440,7 +460,7 @@ impl MixNodeBond {
}
}
pub fn sigma_ratio(&self, params: &NodeRewardParams) -> U128 {
pub fn sigma_ratio(&self, params: &RewardParams) -> U128 {
if self.total_bond_to_circulating_supply(params.circulating_supply()) < params.one_over_k()
{
self.total_bond_to_circulating_supply(params.circulating_supply())
@@ -449,8 +469,13 @@ impl MixNodeBond {
}
}
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &NodeRewardParams) -> u128 {
let reward_params = DelegatorRewardParams::new(self, *params);
pub fn reward_delegation(&self, delegation_amount: Uint128, params: &RewardParams) -> u128 {
let reward_params = DelegatorRewardParams::new(
self.sigma(params),
self.profit_margin(),
self.node_profit(params),
params.to_owned(),
);
reward_params.determine_delegation_reward(delegation_amount)
}
}
@@ -589,6 +614,7 @@ mod tests {
block_height: 100,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix2 = MixNodeBond {
@@ -599,6 +625,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix3 = MixNodeBond {
@@ -609,6 +636,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix4 = MixNodeBond {
@@ -619,6 +647,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
let mix5 = MixNodeBond {
@@ -629,6 +658,7 @@ mod tests {
block_height: 120,
mix_node: mixnode_fixture(),
proxy: None,
accumulated_rewards: Uint128::zero(),
};
// summary:
@@ -1,12 +1,15 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::mixnode::NodeRewardParams;
use crate::reward_params::NodeRewardParams;
use crate::ContractStateParams;
use crate::{Gateway, IdentityKey, MixNode};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
type BlockHeight = u64;
type DelegateAddress = Vec<u8>;
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct InstantiateMsg {
pub rewarding_validator_address: String,
@@ -15,6 +18,19 @@ pub struct InstantiateMsg {
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
#[serde(rename_all = "snake_case")]
pub enum ExecuteMsg {
ReconcileDelegations {},
CheckpointMixnodes {},
CompoundOperatorRewardOnBehalf {
owner: String,
},
CompoundDelegatorRewardOnBehalf {
owner: String,
mix_identity: IdentityKey,
},
CompoundOperatorReward {},
CompoundDelegatorReward {
mix_identity: IdentityKey,
},
BondMixnode {
mix_node: MixNode,
owner_signature: String,
@@ -50,11 +66,11 @@ pub enum ExecuteMsg {
// id of the current rewarding interval
interval_id: u32,
},
RewardNextMixDelegators {
mix_identity: IdentityKey,
// id of the current rewarding interval
interval_id: u32,
},
// RewardNextMixDelegators {
// mix_identity: IdentityKey,
// // id of the current rewarding interval
// interval_id: u32,
// },
DelegateToMixnodeOnBehalf {
mix_identity: IdentityKey,
delegate: String,
@@ -84,6 +100,7 @@ pub enum ExecuteMsg {
expected_active_set_size: u32,
},
AdvanceCurrentInterval {},
AdvanceCurrentEpoch {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -108,7 +125,7 @@ pub enum QueryMsg {
// gets all [paged] delegations in the entire network
// TODO: do we even want that?
GetAllNetworkDelegations {
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, DelegateAddress, BlockHeight)>,
limit: Option<u32>,
},
// gets all [paged] delegations associated with particular mixnode
@@ -116,7 +133,7 @@ pub enum QueryMsg {
mix_identity: IdentityKey,
// since `start_after` is user-provided input, we can't use `Addr` as we
// can't guarantee it's validated.
start_after: Option<String>,
start_after: Option<(String, u64)>,
limit: Option<u32>,
},
// gets all [paged] delegations associated with particular delegator
@@ -154,6 +171,7 @@ pub enum QueryMsg {
GetCurrentRewardedSetHeight {},
GetCurrentInterval {},
GetRewardedSetRefreshBlocks {},
GetEpochsInInterval {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -0,0 +1,260 @@
use crate::{error::MixnetContractError, mixnode::StoredNodeRewardResult, ONE, U128};
use az::CheckedCast;
use cosmwasm_std::Uint128;
use network_defaults::DEFAULT_OPERATOR_INTERVAL_COST;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeEpochRewards {
params: NodeRewardParams,
result: StoredNodeRewardResult,
epoch_id: u32,
}
impl NodeEpochRewards {
pub fn new(params: NodeRewardParams, result: StoredNodeRewardResult, epoch_id: u32) -> Self {
Self {
params,
result,
epoch_id,
}
}
pub fn epoch_id(&self) -> u32 {
self.epoch_id
}
pub fn sigma(&self) -> Uint128 {
self.result.sigma()
}
pub fn lambda(&self) -> Uint128 {
self.result.lambda()
}
pub fn params(&self) -> NodeRewardParams {
self.params
}
pub fn reward(&self) -> Uint128 {
self.result.reward()
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.params.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn node_profit(&self) -> U128 {
let reward = U128::from_num(self.reward().u128());
if reward < self.operator_cost() {
U128::from_num(0u128)
} else {
reward - self.operator_cost()
}
}
pub fn operator_reward(&self, profit_margin: U128) -> Result<Uint128, MixnetContractError> {
let reward = self.node_profit();
let operator_base_reward = reward.min(self.operator_cost());
let operator_reward = (profit_margin
+ (ONE - profit_margin) * U128::from_num(self.lambda().u128())
/ U128::from_num(self.sigma().u128()))
* reward;
let reward = (operator_reward + operator_base_reward).max(U128::from_num(0u128));
if let Some(int_reward) = reward.checked_cast() {
Ok(Uint128::new(int_reward))
} else {
Err(MixnetContractError::CastError)
}
}
pub fn delegation_reward(
&self,
delegation_amount: Uint128,
profit_margin: U128,
epoch_reward_params: EpochRewardParams,
) -> Result<Uint128, MixnetContractError> {
// change all values into their fixed representations
let delegation_amount = U128::from_num(delegation_amount.u128());
let circulating_supply = U128::from_num(epoch_reward_params.circulating_supply());
let scaled_delegation_amount = delegation_amount / circulating_supply;
let delegator_reward = (ONE - profit_margin) * scaled_delegation_amount
/ U128::from_num(self.sigma().u128())
* self.node_profit();
let reward = delegator_reward.max(U128::ZERO);
if let Some(int_reward) = reward.checked_cast() {
Ok(Uint128::new(int_reward))
} else {
Err(MixnetContractError::CastError)
}
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct EpochRewardParams {
epoch_reward_pool: Uint128,
rewarded_set_size: Uint128,
active_set_size: Uint128,
circulating_supply: Uint128,
sybil_resistance_percent: u8,
active_set_work_factor: u8,
}
impl EpochRewardParams {
pub fn new(
epoch_reward_pool: u128,
rewarded_set_size: u128,
active_set_size: u128,
circulating_supply: u128,
sybil_resistance_percent: u8,
active_set_work_factor: u8,
) -> EpochRewardParams {
EpochRewardParams {
epoch_reward_pool: Uint128::new(epoch_reward_pool),
rewarded_set_size: Uint128::new(rewarded_set_size),
active_set_size: Uint128::new(active_set_size),
circulating_supply: Uint128::new(circulating_supply),
sybil_resistance_percent,
active_set_work_factor,
}
}
// technically it's identical to what would have been derived with a Default implementation,
// however, I prefer to be explicit about it, as a `Default::default` value makes no sense
// apart from the `ValidatorCacheInner` context, where this value is not going to be touched anyway
// (it's guarded behind an `initialised` flag)
pub fn new_empty() -> Self {
EpochRewardParams {
epoch_reward_pool: Uint128::new(0),
circulating_supply: Uint128::new(0),
sybil_resistance_percent: 0,
rewarded_set_size: Uint128::new(0),
active_set_size: Uint128::new(0),
active_set_work_factor: 0,
}
}
pub fn rewarded_set_size(&self) -> u128 {
self.rewarded_set_size.u128()
}
pub fn active_set_size(&self) -> u128 {
self.active_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.circulating_supply.u128()
}
pub fn epoch_reward_pool(&self) -> u128 {
self.epoch_reward_pool.u128()
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct NodeRewardParams {
reward_blockstamp: u64,
uptime: Uint128,
in_active_set: bool,
}
impl NodeRewardParams {
pub fn new(reward_blockstamp: u64, uptime: u128, in_active_set: bool) -> NodeRewardParams {
NodeRewardParams {
reward_blockstamp,
uptime: Uint128::new(uptime),
in_active_set,
}
}
pub fn operator_cost(&self) -> U128 {
U128::from_num(self.uptime.u128() / 100u128 * DEFAULT_OPERATOR_INTERVAL_COST as u128)
}
pub fn uptime(&self) -> u128 {
self.uptime.u128()
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.reward_blockstamp = blockstamp;
}
}
#[derive(Debug, Clone, JsonSchema, PartialEq, Serialize, Deserialize, Copy)]
pub struct RewardParams {
pub epoch: EpochRewardParams,
pub node: NodeRewardParams,
}
impl RewardParams {
pub fn new(epoch: EpochRewardParams, node: NodeRewardParams) -> RewardParams {
RewardParams { epoch, node }
}
pub fn omega(&self) -> U128 {
// As per keybase://chat/nymtech#tokeneconomics/1179
let denom = self.active_set_work_factor() * U128::from_num(self.rewarded_set_size())
- (self.active_set_work_factor() - ONE) * U128::from_num(self.idle_nodes().u128());
if self.in_active_set() {
// work_active = factor / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
self.active_set_work_factor() / denom * self.rewarded_set_size()
} else {
// work_idle = 1 / (factor * self.network.k[month] - (factor - 1) * idle_nodes)
ONE / denom * self.rewarded_set_size()
}
}
pub fn idle_nodes(&self) -> Uint128 {
self.epoch.rewarded_set_size - self.epoch.active_set_size
}
pub fn active_set_work_factor(&self) -> U128 {
U128::from_num(self.epoch.active_set_work_factor)
}
pub fn in_active_set(&self) -> bool {
self.node.in_active_set
}
pub fn performance(&self) -> U128 {
U128::from_num(self.node.uptime.u128()) / U128::from_num(100)
}
pub fn set_reward_blockstamp(&mut self, blockstamp: u64) {
self.node.reward_blockstamp = blockstamp;
}
pub fn epoch_reward_pool(&self) -> u128 {
self.epoch.epoch_reward_pool.u128()
}
pub fn rewarded_set_size(&self) -> u128 {
self.epoch.rewarded_set_size.u128()
}
pub fn circulating_supply(&self) -> u128 {
self.epoch.circulating_supply.u128()
}
pub fn reward_blockstamp(&self) -> u64 {
self.node.reward_blockstamp
}
pub fn uptime(&self) -> u128 {
self.node.uptime.u128()
}
pub fn one_over_k(&self) -> U128 {
ONE / U128::from_num(self.epoch.rewarded_set_size.u128())
}
pub fn alpha(&self) -> U128 {
U128::from_num(self.epoch.sybil_resistance_percent) / U128::from_num(100)
}
}
@@ -73,8 +73,7 @@ impl Display for ContractStateParams {
#[derive(Default, Debug, Serialize, Deserialize, PartialEq)]
pub struct RewardingResult {
pub operator_reward: Uint128,
pub total_delegator_reward: Uint128,
pub node_reward: Uint128,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -124,7 +123,7 @@ pub type IdentityKey = String;
pub type IdentityKeyRef<'a> = &'a str;
pub type SphinxKey = String;
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)]
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, JsonSchema)]
pub struct PagedRewardedSetResponse {
pub identities: Vec<(IdentityKey, RewardedSetNodeStatus)>,
pub start_next_after: Option<IdentityKey>,
+1
View File
@@ -6,6 +6,7 @@ edition = "2021"
# 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"] }
thiserror = "1.0"
url = "2.2"
+11 -16
View File
@@ -1,10 +1,11 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::Scalar;
use coconut_interface::{
aggregate_signature_shares, aggregate_verification_keys, prepare_blind_sign,
prove_bandwidth_credential, Attribute, BlindSignRequest, BlindSignRequestBody, Credential,
ElGamalKeyPair, Parameters, Signature, SignatureShare, VerificationKey,
Parameters, Signature, SignatureShare, VerificationKey,
};
use url::Url;
@@ -64,14 +65,13 @@ async fn obtain_partial_credential(
params: &Parameters,
public_attributes: &[Attribute],
private_attributes: &[Attribute],
pedersen_commitments_openings: &[Scalar],
blind_sign_request: &BlindSignRequest,
client: &validator_client::ApiClient,
validator_vk: &VerificationKey,
blind_sign_request: &BlindSignRequest,
elgamal_keypair: &ElGamalKeyPair,
) -> Result<Signature, Error> {
let blind_sign_request_body = BlindSignRequestBody::new(
blind_sign_request,
elgamal_keypair.public_key(),
public_attributes,
(public_attributes.len() + private_attributes.len()) as u32,
);
@@ -83,11 +83,11 @@ async fn obtain_partial_credential(
let unblinded_signature = blinded_signature.unblind(
params,
elgamal_keypair.private_key(),
validator_vk,
private_attributes,
public_attributes,
&blind_sign_request.get_commitment_hash(),
&*pedersen_commitments_openings,
)?;
Ok(unblinded_signature)
@@ -110,22 +110,17 @@ pub async fn obtain_aggregate_signature(
let validator_partial_vk = client.get_coconut_verification_key().await?;
validators_partial_vks.push(validator_partial_vk.key.clone());
let elgamal_keypair = coconut_interface::elgamal_keygen(params);
let blind_sign_request = prepare_blind_sign(
params,
&elgamal_keypair,
private_attributes,
public_attributes,
)?;
let (pedersen_commitments_openings, blind_sign_request) =
prepare_blind_sign(params, private_attributes, public_attributes)?;
let first = obtain_partial_credential(
params,
public_attributes,
private_attributes,
&pedersen_commitments_openings,
&blind_sign_request,
&client,
&validator_partial_vk.key,
&blind_sign_request,
&elgamal_keypair,
)
.await?;
shares.push(SignatureShare::new(first, 1));
@@ -138,10 +133,10 @@ pub async fn obtain_aggregate_signature(
params,
public_attributes,
private_attributes,
&pedersen_commitments_openings,
&blind_sign_request,
&client,
&validator_partial_vk.key,
&blind_sign_request,
&elgamal_keypair,
)
.await?;
let share = SignatureShare::new(signature, (id + 1) as u64);
+7 -15
View File
@@ -2,7 +2,7 @@
// SPDX-License-Identifier: Apache-2.0
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, fmt, ops::Deref, str::FromStr};
use std::{collections::HashMap, fmt, str::FromStr};
use crate::{
DefaultNetworkDetails, ValidatorDetails, MAINNET_DEFAULTS, QA_DEFAULTS, SANDBOX_DEFAULTS,
@@ -98,7 +98,7 @@ pub struct NetworkDetails {
}
impl From<&DefaultNetworkDetails<'_>> for NetworkDetails {
fn from(details: &DefaultNetworkDetails) -> Self {
fn from(details: &DefaultNetworkDetails<'_>) -> Self {
NetworkDetails {
bech32_prefix: details.bech32_prefix.into(),
denom: details.denom.into(),
@@ -118,20 +118,12 @@ pub struct SupportedNetworks {
impl SupportedNetworks {
pub fn new(support: Vec<Network>) -> Self {
let mut networks = HashMap::new();
for network in support {
match network {
Network::MAINNET => {
networks.insert(Network::MAINNET, MAINNET_DEFAULTS.deref().into())
}
Network::SANDBOX => {
networks.insert(Network::SANDBOX, SANDBOX_DEFAULTS.deref().into())
}
Network::QA => networks.insert(Network::QA, QA_DEFAULTS.deref().into()),
};
SupportedNetworks {
networks: support
.into_iter()
.map(|n| (n, n.details().into()))
.collect(),
}
SupportedNetworks { networks }
}
pub fn bech32_prefix(&self, network: Network) -> Option<&str> {
+2 -1
View File
@@ -179,7 +179,8 @@ pub const VALIDATOR_API_VERSION: &str = "v1";
// REWARDING
/// 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 interval costs to Nyms. We'll also assume a cost of 40$ per interval(month), converting that to Nym at our 1$ rate translates to 40_000_000 uNyms
pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$
// pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 40_000_000; // 40$/(30 days) at 1 Nym == 1$
pub const DEFAULT_OPERATOR_INTERVAL_COST: u64 = 55_556; // 40$/1hr at 1 Nym == 1$
// TODO: is there a way to get this from the chain
pub const TOTAL_SUPPLY: u128 = 1_000_000_000_000_000;
+3 -3
View File
@@ -32,9 +32,9 @@ doc-comment = "0.3"
[dev-dependencies.bincode]
version = "1"
#[[bench]]
#name = "benchmarks"
#harness = false
[[bench]]
name = "benchmarks"
harness = false
[features]
default = []
+332
View File
@@ -0,0 +1,332 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use bls12_381::{multi_miller_loop, G1Affine, G1Projective, G2Affine, G2Prepared, Scalar};
use criterion::{criterion_group, criterion_main, Criterion};
use ff::Field;
use group::{Curve, Group};
use nymcoconut::{
aggregate_signature_shares, aggregate_verification_keys, blind_sign, elgamal_keygen,
prepare_blind_sign, prove_bandwidth_credential, setup, ttp_keygen, verify_credential,
Attribute, BlindedSignature, Parameters, Signature, SignatureShare, VerificationKey,
};
use rand::seq::SliceRandom;
use std::ops::Neg;
use std::time::Duration;
fn double_pairing(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let gt1 = bls12_381::pairing(&g11, &g21);
let gt2 = bls12_381::pairing(&g12, &g22);
assert_eq!(gt1, gt2)
}
fn multi_miller_pairing_affine(g11: &G1Affine, g21: &G2Affine, g12: &G1Affine, g22: &G2Affine) {
let miller_loop_result = multi_miller_loop(&[
(g11, &G2Prepared::from(*g21)),
(&g12.neg(), &G2Prepared::from(*g22)),
]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
fn multi_miller_pairing_with_prepared(
g11: &G1Affine,
g21: &G2Prepared,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result = multi_miller_loop(&[(g11, &g21), (&g12.neg(), &g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
// the case of being able to prepare G2 generator
fn multi_miller_pairing_with_semi_prepared(
g11: &G1Affine,
g21: &G2Affine,
g12: &G1Affine,
g22: &G2Prepared,
) {
let miller_loop_result =
multi_miller_loop(&[(g11, &G2Prepared::from(*g21)), (&g12.neg(), &g22)]);
assert!(bool::from(
miller_loop_result.final_exponentiation().is_identity()
))
}
fn unblind_and_aggregate(
params: &Parameters,
blinded_signatures: &[BlindedSignature],
partial_verification_keys: &[VerificationKey],
private_attributes: &[Attribute],
public_attributes: &[Attribute],
commitment_hash: &G1Projective,
pedersen_commitments_openings: &[Scalar],
verification_key: &VerificationKey,
) -> Signature {
// Unblind all partial signatures
let unblinded_signatures: Vec<Signature> = blinded_signatures
.iter()
.zip(partial_verification_keys.iter())
.map(|(signature, partial_verification_key)| {
signature
.unblind(
&params,
&partial_verification_key,
&private_attributes,
&public_attributes,
&commitment_hash,
&pedersen_commitments_openings,
)
.unwrap()
})
.collect();
let unblinded_signature_shares: Vec<SignatureShare> = unblinded_signatures
.iter()
.enumerate()
.map(|(idx, signature)| SignatureShare::new(*signature, (idx + 1) as u64))
.collect();
let mut attributes = vec![];
attributes.extend_from_slice(private_attributes);
attributes.extend_from_slice(public_attributes);
aggregate_signature_shares(
&params,
&verification_key,
&attributes,
&unblinded_signature_shares,
)
.unwrap()
}
struct BenchCase {
num_authorities: u64,
threshold_p: f32,
num_public_attrs: u32,
num_private_attrs: u32,
}
impl BenchCase {
fn threshold(&self) -> u64 {
(self.num_authorities as f32 * self.threshold_p).round() as u64
}
fn num_attrs(&self) -> u32 {
self.num_public_attrs + self.num_private_attrs
}
}
fn bench_pairings(c: &mut Criterion) {
let mut rng = rand::thread_rng();
let g1 = G1Affine::generator();
let g2 = G2Affine::generator();
let r = Scalar::random(&mut rng);
let s = Scalar::random(&mut rng);
let g11 = (g1 * r).to_affine();
let g21 = (g2 * s).to_affine();
let g21_prep = G2Prepared::from(g21);
let g12 = (g1 * s).to_affine();
let g22 = (g2 * r).to_affine();
let g22_prep = G2Prepared::from(g22);
c.bench_function("double pairing", |b| {
b.iter(|| double_pairing(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller in affine", |b| {
b.iter(|| multi_miller_pairing_affine(&g11, &g21, &g12, &g22))
});
c.bench_function("multi miller with prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_prepared(&g11, &g21_prep, &g12, &g22_prep))
});
c.bench_function("multi miller with semi-prepared g2", |b| {
b.iter(|| multi_miller_pairing_with_semi_prepared(&g11, &g21, &g12, &g22_prep))
});
}
fn bench_coconut(c: &mut Criterion) {
let mut group = c.benchmark_group("benchmark-coconut");
group.measurement_time(Duration::from_secs(100));
let case = BenchCase {
num_authorities: 100,
threshold_p: 0.7,
num_public_attrs: 2,
num_private_attrs: 2,
};
let params = setup((case.num_public_attrs + case.num_private_attrs)).unwrap();
let public_attributes = params.n_random_scalars(case.num_public_attrs as usize);
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);
// The prepare blind sign is performed by the user
let (pedersen_commitments_openings, blind_sign_request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
// CLIENT BENCHMARK: Data needed to ask for a credential
// Let's benchmark the operations the client has to perform
// to ask for a credential
group.bench_function(
&format!(
"[Client] prepare_blind_sign_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap())
},
);
// keys for the validators
let coconut_keypairs = ttp_keygen(&params, case.threshold(), case.num_authorities).unwrap();
// VALIDATOR BENCHMARK: Issue partial credential
// we pick only one key pair, as we want to validate how much does it
// take for a single validator to issue a partial credential
let mut rng = rand::thread_rng();
let keypair = coconut_keypairs.choose(&mut rng).unwrap();
group.bench_function(
&format!(
"[Validator] compute_single_blind_sign_for_credential_with_{}_attributes",
case.num_attrs(),
),
|b| {
b.iter(|| {
blind_sign(
&params,
&keypair.secret_key(),
&blind_sign_request,
&public_attributes,
)
.unwrap()
})
},
);
// computing all partial credentials
// NOTE: in reality, each validator computes only single signature
let mut blinded_signatures = Vec::new();
for keypair in coconut_keypairs.iter() {
let blinded_signature = blind_sign(
&params,
&keypair.secret_key(),
&blind_sign_request,
&public_attributes,
)
.unwrap();
blinded_signatures.push(blinded_signature)
}
let verification_keys: Vec<VerificationKey> = coconut_keypairs
.iter()
.map(|keypair| keypair.verification_key())
.collect();
// Lets bench worse case, ie aggregating all
let indices: Vec<u64> = (1..=case.num_authorities).collect();
// aggregate verification keys
let aggr_verification_key =
aggregate_verification_keys(&verification_keys, Some(&indices)).unwrap();
// CLIENT OPERATION: Unblind partial singatures and aggregate into single signature
let aggregated_signature = unblind_and_aggregate(
&params,
&blinded_signatures,
&verification_keys,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
&pedersen_commitments_openings,
&aggr_verification_key,
);
// CLIENT BENCHMARK: aggregate all partial credentials
group.bench_function(
&format!(
"[Client] unblind_and_aggregate_partial_credentials_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
unblind_and_aggregate(
&params,
&blinded_signatures,
&verification_keys,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
&pedersen_commitments_openings,
&aggr_verification_key)
})
},
);
// CLIENT OPERATION: Randomize credentials and generate any cryptographic material to verify them
let theta = prove_bandwidth_credential(
&params,
&aggr_verification_key,
&aggregated_signature,
serial_number,
binding_number,
)
.unwrap();
// CLIENT BENCHMARK
group.bench_function(
&format!(
"[Client] randomize_and_prove_credential_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
prove_bandwidth_credential(
&params,
&aggr_verification_key,
&aggregated_signature,
serial_number,
binding_number,
)
.unwrap()
})
},
);
// VERIFIER OPERATION
// Verify credentials
verify_credential(&params, &aggr_verification_key, &theta, &public_attributes);
// VERIFICATION BENCHMARK
group.bench_function(
&format!(
"[Verifier] verify_credentials_{}_authorities_{}_attributes_{}_threshold",
case.num_authorities,
case.num_attrs(),
case.threshold_p,
),
|b| {
b.iter(|| {
verify_credential(&params, &aggr_verification_key, &theta, &public_attributes)
})
},
);
}
criterion_group!(benches, bench_coconut);
criterion_main!(benches);
+13
View File
@@ -13,6 +13,7 @@ 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};
use crate::Attribute;
/// Type alias for the ephemeral key generated during ElGamal encryption
pub type EphemeralKey = Scalar;
@@ -222,6 +223,18 @@ pub fn elgamal_keygen(params: &Parameters) -> ElGamalKeyPair {
}
}
pub fn compute_attribute_encryption(
params: &Parameters,
private_attributes: &[Attribute],
pub_key: &PublicKey,
commitment_hash: G1Projective,
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
private_attributes
.iter()
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
.unzip()
}
#[cfg(test)]
mod tests {
use super::*;
+101 -157
View File
@@ -13,12 +13,11 @@ 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};
use crate::Attribute;
// as per the reference python implementation
type ChallengeDigest = Sha256;
@@ -28,8 +27,7 @@ type ChallengeDigest = Sha256;
pub struct ProofCmCs {
challenge: Scalar,
response_opening: Scalar,
response_private_elgamal_key: Scalar,
response_keys: Vec<Scalar>,
response_openings: Vec<Scalar>,
response_attributes: Vec<Scalar>,
}
@@ -88,12 +86,11 @@ impl ProofCmCs {
/// using the Fiat-Shamir heuristic.
pub(crate) fn construct(
params: &Parameters,
elgamal_keypair: &ElGamalKeyPair,
ephemeral_keys: &[elgamal::EphemeralKey],
commitment: &G1Projective,
commitment_opening: &Scalar,
commitments: &[G1Projective],
pedersen_commitments_openings: &[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
@@ -101,10 +98,9 @@ impl ProofCmCs {
// 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_pedersen_commitments_openings =
params.n_random_scalars(pedersen_commitments_openings.len());
let witness_attributes = params.n_random_scalars(private_attributes.len());
// recompute h
@@ -118,22 +114,6 @@ impl ProofCmCs {
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])
@@ -144,9 +124,21 @@ impl ProofCmCs {
.map(|(wm_i, hs_i)| hs_i * wm_i)
.sum::<G1Projective>();
let ciphertexts_bytes = priv_attributes_ciphertexts
// zkp commitments for the individual attributes
let commitments_attributes = witness_pedersen_commitments_openings
.iter()
.map(|c| c.to_bytes())
.zip(witness_attributes.iter())
.map(|(o_j, m_j)| g1 * o_j + h * m_j)
.collect::<Vec<_>>();
let commitments_bytes = commitments
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
let commitments_attributes_bytes = commitments_attributes
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// compute challenge
@@ -154,28 +146,20 @@ impl ProofCmCs {
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(commitments_bytes.iter().map(|cm| cm.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())),
.chain(commitments_attributes_bytes.iter().map(|cm| cm.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,
let response_openings = produce_responses(
&witness_pedersen_commitments_openings,
&challenge,
&elgamal_keypair.private_key().0,
&pedersen_commitments_openings.iter().collect::<Vec<_>>(),
);
let response_keys = produce_responses(&witness_keys, &challenge, ephemeral_keys);
let response_attributes = produce_responses(
&witness_attributes,
&challenge,
@@ -185,8 +169,7 @@ impl ProofCmCs {
ProofCmCs {
challenge,
response_opening,
response_private_elgamal_key,
response_keys,
response_openings,
response_attributes,
}
}
@@ -194,11 +177,11 @@ impl ProofCmCs {
pub(crate) fn verify(
&self,
params: &Parameters,
pub_key: &elgamal::PublicKey,
commitment: &G1Projective,
attributes_ciphertexts: &[elgamal::Ciphertext],
commitments: &[G1Projective],
public_attributes: &[Attribute],
) -> bool {
if self.response_keys.len() != attributes_ciphertexts.len() {
if self.response_attributes.len() != commitments.len() {
return false;
}
@@ -213,32 +196,14 @@ impl ProofCmCs {
.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
let commitment_attributes = (commitment
- public_attributes
.iter()
.zip(params.gen_hs().iter().skip(self.response_attributes.len()))
.map(|(pub_attr, hs)| hs * pub_attr)
.sum::<G1Projective>())
* self.challenge
+ g1 * self.response_opening
+ self
.response_attributes
@@ -247,9 +212,22 @@ impl ProofCmCs {
.map(|(res_attr, hs)| hs * res_attr)
.sum::<G1Projective>();
let ciphertexts_bytes = attributes_ciphertexts
let commitments_attributes = izip!(
commitments.iter(),
self.response_openings.iter(),
self.response_attributes.iter()
)
.map(|(cm_j, r_o_j, r_m_j)| cm_j * self.challenge + g1 * r_o_j + h * r_m_j)
.collect::<Vec<_>>();
let commitments_bytes = commitments
.iter()
.map(|c| c.to_bytes())
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
let commitments_attributes_bytes = commitments_attributes
.iter()
.map(|cm| cm.to_bytes())
.collect::<Vec<_>>();
// re-compute the challenge
@@ -257,38 +235,32 @@ impl ProofCmCs {
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(commitments_bytes.iter().map(|cm| cm.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())),
.chain(commitments_attributes_bytes.iter().map(|cm| cm.as_ref())),
);
challenge == self.challenge
}
// challenge || response opening || response private elgamal key || keys len || response keys || attributes len || response attributes
// challenge || response opening || openings len || response openings || attributes len ||
// response attributes
pub(crate) fn to_bytes(&self) -> Vec<u8> {
let keys_len = self.response_keys.len() as u64;
let openings_len = self.response_openings.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);
let mut bytes = Vec::with_capacity(16 + (2 + openings_len + attributes_len) 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(&openings_len.to_le_bytes());
for ro in &self.response_openings {
bytes.extend_from_slice(&ro.to_bytes());
}
bytes.extend_from_slice(&attributes_len.to_le_bytes());
for rm in &self.response_attributes {
bytes.extend_from_slice(&rm.to_bytes());
}
@@ -298,11 +270,11 @@ impl ProofCmCs {
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())
);
if bytes.len() < 32 * 4 + 16 || (bytes.len() - 16) % 32 != 0 {
return Err(CoconutError::Deserialization(
"tried to deserialize proof of commitments with bytes of invalid length"
.to_string(),
));
}
let mut idx = 0;
@@ -310,54 +282,46 @@ impl ProofCmCs {
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());
let ro_len = u64::from_le_bytes(bytes[idx..idx + 8].try_into().unwrap());
idx += 8;
if bytes[idx..].len() < rk_len as usize * 32 + 8 {
if bytes[idx..].len() < ro_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 ro_end = idx + ro_len as usize * 32;
let response_openings = try_deserialize_scalar_vec(
ro_len,
&bytes[idx..ro_end],
CoconutError::Deserialization("Failed to deserialize openings response".to_string()),
)?;
let rm_len = u64::from_le_bytes(bytes[rk_end..rk_end + 8].try_into().unwrap());
let rm_len = u64::from_le_bytes(bytes[ro_end..ro_end + 8].try_into().unwrap());
let response_attributes = try_deserialize_scalar_vec(
rm_len,
&bytes[rk_end + 8..],
&bytes[ro_end + 8..],
CoconutError::Deserialization("Failed to deserialize attributes response".to_string()),
)?;
Ok(ProofCmCs {
challenge,
response_opening,
response_private_elgamal_key,
response_keys,
response_openings,
response_attributes,
})
}
@@ -392,7 +356,7 @@ impl ProofKappaZeta {
let witness_attributes = vec![witness_serial_number, witness_binding_number];
let beta_bytes = verification_key
.beta
.beta_g2
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
@@ -403,7 +367,7 @@ impl ProofKappaZeta {
+ verification_key.alpha
+ witness_attributes
.iter()
.zip(verification_key.beta.iter())
.zip(verification_key.beta_g2.iter())
.map(|(wm_i, beta_i)| beta_i * wm_i)
.sum::<G2Projective>();
@@ -447,7 +411,7 @@ impl ProofKappaZeta {
zeta: &G2Projective,
) -> bool {
let beta_bytes = verification_key
.beta
.beta_g2
.iter()
.map(|beta_i| beta_i.to_bytes())
.collect::<Vec<_>>();
@@ -460,7 +424,7 @@ impl ProofKappaZeta {
+ verification_key.alpha * (Scalar::one() - self.challenge)
+ response_attributes
.iter()
.zip(verification_key.beta.iter())
.zip(verification_key.beta_g2.iter())
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>();
@@ -550,7 +514,6 @@ 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};
@@ -560,51 +523,32 @@ mod tests {
#[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);
let cms: [G1Projective; 1] = [G1Projective::random(&mut rng)];
let rs = params.n_random_scalars(1);
let private_attributes = 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 pi_s = ProofCmCs::construct(&mut params, &cm, &r, &cms, &rs, &private_attributes);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
// 2 private
let mut params = setup(2).unwrap();
let cm = G1Projective::random(&mut rng);
let r = params.random_scalar();
let cms: [G1Projective; 2] = [
G1Projective::random(&mut rng),
G1Projective::random(&mut rng),
];
let rs = params.n_random_scalars(2);
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,
);
// 0 public 2 privates
let pi_s = ProofCmCs::construct(&mut params, &cm, &r, &cms, &rs, &private_attributes);
let bytes = pi_s.to_bytes();
assert_eq!(ProofCmCs::from_bytes(&bytes).unwrap(), pi_s);
@@ -612,9 +556,9 @@ mod tests {
#[test]
fn proof_kappa_zeta_bytes_roundtrip() {
let mut params = setup(4).unwrap();
let params = setup(4).unwrap();
let keypair = keygen(&mut params);
let keypair = keygen(&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();
@@ -627,7 +571,7 @@ mod tests {
// 0 public 2 private
let pi_v = ProofKappaZeta::construct(
&mut params,
&params,
&keypair.verification_key(),
&serial_number,
&binding_number,
@@ -642,11 +586,11 @@ mod tests {
assert_eq!(proof_from_bytes, pi_v);
// 2 public 2 private
let mut params = setup(4).unwrap();
let keypair = keygen(&mut params);
let params = setup(4).unwrap();
let keypair = keygen(&params);
let pi_v = ProofKappaZeta::construct(
&mut params,
&params,
&keypair.verification_key(),
&serial_number,
&binding_number,
+12 -11
View File
@@ -64,7 +64,8 @@ impl Aggregatable for PartialSignature {
/// 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()
keys.iter().map(|vk| vk.beta_g1.len()).all_equal()
&& keys.iter().map(|vk| vk.beta_g2.len()).all_equal()
}
pub fn aggregate_verification_keys(
@@ -98,7 +99,7 @@ pub fn aggregate_signatures(
let tmp = attributes
.iter()
.zip(verification_key.beta.iter())
.zip(verification_key.beta_g2.iter())
.map(|(attr, beta_i)| beta_i * attr)
.sum::<G2Projective>();
@@ -149,8 +150,8 @@ mod tests {
#[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 params = Parameters::new(2).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let vks = keypairs
.into_iter()
@@ -213,7 +214,7 @@ mod tests {
let mut params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (sks, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
@@ -310,9 +311,9 @@ mod tests {
#[test]
fn signature_aggregation_doesnt_work_for_empty_set_of_signatures() {
let signatures: Vec<Signature> = vec![];
let mut params = Parameters::new(2).unwrap();
let params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
@@ -328,9 +329,9 @@ mod tests {
#[test]
fn signature_aggregation_doesnt_work_if_indices_have_invalid_length() {
let signatures = vec![random_signature()];
let mut params = Parameters::new(2).unwrap();
let params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
@@ -354,9 +355,9 @@ mod tests {
#[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 params = Parameters::new(2).unwrap();
let attributes = params.n_random_scalars(2);
let keypairs = ttp_keygen(&mut params, 3, 5).unwrap();
let keypairs = ttp_keygen(&params, 3, 5).unwrap();
let (_, vks): (Vec<_>, Vec<_>) = keypairs
.into_iter()
.map(|keypair| (keypair.secret_key(), keypair.verification_key()))
+88 -88
View File
@@ -7,16 +7,15 @@ 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;
use crate::Attribute;
/// 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.
@@ -33,7 +32,7 @@ pub struct BlindSignRequest {
// h
commitment_hash: G1Projective,
// c
private_attributes_ciphertexts: Vec<elgamal::Ciphertext>,
private_attributes_commitments: Vec<G1Projective>,
// pi_s
pi_s: ProofCmCs,
}
@@ -42,9 +41,9 @@ impl TryFrom<&[u8]> for BlindSignRequest {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindSignRequest> {
if bytes.len() < 48 + 48 + 8 + 96 {
if bytes.len() < 48 + 48 + 8 + 48 {
return Err(CoconutError::DeserializationMinLength {
min: 48 + 48 + 8 + 96,
min: 48 + 48 + 8 + 48,
actual: bytes.len(),
});
}
@@ -73,26 +72,35 @@ impl TryFrom<&[u8]> for BlindSignRequest {
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 {
if bytes[j..].len() < c_len as usize * 48 {
return Err(CoconutError::DeserializationMinLength {
min: c_len as usize * 96,
min: c_len as usize * 48,
actual: bytes[56..].len(),
});
}
let mut private_attributes_ciphertexts = Vec::with_capacity(c_len as usize);
let mut private_attributes_commitments = 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 start = j + i * 48;
let end = start + 48;
let private_attributes_commitment_bytes = bytes[start..end].try_into().unwrap();
let private_attributes_commitment = try_deserialize_g1_projective(
&private_attributes_commitment_bytes,
CoconutError::Deserialization(
"Failed to deserialize compressed commitment".to_string(),
),
)?;
private_attributes_commitments.push(private_attributes_commitment)
}
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 96..])?;
let pi_s = ProofCmCs::from_bytes(&bytes[j + c_len as usize * 48..])?;
Ok(BlindSignRequest {
commitment,
commitment_hash,
private_attributes_ciphertexts,
private_attributes_commitments,
pi_s,
})
}
@@ -102,16 +110,16 @@ 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 c_len = self.private_attributes_commitments.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());
let mut bytes = Vec::with_capacity(48 + 48 + 8 + c_len as usize * 48 + 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());
for c in &self.private_attributes_commitments {
bytes.extend_from_slice(&c.to_affine().to_compressed());
}
bytes.extend_from_slice(&proof_bytes);
@@ -127,12 +135,12 @@ impl Bytable for BlindSignRequest {
impl Base58 for BlindSignRequest {}
impl BlindSignRequest {
fn verify_proof(&self, params: &Parameters, pub_key: &elgamal::PublicKey) -> bool {
fn verify_proof(&self, params: &Parameters, public_attributes: &[Attribute]) -> bool {
self.pi_s.verify(
params,
pub_key,
&self.commitment,
&self.private_attributes_ciphertexts,
&self.private_attributes_commitments,
public_attributes,
)
}
@@ -140,6 +148,10 @@ impl BlindSignRequest {
self.commitment_hash
}
pub fn get_private_attributes_pedersen_commitments(&self) -> Vec<G1Projective> {
self.private_attributes_commitments.clone()
}
pub fn to_bytes(&self) -> Vec<u8> {
self.to_byte_vec()
}
@@ -149,17 +161,19 @@ impl BlindSignRequest {
}
}
pub fn compute_private_attributes_commitment(
pub fn compute_attributes_commitment(
params: &Parameters,
private_attributes: &[Attribute],
public_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
// where m0, m1, ...., mn are attributes
let attr_cm = private_attributes
.iter()
.chain(public_attributes.iter())
.zip(hs)
.map(|(&m, h)| h * m)
.sum::<G1Projective>();
@@ -169,29 +183,34 @@ pub fn compute_private_attributes_commitment(
(commitment_opening, commitment)
}
pub fn compute_commitment_hash(commitment: G1Projective) -> G1Projective {
hash_g1(commitment.to_bytes())
}
pub fn compute_attribute_encryption(
pub fn compute_pedersen_commitments_for_private_attributes(
params: &Parameters,
private_attributes: &[Attribute],
pub_key: &elgamal::PublicKey,
commitment_hash: G1Projective,
) -> (Vec<Ciphertext>, Vec<EphemeralKey>) {
private_attributes
h: &G1Projective,
) -> (Vec<Scalar>, Vec<G1Projective>) {
// Generate openings for Pedersen commitment for each private attribute
let commitments_openings = params.n_random_scalars(private_attributes.len());
// Compute Pedersen commitment for each private attribute
let pedersen_commitments = commitments_openings
.iter()
.map(|m| pub_key.encrypt(params, &commitment_hash, m))
.unzip()
.zip(private_attributes.iter())
.map(|(o_j, m_j)| params.gen1() * o_j + h * m_j)
.collect::<Vec<_>>();
(commitments_openings, pedersen_commitments)
}
pub fn compute_commitment_hash(commitment: G1Projective) -> G1Projective {
hash_g1(commitment.to_bytes())
}
/// 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> {
) -> Result<(Vec<Scalar>, BlindSignRequest)> {
if private_attributes.is_empty() {
return Err(CoconutError::Issuance(
"Tried to prepare blind sign request for an empty set of private attributes"
@@ -208,45 +227,45 @@ pub fn prepare_blind_sign(
}
let (commitment_opening, commitment) =
compute_private_attributes_commitment(params, private_attributes, hs);
compute_attributes_commitment(params, private_attributes, public_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(
let (pedersen_commitments_openings, pedersen_commitments) =
compute_pedersen_commitments_for_private_attributes(
params,
private_attributes,
elgamal_keypair.public_key(),
commitment_hash,
&commitment_hash,
);
let pi_s = ProofCmCs::construct(
params,
elgamal_keypair,
&ephemeral_keys,
&commitment,
&commitment_opening,
&pedersen_commitments,
&pedersen_commitments_openings,
private_attributes,
&*private_attributes_ciphertexts,
);
Ok(BlindSignRequest {
commitment,
commitment_hash,
private_attributes_ciphertexts,
pi_s,
})
Ok((
pedersen_commitments_openings,
BlindSignRequest {
commitment,
commitment_hash,
private_attributes_commitments: pedersen_commitments,
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 num_private = blind_sign_request.private_attributes_commitments.len();
let hs = params.gen_hs();
if num_private + public_attributes.len() > hs.len() {
@@ -265,7 +284,7 @@ pub fn blind_sign(
}
// Verify the ZK proof
if !blind_sign_request.verify_proof(params, prover_pub_key) {
if !blind_sign_request.verify_proof(params, public_attributes) {
return Err(CoconutError::Issuance(
"Failed to verify the proof of knowledge".to_string(),
));
@@ -280,27 +299,17 @@ pub fn blind_sign(
.map(|(attr, yi)| attr * yi)
.sum::<Scalar>();
// c1[0] ^ y[0] * ... * c1[m] ^ y[m]
let sig_1 = blind_sign_request
.private_attributes_ciphertexts
// h ^ x + c[0] ^ y[0] + ... c[m] ^ y[m] + h ^ (pub_m[0] * y[m + 1] + ... + pub_m[n] * y[m + n])
let sig = blind_sign_request
.private_attributes_commitments
.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)
.map(|(c, yi)| c * 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)))
Ok(BlindedSignature(h, sig))
}
#[cfg(test)]
@@ -342,36 +351,27 @@ mod tests {
#[test]
fn blind_sign_request_bytes_roundtrip() {
let mut params = Parameters::new(1).unwrap();
let public_attributes = params.n_random_scalars(0);
// 0 public and 1 private attribute
let params = Parameters::new(1).unwrap();
let private_attributes = params.n_random_scalars(1);
let elgamal_keypair = elgamal::elgamal_keygen(&params);
let public_attributes = params.n_random_scalars(0);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let (_commitments_openings, lambda) =
prepare_blind_sign(&params, &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);
// 2 public and 2 private attributes
let params = Parameters::new(4).unwrap();
let private_attributes = params.n_random_scalars(2);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let public_attributes = params.n_random_scalars(2);
let (_commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &public_attributes).unwrap();
let bytes = lambda.to_bytes();
assert_eq!(
+116 -50
View File
@@ -7,7 +7,7 @@ use core::ops::{Add, Mul};
use std::convert::TryFrom;
use std::convert::TryInto;
use bls12_381::{G2Projective, Scalar};
use bls12_381::{G1Projective, G2Projective, Scalar};
use group::Curve;
use serde_derive::{Deserialize, Serialize};
@@ -17,7 +17,8 @@ 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,
try_deserialize_g1_projective, try_deserialize_g2_projective, try_deserialize_scalar,
try_deserialize_scalar_vec, Polynomial,
};
use crate::Base58;
@@ -32,6 +33,7 @@ impl TryFrom<&[u8]> for SecretKey {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<SecretKey> {
// There should be x and at least one y
if bytes.len() < 32 * 2 + 8 || (bytes.len() - 8) % 32 != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
@@ -71,16 +73,18 @@ impl TryFrom<&[u8]> for SecretKey {
impl SecretKey {
/// Derive verification key using this secret key.
pub fn verification_key(&self, params: &Parameters) -> VerificationKey {
let g1 = params.gen1();
let g2 = params.gen2();
VerificationKey {
alpha: g2 * self.x,
beta: self.ys.iter().map(|y| g2 * y).collect(),
beta_g1: self.ys.iter().map(|y| g1 * y).collect(),
beta_g2: 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 ys_len = self.ys.len();
let mut bytes = Vec::with_capacity(8 + (ys_len + 1) as usize * 32);
bytes.extend_from_slice(&self.x.to_bytes());
@@ -114,33 +118,36 @@ impl Base58 for SecretKey {}
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>,
pub(crate) beta_g1: Vec<G1Projective>,
pub(crate) beta_g2: 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 {
// There should be at least alpha, one betaG1 and one betaG2 and their length
if bytes.len() < 96 * 2 + 48 + 8 || (bytes.len() - 8 - 96) % (96 + 48) != 0 {
return Err(CoconutError::DeserializationInvalidLength {
actual: bytes.len(),
modulus_target: bytes.len() - 8,
target: 96 * 2 + 8,
modulus: 96,
object: "secret key".to_string(),
modulus_target: bytes.len() - 8 - 96,
target: 96 * 2 + 48 + 8,
modulus: 96 + 48,
object: "verification 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;
let betas_len = u64::from_le_bytes(bytes[96..104].try_into().unwrap());
if beta_len as usize != actual_beta_len {
let actual_betas_len = (bytes.len() - 104) / (96 + 48);
if betas_len as usize != actual_betas_len {
return Err(
CoconutError::Deserialization(
format!("Tried to deserialize verification key with inconsistent beta len (expected {}, got {})",
beta_len, actual_beta_len
format!("Tried to deserialize verification key with inconsistent betas len (expected {}, got {})",
betas_len, actual_betas_len
)));
}
@@ -151,10 +158,27 @@ impl TryFrom<&[u8]> for VerificationKey {
),
)?;
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 mut beta_g1 = Vec::with_capacity(betas_len as usize);
let mut beta_g1_end: u64 = 0;
for i in 0..betas_len {
let start = (104 + i * 48) as usize;
let end = (start + 48) as usize;
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g1_projective(
&beta_i_bytes,
CoconutError::Deserialization(
"Failed to deserialize verification key G1 point (beta)".to_string(),
),
)?;
beta_g1_end = end as u64;
beta_g1.push(beta_i)
}
let mut beta_g2 = Vec::with_capacity(betas_len as usize);
for i in 0..betas_len {
let start = (beta_g1_end + i * 96) as usize;
let end = (start + 96) as usize;
let beta_i_bytes = bytes[start..end].try_into().unwrap();
let beta_i = try_deserialize_g2_projective(
&beta_i_bytes,
@@ -163,10 +187,14 @@ impl TryFrom<&[u8]> for VerificationKey {
),
)?;
beta.push(beta_i)
beta_g2.push(beta_i)
}
Ok(VerificationKey { alpha, beta })
Ok(VerificationKey {
alpha,
beta_g1,
beta_g2,
})
}
}
@@ -179,18 +207,42 @@ impl<'b> Add<&'b VerificationKey> for VerificationKey {
// 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"
self.beta_g1.len(),
rhs.beta_g1.len(),
"trying to add verification keys generated for different number of attributes [G1]"
);
assert_eq!(
self.beta_g2.len(),
rhs.beta_g2.len(),
"trying to add verification keys generated for different number of attributes [G2]"
);
assert_eq!(
self.beta_g1.len(),
self.beta_g2.len(),
"this key is incorrect - the number of elements G1 and G2 does not match"
);
assert_eq!(
rhs.beta_g1.len(),
rhs.beta_g2.len(),
"they key you want to add is incorrect - the number of elements G1 and G2 does not match"
);
VerificationKey {
alpha: self.alpha + rhs.alpha,
beta: self
.beta
beta_g1: self
.beta_g1
.iter()
.zip(rhs.beta.iter())
.map(|(self_beta, rhs_beta)| self_beta + rhs_beta)
.zip(rhs.beta_g1.iter())
.map(|(self_beta_g1, rhs_beta_g1)| self_beta_g1 + rhs_beta_g1)
.collect(),
beta_g2: self
.beta_g2
.iter()
.zip(rhs.beta_g2.iter())
.map(|(self_beta_g2, rhs_beta_g2)| self_beta_g2 + rhs_beta_g2)
.collect(),
}
}
@@ -203,7 +255,8 @@ impl<'a> Mul<Scalar> for &'a VerificationKey {
fn mul(self, rhs: Scalar) -> Self::Output {
VerificationKey {
alpha: self.alpha * rhs,
beta: self.beta.iter().map(|b_i| b_i * rhs).collect(),
beta_g1: self.beta_g1.iter().map(|b_i| b_i * rhs).collect(),
beta_g2: self.beta_g2.iter().map(|b_i| b_i * rhs).collect(),
}
}
}
@@ -219,7 +272,7 @@ where
{
let mut peekable = iter.peekable();
let head_attributes = match peekable.peek() {
Some(head) => head.borrow().beta.len(),
Some(head) => head.borrow().beta_g2.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?
@@ -239,7 +292,8 @@ impl VerificationKey {
pub(crate) fn identity(beta_size: usize) -> Self {
VerificationKey {
alpha: G2Projective::identity(),
beta: vec![G2Projective::identity(); beta_size],
beta_g1: vec![G1Projective::identity(); beta_size],
beta_g2: vec![G2Projective::identity(); beta_size],
}
}
@@ -251,19 +305,31 @@ impl VerificationKey {
&self.alpha
}
pub fn beta(&self) -> &Vec<G2Projective> {
&self.beta
pub fn beta_g1(&self) -> &Vec<G1Projective> {
&self.beta_g1
}
pub fn beta_g2(&self) -> &Vec<G2Projective> {
&self.beta_g2
}
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);
let beta_g1_len = self.beta_g1.len();
let beta_g2_len = self.beta_g2.len();
let mut bytes = Vec::with_capacity(96 + 8 + beta_g1_len * 48 + beta_g2_len * 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.extend_from_slice(&beta_g1_len.to_le_bytes());
for beta_g1 in self.beta_g1.iter() {
bytes.extend_from_slice(&beta_g1.to_affine().to_compressed())
}
for beta_g2 in self.beta_g2.iter() {
bytes.extend_from_slice(&beta_g2.to_affine().to_compressed())
}
bytes
}
@@ -488,11 +554,11 @@ mod tests {
#[test]
fn keypair_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = keygen(&mut params1);
let keypair5 = keygen(&mut params5);
let keypair1 = keygen(&params1);
let keypair5 = keygen(&params5);
let bytes1 = keypair1.to_bytes();
let bytes5 = keypair5.to_bytes();
@@ -503,11 +569,11 @@ mod tests {
#[test]
fn secret_key_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = keygen(&mut params1);
let keypair5 = keygen(&mut params5);
let keypair1 = keygen(&params1);
let keypair5 = keygen(&params5);
let bytes1 = keypair1.secret_key.to_bytes();
let bytes5 = keypair5.secret_key.to_bytes();
@@ -518,11 +584,11 @@ mod tests {
#[test]
fn verification_key_bytes_roundtrip() {
let mut params1 = setup(1).unwrap();
let mut params5 = setup(5).unwrap();
let params1 = setup(1).unwrap();
let params5 = setup(5).unwrap();
let keypair1 = &keygen(&mut params1);
let keypair5 = &keygen(&mut params5);
let keypair1 = &keygen(&params1);
let keypair5 = &keygen(&params5);
let bytes1: Vec<u8> = keypair1.verification_key.to_bytes();
let bytes5: Vec<u8> = keypair5.verification_key.to_bytes();
+125 -186
View File
@@ -11,13 +11,12 @@ 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};
use crate::Attribute;
pub mod aggregation;
pub mod issuance;
@@ -105,7 +104,7 @@ impl Base58 for Signature {}
#[derive(Debug)]
#[cfg_attr(test, derive(PartialEq))]
pub struct BlindedSignature(G1Projective, elgamal::Ciphertext);
pub struct BlindedSignature(G1Projective, G1Projective);
impl Bytable for BlindedSignature {
fn to_byte_vec(&self) -> Vec<u8> {
@@ -123,22 +122,26 @@ impl TryFrom<&[u8]> for BlindedSignature {
type Error = CoconutError;
fn try_from(bytes: &[u8]) -> Result<BlindedSignature> {
if bytes.len() != 144 {
if bytes.len() != 96 {
return Err(CoconutError::Deserialization(format!(
"BlindedSignature must be exactly 144 bytes, got {}",
"BlindedSignature must be exactly 96 bytes, got {}",
bytes.len()
)));
}
let h_bytes: &[u8; 48] = &bytes[..48].try_into().expect("Slice size != 48");
let sig_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..])?;
let sig = try_deserialize_g1_projective(
sig_bytes,
CoconutError::Deserialization("Failed to deserialize compressed sig".to_string()),
)?;
Ok(BlindedSignature(h, c_tilde))
Ok(BlindedSignature(h, sig))
}
}
@@ -146,16 +149,15 @@ 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,
pedersen_commitments_openings: &[Scalar],
) -> 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) {
@@ -164,20 +166,29 @@ impl BlindedSignature {
));
}
let blinding_removers = partial_verification_key
.beta_g1
.iter()
.zip(pedersen_commitments_openings.iter())
.map(|(beta, opening)| beta * opening)
.sum::<G1Projective>();
let unblinded_c = c - blinding_removers;
let alpha = partial_verification_key.alpha;
let tmp = private_attributes
let signed_attributes = private_attributes
.iter()
.chain(public_attributes.iter())
.zip(partial_verification_key.beta.iter())
.zip(partial_verification_key.beta_g2.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(),
&G2Prepared::from((alpha + signed_attributes).to_affine()),
&unblinded_c.to_affine(),
params.prepared_miller_g2(),
) {
return Err(CoconutError::Unblind(
@@ -185,13 +196,13 @@ impl BlindedSignature {
));
}
Ok(Signature(self.0, sig2))
Ok(Signature(*h, unblinded_c))
}
pub fn to_bytes(&self) -> [u8; 144] {
let mut bytes = [0u8; 144];
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_bytes());
bytes[48..].copy_from_slice(&self.1.to_affine().to_compressed());
bytes
}
@@ -239,124 +250,97 @@ mod tests {
#[test]
fn unblind_returns_error_if_integrity_check_on_commitment_hash_fails() {
let mut params = Parameters::new(2).unwrap();
let 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 (_commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&mut params);
let keypair1 = keygen(&params);
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap();
let sig1 = blind_sign(&params, &keypair1.secret_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());
let wrong_commitments_openings = params.n_random_scalars(private_attributes.len());
assert!(sig1
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes,
&[],
&fake_commitment_hash,
&wrong_commitments_openings,
)
.is_err());
}
#[test]
fn unblind_returns_error_if_signature_verification_fails() {
let mut params = Parameters::new(2).unwrap();
let 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 (commitments_openings, lambda) =
prepare_blind_sign(&params, &private_attributes, &[]).unwrap();
let keypair1 = keygen(&mut params);
let keypair1 = keygen(&params);
let sig1 = blind_sign(
&mut params,
&keypair1.secret_key(),
elgamal_keypair.public_key(),
&lambda,
&[],
)
.unwrap();
let sig1 = blind_sign(&params, &keypair1.secret_key(), &lambda, &[]).unwrap();
assert!(sig1
.unblind(
&params,
elgamal_keypair.private_key(),
&keypair1.verification_key(),
&private_attributes2,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.is_err());
}
#[test]
fn verification_on_two_private_attributes() {
let mut params = Parameters::new(2).unwrap();
let 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 keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let lambda =
prepare_blind_sign(&mut params, &elgamal_keypair, &private_attributes, &[]).unwrap();
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &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 sig1 = blind_sign(&params, &keypair1.secret_key(), &lambda, &[])
.unwrap()
.unblind(
&params,
&keypair1.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.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 sig2 = blind_sign(&params, &keypair2.secret_key(), &lambda, &[])
.unwrap()
.unblind(
&params,
&keypair2.verification_key(),
&private_attributes,
&[],
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&mut params,
&params,
&keypair1.verification_key(),
&sig1,
serial_number,
@@ -365,7 +349,7 @@ mod tests {
.unwrap();
let theta2 = prove_bandwidth_credential(
&mut params,
&params,
&keypair2.verification_key(),
&sig2,
serial_number,
@@ -400,8 +384,8 @@ mod tests {
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 keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let sig1 = sign(&mut params, &keypair1.secret_key(), &attributes).unwrap();
let sig2 = sign(&mut params, &keypair2.secret_key(), &attributes).unwrap();
@@ -429,62 +413,44 @@ mod tests {
#[test]
fn verification_on_two_public_and_two_private_attributes() {
let mut params = Parameters::new(4).unwrap();
let 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 keypair1 = keygen(&params);
let keypair2 = keygen(&params);
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &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 sig1 = blind_sign(&params, &keypair1.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind(
&params,
&keypair1.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.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 sig2 = blind_sign(&params, &keypair2.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind(
&params,
&keypair2.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap();
let theta1 = prove_bandwidth_credential(
&mut params,
&params,
&keypair1.verification_key(),
&sig1,
serial_number,
@@ -493,7 +459,7 @@ mod tests {
.unwrap();
let theta2 = prove_bandwidth_credential(
&mut params,
&params,
&keypair2.verification_key(),
&sig2,
serial_number,
@@ -525,43 +491,31 @@ mod tests {
#[test]
fn verification_on_two_public_and_two_private_attributes_from_two_signers() {
let mut params = Parameters::new(4).unwrap();
let 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 keypairs = ttp_keygen(&params, 2, 3).unwrap();
let lambda = prepare_blind_sign(
&mut params,
&elgamal_keypair,
&private_attributes,
&public_attributes,
)
.unwrap();
let (commitments_openings, lambda) =
prepare_blind_sign(&params, &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()
blind_sign(&params, &keypair.secret_key(), &lambda, &public_attributes)
.unwrap()
.unblind(
&params,
&keypair.verification_key(),
&private_attributes,
&public_attributes,
&lambda.get_commitment_hash(),
&commitments_openings,
)
.unwrap()
})
.collect::<Vec<_>>();
@@ -579,14 +533,9 @@ mod tests {
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();
let theta =
prove_bandwidth_credential(&params, &aggr_vk, &aggr_sig, serial_number, binding_number)
.unwrap();
assert!(verify_credential(
&params,
@@ -601,14 +550,9 @@ mod tests {
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();
let theta =
prove_bandwidth_credential(&params, &aggr_vk, &aggr_sig, serial_number, binding_number)
.unwrap();
assert!(verify_credential(
&params,
@@ -641,18 +585,13 @@ mod tests {
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 blinded_sig = BlindedSignature(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(),
blinded_sig.1.to_affine().to_compressed(),
]
.concat();
assert_eq!(expected_bytes, bytes);
+11 -9
View File
@@ -124,7 +124,7 @@ pub fn compute_kappa(
+ verification_key.alpha
+ private_attributes
.iter()
.zip(verification_key.beta.iter())
.zip(verification_key.beta_g2.iter())
.map(|(priv_attr, beta_i)| beta_i * priv_attr)
.sum::<G2Projective>()
}
@@ -140,11 +140,11 @@ pub fn prove_bandwidth_credential(
serial_number: Attribute,
binding_number: Attribute,
) -> Result<Theta> {
if verification_key.beta.len() < 2 {
if verification_key.beta_g2.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()
verification_key.beta_g2.len()
)));
}
@@ -205,7 +205,9 @@ pub fn verify_credential(
theta: &Theta,
public_attributes: &[Attribute],
) -> bool {
if public_attributes.len() + theta.pi_v.private_attributes_len() > verification_key.beta.len() {
if public_attributes.len() + theta.pi_v.private_attributes_len()
> verification_key.beta_g2.len()
{
return false;
}
@@ -220,7 +222,7 @@ pub fn verify_credential(
.iter()
.zip(
verification_key
.beta
.beta_g2
.iter()
.skip(theta.pi_v.private_attributes_len()),
)
@@ -249,7 +251,7 @@ pub fn verify(
let kappa = (verification_key.alpha
+ public_attributes
.iter()
.zip(verification_key.beta.iter())
.zip(verification_key.beta_g2.iter())
.map(|(m_i, b_i)| b_i * m_i)
.sum::<G2Projective>())
.to_affine();
@@ -271,9 +273,9 @@ mod tests {
#[test]
fn theta_bytes_roundtrip() {
let mut params = setup(2).unwrap();
let params = setup(2).unwrap();
let keypair = keygen(&mut params);
let keypair = keygen(&params);
let r = params.random_scalar();
let s = params.random_scalar();
@@ -282,7 +284,7 @@ mod tests {
let binding_number = params.random_scalar();
let theta = prove_bandwidth_credential(
&mut params,
&params,
&keypair.verification_key(),
&signature,
serial_number,
+16 -27
View File
@@ -1,9 +1,11 @@
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,
aggregate_signature_shares, aggregate_verification_keys, blind_sign, prepare_blind_sign,
prove_bandwidth_credential, setup, ttp_keygen, verify_credential, CoconutError, Signature,
SignatureShare, VerificationKey,
};
use itertools::izip;
#[test]
fn main() -> Result<(), CoconutError> {
let params = setup(5)?;
@@ -13,15 +15,9 @@ fn main() -> Result<(), CoconutError> {
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 commitment
let (commitments_openings, blind_sign_request) =
prepare_blind_sign(&params, &private_attributes, &public_attributes)?;
// generate_keys
let coconut_keypairs = ttp_keygen(&params, 2, 3)?;
@@ -41,7 +37,6 @@ fn main() -> Result<(), CoconutError> {
let blinded_signature = blind_sign(
&params,
&keypair.secret_key(),
&elgamal_keypair.public_key(),
&blind_sign_request,
&public_attributes,
)?;
@@ -49,26 +44,22 @@ fn main() -> Result<(), CoconutError> {
}
// Unblind
let unblinded_signatures: Vec<Signature> = blinded_signatures
.into_iter()
.zip(verification_keys.iter())
.map(|(signature, verification_key)| {
signature
.unblind(
let unblinded_signatures: Vec<Signature> =
izip!(blinded_signatures.iter(), verification_keys.iter())
.map(|(s, vk)| {
s.unblind(
&params,
&elgamal_keypair.private_key(),
&verification_key,
&vk,
&private_attributes,
&public_attributes,
&blind_sign_request.get_commitment_hash(),
&commitments_openings,
)
.unwrap()
})
.collect();
})
.collect();
// Aggregate signatures
let signature_shares: Vec<SignatureShare> = unblinded_signatures
.iter()
.enumerate()
@@ -84,7 +75,6 @@ fn main() -> Result<(), CoconutError> {
aggregate_signature_shares(&params, &verification_key, &attributes, &signature_shares)?;
// Generate cryptographic material to verify them
let theta = prove_bandwidth_credential(
&params,
&verification_key,
@@ -94,7 +84,6 @@ fn main() -> Result<(), CoconutError> {
)?;
// Verify credentials
assert!(verify_credential(
&params,
&verification_key,
+1 -1
View File
@@ -923,7 +923,7 @@ mod fragment_header {
let header_bytes = fragmented_header.to_bytes();
let header_bytes = &header_bytes[..header_bytes.len() - 1];
assert!(FragmentHeader::try_from_bytes(&header_bytes).is_err())
assert!(FragmentHeader::try_from_bytes(header_bytes).is_err())
}
#[test]
@@ -446,7 +446,7 @@ mod reconstruction_buffer {
.collect();
for raw_fragment in raw_fragments.iter().take(u8::max_value() as usize - 1) {
buf.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap());
buf.insert_fragment(Fragment::try_from_bytes(raw_fragment).unwrap());
}
assert!(!buf.is_complete);
@@ -1268,7 +1268,7 @@ mod message_reconstructor {
.collect();
for raw_fragment in raw_fragments.iter().take(u8::max_value() as usize) {
set_buf1.insert_fragment(Fragment::try_from_bytes(&raw_fragment).unwrap());
set_buf1.insert_fragment(Fragment::try_from_bytes(raw_fragment).unwrap());
}
set_buf2.insert_fragment(Fragment::try_from_bytes(&raw_fragments[255]).unwrap());
+1 -1
View File
@@ -162,7 +162,7 @@ mod tests {
#[test]
fn is_cover_works_for_identical_input() {
assert!(is_cover(&LOOP_COVER_MESSAGE_PAYLOAD))
assert!(is_cover(LOOP_COVER_MESSAGE_PAYLOAD))
}
#[test]
@@ -123,7 +123,7 @@ mod tests {
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none())
assert!(available_reader.next().await.is_none());
}
#[tokio::test]
@@ -135,7 +135,7 @@ mod tests {
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none())
assert!(available_reader.next().await.is_none());
}
#[tokio::test]
@@ -157,7 +157,7 @@ mod tests {
// before dropping the mock, we need to empty it
let mut buf = vec![0u8; second_data_chunk.len()];
reader_mock.read(&mut buf).await.unwrap();
assert_eq!(reader_mock.read(&mut buf).await.unwrap(), 100);
}
#[tokio::test]
@@ -173,7 +173,7 @@ mod tests {
let read_data = available_reader.next().await.unwrap().unwrap();
assert_eq!(read_data, data);
assert!(available_reader.next().await.is_none())
assert!(available_reader.next().await.is_none());
}
// perhaps the issue of tokio io builder will be resolved in tokio 0.3?
+21 -9
View File
@@ -218,9 +218,9 @@ dependencies = [
[[package]]
name = "cosmwasm-crypto"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f903ebbabc0d4880dbc76148efb8be8fc29fa4bf294c440c3d70da1c8bcafff7"
checksum = "8904127a5b9e325ef5d6b2b3f997dcd74943cd35097139b1a4d15b1b6bccae66"
dependencies = [
"digest 0.9.0",
"ed25519-zebra",
@@ -231,9 +231,9 @@ dependencies = [
[[package]]
name = "cosmwasm-derive"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "832bebef577ecb394603de8e2bf0de429b74aa364e17dec18e15ce37e71b0cae"
checksum = "a14364ac4d9d085867929d0cf3e94b1d2100121ce02c33c72961406830002613"
dependencies = [
"syn",
]
@@ -250,9 +250,9 @@ dependencies = [
[[package]]
name = "cosmwasm-std"
version = "1.0.0-beta4"
version = "1.0.0-beta5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6238c45840cc9de5a39f0f619e3a4f7c38c5d2c6ac9e3e4d72751ee045e6d7da"
checksum = "e2ece12e5bbde434b93937d7b2107e6291f11d69ffa72398c50e8bab41d451d3"
dependencies = [
"base64",
"cosmwasm-crypto",
@@ -368,6 +368,17 @@ dependencies = [
"serde",
]
[[package]]
name = "cw-storage-plus"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c087ff98fb0475db4c2b5298a5fd12b2848d2854b39d1115d930ee6da24d1eed"
dependencies = [
"cosmwasm-std",
"schemars",
"serde",
]
[[package]]
name = "der"
version = "0.4.5"
@@ -804,13 +815,14 @@ checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
name = "mixnet-contract"
version = "0.1.0"
dependencies = [
"az",
"bs58",
"config",
"cosmwasm-schema",
"cosmwasm-std",
"cosmwasm-storage",
"crypto",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"fixed",
"mixnet-contract-common",
"rand",
@@ -1512,7 +1524,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.12.1",
"mixnet-contract-common",
"schemars",
"serde",
@@ -1526,7 +1538,7 @@ version = "0.1.0"
dependencies = [
"config",
"cosmwasm-std",
"cw-storage-plus",
"cw-storage-plus 0.11.1",
"mixnet-contract-common",
"schemars",
"serde",
+2 -1
View File
@@ -22,8 +22,9 @@ config = { path = "../../common/config"}
cosmwasm-std = "1.0.0-beta3"
cosmwasm-storage = "1.0.0-beta3"
cw-storage-plus = "0.11.1"
cw-storage-plus = "0.12.1"
az = "1.2.0"
bs58 = "0.4.0"
schemars = "0.8"
serde = { version = "1.0.103", default-features = false, features = ["derive"] }
+1
View File
@@ -16,5 +16,6 @@ pub const ACTIVE_SET_WORK_FACTOR: u8 = 10;
// and we can't change this easily to `Duration`, because then the entire rewarded set storage
// would be messed up... (as we look up stuff "by blocks")
pub const REWARDED_SET_REFRESH_BLOCKS: u64 = 720; // with blocktime being approximately 5s, it should be roughly 1h
pub const EPOCHS_IN_INTERVAL: u64 = 720; // Hours in a month
pub const REWARDING_INTERVAL_LENGTH: Duration = Duration::from_secs(60 * 60 * 720); // 720h, i.e. 30 days
+60 -13
View File
@@ -86,7 +86,7 @@ pub fn instantiate(
mixnet_params_storage::CONTRACT_STATE.save(deps.storage, &state)?;
mixnet_params_storage::LAYERS.save(deps.storage, &Default::default())?;
rewards_storage::REWARD_POOL.save(deps.storage, &Uint128::new(INITIAL_REWARD_POOL))?;
interval_storage::CURRENT_INTERVAL.save(deps.storage, &rewarding_interval)?;
interval_storage::save_interval(deps.storage, &rewarding_interval)?;
interval_storage::CURRENT_REWARDED_SET_HEIGHT.save(deps.storage, &env.block.height)?;
Ok(Response::default())
@@ -112,7 +112,7 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnode {} => {
crate::mixnodes::transactions::try_remove_mixnode(deps, info)
crate::mixnodes::transactions::try_remove_mixnode(env, deps, info)
}
ExecuteMsg::UpdateMixnodeConfig {
profit_margin_percent,
@@ -168,19 +168,20 @@ pub fn execute(
ExecuteMsg::UndelegateFromMixnode { mix_identity } => {
crate::delegations::transactions::try_remove_delegation_from_mixnode(
deps,
env,
info,
mix_identity,
)
}
ExecuteMsg::RewardNextMixDelegators {
mix_identity,
interval_id,
} => crate::rewards::transactions::try_reward_next_mixnode_delegators(
deps,
info,
mix_identity,
interval_id,
),
// ExecuteMsg::RewardNextMixDelegators {
// mix_identity,
// interval_id,
// } => crate::rewards::transactions::try_reward_next_mixnode_delegators(
// deps,
// info,
// mix_identity,
// interval_id,
// ),
ExecuteMsg::DelegateToMixnodeOnBehalf {
mix_identity,
delegate,
@@ -196,6 +197,7 @@ pub fn execute(
delegate,
} => crate::delegations::transactions::try_remove_delegation_from_mixnode_on_behalf(
deps,
env,
info,
mix_identity,
delegate,
@@ -213,7 +215,7 @@ pub fn execute(
owner_signature,
),
ExecuteMsg::UnbondMixnodeOnBehalf { owner } => {
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(deps, info, owner)
crate::mixnodes::transactions::try_remove_mixnode_on_behalf(env, deps, info, owner)
}
ExecuteMsg::BondGatewayOnBehalf {
gateway,
@@ -243,6 +245,45 @@ pub fn execute(
ExecuteMsg::AdvanceCurrentInterval {} => {
crate::interval::transactions::try_advance_interval(env, deps.storage)
}
ExecuteMsg::AdvanceCurrentEpoch {} => {
crate::interval::transactions::try_advance_epoch(env, deps.storage)
}
ExecuteMsg::CompoundDelegatorReward { mix_identity } => {
crate::rewards::transactions::try_compound_delegator_reward(
deps,
env,
info,
mix_identity,
)
}
ExecuteMsg::CompoundOperatorReward {} => {
crate::rewards::transactions::try_compound_operator_reward(deps, env, info)
}
ExecuteMsg::CompoundDelegatorRewardOnBehalf {
owner,
mix_identity,
} => crate::rewards::transactions::try_compound_delegator_reward_on_behalf(
deps,
env,
info,
owner,
mix_identity,
),
ExecuteMsg::CompoundOperatorRewardOnBehalf { owner } => {
crate::rewards::transactions::try_compound_operator_reward_on_behalf(
deps, env, info, owner,
)
}
ExecuteMsg::ReconcileDelegations {} => {
crate::delegations::transactions::try_reconcile_all_delegation_events(deps, info)
}
ExecuteMsg::CheckpointMixnodes {} => {
crate::mixnodes::transactions::try_checkpoint_mixnodes(
deps.storage,
env.block.height,
info,
)
}
}
}
@@ -288,7 +329,12 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetDelegationDetails {
mix_identity,
delegator,
} => to_binary(&query_mixnode_delegation(deps, mix_identity, delegator)?),
} => to_binary(&query_mixnode_delegation(
deps.storage,
deps.api,
mix_identity,
delegator,
)?),
QueryMsg::GetRewardPool {} => to_binary(&query_reward_pool(deps)?),
QueryMsg::GetCirculatingSupply {} => to_binary(&query_circulating_supply(deps)?),
QueryMsg::GetIntervalRewardPercent {} => to_binary(&INTERVAL_REWARD_PERCENT),
@@ -321,6 +367,7 @@ pub fn query(deps: Deps<'_>, env: Env, msg: QueryMsg) -> Result<QueryResponse, C
QueryMsg::GetRewardedSetRefreshBlocks {} => {
to_binary(&query_rewarded_set_refresh_minimum_blocks())
}
QueryMsg::GetEpochsInInterval {} => to_binary(&crate::constants::EPOCHS_IN_INTERVAL),
};
Ok(query_res?)
+64 -49
View File
@@ -3,9 +3,9 @@
use super::storage;
use crate::error::ContractError;
use cosmwasm_std::Deps;
use cosmwasm_std::Order;
use cosmwasm_std::StdResult;
use cosmwasm_std::{Api, Deps, Storage};
use cw_storage_plus::{Bound, PrimaryKey};
use mixnet_contract_common::{
Delegation, IdentityKey, PagedAllDelegationsResponse, PagedDelegatorDelegationsResponse,
@@ -14,16 +14,14 @@ use mixnet_contract_common::{
pub(crate) fn query_all_network_delegations_paged(
deps: Deps<'_>,
start_after: Option<(IdentityKey, String)>,
start_after: Option<(IdentityKey, Vec<u8>, u64)>,
limit: Option<u32>,
) -> StdResult<PagedAllDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|start| start.joined_key())
.map(Bound::exclusive);
let start = start_after.map(Bound::exclusive);
let delegations = storage::delegations()
.range(deps.storage, start, None, Order::Ascending)
@@ -52,8 +50,9 @@ pub(crate) fn query_delegator_delegations_paged(
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|mix_identity| Bound::Exclusive((mix_identity, validated_owner.clone()).joined_key()));
let start = start_after.map(|mix_identity| {
Bound::ExclusiveRaw((mix_identity, validated_owner.clone()).joined_key())
});
let delegations = storage::delegations()
.idx
@@ -76,35 +75,47 @@ pub(crate) fn query_delegator_delegations_paged(
// queries for delegation value of given address for particular node
pub(crate) fn query_mixnode_delegation(
deps: Deps<'_>,
storage: &dyn Storage,
api: &dyn Api,
mix_identity: IdentityKey,
delegator: String,
) -> Result<Delegation, ContractError> {
let validated_delegator = deps.api.addr_validate(&delegator)?;
let storage_key = (mix_identity.clone(), validated_delegator.clone()).joined_key();
) -> Result<Vec<Delegation>, ContractError> {
let validated_delegator = api.addr_validate(&delegator)?;
let storage_key = (
mix_identity.clone(),
validated_delegator.as_bytes().to_vec(),
);
storage::delegations()
.may_load(deps.storage, storage_key)?
.ok_or(ContractError::NoMixnodeDelegationFound {
let delegations = storage::delegations()
.prefix(storage_key)
.range(storage, None, None, Order::Ascending)
.filter_map(|d| d.ok())
.map(|r| r.1)
.collect::<Vec<Delegation>>();
if delegations.is_empty() {
Err(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: validated_delegator,
address: delegator,
})
} else {
Ok(delegations)
}
}
pub(crate) fn query_mixnode_delegations_paged(
deps: Deps<'_>,
mix_identity: IdentityKey,
start_after: Option<String>,
start_after: Option<(String, u64)>,
limit: Option<u32>,
) -> StdResult<PagedMixDelegationsResponse> {
let limit = limit
.unwrap_or(storage::DELEGATION_PAGE_DEFAULT_LIMIT)
.min(storage::DELEGATION_PAGE_MAX_LIMIT) as usize;
let start = start_after
.map(|addr| deps.api.addr_validate(&addr))
.transpose()?
.map(|addr| Bound::Exclusive((mix_identity.clone(), addr).joined_key()));
let start = start_after.map(|(addr, height)| {
Bound::ExclusiveRaw((mix_identity.clone(), addr.as_bytes(), height).joined_key())
});
let delegations = storage::delegations()
.idx
@@ -115,7 +126,9 @@ pub(crate) fn query_mixnode_delegations_paged(
.map(|record| record.map(|r| r.1))
.collect::<StdResult<Vec<_>>>()?;
let start_next_after = delegations.last().map(|delegation| delegation.owner());
let start_next_after = delegations
.last()
.map(|delegation| (delegation.owner(), delegation.block_height()));
Ok(PagedMixDelegationsResponse::new(
delegations,
@@ -245,25 +258,26 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page1);
let start_after = page1.start_next_after.unwrap();
assert_eq!(2, page1.delegations.len());
assert_eq!("200".to_string(), page1.start_next_after.unwrap());
assert_eq!(("200".to_string(), 12345), start_after);
// retrieving the next page should start after the last key on this page
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity.clone(),
Option::from(start_after),
Option::from(start_after.clone()),
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page2);
assert_eq!(1, page2.delegations.len());
// save another one
test_helpers::save_dummy_delegation(&mut deps.storage, &node_identity, "400");
let start_after = "200".to_string();
let page2 = query_mixnode_delegations_paged(
deps.as_ref(),
node_identity,
@@ -271,6 +285,7 @@ pub(crate) mod tests {
Option::from(per_page),
)
.unwrap();
// println!("{:?}", page2);
// now we have 2 pages, with 2 results on the second page
assert_eq!(2, page2.delegations.len());
@@ -408,16 +423,17 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Ok(delegation),
query_mixnode_delegation(deps.as_ref(), node_identity, delegation_owner.to_string())
Ok(vec![delegation]),
query_mixnode_delegation(
&deps.storage,
&deps.api,
node_identity,
delegation_owner.to_string()
)
)
}
@@ -433,10 +449,11 @@ pub(crate) mod tests {
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
address: delegation_owner1.to_string(),
}),
query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
)
@@ -452,20 +469,17 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: delegation_owner1.clone(),
address: delegation_owner1.to_string(),
}),
query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
node_identity1.clone(),
delegation_owner1.to_string()
)
@@ -481,19 +495,20 @@ pub(crate) mod tests {
);
storage::delegations()
.save(
deps.as_mut().storage,
delegation.storage_key().joined_key(),
&delegation,
)
.save(deps.as_mut().storage, delegation.storage_key(), &delegation)
.unwrap();
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: node_identity1.clone(),
address: Addr::unchecked(delegation_owner1.clone())
address: delegation_owner1.to_string()
}),
query_mixnode_delegation(deps.as_ref(), node_identity1, delegation_owner1.to_string())
query_mixnode_delegation(
&deps.storage,
&deps.api,
node_identity1,
delegation_owner1.to_string()
)
)
}
+24 -9
View File
@@ -1,20 +1,27 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use cw_storage_plus::{Index, IndexList, IndexedMap, MultiIndex};
use mixnet_contract_common::{Addr, Delegation, IdentityKey};
use cw_storage_plus::{Index, IndexList, IndexedMap, Map, MultiIndex};
use mixnet_contract_common::{mixnode::DelegationEvent, Addr, Delegation, IdentityKey};
// storage prefixes
const DELEGATION_PK_NAMESPACE: &str = "dl";
const DELEGATION_OWNER_IDX_NAMESPACE: &str = "dlo";
const DELEGATION_MIXNODE_IDX_NAMESPACE: &str = "dlm";
pub const PENDING_DELEGATION_EVENTS: Map<
(BlockHeight, IdentityKey, OwnerAddress),
DelegationEvent,
> = Map::new("pend");
// paged retrieval limits for all queries and transactions
pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 500;
pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 250;
// It's a composite key on node's identity and delegator address
type PrimaryKey = Vec<u8>;
type BlockHeight = u64;
type OwnerAddress = Vec<u8>;
// It's a composite key on node's identity, delegator address, and block height
type PrimaryKey = (IdentityKey, OwnerAddress, BlockHeight);
pub(crate) struct DelegationIndex<'a> {
pub(crate) owner: MultiIndex<'a, Addr, Delegation>,
@@ -73,7 +80,6 @@ mod tests {
use config::defaults::DENOM;
use cosmwasm_std::testing::mock_env;
use cosmwasm_std::{coin, Order};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::Delegation;
#[test]
@@ -94,7 +100,7 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity, delegation_owner.clone()).joined_key(),
(node_identity, delegation_owner.as_bytes().to_vec(), 0),
&dummy_data,
)
.unwrap();
@@ -124,7 +130,8 @@ mod tests {
assert!(test_helpers::read_delegation(
deps.as_ref().storage,
&node_identity1,
&delegation_owner1
delegation_owner1.as_bytes(),
mock_env().block.height
)
.is_none());
@@ -139,7 +146,11 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner1.clone()).joined_key(),
(
node_identity1.clone(),
delegation_owner1.as_bytes().to_vec(),
0,
),
&dummy_data,
)
.unwrap();
@@ -163,7 +174,11 @@ mod tests {
storage::delegations()
.save(
&mut deps.storage,
(node_identity1.clone(), delegation_owner2).joined_key(),
(
node_identity1.clone(),
delegation_owner2.as_bytes().to_vec(),
0,
),
&dummy_data,
)
.unwrap();
+462 -168
View File
@@ -1,17 +1,69 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use super::storage;
use super::storage::{self, PENDING_DELEGATION_EVENTS};
use crate::error::ContractError;
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::support::helpers::generate_storage_key;
use config::defaults::DENOM;
use cosmwasm_std::{coins, wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::events::{new_delegation_event, new_undelegation_event};
use cosmwasm_std::{
coins, wasm_execute, Addr, Api, BankMsg, Coin, DepsMut, Env, Event, MessageInfo, Order,
Response, Storage, Uint128, WasmMsg,
};
use mixnet_contract_common::events::{
new_pending_delegation_event, new_pending_undelegation_event, new_undelegation_event,
};
use mixnet_contract_common::mixnode::{DelegationEvent, PendingUndelegate};
use mixnet_contract_common::{Delegation, IdentityKey};
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
pub fn try_reconcile_all_delegation_events(
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, ContractError> {
let state = mixnet_params_storage::CONTRACT_STATE.load(deps.storage)?;
// check if this is executed by the permitted validator, if not reject the transaction
if info.sender != state.rewarding_validator_address {
return Err(ContractError::Unauthorized);
}
_try_reconcile_all_delegation_events(deps.storage, deps.api)
}
// TODO: Error handling?
pub(crate) fn _try_reconcile_all_delegation_events(
storage: &mut dyn Storage,
api: &dyn Api,
) -> Result<Response, ContractError> {
let pending_delegation_events = PENDING_DELEGATION_EVENTS
.range(storage, None, None, Order::Ascending)
.filter_map(|r| r.ok())
.collect::<Vec<((u64, String, Vec<u8>), DelegationEvent)>>();
let mut response = Response::new();
for (key, delegation_event) in pending_delegation_events {
match delegation_event {
DelegationEvent::Delegate(delegation) => {
let event = try_reconcile_delegation(storage, delegation)?;
response = response.add_event(event);
}
DelegationEvent::Undelegate(pending_undelegate) => {
let undelegate_response =
try_reconcile_undelegation(storage, api, &pending_undelegate)?;
response = response.add_event(undelegate_response.event);
response = response.add_message(undelegate_response.bank_msg);
if let Some(msg) = undelegate_response.wasm_msg {
response = response.add_message(msg);
}
}
}
PENDING_DELEGATION_EVENTS.remove(storage, key);
}
Ok(response)
}
fn validate_delegation_stake(mut delegation: Vec<Coin>) -> Result<Coin, ContractError> {
// check if anything was put as delegation
if delegation.is_empty() {
@@ -44,7 +96,15 @@ pub(crate) fn try_delegate_to_mixnode(
// check if the delegation contains any funds of the appropriate denomination
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(deps, env, mix_identity, info.sender.as_str(), amount, None)
_try_delegate_to_mixnode(
deps.storage,
deps.api,
env.block.height,
&mix_identity,
info.sender.as_str(),
amount,
None,
)
}
pub(crate) fn try_delegate_to_mixnode_on_behalf(
@@ -58,173 +118,277 @@ pub(crate) fn try_delegate_to_mixnode_on_behalf(
let amount = validate_delegation_stake(info.funds)?;
_try_delegate_to_mixnode(
deps,
env,
mix_identity,
deps.storage,
deps.api,
env.block.height,
&mix_identity,
&delegate,
amount,
Some(info.sender),
)
}
pub(crate) fn _try_delegate_to_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_identity: IdentityKey,
delegate: &str,
amount: Coin,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
// check if the target node actually exists
if mixnodes_storage::mixnodes()
.may_load(deps.storage, &mix_identity)?
.is_none()
{
return Err(ContractError::MixNodeBondNotFound {
identity: mix_identity,
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
pub(crate) fn try_reconcile_delegation(
storage: &mut dyn Storage,
delegation: Delegation,
) -> Result<Event, ContractError> {
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
storage,
&delegation.node_identity,
|total_delegation| {
// since we know that the target node exists and because the total_delegation bucket
// entry is created whenever the node itself is added, the unwrap here is fine
// as the entry MUST exist
Ok(total_delegation.unwrap() + amount.amount)
Ok(total_delegation.unwrap() + delegation.amount.amount)
},
)?;
// update [or create new] delegation of this delegator
// update [or create new] pending delegation of this delegator
storage::delegations().update::<_, ContractError>(
deps.storage,
storage_key,
storage,
delegation.storage_key(),
|existing_delegation| {
Ok(match existing_delegation {
Some(mut existing_delegation) => {
existing_delegation.increment_amount(amount.amount, Some(env.block.height));
existing_delegation
.increment_amount(delegation.amount.amount, Some(delegation.block_height));
existing_delegation
}
None => Delegation::new(
delegate.to_owned(),
mix_identity.clone(),
amount.clone(),
env.block.height,
proxy.clone(),
),
None => delegation.clone(),
})
},
)?;
Ok(Response::new().add_event(new_delegation_event(
Ok(new_pending_delegation_event(
&delegation.owner,
&delegation.proxy,
&delegation.amount,
&delegation.node_identity,
))
}
pub(crate) fn _try_delegate_to_mixnode(
storage: &mut dyn Storage,
api: &dyn Api,
block_height: u64,
mix_identity: &str,
delegate: &str,
amount: Coin,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = api.addr_validate(delegate)?;
// check if the target node actually exists
if mixnodes_storage::mixnodes()
.may_load(storage, mix_identity)?
.is_none()
{
return Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
});
}
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (block_height, mix_identity.to_string(), maybe_proxy_storage);
storage::PENDING_DELEGATION_EVENTS.save(
storage,
storage_key,
&DelegationEvent::Delegate(Delegation::new(
delegate.to_owned(),
mix_identity.to_string(),
amount.clone(),
block_height,
proxy.clone(),
)),
)?;
Ok(Response::new().add_event(new_pending_delegation_event(
&delegate,
&proxy,
&amount,
&mix_identity,
mix_identity,
)))
}
pub(crate) fn try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, info.sender.as_str(), None)
_try_remove_delegation_from_mixnode(deps, env, mix_identity, info.sender.as_str(), None)
}
pub(crate) fn try_remove_delegation_from_mixnode_on_behalf(
deps: DepsMut<'_>,
env: Env,
info: MessageInfo,
mix_identity: IdentityKey,
delegate: String,
) -> Result<Response, ContractError> {
_try_remove_delegation_from_mixnode(deps, mix_identity, &delegate, Some(info.sender))
_try_remove_delegation_from_mixnode(deps, env, mix_identity, &delegate, Some(info.sender))
}
pub struct ReconcileUndelegateResponse {
bank_msg: BankMsg,
wasm_msg: Option<WasmMsg>,
event: Event,
}
pub(crate) fn try_reconcile_undelegation(
storage: &mut dyn Storage,
api: &dyn Api,
pending_undelegate: &PendingUndelegate,
) -> Result<ReconcileUndelegateResponse, ContractError> {
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(
&pending_undelegate.delegate(),
pending_undelegate.proxy().as_ref(),
);
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
);
let any_delegations = delegation_map
.prefix(storage_key.clone())
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.next()
.is_some();
if !any_delegations {
return Err(ContractError::NoMixnodeDelegationFound {
identity: pending_undelegate.mix_identity(),
address: pending_undelegate.delegate().to_string(),
});
}
let reward = crate::rewards::transactions::_try_compound_delegator_reward(
pending_undelegate.block_height(),
api,
storage,
pending_undelegate.delegate().as_str(),
&pending_undelegate.mix_identity(),
None,
)?;
// Might want to introduce paging here
let delegation_heights = delegation_map
.prefix(storage_key)
.keys(storage, None, None, cosmwasm_std::Order::Ascending)
.filter_map(|v| v.ok())
.collect::<Vec<u64>>();
if delegation_heights.is_empty() {
return Err(ContractError::NoMixnodeDelegationFound {
identity: pending_undelegate.mix_identity(),
address: pending_undelegate.delegate().to_string(),
});
}
let mut total_delegation = Uint128::zero();
if crate::mixnodes::storage::mixnodes()
.may_load(storage, &pending_undelegate.mix_identity())?
.is_none()
{
// Since the mixnode is no longer bonded the reward did not compound and we need to manually add it to the total
total_delegation = reward;
}
for h in delegation_heights {
let storage_key = (
pending_undelegate.mix_identity(),
maybe_proxy_storage.clone(),
h,
);
let delegation = delegation_map.load(storage, storage_key.clone())?;
total_delegation += delegation.amount.amount;
delegation_map.replace(storage, storage_key, None, Some(&delegation))?;
}
let bank_msg = BankMsg::Send {
to_address: pending_undelegate
.proxy()
.as_ref()
.unwrap_or(&pending_undelegate.delegate())
.to_string(),
amount: coins(total_delegation.u128(), DENOM),
};
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
storage,
&pending_undelegate.mix_identity(),
|total_node_delegation| {
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
// have never gotten here in the first place
// the second unwrap is also fine because we should NEVER underflow here,
// if we do, it means we have some serious error in our logic
Ok(total_node_delegation
.unwrap()
.checked_sub(total_delegation)
.unwrap())
},
)?;
let mut wasm_msg = None;
if let Some(proxy) = &pending_undelegate.proxy() {
let msg = Some(VestingContractExecuteMsg::TrackUndelegation {
owner: pending_undelegate.delegate().as_str().to_string(),
mix_identity: pending_undelegate.mix_identity(),
amount: Coin::new(total_delegation.u128(), DENOM),
});
wasm_msg = Some(wasm_execute(proxy, &msg, vec![one_ucoin()])?);
}
let event = new_undelegation_event(
&pending_undelegate.delegate(),
&pending_undelegate.proxy(),
&pending_undelegate.mix_identity(),
total_delegation,
);
Ok(ReconcileUndelegateResponse {
bank_msg,
wasm_msg,
event,
})
}
pub(crate) fn _try_remove_delegation_from_mixnode(
deps: DepsMut<'_>,
env: Env,
mix_identity: IdentityKey,
delegate: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let delegate = deps.api.addr_validate(delegate)?;
let delegation_map = storage::delegations();
let maybe_proxy_storage = generate_storage_key(&delegate, proxy.as_ref());
let storage_key = (mix_identity.clone(), maybe_proxy_storage).joined_key();
match delegation_map.may_load(deps.storage, storage_key.clone())? {
None => Err(ContractError::NoMixnodeDelegationFound {
identity: mix_identity,
address: delegate,
}),
Some(old_delegation) => {
// remove all delegation associated with this delegator
if proxy != old_delegation.proxy {
return Err(ContractError::ProxyMismatch {
existing: old_delegation
.proxy
.map_or_else(|| "None".to_string(), |a| a.to_string()),
incoming: proxy.map_or_else(|| "None".to_string(), |a| a.to_string()),
});
}
// remove old delegation data from the store
// note for reviewers: I'm using `replace` as `remove` is just `may_load` followed by `replace`
// and we've already performed `may_load` and have access to pre-existing data
delegation_map.replace(deps.storage, storage_key, None, Some(&old_delegation))?;
PENDING_DELEGATION_EVENTS.save(
deps.storage,
(
env.block.height,
mix_identity.to_string(),
delegate.as_bytes().to_vec(),
),
&DelegationEvent::Undelegate(PendingUndelegate::new(
mix_identity.to_string(),
delegate.clone(),
proxy.clone(),
env.block.height,
)),
)?;
// send delegated funds back to the delegation owner
let return_tokens = BankMsg::Send {
to_address: proxy.as_ref().unwrap_or(&delegate).to_string(),
amount: coins(
old_delegation.amount.amount.u128(),
old_delegation.amount.denom.clone(),
),
};
// update total_delegation of this node
mixnodes_storage::TOTAL_DELEGATION.update::<_, ContractError>(
deps.storage,
&mix_identity,
|total_delegation| {
// the first unwrap is fine because the delegation information MUST exist, otherwise we would
// have never gotten here in the first place
// the second unwrap is also fine because we should NEVER underflow here,
// if we do, it means we have some serious error in our logic
Ok(total_delegation
.unwrap()
.checked_sub(old_delegation.amount.amount)
.unwrap())
},
)?;
let mut response = Response::new().add_message(return_tokens);
if let Some(proxy) = &proxy {
let msg = Some(VestingContractExecuteMsg::TrackUndelegation {
owner: delegate.as_str().to_string(),
mix_identity: mix_identity.clone(),
amount: old_delegation.amount.clone(),
});
let track_undelegation_msg = wasm_execute(proxy, &msg, vec![one_ucoin()])?;
response = response.add_message(track_undelegation_msg);
}
Ok(response.add_event(new_undelegation_event(
&delegate,
&proxy,
&old_delegation,
&mix_identity,
)))
}
}
Ok(Response::new().add_event(new_pending_undelegation_event(
&delegate,
&proxy,
&mix_identity,
)))
}
#[cfg(test)]
@@ -332,6 +496,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
@@ -342,7 +508,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
// node's "total_delegation" is increased
@@ -364,7 +536,7 @@ mod tests {
deps.as_mut(),
);
let delegation_owner = Addr::unchecked("sender");
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -387,7 +559,7 @@ mod tests {
tests::fixtures::good_mixnode_pledge(),
deps.as_mut(),
);
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -403,6 +575,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
@@ -413,7 +587,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
// node's "total_delegation" is increased
@@ -437,33 +617,47 @@ mod tests {
let delegation_owner = Addr::unchecked("sender");
let delegation1 = coin(100, DENOM);
let delegation2 = coin(50, DENOM);
let mut env = mock_env();
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
env.clone(),
mock_info(delegation_owner.as_str(), &[delegation1.clone()]),
identity.clone(),
)
.unwrap();
env.block.height += 1;
try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
env,
mock_info(delegation_owner.as_str(), &[delegation2.clone()]),
identity.clone(),
)
.unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
identity.clone(),
coin(delegation1.amount.u128() + delegation2.amount.u128(), DENOM),
mock_env().block.height,
None,
);
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, &identity, delegation_owner).unwrap()
);
// let expected = Delegation::new(
// delegation_owner.clone(),
// identity.clone(),
// coin(delegation1.amount.u128() + delegation2.amount.u128(), DENOM),
// mock_env().block.height,
// None,
// );
// assert_eq!(
// expected,
// test_helpers::read_delegation(
// &deps.storage,
// &identity,
// delegation_owner.as_bytes(),
// mock_env().block.height
// )
// .unwrap()
// );
// node's "total_delegation" is sum of both
assert_eq!(
@@ -493,16 +687,24 @@ mod tests {
env2.block.height = updated_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
env1.clone(),
mock_info(delegation_owner.as_str(), &[delegation.clone()]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
@@ -512,11 +714,21 @@ mod tests {
)
.unwrap();
let updated =
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner).unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(delegation.amount + delegation.amount, updated.amount.amount);
assert_eq!(updated_height, updated.block_height);
let delegations = crate::delegations::queries::query_mixnode_delegation(
&deps.storage,
&deps.api,
identity,
delegation_owner.to_string(),
)
.unwrap();
let total_delegation = delegations
.iter()
.fold(Uint128::zero(), |acc, d| acc + d.amount.amount);
assert_eq!(delegation.amount + delegation.amount, total_delegation);
}
#[test]
@@ -540,37 +752,56 @@ mod tests {
env2.block.height = second_height;
try_delegate_to_mixnode(
deps.as_mut(),
env1,
env1.clone(),
mock_info(delegation_owner1.as_str(), &[delegation1]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner1.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
try_delegate_to_mixnode(
deps.as_mut(),
env2,
env2.clone(),
mock_info(delegation_owner2.as_str(), &[delegation2]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert_eq!(
initial_height,
test_helpers::read_delegation(&deps.storage, &identity, &delegation_owner1)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
&identity,
delegation_owner1.as_bytes(),
env1.block.height
)
.unwrap()
.block_height
);
assert_eq!(
second_height,
test_helpers::read_delegation(&deps.storage, identity, &delegation_owner2)
.unwrap()
.block_height
test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner2.as_bytes(),
env2.block.height
)
.unwrap()
.block_height
);
}
@@ -591,7 +822,7 @@ mod tests {
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Err(ContractError::MixNodeBondNotFound {
identity: identity.clone()
@@ -636,6 +867,8 @@ mod tests {
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let expected1 = Delegation::new(
delegation_owner.clone(),
identity1.clone(),
@@ -654,11 +887,23 @@ mod tests {
assert_eq!(
expected1,
test_helpers::read_delegation(&deps.storage, identity1, &delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity1,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
assert_eq!(
expected2,
test_helpers::read_delegation(&deps.storage, identity2, &delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity2,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
);
}
@@ -687,6 +932,8 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// node's "total_delegation" is sum of both
assert_eq!(
delegation1.amount + delegation2.amount,
@@ -714,7 +961,10 @@ mod tests {
identity.clone(),
)
.unwrap();
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
let expected = Delegation::new(
delegation_owner.clone(),
@@ -726,7 +976,13 @@ mod tests {
assert_eq!(
expected,
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).unwrap()
test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.unwrap()
)
}
}
@@ -746,9 +1002,12 @@ mod tests {
use super::storage;
use super::*;
// TODO: Probably delete due to reconciliation logic
#[ignore]
#[test]
fn fails_if_delegation_never_existed() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
let mixnode_owner = "bob";
let identity = test_helpers::add_mixnode(
mixnode_owner,
@@ -759,20 +1018,24 @@ mod tests {
assert_eq!(
Err(ContractError::NoMixnodeDelegationFound {
identity: identity.clone(),
address: delegation_owner.clone(),
address: delegation_owner.to_string(),
}),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity,
)
);
}
// TODO: Update to work with reconciliation
#[ignore]
#[test]
fn succeeds_if_delegation_existed() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let env = mock_env();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -786,8 +1049,12 @@ mod tests {
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let delegation = query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
)
@@ -801,14 +1068,15 @@ mod tests {
.add_event(new_undelegation_event(
&delegation_owner,
&None,
&delegation,
&identity,
Uint128::new(100),
));
assert_eq!(
Ok(expected_response),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
@@ -816,7 +1084,7 @@ mod tests {
assert!(storage::delegations()
.may_load(
&deps.storage,
(identity.clone(), delegation_owner).joined_key(),
(identity.clone(), delegation_owner.as_bytes().to_vec(), 0),
)
.unwrap()
.is_none());
@@ -830,10 +1098,13 @@ mod tests {
)
}
// TODO: Update to work with reconciliation
#[ignore]
#[test]
fn succeeds_if_delegation_existed_even_if_node_unbonded() {
let mut deps = test_helpers::init_contract();
let mixnode_owner = "bob";
let env = mock_env();
let identity = test_helpers::add_mixnode(
mixnode_owner,
tests::fixtures::good_mixnode_pledge(),
@@ -847,12 +1118,17 @@ mod tests {
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
let delegation = query_mixnode_delegation(
deps.as_ref(),
&deps.storage,
&deps.api,
identity.clone(),
delegation_owner.clone().into_string(),
)
.unwrap();
let expected_response = Response::new()
.add_message(BankMsg::Send {
to_address: delegation_owner.clone().into(),
@@ -861,28 +1137,37 @@ mod tests {
.add_event(new_undelegation_event(
&delegation_owner,
&None,
&delegation,
&identity,
Uint128::new(100),
));
try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
try_remove_mixnode(mock_env(), deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap();
assert_eq!(
Ok(expected_response),
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner.as_str(), &[]),
identity.clone(),
)
);
assert!(
test_helpers::read_delegation(&deps.storage, identity, delegation_owner).is_none()
);
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert!(test_helpers::read_delegation(
&deps.storage,
identity,
delegation_owner.as_bytes(),
mock_env().block.height
)
.is_none());
}
#[test]
fn total_delegation_is_preserved_if_only_some_undelegate() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
let mixnode_owner = "bob";
let identity = test_helpers::add_mixnode(
mixnode_owner,
@@ -900,6 +1185,9 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
assert!(try_delegate_to_mixnode(
deps.as_mut(),
mock_env(),
@@ -907,13 +1195,19 @@ mod tests {
identity.clone(),
)
.is_ok());
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// sender1 undelegates
try_remove_delegation_from_mixnode(
deps.as_mut(),
env,
mock_info(delegation_owner1.as_str(), &[]),
identity.clone(),
)
.unwrap();
_try_reconcile_all_delegation_events(&mut deps.storage, &deps.api).unwrap();
// but total delegation should still equal to what sender2 sent
// node's "total_delegation" is sum of both
assert_eq!(
+19 -2
View File
@@ -3,7 +3,7 @@
use config::defaults::DENOM;
use cosmwasm_std::{Addr, StdError};
use mixnet_contract_common::IdentityKey;
use mixnet_contract_common::{error::MixnetContractError, IdentityKey};
use thiserror::Error;
/// Custom errors for contract failure conditions.
@@ -69,7 +69,7 @@ pub enum ContractError {
#[error("MIXNET ({}): Could not find any delegation information associated with mixnode {identity} for {address}", line!())]
NoMixnodeDelegationFound {
identity: IdentityKey,
address: Addr,
address: String,
},
#[error("MIXNET ({}): We tried to remove more funds then are available in the Reward pool. Wanted to remove {to_remove}, but have only {reward_pool}", line!())]
@@ -124,4 +124,21 @@ pub enum ContractError {
interval_start: i64,
interval_end: i64,
},
#[error("MIXNET ({}): Can't change to the desired interval as it's not in progress yet. It starts at {epoch_start} and finishes at {epoch_end}, while the current block time is {current_block_time}", line!())]
EpochNotInProgress {
current_block_time: u64,
epoch_start: i64,
epoch_end: i64,
},
#[error("Could not cast reward to a u128, this should be impossible, at {}", line!())]
CastError,
#[error("{source}")]
MixnetCommonError {
#[from]
source: MixnetContractError,
},
#[error("No rewards to claim for mixnode {identity} for delegate {delegate}")]
NoRewardsToClaim { identity: String, delegate: String },
}
+2 -1
View File
@@ -17,7 +17,8 @@ pub(crate) fn query_gateways_paged(
let limit = limit
.unwrap_or(BOND_PAGE_DEFAULT_LIMIT)
.min(BOND_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = storage::gateways()
.range(deps.storage, start, None, Order::Ascending)
+1 -1
View File
@@ -11,7 +11,7 @@ use mixnet_contract_common::{
};
pub fn query_current_interval(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(storage::CURRENT_INTERVAL.load(storage)?)
storage::current_interval(storage)
}
pub(crate) fn query_rewarded_set_refresh_minimum_blocks() -> u64 {
+47 -2
View File
@@ -3,7 +3,11 @@
use cosmwasm_std::{StdResult, Storage};
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::{IdentityKey, Interval, RewardedSetNodeStatus};
use mixnet_contract_common::{
reward_params::EpochRewardParams, IdentityKey, Interval, RewardedSetNodeStatus,
};
use crate::{error::ContractError, support::helpers::epoch_reward_params};
// type aliases for better reasoning for storage keys
// (I found it helpful)
@@ -14,7 +18,9 @@ type IntervalId = u32;
pub(crate) const REWARDED_NODE_DEFAULT_PAGE_LIMIT: u32 = 1000;
pub(crate) const REWARDED_NODE_MAX_PAGE_LIMIT: u32 = 1500;
pub(crate) const CURRENT_INTERVAL: Item<'_, Interval> = Item::new("cep");
const CURRENT_INTERVAL: Item<'_, Interval> = Item::new("cei");
const CURRENT_EPOCH: Item<'_, Interval> = Item::new("cep");
const CURRENT_EPOCH_REWARD_PARAMS: Item<'_, EpochRewardParams> = Item::new("erp");
pub(crate) const CURRENT_REWARDED_SET_HEIGHT: Item<'_, BlockHeight> = Item::new("crh");
// I've changed the `()` data to an `u8` as after serializing `()` is represented as "null",
@@ -26,6 +32,45 @@ pub(crate) const REWARDED_SET_HEIGHTS_FOR_INTERVAL: Map<'_, (IntervalId, BlockHe
pub(crate) const REWARDED_SET: Map<'_, (BlockHeight, IdentityKey), RewardedSetNodeStatus> =
Map::new("rs");
pub(crate) const INTERVALS: Map<'_, IntervalId, Interval> = Map::new("ins");
pub(crate) const EPOCHS: Map<'_, IntervalId, Interval> = Map::new("ephs");
pub fn save_interval(storage: &mut dyn Storage, interval: &Interval) -> Result<(), ContractError> {
CURRENT_INTERVAL.save(storage, interval)?;
INTERVALS.save(storage, interval.id(), interval)?;
Ok(())
}
pub fn save_epoch(storage: &mut dyn Storage, interval: &Interval) -> Result<(), ContractError> {
CURRENT_EPOCH.save(storage, interval)?;
EPOCHS.save(storage, interval.id(), interval)?;
Ok(())
}
pub fn current_epoch_reward_params(
storage: &dyn Storage,
) -> Result<EpochRewardParams, ContractError> {
Ok(CURRENT_EPOCH_REWARD_PARAMS.load(storage)?)
}
pub fn save_epoch_reward_params(
epoch_id: u32,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
let epoch_reward_params = epoch_reward_params(epoch_id, storage)?;
CURRENT_EPOCH_REWARD_PARAMS.save(storage, &epoch_reward_params)?;
crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?;
Ok(())
}
pub fn current_interval(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(CURRENT_INTERVAL.load(storage)?)
}
pub fn current_epoch(storage: &dyn Storage) -> Result<Interval, ContractError> {
Ok(CURRENT_EPOCH.load(storage)?)
}
pub(crate) fn save_rewarded_set(
storage: &mut dyn Storage,
height: BlockHeight,
+42 -14
View File
@@ -3,12 +3,15 @@
use super::storage;
use crate::error::ContractError;
use crate::error::ContractError::IntervalNotInProgress;
use crate::error::ContractError::{EpochNotInProgress, IntervalNotInProgress};
use crate::mixnet_contract_settings::storage as mixnet_params_storage;
use cosmwasm_std::{DepsMut, Env, MessageInfo, Response, Storage};
use mixnet_contract_common::events::{new_advance_interval_event, new_change_rewarded_set_event};
use mixnet_contract_common::IdentityKey;
// We've distributed the rewards to the rewarded set from the validator api before making this call (implicit order, should be solved in the future)
// We now write the new rewarded set, snapshot the mixnodes and finally reconcile all delegations and undelegations. That way the rewards for the previous
// epoch will be calculated correctly as the delegations and undelegations from the previous epoch will only take effect in the next (current) one.
pub fn try_write_rewarded_set(
deps: DepsMut<'_>,
env: Env,
@@ -50,7 +53,7 @@ pub fn try_write_rewarded_set(
});
}
let current_interval = storage::CURRENT_INTERVAL.load(deps.storage)?.id();
let current_interval = storage::current_interval(deps.storage)?.id();
let num_nodes = rewarded_set.len();
storage::save_rewarded_set(deps.storage, block_height, active_set_size, rewarded_set)?;
@@ -76,8 +79,8 @@ pub fn try_advance_interval(
// in theory, we could have just changed the state and relied on its reversal upon failed
// execution, but better safe than sorry and do not modify the state at all unless we know
// all checks have succeeded.
let current_interval = storage::CURRENT_INTERVAL.load(storage)?;
let next_interval = current_interval.next_interval();
let current_interval = storage::current_interval(storage)?;
let next_interval = current_interval.next();
if next_interval.start_unix_timestamp() > env.block.time.seconds() as i64 {
// the reason for this check is as follows:
@@ -95,11 +98,40 @@ pub fn try_advance_interval(
});
}
storage::CURRENT_INTERVAL.save(storage, &next_interval)?;
storage::save_interval(storage, &next_interval)?;
Ok(Response::new().add_event(new_advance_interval_event(next_interval)))
}
pub fn try_advance_epoch(env: Env, storage: &mut dyn Storage) -> Result<Response, ContractError> {
// in theory, we could have just changed the state and relied on its reversal upon failed
// execution, but better safe than sorry and do not modify the state at all unless we know
// all checks have succeeded.
let current_epoch = storage::current_epoch(storage)?;
let next_epoch = current_epoch.next();
if next_epoch.start_unix_timestamp() > env.block.time.seconds() as i64 {
// the reason for this check is as follows:
// nobody, even trusted validators, should be able to continuously keep advancing epochs,
// because otherwise it would be possible for them to continuously keep rewarding nodes.
//
// Therefore, even if "trusted" validator, responsible for rewarding, is malicious,
// they can't send rewards more often than every `REWARDED_SET_REFRESH_BLOCKS`
// and changing this value requires going through governance and having agreement of
// the super-majority of the validators (by stake)
return Err(EpochNotInProgress {
current_block_time: env.block.time.seconds(),
epoch_start: next_epoch.start_unix_timestamp(),
epoch_end: next_epoch.end_unix_timestamp(),
});
}
storage::save_epoch(storage, &next_epoch)?;
storage::save_epoch_reward_params(next_epoch.id(), storage)?;
Ok(Response::new().add_event(new_advance_interval_event(next_epoch)))
}
#[cfg(test)]
mod tests {
use super::*;
@@ -251,10 +283,8 @@ mod tests {
OffsetDateTime::from_unix_timestamp(1640995200).unwrap(),
Duration::from_secs(60 * 60 * 720),
);
let next_interval = current_interval.next_interval();
storage::CURRENT_INTERVAL
.save(deps.as_mut().storage, &current_interval)
.unwrap();
let next_interval = current_interval.next();
storage::save_interval(deps.as_mut().storage, &current_interval).unwrap();
// fails if the current interval hasn't finished yet i.e. the new interval hasn't begun
env.block.time = Timestamp::from_seconds(1641081600);
@@ -284,7 +314,7 @@ mod tests {
// interval that has just finished
env.block.time =
Timestamp::from_seconds(next_interval.start_unix_timestamp() as u64 + 10000);
let expected_new_interval = current_interval.next_interval();
let expected_new_interval = current_interval.next();
let expected_response =
Response::new().add_event(new_advance_interval_event(expected_new_interval));
assert_eq!(
@@ -294,10 +324,8 @@ mod tests {
// interval way back in the past (i.e. 'somebody' failed to advance it for a long time)
env.block.time = Timestamp::from_seconds(1672531200);
storage::CURRENT_INTERVAL
.save(deps.as_mut().storage, &current_interval)
.unwrap();
let expected_new_interval = current_interval.next_interval();
storage::save_interval(deps.as_mut().storage, &current_interval).unwrap();
let expected_new_interval = current_interval.next();
let expected_response =
Response::new().add_event(new_advance_interval_event(expected_new_interval));
assert_eq!(
@@ -17,7 +17,7 @@ pub fn query_mixnodes_paged(
.unwrap_or(storage::BOND_PAGE_DEFAULT_LIMIT)
.min(storage::BOND_PAGE_MAX_LIMIT) as usize;
let start = start_after.map(Bound::exclusive);
let start = start_after.as_deref().map(Bound::exclusive);
let nodes = storage::mixnodes()
.range(deps.storage, start, None, Order::Ascending)
@@ -203,6 +203,7 @@ pub(crate) mod tests {
#[test]
fn query_for_mixnode_owner_works() {
let mut deps = test_helpers::init_contract();
let env = mock_env();
// "fred" does not own a mixnode if there are no mixnodes
let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap();
@@ -225,8 +226,12 @@ pub(crate) mod tests {
assert!(res.mixnode.is_some());
// but after unbonding it, he doesn't own one anymore
crate::mixnodes::transactions::try_remove_mixnode(deps.as_mut(), mock_info("fred", &[]))
.unwrap();
crate::mixnodes::transactions::try_remove_mixnode(
env,
deps.as_mut(),
mock_info("fred", &[]),
)
.unwrap();
let res = query_owns_mixnode(deps.as_ref(), "fred".to_string()).unwrap();
assert!(res.mixnode.is_none());
+48 -7
View File
@@ -3,14 +3,19 @@
use config::defaults::DENOM;
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::{Index, IndexList, IndexedMap, Map, UniqueIndex};
use mixnet_contract_common::{Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond};
use cw_storage_plus::{Index, IndexList, IndexedSnapshotMap, Map, Strategy, UniqueIndex};
use mixnet_contract_common::U128;
use mixnet_contract_common::{
reward_params::NodeEpochRewards, Addr, Coin, IdentityKeyRef, Layer, MixNode, MixNodeBond,
};
use serde::{Deserialize, Serialize};
use std::fmt::{Display, Formatter};
// storage prefixes
const TOTAL_DELEGATION_NAMESPACE: &str = "td";
const MIXNODES_PK_NAMESPACE: &str = "mn";
const MIXNODES_PK_CHECKPOINTS: &str = "mn__check";
const MIXNODES_PK_CHANGELOG: &str = "mn__change";
const MIXNODES_OWNER_IDX_NAMESPACE: &str = "mno";
// paged retrieval limits for all queries and transactions
@@ -35,11 +40,17 @@ impl<'a> IndexList<StoredMixnodeBond> for MixnodeBondIndex<'a> {
// mixnodes() is the storage access function.
pub(crate) fn mixnodes<'a>(
) -> IndexedMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> {
) -> IndexedSnapshotMap<'a, IdentityKeyRef<'a>, StoredMixnodeBond, MixnodeBondIndex<'a>> {
let indexes = MixnodeBondIndex {
owner: UniqueIndex::new(|d| d.owner.clone(), MIXNODES_OWNER_IDX_NAMESPACE),
};
IndexedMap::new(MIXNODES_PK_NAMESPACE, indexes)
IndexedSnapshotMap::new(
MIXNODES_PK_NAMESPACE,
MIXNODES_PK_CHECKPOINTS,
MIXNODES_PK_CHANGELOG,
Strategy::Never,
indexes,
)
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
@@ -50,9 +61,27 @@ pub(crate) struct StoredMixnodeBond {
pub block_height: u64,
pub mix_node: MixNode,
pub proxy: Option<Addr>,
pub accumulated_rewards: Uint128,
pub epoch_rewards: Option<NodeEpochRewards>,
}
impl From<MixNodeBond> for StoredMixnodeBond {
fn from(mixnode_bond: MixNodeBond) -> StoredMixnodeBond {
StoredMixnodeBond {
pledge_amount: mixnode_bond.pledge_amount,
owner: mixnode_bond.owner,
layer: mixnode_bond.layer,
block_height: mixnode_bond.block_height,
mix_node: mixnode_bond.mix_node,
proxy: mixnode_bond.proxy,
accumulated_rewards: mixnode_bond.accumulated_rewards,
epoch_rewards: None,
}
}
}
impl StoredMixnodeBond {
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
pledge_amount: Coin,
owner: Addr,
@@ -60,6 +89,8 @@ impl StoredMixnodeBond {
block_height: u64,
mix_node: MixNode,
proxy: Option<Addr>,
accumulated_rewards: Uint128,
epoch_rewards: Option<NodeEpochRewards>,
) -> Self {
StoredMixnodeBond {
pledge_amount,
@@ -68,6 +99,8 @@ impl StoredMixnodeBond {
block_height,
mix_node,
proxy,
accumulated_rewards,
epoch_rewards,
}
}
@@ -83,6 +116,7 @@ impl StoredMixnodeBond {
block_height: self.block_height,
mix_node: self.mix_node,
proxy: self.proxy,
accumulated_rewards: self.accumulated_rewards,
}
}
@@ -93,6 +127,10 @@ impl StoredMixnodeBond {
pub(crate) fn pledge_amount(&self) -> Coin {
self.pledge_amount.clone()
}
pub fn profit_margin(&self) -> U128 {
U128::from_num(self.mix_node.profit_margin_percent) / U128::from_num(100)
}
}
impl Display for StoredMixnodeBond {
@@ -125,6 +163,7 @@ pub(crate) fn read_full_mixnode_bond(
block_height: stored_bond.block_height,
mix_node: stored_bond.mix_node,
proxy: stored_bond.proxy,
accumulated_rewards: stored_bond.accumulated_rewards,
}))
}
}
@@ -145,8 +184,8 @@ mod tests {
let mut storage = MockStorage::new();
let bond1 = tests::fixtures::stored_mixnode_bond_fixture("owner1");
let bond2 = tests::fixtures::stored_mixnode_bond_fixture("owner2");
mixnodes().save(&mut storage, "bond1", &bond1).unwrap();
mixnodes().save(&mut storage, "bond2", &bond2).unwrap();
mixnodes().save(&mut storage, "bond1", &bond1, 1).unwrap();
mixnodes().save(&mut storage, "bond2", &bond2, 1).unwrap();
let res1 = mixnodes().load(&storage, "bond1").unwrap();
let res2 = mixnodes().load(&storage, "bond2").unwrap();
@@ -177,10 +216,12 @@ mod tests {
..tests::fixtures::mix_node_fixture()
},
proxy: None,
accumulated_rewards: Uint128::zero(),
epoch_rewards: None,
};
storage::mixnodes()
.save(&mut mock_storage, &node_identity, &mixnode_bond)
.save(&mut mock_storage, &node_identity, &mixnode_bond, 1)
.unwrap();
assert_eq!(
+59 -18
View File
@@ -9,13 +9,31 @@ use crate::mixnodes::storage::StoredMixnodeBond;
use crate::support::helpers::{ensure_no_existing_bond, validate_node_identity_signature};
use config::defaults::DENOM;
use cosmwasm_std::{
wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Uint128,
wasm_execute, Addr, BankMsg, Coin, DepsMut, Env, MessageInfo, Response, Storage, Uint128,
};
use mixnet_contract_common::events::{
new_checkpoint_mixnodes_event, new_mixnode_bonding_event, new_mixnode_unbonding_event,
};
use mixnet_contract_common::events::{new_mixnode_bonding_event, new_mixnode_unbonding_event};
use mixnet_contract_common::MixNode;
use vesting_contract_common::messages::ExecuteMsg as VestingContractExecuteMsg;
use vesting_contract_common::one_ucoin;
pub fn try_checkpoint_mixnodes(
storage: &mut dyn Storage,
block_height: u64,
info: MessageInfo,
) -> Result<Response, ContractError> {
let state = mixnet_params_storage::CONTRACT_STATE.load(storage)?;
// check if this is executed by the permitted validator, if not reject the transaction
if info.sender != state.rewarding_validator_address {
return Err(ContractError::Unauthorized);
}
crate::mixnodes::storage::mixnodes().add_checkpoint(storage, block_height)?;
Ok(Response::new().add_event(new_checkpoint_mixnodes_event(block_height)))
}
pub fn try_add_mixnode(
deps: DepsMut<'_>,
env: Env,
@@ -117,12 +135,14 @@ fn _try_add_mixnode(
env.block.height,
mix_node,
proxy.clone(),
Uint128::zero(),
None,
);
// technically we don't have to set the total_delegation bucket, but it makes things easier
// in different places that we can guarantee that if node exists, so does the data behind the total delegation
let identity = stored_bond.identity();
storage::mixnodes().save(deps.storage, identity, &stored_bond)?;
storage::mixnodes().save(deps.storage, identity, &stored_bond, env.block.height)?;
// if this is a fresh mixnode - write 0 total delegation, otherwise, don't touch it since the node has just rebonded
if storage::TOTAL_DELEGATION
@@ -144,25 +164,38 @@ fn _try_add_mixnode(
}
pub fn try_remove_mixnode_on_behalf(
env: Env,
deps: DepsMut<'_>,
info: MessageInfo,
owner: String,
) -> Result<Response, ContractError> {
let proxy = info.sender;
_try_remove_mixnode(deps, &owner, Some(proxy))
_try_remove_mixnode(env, deps, &owner, Some(proxy))
}
pub fn try_remove_mixnode(deps: DepsMut<'_>, info: MessageInfo) -> Result<Response, ContractError> {
_try_remove_mixnode(deps, info.sender.as_ref(), None)
pub fn try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
info: MessageInfo,
) -> Result<Response, ContractError> {
_try_remove_mixnode(env, deps, info.sender.as_ref(), None)
}
pub(crate) fn _try_remove_mixnode(
env: Env,
deps: DepsMut<'_>,
owner: &str,
proxy: Option<Addr>,
) -> Result<Response, ContractError> {
let owner = deps.api.addr_validate(owner)?;
crate::rewards::transactions::_try_compound_operator_reward(
deps.storage,
env.block.height,
&owner,
None,
)?;
// try to find the node of the sender
let mixnode_bond = match storage::mixnodes()
.idx
@@ -188,7 +221,7 @@ pub(crate) fn _try_remove_mixnode(
};
// remove the bond
storage::mixnodes().remove(deps.storage, mixnode_bond.identity())?;
storage::mixnodes().remove(deps.storage, mixnode_bond.identity(), env.block.height)?;
// decrement layer count
mixnet_params_storage::decrement_layer_count(deps.storage, mixnode_bond.layer)?;
@@ -267,15 +300,20 @@ pub(crate) fn _try_update_mixnode_config(
));
}
storage::mixnodes().update(deps.storage, mixnode_bond.identity(), |mixnode_bond_opt| {
mixnode_bond_opt
.map(|mut mixnode_bond| {
mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent;
mixnode_bond.block_height = env.block.height;
mixnode_bond
})
.ok_or(ContractError::NoBondFound)
})?;
storage::mixnodes().update(
deps.storage,
mixnode_bond.identity(),
env.block.height,
|mixnode_bond_opt| {
mixnode_bond_opt
.map(|mut mixnode_bond| {
mixnode_bond.mix_node.profit_margin_percent = profit_margin_percent;
mixnode_bond.block_height = env.block.height;
mixnode_bond
})
.ok_or(ContractError::NoBondFound)
},
)?;
let mut response = Response::new();
@@ -607,7 +645,7 @@ pub mod tests {
.add_event(new_mixnode_unbonding_event(
&Addr::unchecked("fred"),
&None,
&tests::fixtures::good_gateway_pledge()[0],
&tests::fixtures::good_mixnode_pledge()[0],
&fred_identity,
));
@@ -615,6 +653,7 @@ pub mod tests {
// only 1 node now exists, owned by bob:
let mix_node_bonds = tests::queries::get_mix_nodes(&mut deps);
assert_eq!(1, mix_node_bonds.len());
assert_eq!(&Addr::unchecked("bob"), mix_node_bonds[0].owner());
}
@@ -642,7 +681,9 @@ pub mod tests {
let info = mock_info("mix-owner", &[]);
let msg = ExecuteMsg::UnbondMixnode {};
assert!(execute(deps.as_mut(), mock_env(), info, msg).is_ok());
let response = execute(deps.as_mut(), mock_env(), info, msg);
assert!(response.is_ok());
assert!(storage::mixnodes()
.idx
+8 -69
View File
@@ -3,81 +3,20 @@
use super::storage;
use crate::error::ContractError;
use crate::mixnodes::storage as mixnodes_storage;
use cosmwasm_std::{Addr, Storage, Uint128};
use mixnet_contract_common::mixnode::DelegatorRewardParams;
use mixnet_contract_common::{
IdentityKey, IdentityKeyRef, PendingDelegatorRewarding, RewardingResult, RewardingStatus,
};
pub(crate) fn update_post_rewarding_storage(
storage: &mut dyn Storage,
mix_identity: IdentityKeyRef<'_>,
operator_reward: Uint128,
delegators_reward: Uint128,
) -> Result<(), ContractError> {
if operator_reward == Uint128::zero() && delegators_reward == Uint128::zero() {
return Ok(());
}
// update pledge
if operator_reward > Uint128::zero() {
mixnodes_storage::mixnodes().update(storage, mix_identity, |current_bond| {
match current_bond {
None => Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
}),
Some(mut mixnode_bond) => {
mixnode_bond.pledge_amount.amount += operator_reward;
Ok(mixnode_bond)
}
}
})?;
}
// update total_delegation
if delegators_reward > Uint128::zero() {
mixnodes_storage::TOTAL_DELEGATION.update(storage, mix_identity, |current_total| {
match current_total {
None => Err(ContractError::MixNodeBondNotFound {
identity: mix_identity.to_string(),
}),
Some(current_total) => Ok(current_total + delegators_reward),
}
})?;
}
// update reward pool
storage::decr_reward_pool(storage, operator_reward + delegators_reward)?;
Ok(())
}
use cosmwasm_std::Storage;
use mixnet_contract_common::{IdentityKey, RewardingResult, RewardingStatus};
pub(crate) fn update_rewarding_status(
storage: &mut dyn Storage,
interval_id: u32,
mix_identity: IdentityKey,
rewarding_results: RewardingResult,
next_start: Option<Addr>,
delegators_rewarding_params: DelegatorRewardParams,
rewarding_result: RewardingResult,
) -> Result<(), ContractError> {
if let Some(next_start) = next_start {
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::PendingNextDelegatorPage(PendingDelegatorRewarding {
running_results: rewarding_results,
next_start,
rewarding_params: delegators_rewarding_params,
}),
)?;
} else {
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::Complete(rewarding_results),
)?;
}
storage::REWARDING_STATUS.save(
storage,
(interval_id, mix_identity),
&RewardingStatus::Complete(rewarding_result),
)?;
Ok(())
}
+16 -90
View File
@@ -35,11 +35,10 @@ pub(crate) mod tests {
#[cfg(test)]
mod querying_for_rewarding_status {
use super::*;
use crate::constants;
use crate::delegations::transactions::try_delegate_to_mixnode;
use crate::rewards::transactions::{
try_reward_mixnode, try_reward_next_mixnode_delegators,
};
use crate::interval::storage::{save_epoch, save_epoch_reward_params};
use crate::rewards::transactions::try_reward_mixnode;
use crate::{constants, support::tests::fixtures::epoch_fixture};
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr};
use mixnet_contract_common::{
@@ -75,7 +74,7 @@ pub(crate) mod tests {
env,
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
0,
)
.unwrap();
@@ -87,6 +86,7 @@ pub(crate) mod tests {
}
#[test]
fn returns_complete_status_for_fully_rewarded_node() {
// with single page
let mut deps = test_helpers::init_contract();
@@ -106,12 +106,17 @@ pub(crate) mod tests {
env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING;
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
let epoch = epoch_fixture();
save_epoch(&mut deps.storage, &epoch).unwrap();
save_epoch_reward_params(epoch.id(), &mut deps.storage).unwrap();
try_reward_mixnode(
deps.as_mut(),
env.clone(),
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
0,
)
.unwrap();
@@ -121,14 +126,7 @@ pub(crate) mod tests {
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
);
assert_ne!(RewardingResult::default().node_reward, result.node_reward);
}
_ => unreachable!(),
}
@@ -161,93 +159,21 @@ pub(crate) mod tests {
env,
info.clone(),
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
tests::fixtures::node_reward_params_fixture(100),
1,
)
.unwrap();
// rewards all pending
try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 1)
.unwrap();
// try_reward_next_mixnode_delegators(deps.as_mut(), info, node_identity.to_string(), 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_ne!(
RewardingResult::default().total_delegator_reward,
result.total_delegator_reward
);
}
_ => unreachable!(),
}
}
#[test]
fn returns_pending_next_delegator_page_status_when_there_are_more_delegators_to_reward() {
let mut deps = test_helpers::init_contract();
let mut env = mock_env();
let current_state = mixnet_params_storage::CONTRACT_STATE
.load(deps.as_mut().storage)
.unwrap();
let rewarding_validator_address = current_state.rewarding_validator_address;
let node_owner: Addr = Addr::unchecked("bob");
let node_identity = test_helpers::add_mixnode(
node_owner.as_str(),
tests::fixtures::good_mixnode_pledge(),
deps.as_mut(),
);
for i in 0..MIXNODE_DELEGATORS_PAGE_LIMIT + 123 {
try_delegate_to_mixnode(
deps.as_mut(),
env.clone(),
mock_info(&*format!("delegator{:04}", i), &[coin(200_000000, DENOM)]),
node_identity.clone(),
)
.unwrap();
}
env.block.height += constants::MINIMUM_BLOCK_AGE_FOR_REWARDING;
let info = mock_info(rewarding_validator_address.as_ref(), &[]);
try_reward_mixnode(
deps.as_mut(),
env,
info,
node_identity.clone(),
tests::fixtures::node_rewarding_params_fixture(100),
0,
)
.unwrap();
let res = query_rewarding_status(deps.as_ref(), node_identity, 0).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
);
assert_ne!(RewardingResult::default().node_reward, result.node_reward);
}
_ => unreachable!(),
}
+20 -1
View File
@@ -5,12 +5,31 @@ use crate::error::ContractError;
use config::defaults::TOTAL_SUPPLY;
use cosmwasm_std::{StdResult, Storage, Uint128};
use cw_storage_plus::{Item, Map};
use mixnet_contract_common::{IdentityKey, RewardingStatus};
use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKey, RewardingStatus};
type BlockHeight = u64;
type Address = String;
pub(crate) const REWARD_POOL: Item<'_, Uint128> = Item::new("pool");
// TODO: Do we need a migration for this?
pub(crate) const REWARDING_STATUS: Map<'_, (u32, IdentityKey), RewardingStatus> = Map::new("rm");
pub(crate) const DELEGATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> =
Map::new("drc");
pub(crate) const OPERATOR_REWARD_CLAIMED_HEIGHT: Map<'_, (Address, IdentityKey), BlockHeight> =
Map::new("orc");
type EpochId = u32;
pub(crate) const EPOCH_REWARD_PARAMS: Map<'_, EpochId, EpochRewardParams> = Map::new("epr");
pub fn epoch_reward_params_for_id(
storage: &dyn Storage,
id: EpochId,
) -> StdResult<EpochRewardParams> {
EPOCH_REWARD_PARAMS.load(storage, id)
}
#[allow(dead_code)]
pub fn incr_reward_pool(
amount: Uint128,
File diff suppressed because it is too large Load Diff
+29 -3
View File
@@ -1,11 +1,37 @@
// Copyright 2021 - Nym Technologies SA <contact@nymtech.net>
// SPDX-License-Identifier: Apache-2.0
use crate::error::ContractError;
use crate::gateways::storage as gateways_storage;
use crate::mixnodes::storage as mixnodes_storage;
use crate::{constants, gateways::storage as gateways_storage};
use crate::error::ContractError;
use cosmwasm_std::{Addr, Deps, Storage};
use mixnet_contract_common::IdentityKeyRef;
use mixnet_contract_common::{reward_params::EpochRewardParams, IdentityKeyRef};
pub(crate) fn epoch_reward_params(
epoch_id: u32,
storage: &mut dyn Storage,
) -> Result<EpochRewardParams, ContractError> {
let state = crate::mixnet_contract_settings::storage::CONTRACT_STATE
.load(storage)
.map(|settings| settings.params)?;
let reward_pool = crate::rewards::storage::REWARD_POOL.load(storage)?;
let interval_reward_percent = crate::constants::INTERVAL_REWARD_PERCENT;
let epochs_in_interval = crate::constants::EPOCHS_IN_INTERVAL;
let epoch_reward_params = EpochRewardParams::new(
(reward_pool.u128() / 100 / epochs_in_interval as u128) * interval_reward_percent as u128,
state.mixnode_rewarded_set_size as u128,
state.mixnode_active_set_size as u128,
crate::rewards::storage::circulating_supply(storage)?.u128(),
constants::SYBIL_RESISTANCE_PERCENT,
constants::ACTIVE_SET_WORK_FACTOR,
);
crate::rewards::storage::EPOCH_REWARD_PARAMS.save(storage, epoch_id, &epoch_reward_params)?;
Ok(epoch_reward_params)
}
pub fn generate_storage_key(address: &Addr, proxy: Option<&Addr>) -> Vec<u8> {
if let Some(proxy) = &proxy {
+16 -19
View File
@@ -1,11 +1,13 @@
use crate::constants::{INTERVAL_REWARD_PERCENT, SYBIL_RESISTANCE_PERCENT};
use crate::contract::{INITIAL_MIXNODE_PLEDGE, INITIAL_REWARD_POOL};
use std::time::Duration;
use crate::contract::INITIAL_MIXNODE_PLEDGE;
use crate::mixnodes::storage as mixnodes_storage;
use crate::{mixnodes::storage::StoredMixnodeBond, support::tests};
use config::defaults::{DENOM, TOTAL_SUPPLY};
use cosmwasm_std::{coin, Addr, Coin};
use mixnet_contract_common::mixnode::NodeRewardParams;
use mixnet_contract_common::{Gateway, GatewayBond, Layer, MixNode};
use config::defaults::DENOM;
use cosmwasm_std::{coin, Addr, Coin, Uint128};
use mixnet_contract_common::reward_params::NodeRewardParams;
use mixnet_contract_common::{Gateway, GatewayBond, Interval, Layer, MixNode};
use time::OffsetDateTime;
pub fn mix_node_fixture() -> MixNode {
MixNode {
@@ -57,6 +59,8 @@ pub(crate) fn stored_mixnode_bond_fixture(owner: &str) -> mixnodes_storage::Stor
..super::fixtures::mix_node_fixture()
},
None,
Uint128::zero(),
None,
)
}
@@ -74,17 +78,10 @@ pub fn good_gateway_pledge() -> Vec<Coin> {
}]
}
// 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) * INTERVAL_REWARD_PERCENT as u128,
50 as u128,
25 as u128,
0,
TOTAL_SUPPLY - INITIAL_REWARD_POOL,
uptime,
SYBIL_RESISTANCE_PERCENT,
true,
10,
)
pub fn node_reward_params_fixture(uptime: u128) -> NodeRewardParams {
NodeRewardParams::new(0, uptime, true)
}
pub fn epoch_fixture() -> Interval {
Interval::new(1, OffsetDateTime::now_utc(), Duration::from_secs(3600))
}
+6 -7
View File
@@ -29,7 +29,6 @@ pub mod test_helpers {
use cosmwasm_std::{coin, Env, Timestamp};
use cosmwasm_std::{Addr, StdResult, Storage};
use cosmwasm_std::{Empty, MemoryStorage};
use cw_storage_plus::PrimaryKey;
use mixnet_contract_common::{Delegation, Gateway, IdentityKeyRef, InstantiateMsg, MixNode};
use rand::thread_rng;
@@ -114,27 +113,27 @@ pub mod test_helpers {
};
delegations_storage::delegations()
.save(storage, delegation.storage_key().joined_key(), &delegation)
.save(storage, delegation.storage_key(), &delegation)
.unwrap();
}
pub(crate) fn read_delegation(
storage: &dyn Storage,
mix: impl Into<String>,
owner: impl Into<String>,
owner: impl Into<Vec<u8>>,
block_height: u64,
) -> Option<Delegation> {
delegations_storage::delegations()
.may_load(storage, (mix.into(), owner.into()).joined_key())
.may_load(storage, (mix.into(), owner.into(), block_height))
.unwrap()
}
pub(crate) fn update_env_and_progress_interval(env: &mut Env, storage: &mut dyn Storage) {
// make sure current block time is within the expected next interval
env.block.time = Timestamp::from_seconds(
(interval_storage::CURRENT_INTERVAL
.load(storage)
(interval_storage::current_interval(storage)
.unwrap()
.next_interval()
.next()
.start_unix_timestamp()
+ 123) as u64,
);
+1 -1
View File
@@ -19,7 +19,7 @@ vesting-contract-common = { path = "../../common/cosmwasm-smart-contracts/vestin
config = { path = "../../common/config" }
cosmwasm-std = { version = "1.0.0-beta4"}
cw-storage-plus = { version = "0.11.1", features = ["iterator"] }
cw-storage-plus = { version = "0.12.1", features = ["iterator"] }
schemars = "0.8"
serde = { version = "1.0", default-features = false, features = ["derive"] }
+2 -6
View File
@@ -7,7 +7,7 @@ use crate::vesting::{populate_vesting_periods, Account};
use config::defaults::DENOM;
use cosmwasm_std::{
coin, entry_point, to_binary, BankMsg, Coin, Deps, DepsMut, Env, MessageInfo, QueryResponse,
Response, Timestamp, Uint128,
Response, Timestamp,
};
use mixnet_contract_common::{Gateway, IdentityKey, MixNode};
use vesting_contract_common::events::{
@@ -146,11 +146,7 @@ pub fn try_withdraw_vested_coins(
}
let spendable_coins = account.spendable_coins(None, &env, deps.storage)?;
if amount.amount <= spendable_coins.amount {
let new_balance = account
.load_balance(deps.storage)?
.u128()
.saturating_sub(amount.amount.u128());
account.save_balance(Uint128::new(new_balance), deps.storage)?;
let new_balance = account.withdraw(&amount, deps.storage)?;
let send_tokens = BankMsg::Send {
to_address: account.owner_address().as_str().to_string(),
+17
View File
@@ -11,6 +11,7 @@ pub const KEY: Item<'_, u32> = Item::new("key");
const ACCOUNTS: Map<'_, String, Account> = Map::new("acc");
// Holds data related to individual accounts
const BALANCES: Map<'_, u32, Uint128> = Map::new("blc");
const WITHDRAWNS: Map<'_, u32, Uint128> = Map::new("wthd");
const BOND_PLEDGES: Map<'_, u32, PledgeData> = Map::new("bnd");
const GATEWAY_PLEDGES: Map<'_, u32, PledgeData> = Map::new("gtw");
pub const DELEGATIONS: Map<'_, (u32, IdentityKey, BlockHeight), Uint128> = Map::new("dlg");
@@ -39,6 +40,13 @@ pub fn delete_account(address: &Addr, storage: &mut dyn Storage) -> Result<(), C
Ok(())
}
pub fn load_withdrawn(key: u32, storage: &dyn Storage) -> Result<Uint128, ContractError> {
Ok(WITHDRAWNS
.may_load(storage, key)
.unwrap_or(None)
.unwrap_or_else(Uint128::zero))
}
pub fn load_balance(key: u32, storage: &dyn Storage) -> Result<Uint128, ContractError> {
Ok(BALANCES
.may_load(storage, key)
@@ -55,6 +63,15 @@ pub fn save_balance(
Ok(())
}
pub fn save_withdrawn(
key: u32,
value: Uint128,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
WITHDRAWNS.save(storage, key, &value)?;
Ok(())
}
pub fn load_bond_pledge(
key: u32,
storage: &dyn Storage,
+23 -1
View File
@@ -18,7 +18,29 @@ pub mod helpers {
return deps;
}
pub fn vesting_account_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
pub fn vesting_account_mid_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
let start_time_ts = env.block.time.clone();
let start_time = env.block.time.seconds() - 7200;
let periods = populate_vesting_periods(
start_time,
VestingSpecification::new(None, Some(3600), None),
);
Account::new(
Addr::unchecked("owner"),
Some(Addr::unchecked("staking")),
Coin {
amount: Uint128::new(1_000_000_000_000),
denom: DENOM.to_string(),
},
start_time_ts,
periods,
storage,
)
.unwrap()
}
pub fn vesting_account_new_fixture(storage: &mut dyn Storage, env: &Env) -> Account {
let start_time = env.block.time;
let periods =
populate_vesting_periods(start_time.seconds(), VestingSpecification::default());
-47
View File
@@ -1,47 +0,0 @@
use crate::errors::ContractError;
use cosmwasm_std::{Coin, Env, Response, Storage};
use mixnet_contract_common::{Gateway, MixNode};
pub trait MixnodeBondingAccount {
fn try_bond_mixnode(
&self,
mix_node: MixNode,
owner_signature: String,
pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_mixnode(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
fn try_update_mixnode_config(
&self,
profit_margin_percent: u8,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
}
pub trait GatewayBondingAccount {
fn try_bond_gateway(
&self,
gateway: Gateway,
owner_signature: String,
pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_gateway(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_gateway(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
}
@@ -1 +1,47 @@
use crate::errors::ContractError;
use cosmwasm_std::{Coin, Env, Response, Storage};
use mixnet_contract_common::{Gateway, MixNode};
pub trait MixnodeBondingAccount {
fn try_bond_mixnode(
&self,
mix_node: MixNode,
owner_signature: String,
pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_mixnode(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_mixnode(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
fn try_update_mixnode_config(
&self,
profit_margin_percent: u8,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
}
pub trait GatewayBondingAccount {
fn try_bond_gateway(
&self,
gateway: Gateway,
owner_signature: String,
pledge: Coin,
env: &Env,
storage: &mut dyn Storage,
) -> Result<Response, ContractError>;
fn try_unbond_gateway(&self, storage: &dyn Storage) -> Result<Response, ContractError>;
fn try_track_unbond_gateway(
&self,
amount: Coin,
storage: &mut dyn Storage,
) -> Result<(), ContractError>;
}
+2 -2
View File
@@ -1,7 +1,7 @@
pub mod bonding;
pub mod bonding_account;
pub mod delegating_account;
pub mod vesting_account;
pub use self::bonding::{GatewayBondingAccount, MixnodeBondingAccount};
pub use self::bonding_account::{GatewayBondingAccount, MixnodeBondingAccount};
pub use self::delegating_account::DelegatingAccount;
pub use self::vesting_account::VestingAccount;
+31 -4
View File
@@ -1,9 +1,9 @@
use super::VestingPeriod;
use crate::errors::ContractError;
use crate::storage::{
load_balance, load_bond_pledge, load_gateway_pledge, remove_bond_pledge, remove_delegation,
remove_gateway_pledge, save_account, save_balance, save_bond_pledge, save_gateway_pledge,
DELEGATIONS, KEY,
load_balance, load_bond_pledge, load_gateway_pledge, load_withdrawn, remove_bond_pledge,
remove_delegation, remove_gateway_pledge, save_account, save_balance, save_bond_pledge,
save_gateway_pledge, save_withdrawn, DELEGATIONS, KEY,
};
use cosmwasm_std::{Addr, Coin, Order, Storage, Timestamp, Uint128};
use cw_storage_plus::Bound;
@@ -121,6 +121,33 @@ impl Account {
}
}
pub fn withdraw(
&self,
amount: &Coin,
storage: &mut dyn Storage,
) -> Result<u128, ContractError> {
let new_balance = self
.load_balance(storage)?
.u128()
.saturating_sub(amount.amount.u128());
self.save_balance(Uint128::new(new_balance), storage)?;
let withdrawn = self.load_withdrawn(storage)?;
self.save_withdrawn(withdrawn + amount.amount, storage)?;
Ok(new_balance)
}
pub fn load_withdrawn(&self, storage: &dyn Storage) -> Result<Uint128, ContractError> {
load_withdrawn(self.storage_key, storage)
}
pub fn save_withdrawn(
&self,
withdrawn: Uint128,
storage: &mut dyn Storage,
) -> Result<(), ContractError> {
save_withdrawn(self.storage_key, withdrawn, storage)
}
pub fn load_balance(&self, storage: &dyn Storage) -> Result<Uint128, ContractError> {
load_balance(self.storage_key(), storage)
}
@@ -204,7 +231,7 @@ impl Account {
prev_len = block_heights.len();
start_after = block_heights.last().map(|last| Bound::exclusive_int(*last));
start_after = block_heights.last().map(|last| Bound::exclusive(*last));
if start_after.is_none() {
break;
}
@@ -114,7 +114,11 @@ impl VestingAccount for Account {
) -> Result<Coin, ContractError> {
let block_time = block_time.unwrap_or(env.block.time);
let period = self.get_current_vesting_period(block_time);
let max_vested = self.get_vested_coins(Some(block_time), env)?;
let withdrawn = self.load_withdrawn(storage)?;
let max_available = self
.get_vested_coins(Some(block_time), env)?
.amount
.saturating_sub(withdrawn);
let start_time = match period {
Period::Before => 0,
Period::After => u64::MAX,
@@ -130,7 +134,7 @@ impl VestingAccount for Account {
acc + amount
});
let amount = Uint128::new(coin.u128().min(max_vested.amount.u128()));
let amount = Uint128::new(coin.u128().min(max_available.u128()));
Ok(Coin {
amount,
+146 -7
View File
@@ -38,7 +38,9 @@ pub fn populate_vesting_periods(
mod tests {
use crate::contract::execute;
use crate::storage::load_account;
use crate::support::tests::helpers::{init_contract, vesting_account_fixture};
use crate::support::tests::helpers::{
init_contract, vesting_account_mid_fixture, vesting_account_new_fixture,
};
use crate::traits::DelegatingAccount;
use crate::traits::VestingAccount;
use crate::traits::{GatewayBondingAccount, MixnodeBondingAccount};
@@ -81,7 +83,7 @@ mod tests {
let _response = execute(deps.as_mut(), env.clone(), info, msg.clone());
assert!(response.is_err());
let account_again = vesting_account_fixture(&mut deps.storage, &env);
let account_again = vesting_account_new_fixture(&mut deps.storage, &env);
assert_eq!(created_account.storage_key(), 1);
assert_ne!(created_account.storage_key(), account_again.storage_key());
}
@@ -91,7 +93,7 @@ mod tests {
let mut deps = init_contract();
let mut env = mock_env();
let info = mock_info("owner", &[]);
let account = vesting_account_fixture(&mut deps.storage, &env);
let account = vesting_account_new_fixture(&mut deps.storage, &env);
let msg = ExecuteMsg::TransferOwnership {
to_address: "new_owner".to_string(),
};
@@ -172,7 +174,7 @@ mod tests {
let num_vesting_periods = 8;
let vesting_period = 3 * 30 * 86400;
let account = vesting_account_fixture(&mut deps.storage, &env);
let account = vesting_account_new_fixture(&mut deps.storage, &env);
assert_eq!(account.periods().len(), num_vesting_periods as usize);
@@ -236,12 +238,149 @@ mod tests {
assert_eq!(vesting_coins.amount, Uint128::zero());
}
#[test]
fn test_withdraw_case() {
let mut deps = init_contract();
let env = mock_env();
let account = vesting_account_mid_fixture(&mut deps.storage, &env);
let vested_coins = account.get_vested_coins(None, &env).unwrap();
let vesting_coins = account.get_vesting_coins(None, &env).unwrap();
let locked_coins = account.locked_coins(None, &env, &mut deps.storage).unwrap();
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
assert_eq!(locked_coins.amount, Uint128::new(750_000_000_000));
let spendable = account
.spendable_coins(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(spendable.amount, Uint128::new(250_000_000_000));
let withdrawn = account.load_withdrawn(&deps.storage).unwrap();
assert_eq!(withdrawn, Uint128::zero());
let mix_identity = "alice".to_string();
let delegation = Coin {
amount: Uint128::new(500_000_000_000),
denom: DENOM.to_string(),
};
let ok = account.try_delegate_to_mixnode(
mix_identity.clone(),
delegation.clone(),
&env,
&mut deps.storage,
);
assert!(ok.is_ok());
let vested_coins = account.get_vested_coins(None, &env).unwrap();
let vesting_coins = account.get_vesting_coins(None, &env).unwrap();
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
let delegated_free = account
.get_delegated_free(None, &env, &mut deps.storage)
.unwrap();
let delegated_vesting = account
.get_delegated_vesting(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(delegated_free.amount, Uint128::new(250_000_000_000));
assert_eq!(delegated_vesting.amount, Uint128::new(250_000_000_000));
let locked_coins = account.locked_coins(None, &env, &mut deps.storage).unwrap();
// vesting - delegated_vesting - pledged_vesting
assert_eq!(locked_coins.amount, Uint128::new(500_000_000_000));
let spendable = account
.spendable_coins(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(spendable.amount, Uint128::zero());
let ok = account.try_undelegate_from_mixnode(mix_identity.clone(), &mut deps.storage);
assert!(ok.is_ok());
account
.track_undelegation(mix_identity.clone(), delegation.clone(), &mut deps.storage)
.unwrap();
let delegated_free = account
.get_delegated_free(None, &env, &mut deps.storage)
.unwrap();
let delegated_vesting = account
.get_delegated_vesting(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(delegated_free.amount, Uint128::zero());
assert_eq!(delegated_vesting.amount, Uint128::zero());
assert_eq!(
account.load_balance(&deps.storage).unwrap(),
Uint128::new(1000_000_000_000)
);
account
.withdraw(
&account.spendable_coins(None, &env, &deps.storage).unwrap(),
&mut deps.storage,
)
.unwrap();
assert_eq!(
account.load_balance(&deps.storage).unwrap(),
Uint128::new(750_000_000_000)
);
let withdrawn = account.load_withdrawn(&deps.storage).unwrap();
assert_eq!(withdrawn, Uint128::new(250_000_000_000));
let vested_coins = account.get_vested_coins(None, &env).unwrap();
let vesting_coins = account.get_vesting_coins(None, &env).unwrap();
let locked_coins = account.locked_coins(None, &env, &mut deps.storage).unwrap();
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
assert_eq!(locked_coins.amount, Uint128::new(750_000_000_000));
let spendable = account
.spendable_coins(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(spendable.amount, Uint128::zero());
let ok = account.try_delegate_to_mixnode(
mix_identity.clone(),
delegation.clone(),
&env,
&mut deps.storage,
);
assert!(ok.is_ok());
let vested_coins = account.get_vested_coins(None, &env).unwrap();
let vesting_coins = account.get_vesting_coins(None, &env).unwrap();
let locked_coins = account.locked_coins(None, &env, &mut deps.storage).unwrap();
assert_eq!(vested_coins.amount, Uint128::new(250_000_000_000));
assert_eq!(vesting_coins.amount, Uint128::new(750_000_000_000));
let spendable = account
.spendable_coins(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(spendable.amount, Uint128::zero());
let delegated_free = account
.get_delegated_free(None, &env, &mut deps.storage)
.unwrap();
let delegated_vesting = account
.get_delegated_vesting(None, &env, &mut deps.storage)
.unwrap();
assert_eq!(delegated_free.amount, Uint128::zero());
assert_eq!(delegated_vesting.amount, Uint128::new(500_000_000_000));
// vesting - delegated_vesting - pledged_vesting
assert_eq!(locked_coins.amount, Uint128::new(250_000_000_000));
}
#[test]
fn test_delegations() {
let mut deps = init_contract();
let env = mock_env();
let account = vesting_account_fixture(&mut deps.storage, &env);
let account = vesting_account_new_fixture(&mut deps.storage, &env);
// Try delegating too much
let err = account.try_delegate_to_mixnode(
@@ -352,7 +491,7 @@ mod tests {
let mut deps = init_contract();
let env = mock_env();
let account = vesting_account_fixture(&mut deps.storage, &env);
let account = vesting_account_new_fixture(&mut deps.storage, &env);
let mix_node = MixNode {
host: "mix.node.org".to_string(),
@@ -472,7 +611,7 @@ mod tests {
let mut deps = init_contract();
let env = mock_env();
let account = vesting_account_fixture(&mut deps.storage, &env);
let account = vesting_account_new_fixture(&mut deps.storage, &env);
let gateway = Gateway {
host: "1.1.1.1".to_string(),
-103
View File
@@ -1,103 +0,0 @@
{
"env": {
"browser": true,
"es6": true,
"node": true,
"jest": true
},
"parserOptions": {
"ecmaFeatures": {
"jsx": true
},
"ecmaVersion": 2019,
"sourceType": "module"
},
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"plugins": ["react", "react-hooks", "jsx-a11y", "prettier", "jest"],
"extends": ["plugin:react/recommended", "airbnb", "prettier", "plugin:jest/recommended", "plugin:jest/style"],
"rules": {
"jest/prefer-strict-equal": "error",
"jest/prefer-to-have-length": "warn",
"prettier/prettier": "error",
"import/prefer-default-export": "off",
"react/prop-types": "off",
"react/jsx-filename-extension": "off",
"react/jsx-props-no-spreading": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.[jt]s",
"**/*.spec.[jt]s",
"**/*.test.[jt]sx",
"**/*.spec.[jt]sx"
]
}
],
"import/extensions": [
"error",
"ignorePackages",
{
"ts": "never",
"tsx": "never",
"js": "never",
"jsx": "never"
}
]
},
"overrides": [
{
"files": "**/*.+(ts|tsx)",
"parser": "@typescript-eslint/parser",
"parserOptions": {
"project": "./tsconfig.json"
},
"plugins": ["@typescript-eslint/eslint-plugin"],
"extends": [
"plugin:@typescript-eslint/eslint-recommended",
"plugin:@typescript-eslint/recommended",
"prettier"
],
"rules": {
"@typescript-eslint/explicit-function-return-type": "off",
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/no-var-requires": "off",
"no-use-before-define": [0],
"@typescript-eslint/no-use-before-define": [1],
"import/no-unresolved": 0,
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": [
"**/*.test.ts",
"**/*.spec.ts",
"**/*.test.tsx",
"**/*.spec.tsx"
]
}
],
"quotes": "off",
"@typescript-eslint/quotes": [
2,
"single",
{
"avoidEscape": true
}
],
"@typescript-eslint/no-unused-vars": [2, { "argsIgnorePattern": "^_" }]
}
}
],
"settings": {
"import/resolver": {
"root-import": {
"rootPathPrefix": "@",
"rootPathSuffix": "src",
"extensions": [".js", ".ts", ".tsx", ".jsx", ".mdx"]
}
}
}
}
+5
View File
@@ -0,0 +1,5 @@
{
"extends": [
"@nymproject/eslint-config-react-typescript"
]
}
+1 -1
View File
@@ -1 +1 @@
14
16
+1 -1
View File
@@ -1,6 +1,6 @@
{
"trailingComma": "all",
"singleQuote": true,
"printWidth": 80,
"printWidth": 120,
"tabWidth": 2
}
+17 -5
View File
@@ -8,12 +8,22 @@ You will need:
- NodeJS (use `nvm install` to automatically install the correct version)
- `npm`
- `yarn`
> **Note**: This project ipart of a mono repo, so you will need to build the shared packages before starting. And any time they change, you'll need to rebuild them.
From the [root of the repository](../README.md) run the following to build shared packages:
```
yarn
yarn build
```
From the `explorer` directory of the `nym` monorepo, run:
```
npm install
npm run start
cd explorer
yarn start
```
You can then open a browser to http://localhost:3000 and start development.
@@ -24,11 +34,13 @@ Documentation for developers [can be found here](./docs).
## Deployment
Build the UI with:
Build the UI with (starting in the repository root):
```
npm install
npm run build
yarn
yarn build
cd explorer
yarn build
```
The output will be in the `dist` directory. Serve this with `nginx` or `httpd`.
-30225
View File
File diff suppressed because it is too large Load Diff
+35 -24
View File
@@ -12,20 +12,27 @@
"@mui/styles": "^5.0.1",
"@mui/system": "^5.0.1",
"@mui/x-data-grid": "^5.0.0-beta.5",
"@nymproject/mui-theme": "^1.0.0",
"@nymproject/nym-validator-client": "^0.18.0",
"@nymproject/react": "^1.0.0",
"d3-scale": "^4.0.0",
"date-fns": "^2.24.0",
"i18n-iso-countries": "^6.8.0",
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-error-boundary": "^3.1.4",
"react-google-charts": "^3.0.15",
"react-identicons": "^1.2.5",
"react-router": "^5.2.1",
"react-router-dom": "^5.3.0",
"react-simple-maps": "^2.3.0",
"react-tooltip": "^4.2.21"
"react-tooltip": "^4.2.21",
"use-clipboard-copy": "^0.2.0"
},
"devDependencies": {
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.0-rc.3",
"@nymproject/eslint-config-react-typescript": "^1.0.0",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.4",
"@svgr/webpack": "^6.1.1",
"@testing-library/jest-dom": "^5.14.1",
"@testing-library/react": "^12.0.0",
"@types/d3-fetch": "^3.0.1",
@@ -36,32 +43,36 @@
"@types/node": "^16.7.13",
"@types/react": "^17.0.34",
"@types/react-dom": "^17.0.9",
"@types/react-router": "^5.1.18",
"@types/react-router-dom": "^5.1.8",
"@types/react-simple-maps": "^1.0.6",
"@types/react-tooltip": "^4.2.4",
"@types/topojson-client": "^3.1.0",
"@typescript-eslint/eslint-plugin": "4.31.0",
"@typescript-eslint/parser": "4.31.0",
"@typescript-eslint/eslint-plugin": "^5.13.0",
"@typescript-eslint/parser": "^5.13.0",
"babel-loader": "^8.2.2",
"babel-plugin-root-import": "^6.6.0",
"clean-webpack-plugin": "^4.0.0",
"css-loader": "^6.2.0",
"css-minimizer-webpack-plugin": "^3.0.2",
"dotenv-webpack": "^7.0.3",
"eslint": "7.32.0",
"eslint-config-airbnb": "18.2.1",
"eslint-config-prettier": "8.3.0",
"eslint-import-resolver-root-import": "1.0.4",
"eslint-plugin-import": "2.24.2",
"eslint-plugin-jest": "^24.4.0",
"eslint-plugin-jsx-a11y": "6.4.1",
"eslint-plugin-prettier": "4.0.0",
"eslint-plugin-react": "7.25.1",
"eslint-plugin-react-hooks": "4.2.0",
"fork-ts-checker-webpack-plugin": "^6.3.3",
"eslint": "^8.10.0",
"eslint-config-airbnb": "^19.0.4",
"eslint-config-airbnb-typescript": "^16.1.0",
"eslint-config-prettier": "^8.5.0",
"eslint-import-resolver-root-import": "^1.0.4",
"eslint-plugin-import": "^2.25.4",
"eslint-plugin-jest": "^26.1.1",
"eslint-plugin-jsx-a11y": "^6.5.1",
"eslint-plugin-prettier": "^4.0.0",
"eslint-plugin-react": "^7.29.2",
"eslint-plugin-react-hooks": "^4.3.0",
"eslint-plugin-storybook": "^0.5.7",
"favicons-webpack-plugin": "^5.0.2",
"file-loader": "^6.2.0",
"fork-ts-checker-webpack-plugin": "^7.2.1",
"html-webpack-plugin": "^5.3.2",
"imports-loader": "^3.0.0",
"jest": "^27.1.0",
"loader-utils": "^2.0.0",
"mini-css-extract-plugin": "^2.2.2",
"prettier": "2.3.2",
"react-refresh": "^0.10.0",
@@ -69,14 +80,14 @@
"style-loader": "^3.2.1",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.5",
"tsconfig-paths-webpack-plugin": "^3.5.1",
"typescript": "^4.4.2",
"webpack": "^5.52.0",
"tsconfig-paths-webpack-plugin": "^3.5.2",
"typescript": "^4.6.2",
"url-loader": "^4.1.1",
"webpack": "^5.64.3",
"webpack-cli": "^4.8.0",
"webpack-dev-server": "^4.1.0",
"webpack-favicons": "^1.0.7",
"webpack-merge": "^5.8.0",
"yaml-loader": "^0.6.0"
"webpack-dev-server": "^4.5.0",
"webpack-favicons": "^1.3.8",
"webpack-merge": "^5.8.0"
},
"scripts": {
"start": "webpack serve --progress --port 3000",
+1 -2
View File
@@ -15,7 +15,6 @@ export const COUNTRY_DATA_API = `${API_BASE_URL}/countries`;
export const UPTIME_STORY_API = `${VALIDATOR_API_BASE_URL}/api/v1/status/mixnode`; // add ID then '/history' to this.
// errors
export const MIXNODE_API_ERROR =
"We're having trouble finding that record, please try again or Contact Us.";
export const MIXNODE_API_ERROR = "We're having trouble finding that record, please try again or Contact Us.";
export const NYM_WEBSITE = 'https://nymtech.net';
+6 -17
View File
@@ -63,9 +63,7 @@ export class Api {
return json;
};
static fetchMixnodesActiveSetByStatus = async (
status: MixnodeStatus,
): Promise<MixNodeResponse> => {
static fetchMixnodesActiveSetByStatus = async (status: MixnodeStatus): Promise<MixNodeResponse> => {
const cachedMixnodes = getFromCache(`mixnodes-${status}`);
if (cachedMixnodes) {
return cachedMixnodes;
@@ -76,9 +74,7 @@ export class Api {
return json;
};
static fetchMixnodeByID = async (
id: string,
): Promise<MixNodeResponseItem | undefined> => {
static fetchMixnodeByID = async (id: string): Promise<MixNodeResponseItem | undefined> => {
const response = await fetch(`${MIXNODE_API}/${id}`);
// when the mixnode is not found, returned undefined
@@ -117,24 +113,17 @@ export class Api {
return result;
};
static fetchDelegationsById = async (
id: string,
): Promise<DelegationsResponse> =>
static fetchDelegationsById = async (id: string): Promise<DelegationsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/delegations`)).json();
static fetchStatsById = async (id: string): Promise<StatsResponse> =>
(await fetch(`${MIXNODE_API}/${id}/stats`)).json();
static fetchMixnodeDescriptionById = async (
id: string,
): Promise<MixNodeDescriptionResponse> =>
static fetchMixnodeDescriptionById = async (id: string): Promise<MixNodeDescriptionResponse> =>
(await fetch(`${MIXNODE_API}/${id}/description`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> =>
(await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchStatusById = async (id: string): Promise<StatusResponse> => (await fetch(`${MIXNODE_PING}/${id}`)).json();
static fetchUptimeStoryById = async (
id: string,
): Promise<UptimeStoryResponse> =>
static fetchUptimeStoryById = async (id: string): Promise<UptimeStoryResponse> =>
(await fetch(`${UPTIME_STORY_API}/${id}/history`)).json();
}
+1 -8
View File
@@ -20,14 +20,7 @@ export const ContentCard: React.FC<ContentCardProps> = ({
onClick,
}) => (
<Card onClick={onClick} sx={{ height: '100%' }}>
{title && (
<CardHeader
title={title || ''}
avatar={Icon}
action={Action}
subheader={subtitle}
/>
)}
{title && <CardHeader title={title || ''} avatar={Icon} action={Action} subheader={subtitle} />}
{children && <CardContent>{children}</CardContent>}
{errorMsg && (
<Typography variant="body2" sx={{ color: 'danger', padding: 2 }}>
@@ -2,9 +2,7 @@ import * as React from 'react';
import { Box, Typography } from '@mui/material';
import { ExpandLess, ExpandMore } from '@mui/icons-material';
export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({
headingTitle,
}) => {
export const CustomColumnHeading: React.FC<{ headingTitle: string }> = ({ headingTitle }) => {
const [filter, toggleFilter] = React.useState<boolean>(false);
const handleClick = () => {
+17 -17
View File
@@ -1,13 +1,7 @@
import * as React from 'react';
import {
Paper,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
} from '@mui/material';
import { Paper, Table, TableBody, TableCell, TableContainer, TableHead, TableRow } from '@mui/material';
import { CopyToClipboard } from '@nymproject/react';
import { Box } from '@mui/system';
import { cellStyles } from './Universal-DataGrid';
import { currencyToString } from '../utils/currency';
import { MixnodeRowType } from './MixNodes';
@@ -27,6 +21,18 @@ export interface UniversalTableProps {
}
function formatCellValues(val: string | number, field: string) {
if (field === 'identity_key' && typeof val === 'string') {
return (
<Box display="flex" justifyContent="flex-end">
<CopyToClipboard
sx={{ mr: 1, mt: 0.5, fontSize: '18px' }}
value={val}
tooltip={`Copy identity key ${val} to clipboard`}
/>
<span>{val}</span>
</Box>
);
}
if (field === 'bond') {
return currencyToString(val.toString());
}
@@ -51,10 +57,7 @@ export const DetailTable: React.FC<{
</TableHead>
<TableBody>
{rows.map((eachRow) => (
<TableRow
key={eachRow.id}
sx={{ '&:last-child td, &:last-child th': { border: 0 } }}
>
<TableRow key={eachRow.id} sx={{ '&:last-child td, &:last-child th': { border: 0 } }}>
{columnsData?.map((_, index) => (
<TableCell
key={_.title}
@@ -69,10 +72,7 @@ export const DetailTable: React.FC<{
}}
data-testid={`${_.title.replace(/ /g, '-')}-value`}
>
{formatCellValues(
eachRow[columnsData[index].field],
columnsData[index].field,
)}
{formatCellValues(eachRow[columnsData[index].field], columnsData[index].field)}
</TableCell>
))}
</TableRow>
+1 -1
View File
@@ -39,7 +39,7 @@ export const Footer: React.FC = () => {
sx={{
fontSize: 12,
textAlign: isMobile ? 'center' : 'end',
color: theme.palette.nym.text.footer,
color: theme.palette.nym.muted.onDarkBg,
}}
>
© {new Date().getFullYear()} Nym Technologies SA, all rights reserved
@@ -1,4 +1,3 @@
import * as React from 'react';
import { GatewayResponse } from '../typeDefs/explorer-api';
export type GatewayRowType = {
@@ -10,9 +9,7 @@ export type GatewayRowType = {
location: string;
};
export function gatewayToGridRow(
arrayOfGateways: GatewayResponse,
): GatewayRowType[] {
export function gatewayToGridRow(arrayOfGateways: GatewayResponse): GatewayRowType[] {
return !arrayOfGateways
? []
: arrayOfGateways.map((gw) => ({
@@ -1,15 +1,14 @@
import * as React from 'react';
import CheckCircleOutlineIcon from '@mui/icons-material/CheckCircleOutline';
import PauseCircleOutlineIcon from '@mui/icons-material/PauseCircleOutline';
import CircleOutlinedIcon from '@mui/icons-material/CircleOutlined';
import { MixnodeStatus } from '../typeDefs/explorer-api';
export const Icons = {
mixnodes: {
status: {
active: CheckCircleOutlineIcon,
standby: PauseCircleOutlineIcon,
inactive: CircleOutlinedIcon,
Mixnodes: {
Status: {
Active: CheckCircleOutlineIcon,
Standby: PauseCircleOutlineIcon,
Inactive: CircleOutlinedIcon,
},
},
};
@@ -18,12 +17,12 @@ export const getMixNodeIcon = (value: any) => {
if (value && typeof value === 'string') {
switch (value) {
case MixnodeStatus.active:
return Icons.mixnodes.status.active;
return Icons.Mixnodes.Status.Active;
case MixnodeStatus.standby:
return Icons.mixnodes.status.standby;
return Icons.Mixnodes.Status.Standby;
default:
return Icons.mixnodes.status.inactive;
return Icons.Mixnodes.Status.Inactive;
}
}
return Icons.mixnodes.status.inactive;
return Icons.Mixnodes.Status.Inactive;
};
@@ -14,8 +14,7 @@ import { useMixnodeContext } from '../../context/mixnode';
export const BondBreakdownTable: React.FC = () => {
const { mixNode, delegations } = useMixnodeContext();
const [showDelegations, toggleShowDelegations] =
React.useState<boolean>(false);
const [showDelegations, toggleShowDelegations] = React.useState<boolean>(false);
const [bonds, setBonds] = React.useState({
delegations: '0',
@@ -43,9 +42,7 @@ export const BondBreakdownTable: React.FC = () => {
// bonds total (del + pledges)
const pledgesSum = Number(mixNode.data.pledge_amount.amount);
const delegationsSum = Number(mixNode.data.total_delegation.amount);
const bondsTotal = currencyToString(
(delegationsSum + pledgesSum).toString(),
);
const bondsTotal = currencyToString((delegationsSum + pledgesSum).toString());
setBonds({
delegations: decimalisedDelegations,
@@ -79,116 +76,95 @@ export const BondBreakdownTable: React.FC = () => {
return <Alert severity="error">Mixnode not found</Alert>;
}
if (delegations?.error) {
return (
<Alert severity="error">Unable to get delegations for mixnode</Alert>
);
return <Alert severity="error">Unable to get delegations for mixnode</Alert>;
}
return (
<>
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
<TableBody>
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
<TableCell
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} aria-label="bond breakdown totals">
<TableBody>
<TableRow sx={matches ? { minWidth: '70vw' } : null}>
<TableCell
sx={{
fontWeight: 400,
width: '150px',
}}
align="left"
>
Bond total
</TableCell>
<TableCell align="left" data-testid="bond-total-amount">
{bonds.bondsTotal}
</TableCell>
</TableRow>
<TableRow>
<TableCell align="left">Pledge total</TableCell>
<TableCell align="left" data-testid="pledge-total-amount">
{bonds.pledges}
</TableCell>
</TableRow>
<TableRow>
<TableCell onClick={expandDelegations} align="left">
<Box
sx={{
fontWeight: 400,
width: '150px',
display: 'flex',
alignItems: 'center',
}}
align="left"
>
Bond total
</TableCell>
<TableCell align="left" data-testid="bond-total-amount">
{bonds.bondsTotal}
</TableCell>
</TableRow>
<TableRow>
<TableCell align="left">Pledge total</TableCell>
<TableCell align="left" data-testid="pledge-total-amount">
{bonds.pledges}
</TableCell>
</TableRow>
<TableRow>
<TableCell onClick={expandDelegations} align="left">
<Box
Delegation total {'\u00A0'}
{delegations?.data && delegations?.data?.length > 0 && <ExpandMore />}
</Box>
</TableCell>
<TableCell align="left" data-testid="delegation-total-amount">
{bonds.delegations}
</TableCell>
</TableRow>
</TableBody>
</Table>
{showDelegations && (
<Box
sx={{
maxHeight: 400,
overflowY: 'scroll',
}}
>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
Delegators
</TableCell>
<TableCell sx={{ fontWeight: 600, background: '#242C3D' }} align="left">
Stake
</TableCell>
<TableCell
sx={{
display: 'flex',
alignItems: 'center',
fontWeight: 600,
background: '#242C3D',
width: '200px',
}}
align="left"
>
Delegation total {'\u00A0'}
{delegations?.data && delegations?.data?.length > 0 && (
<ExpandMore />
)}
</Box>
</TableCell>
<TableCell align="left" data-testid="delegation-total-amount">
{bonds.delegations}
</TableCell>
</TableRow>
</TableBody>
</Table>
Share from bond
</TableCell>
</TableRow>
</TableHead>
{showDelegations && (
<Box
sx={{
maxHeight: 400,
overflowY: 'scroll',
}}
>
<Table stickyHeader>
<TableHead>
<TableRow>
<TableCell
sx={{ fontWeight: 600, background: '#242C3D' }}
align="left"
>
Delegators
</TableCell>
<TableCell
sx={{ fontWeight: 600, background: '#242C3D' }}
align="left"
>
Stake
</TableCell>
<TableCell
sx={{
fontWeight: 600,
background: '#242C3D',
width: '200px',
}}
align="left"
>
Share from bond
<TableBody>
{delegations?.data?.map(({ owner, amount: { amount, denom } }) => (
<TableRow key={owner}>
<TableCell sx={matches ? { width: 190 } : null} align="left">
{owner}
</TableCell>
<TableCell align="left">{currencyToString(amount.toString(), denom)}</TableCell>
<TableCell align="left">{calcBondPercentage(amount)}%</TableCell>
</TableRow>
</TableHead>
<TableBody>
{delegations?.data?.map(
({ owner, amount: { amount, denom } }) => (
<TableRow key={owner}>
<TableCell
sx={matches ? { width: 190 } : null}
align="left"
>
{owner}
</TableCell>
<TableCell align="left">
{currencyToString(amount.toString(), denom)}
</TableCell>
<TableCell align="left">
{calcBondPercentage(amount)}%
</TableCell>
</TableRow>
),
)}
</TableBody>
</Table>
</Box>
)}
</TableContainer>
</>
))}
</TableBody>
</Table>
</Box>
)}
</TableContainer>
);
};
@@ -11,17 +11,11 @@ interface MixNodeDetailProps {
mixnodeDescription: MixNodeDescriptionResponse;
}
export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({
mixNodeRow,
mixnodeDescription,
}) => {
export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({ mixNodeRow, mixnodeDescription }) => {
const theme = useTheme();
const palette = [theme.palette.text.primary];
const matches = useMediaQuery(theme.breakpoints.down('sm'));
const statusText = React.useMemo(
() => getMixNodeStatusText(mixNodeRow.status),
[mixNodeRow.status],
);
const statusText = React.useMemo(() => getMixNodeStatusText(mixNodeRow.status), [mixNodeRow.status]);
return (
<Grid container>
<Grid item xs={12} sm={6}>
@@ -40,17 +34,11 @@ export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({
placeItems: 'center',
}}
>
<Identicon
size={43}
string={mixNodeRow.identity_key}
palette={palette}
/>
<Identicon size={43} string={mixNodeRow.identity_key} palette={palette} />
</Box>
<Box ml={2}>
<Typography fontSize={21}>{mixnodeDescription.name}</Typography>
<Typography>
{(mixnodeDescription.description || '').slice(0, 1000)}
</Typography>
<Typography>{(mixnodeDescription.description || '').slice(0, 1000)}</Typography>
<Button
component="a"
variant="text"
@@ -76,14 +64,7 @@ export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({
</Box>
</Box>
</Grid>
<Grid
item
xs={12}
sm={6}
display="flex"
justifyContent="end"
mt={matches ? 5 : undefined}
>
<Grid item xs={12} sm={6} display="flex" justifyContent="end" mt={matches ? 5 : undefined}>
<Box display="flex" flexDirection="column">
<Typography fontWeight="600" alignSelf="self-end">
Node status:
@@ -91,12 +72,7 @@ export const MixNodeDetailSection: React.FC<MixNodeDetailProps> = ({
<Box mt={2} alignSelf="self-end">
<MixNodeStatus status={mixNodeRow.status} />
</Box>
<Typography
mt={1}
alignSelf="self-end"
color={theme.palette.text.secondary}
fontSize="smaller"
>
<Typography mt={1} alignSelf="self-end" color={theme.palette.text.secondary} fontSize="smaller">
This node is {statusText} in this epoch
</Typography>
</Box>
+15 -16
View File
@@ -1,7 +1,6 @@
import { Typography } from '@mui/material';
import * as React from 'react';
import { Theme, useTheme } from '@mui/material/styles';
import { MixnodeRowType } from '.';
import { getMixNodeIcon } from '../Icons';
import { MixnodeStatus } from '../../typeDefs/explorer-api';
@@ -9,21 +8,6 @@ interface MixNodeStatusProps {
status: MixnodeStatus;
}
export const MixNodeStatus: React.FC<MixNodeStatusProps> = ({ status }) => {
const theme = useTheme();
const Icon = React.useMemo(() => getMixNodeIcon(status), [status]);
const color = React.useMemo(() => getMixNodeStatusColor(theme, status), [status, theme]);
return (
<Typography color={color} display="flex" alignItems="center">
<Icon />
<Typography ml={1} component="span" color="inherit">
{`${status[0].toUpperCase()}${status.slice(1)}`}
</Typography>
</Typography>
);
};
export const getMixNodeStatusColor = (theme: Theme, status: MixnodeStatus) => {
let color;
switch (status) {
@@ -51,3 +35,18 @@ export const getMixNodeStatusText = (status: MixnodeStatus) => {
return 'inactive';
}
};
export const MixNodeStatus: React.FC<MixNodeStatusProps> = ({ status }) => {
const theme = useTheme();
const Icon = React.useMemo(() => getMixNodeIcon(status), [status]);
const color = React.useMemo(() => getMixNodeStatusColor(theme, status), [status, theme]);
return (
<Typography color={color} display="flex" alignItems="center">
<Icon />
<Typography ml={1} component="span" color="inherit">
{`${status[0].toUpperCase()}${status.slice(1)}`}
</Typography>
</Typography>
);
};
@@ -5,10 +5,7 @@ import { SelectInputProps } from '@mui/material/Select/SelectInput';
import { useTheme } from '@mui/material/styles';
import { SxProps } from '@mui/system';
import { MixNodeStatus } from './Status';
import {
MixnodeStatus,
MixnodeStatusWithAll,
} from '../../typeDefs/explorer-api';
import { MixnodeStatus, MixnodeStatusWithAll } from '../../typeDefs/explorer-api';
// TODO: replace with i18n
const ALL_NODES = 'All nodes';
@@ -19,26 +16,19 @@ interface MixNodeStatusDropdownProps {
onSelectionChanged?: (status?: MixnodeStatusWithAll) => void;
}
export const MixNodeStatusDropdown: React.FC<MixNodeStatusDropdownProps> = ({
status,
onSelectionChanged,
sx,
}) => {
export const MixNodeStatusDropdown: React.FC<MixNodeStatusDropdownProps> = ({ status, onSelectionChanged, sx }) => {
const theme = useTheme();
const matches = useMediaQuery(theme.breakpoints.down('sm'));
const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(
status || MixnodeStatusWithAll.all,
const [statusValue, setStatusValue] = React.useState<MixnodeStatusWithAll>(status || MixnodeStatusWithAll.all);
const onChange: SelectInputProps<MixnodeStatusWithAll>['onChange'] = React.useCallback(
({ target: { value } }) => {
setStatusValue(value);
if (onSelectionChanged) {
onSelectionChanged(value);
}
},
[onSelectionChanged],
);
const onChange: SelectInputProps<MixnodeStatusWithAll>['onChange'] =
React.useCallback(
({ target: { value } }) => {
setStatusValue(value);
if (onSelectionChanged) {
onSelectionChanged(value);
}
},
[onSelectionChanged],
);
return (
<Select
@@ -61,28 +51,16 @@ export const MixNodeStatusDropdown: React.FC<MixNodeStatusDropdownProps> = ({
...sx,
}}
>
<MenuItem
value={MixnodeStatus.active}
data-testid="mixnodeStatusSelectOption_active"
>
<MenuItem value={MixnodeStatus.active} data-testid="mixnodeStatusSelectOption_active">
<MixNodeStatus status={MixnodeStatus.active} />
</MenuItem>
<MenuItem
value={MixnodeStatus.standby}
data-testid="mixnodeStatusSelectOption_standby"
>
<MenuItem value={MixnodeStatus.standby} data-testid="mixnodeStatusSelectOption_standby">
<MixNodeStatus status={MixnodeStatus.standby} />
</MenuItem>
<MenuItem
value={MixnodeStatus.inactive}
data-testid="mixnodeStatusSelectOption_inactive"
>
<MenuItem value={MixnodeStatus.inactive} data-testid="mixnodeStatusSelectOption_inactive">
<MixNodeStatus status={MixnodeStatus.inactive} />
</MenuItem>
<MenuItem
value={MixnodeStatusWithAll.all}
data-testid="mixnodeStatusSelectOption_allNodes"
>
<MenuItem value={MixnodeStatusWithAll.all} data-testid="mixnodeStatusSelectOption_allNodes">
{ALL_NODES}
</MenuItem>
</Select>
+3 -11
View File
@@ -1,9 +1,5 @@
/* eslint-disable camelcase */
import {
MixNodeResponse,
MixNodeResponseItem,
MixnodeStatus,
} from '../../typeDefs/explorer-api';
import { MixNodeResponse, MixNodeResponseItem, MixnodeStatus } from '../../typeDefs/explorer-api';
export type MixnodeRowType = {
id: string;
@@ -17,15 +13,11 @@ export type MixnodeRowType = {
layer: string;
};
export function mixnodeToGridRow(
arrayOfMixnodes?: MixNodeResponse,
): MixnodeRowType[] {
export function mixnodeToGridRow(arrayOfMixnodes?: MixNodeResponse): MixnodeRowType[] {
return (arrayOfMixnodes || []).map(mixNodeResponseItemToMixnodeRowType);
}
export function mixNodeResponseItemToMixnodeRowType(
item: MixNodeResponseItem,
): MixnodeRowType {
export function mixNodeResponseItemToMixnodeRowType(item: MixNodeResponseItem): MixnodeRowType {
const pledge = Number(item.pledge_amount.amount) || 0;
const delegations = Number(item.total_delegation.amount) || 0;
const totalBond = pledge + delegations;
+6 -13
View File
@@ -16,9 +16,9 @@ import {
Typography,
} from '@mui/material';
import { Menu } from '@mui/icons-material';
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
import { useMainContext } from 'src/context/main';
import { MobileDrawerClose } from 'src/icons/MobileDrawerClose';
import { NymLogo } from '@nymproject/react';
import { useMainContext } from '../context/main';
import { MobileDrawerClose } from '../icons/MobileDrawerClose';
import { Footer } from './Footer';
import { NYM_WEBSITE } from '../api/constants';
import { ExpandableButton } from './Nav';
@@ -28,9 +28,7 @@ type MobileNavProps = {
children: React.ReactNode;
};
export const MobileNav: React.FC<{ children: React.ReactNode }> = ({
children,
}: MobileNavProps) => {
export const MobileNav: React.FC<{ children: React.ReactNode }> = ({ children }: MobileNavProps) => {
const theme = useTheme();
const { navState, updateNavState } = useMainContext();
const [drawerOpen, setDrawerOpen] = React.useState(false);
@@ -73,7 +71,7 @@ export const MobileNav: React.FC<{ children: React.ReactNode }> = ({
}}
>
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
<NymLogoSVG />
<NymLogo height="40px" width="40px" />
</IconButton>
<Typography
variant="h6"
@@ -85,12 +83,7 @@ export const MobileNav: React.FC<{ children: React.ReactNode }> = ({
ml: 2,
}}
>
<MuiLink
component={Link}
to="/overview"
underline="none"
color="inherit"
>
<MuiLink component={Link} to="/overview" underline="none" color="inherit">
Network Explorer
</MuiLink>
</Typography>
+12 -23
View File
@@ -14,10 +14,10 @@ import IconButton from '@mui/material/IconButton';
import ListItemButton from '@mui/material/ListItemButton';
import ListItemIcon from '@mui/material/ListItemIcon';
import ListItemText from '@mui/material/ListItemText';
import { NymLogoSVG } from 'src/icons/NymLogoSVG';
import { BIG_DIPPER, NYM_WEBSITE } from 'src/api/constants';
import { useMainContext } from 'src/context/main';
import { MobileDrawerClose } from 'src/icons/MobileDrawerClose';
import { NymLogo } from '@nymproject/react';
import { BIG_DIPPER, NYM_WEBSITE } from '../api/constants';
import { useMainContext } from '../context/main';
import { MobileDrawerClose } from '../icons/MobileDrawerClose';
import { OverviewSVG } from '../icons/OverviewSVG';
import { NetworkComponentsSVG } from '../icons/NetworksSVG';
import { NodemapSVG } from '../icons/NodemapSVG';
@@ -70,17 +70,17 @@ const Drawer = styled(MuiDrawer, {
}),
}));
type navOptionType = {
type NavOptionType = {
id: number;
isActive?: boolean;
url: string;
title: string;
Icon?: React.ReactNode;
nested?: navOptionType[];
nested?: NavOptionType[];
isExpandedChild?: boolean;
};
export const originalNavOptions: navOptionType[] = [
export const originalNavOptions: NavOptionType[] = [
{
id: 0,
isActive: false,
@@ -127,7 +127,7 @@ type ExpandableButtonType = {
url: string;
isActive?: boolean;
Icon?: React.ReactNode;
nested?: navOptionType[];
nested?: NavOptionType[];
isChild?: boolean;
openDrawer: () => void;
closeDrawer?: () => void;
@@ -226,9 +226,7 @@ export const ExpandableButton: React.FC<ExpandableButtonType> = ({
sx={{
pt: 2,
pb: 2,
background: isChild
? palette.nym.networkExplorer.nav.selected.nested
: 'none',
background: isChild ? palette.nym.networkExplorer.nav.selected.nested : 'none',
}}
>
<ListItemIcon>{Icon}</ListItemIcon>
@@ -334,7 +332,7 @@ export const Nav: React.FC = ({ children }) => {
}}
>
<IconButton component="a" href={NYM_WEBSITE} target="_blank">
<NymLogoSVG />
<NymLogo height="40px" width="40px" />
</IconButton>
<Typography
variant="h6"
@@ -345,12 +343,7 @@ export const Nav: React.FC = ({ children }) => {
fontWeight: 600,
}}
>
<MuiLink
component={Link}
to="/overview"
underline="none"
color="inherit"
>
<MuiLink component={Link} to="/overview" underline="none" color="inherit">
Network Explorer
</MuiLink>
</Typography>
@@ -408,11 +401,7 @@ export const Nav: React.FC = ({ children }) => {
</IconButton>
</DrawerHeader>
<List
sx={{ pt: 0, pb: 0 }}
onMouseEnter={tempDrawerOpen}
onMouseLeave={tempDrawerClose}
>
<List sx={{ pt: 0, pb: 0 }} onMouseEnter={tempDrawerOpen} onMouseLeave={tempDrawerClose}>
{navState.map((props) => (
<ExpandableButton
key={props.url}
+4 -24
View File
@@ -19,38 +19,18 @@ export const Socials: React.FC<{ isFooter?: boolean }> = ({ isFooter }) => {
: theme.palette.nym.networkExplorer.topNav.socialIcons;
return (
<Box>
<IconButton
component="a"
href={TELEGRAM_LINK}
target="_blank"
data-testid="telegram"
>
<IconButton component="a" href={TELEGRAM_LINK} target="_blank" data-testid="telegram">
<TelegramIcon color={color} size={24} />
</IconButton>
{false && (
<IconButton
component="a"
href={DISCORD_LINK}
target="_blank"
data-testid="discord"
>
<IconButton component="a" href={DISCORD_LINK} target="_blank" data-testid="discord">
<DiscordIcon color={color} size={24} />
</IconButton>
)}
<IconButton
component="a"
href={TWITTER_LINK}
target="_blank"
data-testid="twitter"
>
<IconButton component="a" href={TWITTER_LINK} target="_blank" data-testid="twitter">
<TwitterIcon color={color} size={24} />
</IconButton>
<IconButton
component="a"
href={GITHUB_LINK}
target="_blank"
data-testid="github"
>
<IconButton component="a" href={GITHUB_LINK} target="_blank" data-testid="github">
<GitHubIcon color={color} size={24} />
</IconButton>
</Box>
+3 -21
View File
@@ -11,14 +11,7 @@ interface StatsCardProps {
onClick?: () => void;
color?: string;
}
export const StatsCard: React.FC<StatsCardProps> = ({
icon,
title,
count,
onClick,
errorMsg,
color: colorProp,
}) => {
export const StatsCard: React.FC<StatsCardProps> = ({ icon, title, count, onClick, errorMsg, color: colorProp }) => {
const theme = useTheme();
const color = colorProp || theme.palette.text.primary;
return (
@@ -38,21 +31,10 @@ export const StatsCard: React.FC<StatsCardProps> = ({
<Box display="flex" alignItems="center" color={color}>
<Box display="flex">
{icon}
<Typography
ml={3}
mr={0.75}
fontSize="inherit"
fontWeight="inherit"
data-testid={`${title}-amount`}
>
<Typography ml={3} mr={0.75} fontSize="inherit" fontWeight="inherit" data-testid={`${title}-amount`}>
{count === undefined || count === null ? '' : count}
</Typography>
<Typography
mr={1}
fontSize="inherit"
fontWeight="inherit"
data-testid={title}
>
<Typography mr={1} fontSize="inherit" fontWeight="inherit" data-testid={title}>
{title}
</Typography>
</Box>

Some files were not shown because too many files have changed in this diff Show More