diff --git a/clients/validator/src/caches/mixnodes.ts b/clients/validator/src/caches/mixnodes.ts index f1f256ad20..a80d89ac29 100644 --- a/clients/validator/src/caches/mixnodes.ts +++ b/clients/validator/src/caches/mixnodes.ts @@ -1,7 +1,7 @@ import { MixNodeBond } from "../types"; import { INetClient } from "../net-client" import {IQueryClient} from "../query-client"; -import {PagedResponse} from "../index"; +import {PagedMixnodeResponse} from "../index"; export { MixnodesCache }; @@ -27,7 +27,7 @@ export default class MixnodesCache { // returns true. async refreshMixNodes(contractAddress: string): Promise { let newMixnodes: MixNodeBond[] = []; - let response: PagedResponse; + let response: PagedMixnodeResponse; let next: string | undefined = undefined; for (;;) { response = await this.client.getMixNodes(contractAddress, this.perPage, next); diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index 377fa33092..6c2115b035 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -598,7 +598,7 @@ export default class ValidatorClient { /// which has just been deleted from the database. /// /// TODO: more robust error handling on the "deleted key" case. -export type PagedResponse = { +export type PagedMixnodeResponse = { nodes: MixNodeBond[], per_page: number, // TODO: camelCase start_next_after: string, // TODO: camelCase diff --git a/clients/validator/src/net-client.ts b/clients/validator/src/net-client.ts index 9044153133..32f9b1f045 100644 --- a/clients/validator/src/net-client.ts +++ b/clients/validator/src/net-client.ts @@ -4,7 +4,7 @@ import { GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, - PagedResponse, + PagedMixnodeResponse, StateParams } from "./index"; import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; @@ -17,7 +17,7 @@ export interface INetClient { clientAddress: string; getBalance(address: string, stakeDenom: string): Promise; - getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; + getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; getGateways(contractAddress: string, limit: number, start_after?: string): Promise; getMixDelegations(contractAddress: string, mixIdentity: string, limit: number, start_after?: string): Promise getMixDelegation(contractAddress: string, mixIdentity: string, delegatorAddress: string): Promise @@ -71,7 +71,7 @@ export default class NetClient implements INetClient { this.cosmClient = await SigningCosmWasmClient.connectWithSigner(url, this.wallet, this.signerOptions); } - public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise { + public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise { if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here. return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } }); } else { diff --git a/clients/validator/src/query-client.ts b/clients/validator/src/query-client.ts index 14bd41ed58..8277d7a0a8 100644 --- a/clients/validator/src/query-client.ts +++ b/clients/validator/src/query-client.ts @@ -5,13 +5,13 @@ import { GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, - PagedResponse, + PagedMixnodeResponse, StateParams } from "./index"; export interface IQueryClient { getBalance(address: string, stakeDenom: string): Promise; - getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; + getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; getGateways(contractAddress: string, limit: number, start_after?: string): Promise; getMixDelegations(contractAddress: string, mixIdentity: string, limit: number, start_after?: string): Promise getMixDelegation(contractAddress: string, mixIdentity: string, delegatorAddress: string): Promise @@ -47,7 +47,7 @@ export default class QueryClient implements IQueryClient { this.cosmClient = await CosmWasmClient.connect(url) } - public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise { + public getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise { if (start_after == undefined) { // TODO: check if we can take this out, I'm not sure what will happen if we send an "undefined" so I'm playing it safe here. return this.cosmClient.queryContractSmart(contractAddress, { get_mix_nodes: { limit } }); } else { diff --git a/clients/validator/src/types.ts b/clients/validator/src/types.ts index 201fac8943..81465d465f 100644 --- a/clients/validator/src/types.ts +++ b/clients/validator/src/types.ts @@ -4,7 +4,8 @@ export type MixNodeBond = { // TODO: change name to MixNodeBond owner: string, mix_node: MixNode, // TODO: camelCase this later once everything else works - amount: Coin[], + bond_amount: Coin, + total_delegation: Coin, } export type MixNode = { @@ -22,7 +23,9 @@ export type MixNode = { export type GatewayBond = { owner: string gateway: Gateway, - amount: Coin[], + + bond_amount: Coin, + total_delegation: Coin, } export type Gateway = { diff --git a/clients/validator/tests/fixtures.ts b/clients/validator/tests/fixtures.ts index 90deac6acf..73140317a2 100644 --- a/clients/validator/tests/fixtures.ts +++ b/clients/validator/tests/fixtures.ts @@ -1,5 +1,5 @@ import { coins } from "@cosmjs/launchpad"; -import {PagedGatewayResponse, PagedResponse} from "../src/net-client"; +import {PagedGatewayResponse, PagedMixnodeResponse} from "../src/net-client"; import {GatewayBond, MixNodeBond} from "../src/types" export namespace Fixtures { diff --git a/common/client-libs/validator-client/src/lib.rs b/common/client-libs/validator-client/src/lib.rs index e18c5fd0b0..4325edb4b4 100644 --- a/common/client-libs/validator-client/src/lib.rs +++ b/common/client-libs/validator-client/src/lib.rs @@ -4,7 +4,8 @@ pub use crate::error::ValidatorClientError; use crate::models::{QueryRequest, QueryResponse}; use mixnet_contract::{ - GatewayBond, IdentityKey, LayerDistribution, MixNodeBond, PagedGatewayResponse, PagedResponse, + GatewayBond, IdentityKey, LayerDistribution, MixNodeBond, PagedGatewayResponse, + PagedMixnodeResponse, }; use rand::seq::SliceRandom; use rand::thread_rng; @@ -155,7 +156,7 @@ impl Client { async fn get_mix_nodes_paged( &mut self, start_after: Option, - ) -> Result { + ) -> Result { let query_content_json = serde_json::to_string(&QueryRequest::GetMixNodes { limit: self.config.mixnode_page_limit, start_after, diff --git a/common/mixnet-contract/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 3c8d3c0a9a..7b9c816b4a 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -2,7 +2,7 @@ #![allow(clippy::field_reassign_with_default)] use crate::{IdentityKey, SphinxKey}; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{coin, Addr, Coin}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -21,15 +21,17 @@ pub struct Gateway { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct GatewayBond { - pub amount: Vec, + pub bond_amount: Coin, + pub total_delegation: Coin, pub owner: Addr, pub gateway: Gateway, } impl GatewayBond { - pub fn new(amount: Vec, owner: Addr, gateway: Gateway) -> Self { + pub fn new(bond_amount: Coin, owner: Addr, gateway: Gateway) -> Self { GatewayBond { - amount, + total_delegation: coin(0, &bond_amount.denom), + bond_amount, owner, gateway, } @@ -39,8 +41,8 @@ impl GatewayBond { &self.gateway.identity_key } - pub fn amount(&self) -> &[Coin] { - &self.amount + pub fn bond_amount(&self) -> Coin { + self.bond_amount.clone() } pub fn owner(&self) -> &Addr { @@ -54,15 +56,11 @@ impl GatewayBond { impl Display for GatewayBond { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - if self.amount.len() != 1 { - write!(f, "amount: {:?}, owner: {}", self.amount, self.owner) - } else { - write!( - f, - "amount: {} {}, owner: {}", - self.amount[0].amount, self.amount[0].denom, self.owner - ) - } + write!( + f, + "amount: {} {}, owner: {}, identity: {}", + self.bond_amount.amount, self.bond_amount.denom, self.owner, self.gateway.identity_key + ) } } diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index d43d9d33fe..97ff5bbbbf 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -7,7 +7,7 @@ mod mixnode; pub use cosmwasm_std::{Addr, Coin}; pub use delegation::{Delegation, PagedGatewayDelegationsResponse, PagedMixDelegationsResponse}; pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; -pub use mixnode::{MixNode, MixNodeBond, MixOwnershipResponse, PagedResponse}; +pub use mixnode::{MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; #[derive(Debug, Default, Serialize, Deserialize, Copy, Clone, Eq, PartialEq)] pub struct LayerDistribution { diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index a58f48b96e..ecc818ab42 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -2,7 +2,7 @@ #![allow(clippy::field_reassign_with_default)] use crate::{IdentityKey, SphinxKey}; -use cosmwasm_std::{Addr, Coin}; +use cosmwasm_std::{coin, Addr, Coin}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Display; @@ -22,27 +22,17 @@ pub struct MixNode { #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNodeBond { - // TODO: - // JS: When we go onto the next testnet and we have to make incompatible changes in the contract (such as upgrade to cosmwasm 0.14+) - // I'd change `amount` from `Vec` to just `Coin` (or maybe even `Uint128` since denomination is implicit) - // I would also put here field like `total_delegation` which would also be a `Coin` or `Uint128` that - // indicates the sum of all delegations towards this node - // - // I would also modify the `MixNode` struct: - // - remove `location` field - // - introduce `rest_api_port` field - // - [POTENTIALLY] introduce `verloc_port` field or keep it accessible via http api - // - // I would also introduce the identical changes to GatewayBond - pub amount: Vec, + pub bond_amount: Coin, + pub total_delegation: Coin, pub owner: Addr, pub mix_node: MixNode, } impl MixNodeBond { - pub fn new(amount: Vec, owner: Addr, mix_node: MixNode) -> Self { + pub fn new(bond_amount: Coin, owner: Addr, mix_node: MixNode) -> Self { MixNodeBond { - amount, + total_delegation: coin(0, &bond_amount.denom), + bond_amount, owner, mix_node, } @@ -52,8 +42,8 @@ impl MixNodeBond { &self.mix_node.identity_key } - pub fn amount(&self) -> &[Coin] { - &self.amount + pub fn bond_amount(&self) -> Coin { + self.bond_amount.clone() } pub fn owner(&self) -> &Addr { @@ -67,36 +57,28 @@ impl MixNodeBond { impl Display for MixNodeBond { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { - // Write strictly the first element into the supplied output - // stream: `f`. Returns `fmt::Result` which indicates whether the - // operation succeeded or failed. Note that `write!` uses syntax which - // is very similar to `println!`. - if self.amount.len() != 1 { - write!(f, "amount: {:?}, owner: {}", self.amount, self.owner) - } else { - write!( - f, - "amount: {} {}, owner: {}", - self.amount[0].amount, self.amount[0].denom, self.owner - ) - } + write!( + f, + "amount: {} {}, owner: {}, identity: {}", + self.bond_amount.amount, self.bond_amount.denom, self.owner, self.mix_node.identity_key + ) } } #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] -pub struct PagedResponse { +pub struct PagedMixnodeResponse { pub nodes: Vec, pub per_page: usize, pub start_next_after: Option, } -impl PagedResponse { +impl PagedMixnodeResponse { pub fn new( nodes: Vec, per_page: usize, start_next_after: Option, ) -> Self { - PagedResponse { + PagedMixnodeResponse { nodes, per_page, start_next_after, diff --git a/common/nymsphinx/src/receiver.rs b/common/nymsphinx/src/receiver.rs index c9cbb76229..591c3de098 100644 --- a/common/nymsphinx/src/receiver.rs +++ b/common/nymsphinx/src/receiver.rs @@ -216,6 +216,7 @@ mod message_receiver { vec![mix::Node { owner: "foomp1".to_string(), stake: 123, + delegation: 456, host: "10.20.30.40".parse().unwrap(), mix_host: "10.20.30.40:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -236,6 +237,7 @@ mod message_receiver { vec![mix::Node { owner: "foomp2".to_string(), stake: 123, + delegation: 456, host: "11.21.31.41".parse().unwrap(), mix_host: "11.21.31.41:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -256,6 +258,7 @@ mod message_receiver { vec![mix::Node { owner: "foomp3".to_string(), stake: 123, + delegation: 456, host: "12.22.32.42".parse().unwrap(), mix_host: "12.22.32.42:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( @@ -277,6 +280,7 @@ mod message_receiver { vec![gateway::Node { owner: "foomp4".to_string(), stake: 123, + delegation: 456, location: "unknown".to_string(), host: "1.2.3.4".parse().unwrap(), mix_host: "1.2.3.4:1789".parse().unwrap(), diff --git a/common/topology/src/gateway.rs b/common/topology/src/gateway.rs index b285c9d48e..388b77195d 100644 --- a/common/topology/src/gateway.rs +++ b/common/topology/src/gateway.rs @@ -83,6 +83,7 @@ pub struct Node { // somebody correct me if I'm wrong, but we should only ever have a single denom of currency // on the network at a type, right? pub stake: u128, + pub delegation: u128, pub location: String, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential @@ -124,10 +125,6 @@ impl<'a> TryFrom<&'a GatewayBond> for Node { type Error = GatewayConversionError; fn try_from(bond: &'a GatewayBond) -> Result { - if bond.amount.len() > 1 { - return Err(GatewayConversionError::InvalidStake); - } - let host: NetworkAddress = bond.gateway.host.parse().map_err(|err| { GatewayConversionError::InvalidAddress(bond.gateway.host.clone(), err) })?; @@ -140,11 +137,8 @@ impl<'a> TryFrom<&'a GatewayBond> for Node { Ok(Node { owner: bond.owner.as_str().to_owned(), - stake: bond - .amount - .first() - .map(|stake| stake.amount.into()) - .unwrap_or(0), + stake: bond.bond_amount.amount.into(), + delegation: bond.total_delegation.amount.into(), location: bond.gateway.location.clone(), host, mix_host, diff --git a/common/topology/src/lib.rs b/common/topology/src/lib.rs index 3d19e04987..6cbb046c06 100644 --- a/common/topology/src/lib.rs +++ b/common/topology/src/lib.rs @@ -289,6 +289,7 @@ mod converting_mixes_to_vec { let node1 = mix::Node { owner: "N/A".to_string(), stake: 0, + delegation: 0, host: "3.3.3.3".parse().unwrap(), mix_host: "3.3.3.3:1789".parse().unwrap(), identity_key: identity::PublicKey::from_base58_string( diff --git a/common/topology/src/mix.rs b/common/topology/src/mix.rs index 8f4c8f7baf..9bf920d0c2 100644 --- a/common/topology/src/mix.rs +++ b/common/topology/src/mix.rs @@ -83,6 +83,7 @@ pub struct Node { // somebody correct me if I'm wrong, but we should only ever have a single denom of currency // on the network at a type, right? pub stake: u128, + pub delegation: u128, pub host: NetworkAddress, // we're keeping this as separate resolved field since we do not want to be resolving the potential // hostname every time we want to construct a path via this node @@ -113,10 +114,6 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node { type Error = MixnodeConversionError; fn try_from(bond: &'a MixNodeBond) -> Result { - if bond.amount.len() > 1 { - return Err(MixnodeConversionError::InvalidStake); - } - let host: NetworkAddress = bond.mix_node.host.parse().map_err(|err| { MixnodeConversionError::InvalidAddress(bond.mix_node.host.clone(), err) })?; @@ -131,11 +128,8 @@ impl<'a> TryFrom<&'a MixNodeBond> for Node { Ok(Node { owner: bond.owner.as_str().to_owned(), - stake: bond - .amount - .first() - .map(|stake| stake.amount.into()) - .unwrap_or(0), + stake: bond.bond_amount.amount.into(), + delegation: bond.total_delegation.amount.into(), host, mix_host, identity_key: identity::PublicKey::from_base58_string(&bond.mix_node.identity_key)?, diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 0abbc796af..bd2fa25984 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -178,7 +178,7 @@ pub mod tests { use crate::support::tests::helpers::*; use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info}; use cosmwasm_std::{coins, from_binary}; - use mixnet_contract::PagedResponse; + use mixnet_contract::PagedMixnodeResponse; #[test] fn initialize_contract() { @@ -200,7 +200,7 @@ pub mod tests { }, ) .unwrap(); - let page: PagedResponse = from_binary(&res).unwrap(); + let page: PagedMixnodeResponse = from_binary(&res).unwrap(); assert_eq!(0, page.nodes.len()); // there are no mixnodes in the list when it's just been initialized // Contract balance should match what we initialized it as diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 2ff1ad6682..a84cd06c82 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -12,7 +12,7 @@ use cosmwasm_std::{coin, Addr}; use mixnet_contract::{ Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond, MixOwnershipResponse, PagedGatewayDelegationsResponse, PagedGatewayResponse, - PagedMixDelegationsResponse, PagedResponse, + PagedMixDelegationsResponse, PagedMixnodeResponse, }; const BOND_PAGE_MAX_LIMIT: u32 = 100; @@ -26,7 +26,7 @@ pub fn query_mixnodes_paged( deps: Deps, start_after: Option, limit: Option, -) -> StdResult { +) -> StdResult { let limit = limit .unwrap_or(BOND_PAGE_DEFAULT_LIMIT) .min(BOND_PAGE_MAX_LIMIT) as usize; @@ -40,7 +40,7 @@ pub fn query_mixnodes_paged( let start_next_after = nodes.last().map(|node| node.identity().clone()); - Ok(PagedResponse::new(nodes, limit, start_next_after)) + Ok(PagedMixnodeResponse::new(nodes, limit, start_next_after)) } pub(crate) fn query_gateways_paged( diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index a2ee80285d..4716ed76f2 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,6 +1,6 @@ use crate::queries; use crate::state::{State, StateParams}; -use cosmwasm_std::{Decimal, Order, StdError, StdResult, Storage, Uint128}; +use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, Singleton, @@ -162,33 +162,18 @@ pub fn mixnodes_owners_read(storage: &dyn Storage) -> ReadonlyBucket StdResult<()> { - if bond.amount.len() != 1 { - return Err(StdError::generic_err( - "mixnode seems to have been bonded with multiple coin types", - )); - } - - let reward = bond.amount[0].amount * scaled_reward_rate; - bond.amount[0].amount += reward; - mixnodes(storage).save(bond.identity().as_bytes(), &bond) -} - pub(crate) fn increase_mix_delegated_stakes( storage: &mut dyn Storage, - mix_identity: IdentityKey, + mix_identity: IdentityKeyRef, scaled_reward_rate: Decimal, -) -> StdResult<()> { +) -> StdResult { let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + let mut total_rewarded = Uint128::zero(); let mut chunk_start: Option> = None; loop { // get `chunk_size` of delegations - let delegations_chunk = mix_delegations_read(storage, &mix_identity) + let delegations_chunk = mix_delegations_read(storage, mix_identity) .range(chunk_start.as_deref(), None, Order::Ascending) .take(chunk_size) .collect::>>()?; @@ -197,7 +182,7 @@ pub(crate) fn increase_mix_delegated_stakes( break; } - // append 0 byte to the last value to start with whatever is the next suceeding key + // append 0 byte to the last value to start with whatever is the next succeeding key chunk_start = Some( delegations_chunk .last() @@ -213,24 +198,26 @@ pub(crate) fn increase_mix_delegated_stakes( for (delegator_address, amount) in delegations_chunk.into_iter() { let reward = amount * scaled_reward_rate; let new_amount = amount + reward; - mix_delegations(storage, &mix_identity).save(&delegator_address, &new_amount)?; + total_rewarded += reward; + mix_delegations(storage, mix_identity).save(&delegator_address, &new_amount)?; } } - Ok(()) + Ok(total_rewarded) } pub(crate) fn increase_gateway_delegated_stakes( storage: &mut dyn Storage, - gateway_identity: IdentityKey, + gateway_identity: IdentityKeyRef, scaled_reward_rate: Decimal, -) -> StdResult<()> { +) -> StdResult { let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + let mut total_rewarded = Uint128::zero(); let mut chunk_start: Option> = None; loop { // get `chunk_size` of delegations - let delegations_chunk = gateway_delegations_read(storage, &gateway_identity) + let delegations_chunk = gateway_delegations_read(storage, gateway_identity) .range(chunk_start.as_deref(), None, Order::Ascending) .take(chunk_size) .collect::>>()?; @@ -255,12 +242,12 @@ pub(crate) fn increase_gateway_delegated_stakes( for (delegator_address, amount) in delegations_chunk.into_iter() { let reward = amount * scaled_reward_rate; let new_amount = amount + reward; - gateway_delegations(storage, &gateway_identity) - .save(&delegator_address, &new_amount)?; + total_rewarded += reward; + gateway_delegations(storage, gateway_identity).save(&delegator_address, &new_amount)?; } } - Ok(()) + Ok(total_rewarded) } // currently not used outside tests @@ -271,12 +258,7 @@ pub(crate) fn read_mixnode_bond( ) -> StdResult { let bucket = mixnodes_read(storage); let node = bucket.load(identity)?; - if node.amount.len() != 1 { - return Err(StdError::generic_err( - "mixnode seems to have been bonded with multiple coin types", - )); - } - Ok(node.amount[0].amount) + Ok(node.bond_amount.amount) } // Gateway-related stuff @@ -298,22 +280,6 @@ pub fn gateways_owners_read(storage: &dyn Storage) -> ReadonlyBucket StdResult<()> { - if bond.amount.len() != 1 { - return Err(StdError::generic_err( - "gateway seems to have been bonded with multiple coin types", - )); - } - let reward = bond.amount[0].amount * scaled_reward_rate; - bond.amount[0].amount += reward; - gateways(storage).save(bond.identity().as_bytes(), &bond) -} - // delegation related pub fn mix_delegations<'a>( storage: &'a mut dyn Storage, @@ -357,12 +323,7 @@ pub(crate) fn read_gateway_bond( ) -> StdResult { let bucket = gateways_read(storage); let node = bucket.load(identity)?; - if node.amount.len() != 1 { - return Err(StdError::generic_err( - "gateway seems to have been bonded with multiple coin types", - )); - } - Ok(node.amount[0].amount) + Ok(node.bond_amount.amount) } #[cfg(test)] @@ -373,7 +334,7 @@ mod tests { gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture, }; use cosmwasm_std::testing::MockStorage; - use cosmwasm_std::{coins, Addr, Uint128}; + use cosmwasm_std::{coin, Addr, Uint128}; use mixnet_contract::{Gateway, MixNode}; #[test] @@ -404,34 +365,6 @@ mod tests { assert_eq!(bond2, res2); } - #[test] - fn increasing_mixnode_bond() { - let mut storage = MockStorage::new(); - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - // increases the reward appropriately - let mixnode_bond = MixNodeBond { - amount: coins(1000, DENOM), - owner: node_owner.clone(), - mix_node: MixNode { - identity_key: node_identity.clone(), - ..mix_node_fixture() - }, - }; - - mixnodes(&mut storage) - .save(node_identity.as_bytes(), &mixnode_bond) - .unwrap(); - - increase_mixnode_bond(&mut storage, mixnode_bond, reward).unwrap(); - let new_bond = read_mixnode_bond(&storage, node_identity.as_bytes()).unwrap(); - assert_eq!(Uint128(1001), new_bond); - } - #[test] fn reading_mixnode_bond() { let mut storage = MockStorage::new(); @@ -446,7 +379,8 @@ mod tests { let bond_value = 1000; let mixnode_bond = MixNodeBond { - amount: coins(bond_value, DENOM), + bond_amount: coin(bond_value, DENOM), + total_delegation: coin(0, DENOM), owner: node_owner.clone(), mix_node: MixNode { identity_key: node_identity.clone(), @@ -464,34 +398,6 @@ mod tests { ); } - #[test] - fn increasing_gateway_bond() { - let mut storage = MockStorage::new(); - let node_owner: Addr = Addr::unchecked("node-owner"); - let node_identity: IdentityKey = "nodeidentity".into(); - - // 0.001 - let reward = Decimal::from_ratio(1u128, 1000u128); - - // increases the reward appropriately - let gateway_bond = GatewayBond { - amount: coins(1000, DENOM), - owner: node_owner.clone(), - gateway: Gateway { - identity_key: node_identity.clone(), - ..gateway_fixture() - }, - }; - - gateways(&mut storage) - .save(node_identity.as_bytes(), &gateway_bond) - .unwrap(); - - increase_gateway_bond(&mut storage, gateway_bond, reward).unwrap(); - let new_bond = read_gateway_bond(&storage, node_identity.as_bytes()).unwrap(); - assert_eq!(Uint128(1001), new_bond); - } - #[test] fn reading_gateway_bond() { let mut storage = MockStorage::new(); @@ -506,7 +412,8 @@ mod tests { let bond_value = 1000; let gateway_bond = GatewayBond { - amount: coins(bond_value, DENOM), + bond_amount: coin(bond_value, DENOM), + total_delegation: coin(0, DENOM), owner: node_owner.clone(), gateway: Gateway { identity_key: node_identity.clone(), @@ -538,8 +445,12 @@ mod tests { // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); - increase_mix_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = + increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) + .unwrap(); + + // there was no increase + assert!(total_increase.is_zero()); // there are no 'new' delegations magically added assert!( @@ -563,8 +474,12 @@ mod tests { .save(delegator_address.as_bytes(), &Uint128(1000)) .unwrap(); - increase_mix_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = + increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) + .unwrap(); + + assert_eq!(Uint128(1), total_increase); + assert_eq!( Uint128(1001), mix_delegations_read(&mut deps.storage, &node_identity) @@ -588,8 +503,11 @@ mod tests { .unwrap(); } - increase_mix_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = + increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) + .unwrap(); + + assert_eq!(Uint128(100), total_increase); for i in 0..100 { let delegator_address = Addr::unchecked(format!("address{}", i)); @@ -617,8 +535,14 @@ mod tests { .unwrap(); } - increase_mix_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = + increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) + .unwrap(); + + assert_eq!( + Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10), + total_increase + ); for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { let delegator_address = Addr::unchecked(format!("address{}", i)); @@ -646,8 +570,15 @@ mod tests { // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); - increase_gateway_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + ) + .unwrap(); + + // there was no increase + assert!(total_increase.is_zero()); // there are no 'new' delegations magically added assert!( @@ -671,8 +602,15 @@ mod tests { .save(delegator_address.as_bytes(), &Uint128(1000)) .unwrap(); - increase_gateway_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + ) + .unwrap(); + + assert_eq!(Uint128(1), total_increase); + assert_eq!( Uint128(1001), gateway_delegations_read(&mut deps.storage, &node_identity) @@ -696,8 +634,14 @@ mod tests { .unwrap(); } - increase_gateway_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + ) + .unwrap(); + + assert_eq!(Uint128(100), total_increase); for i in 0..100 { let delegator_address = Addr::unchecked(format!("address{}", i)); @@ -725,8 +669,17 @@ mod tests { .unwrap(); } - increase_gateway_delegated_stakes(&mut deps.storage, node_identity.clone(), reward) - .unwrap(); + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + ) + .unwrap(); + + assert_eq!( + Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10), + total_increase + ); for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { let delegator_address = Addr::unchecked(format!("address{}", i)); diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index af83ac8dbc..7f328b1148 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -7,7 +7,7 @@ pub mod helpers { use crate::msg::InstantiateMsg; use crate::msg::QueryMsg; use crate::transactions::{try_add_gateway, try_add_mixnode}; - use cosmwasm_std::coins; + use cosmwasm_std::coin; use cosmwasm_std::from_binary; use cosmwasm_std::testing::mock_dependencies; use cosmwasm_std::testing::mock_env; @@ -20,7 +20,7 @@ pub mod helpers { use cosmwasm_std::OwnedDeps; use cosmwasm_std::{Empty, MemoryStorage}; use mixnet_contract::{ - Gateway, GatewayBond, MixNode, MixNodeBond, PagedGatewayResponse, PagedResponse, + Gateway, GatewayBond, MixNode, MixNodeBond, PagedGatewayResponse, PagedMixnodeResponse, }; pub fn add_mixnode( @@ -55,7 +55,7 @@ pub mod helpers { ) .unwrap(); - let page: PagedResponse = from_binary(&result).unwrap(); + let page: PagedMixnodeResponse = from_binary(&result).unwrap(); page.nodes } @@ -128,7 +128,7 @@ pub mod helpers { identity_key: "aaaa".to_string(), version: "0.10.0".to_string(), }; - MixNodeBond::new(coins(50, DENOM), Addr::unchecked("foo"), mix_node) + MixNodeBond::new(coin(50, DENOM), Addr::unchecked("foo"), mix_node) } pub fn gateway_fixture() -> Gateway { @@ -154,7 +154,7 @@ pub mod helpers { identity_key: "identity".to_string(), version: "0.10.0".to_string(), }; - GatewayBond::new(coins(50, DENOM), Addr::unchecked("foo"), gateway) + GatewayBond::new(coin(50, DENOM), Addr::unchecked("foo"), gateway) } pub fn query_contract_balance( diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 7640f4ea5a..221390fb24 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -2,16 +2,57 @@ use crate::contract::DENOM; use crate::error::ContractError; use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime}; use crate::state::StateParams; -use crate::storage::{ - config, config_read, decrement_layer_count, gateway_delegations, gateways, gateways_owners, - gateways_owners_read, gateways_read, increase_gateway_bond, increase_gateway_delegated_stakes, - increase_mix_delegated_stakes, increase_mixnode_bond, increment_layer_count, mix_delegations, - mixnodes, mixnodes_owners, mixnodes_owners_read, mixnodes_read, read_gateway_epoch_reward_rate, - read_mixnode_epoch_reward_rate, read_state_params, Layer, +use crate::storage::*; +use cosmwasm_std::{ + attr, coins, BankMsg, Coin, Decimal, DepsMut, MessageInfo, Order, Response, StdResult, Uint128, }; -use cosmwasm_std::{attr, coins, BankMsg, Coin, Decimal, DepsMut, MessageInfo, Response, Uint128}; +use cosmwasm_storage::ReadonlyBucket; use mixnet_contract::{Gateway, GatewayBond, IdentityKey, MixNode, MixNodeBond}; +const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; + +// Looks for the total amount of delegations towards a particular node. +// This function is used only in very specific circumstances: +// 1. The mixnode/gateway bonds +// 2. Some addresses start to delegate to the node +// 3. The node unbonds +// 4. Some of the addresses that delegated in the past have not removed the delegation yet +// 5. The node rebonds with the same identity +fn find_old_delegations(delegations_bucket: ReadonlyBucket) -> StdResult { + // I think it's incredibly unlikely to ever read more than that + // but in case we do, we should guard ourselves against possible + // out of memory errors (wasm contracts can only allocate at most 2MB + // of RAM, so we don't want to box the entire iterator) + let mut total_delegation = Coin::new(0, DENOM); + let mut start = None; + loop { + let iterator = delegations_bucket + .range(start.as_deref(), None, Order::Ascending) + .take(OLD_DELEGATIONS_CHUNK_SIZE + 1); + + let mut iterated = 0; + + for delegation in iterator { + iterated += 1; + if iterated == OLD_DELEGATIONS_CHUNK_SIZE + 1 { + // we reached start of next chunk, don't process it, mark it for the next iteration of the loop + start = Some(delegation?.0); + continue; + } + + let value = delegation?.1; + total_delegation.amount += value; + } + + if iterated <= OLD_DELEGATIONS_CHUNK_SIZE { + // that was the final chunk + break; + } + } + + Ok(total_delegation) +} + fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> { // check if anything was put as bond if bond.is_empty() { @@ -76,15 +117,20 @@ pub(crate) fn try_add_mixnode( let minimum_bond = read_state_params(deps.storage).minimum_mixnode_bond; validate_mixnode_bond(&info.funds, minimum_bond)?; - let bond = MixNodeBond::new(info.funds, info.sender.clone(), mix_node); + let mut bond = MixNodeBond::new(info.funds[0].clone(), info.sender.clone(), mix_node); - let attributes = vec![attr("overwritten", was_present)]; + // this might potentially require more gas if a significant number of delegations was there + let delegations_bucket = mix_delegations_read(deps.storage, &bond.mix_node.identity_key); + let existing_delegation = find_old_delegations(delegations_bucket)?; + bond.total_delegation = existing_delegation; let identity = bond.identity(); + mixnodes(deps.storage).save(identity.as_bytes(), &bond)?; mixnodes_owners(deps.storage).save(sender_bytes, identity)?; increment_layer_count(deps.storage, bond.mix_node.layer.into())?; + let attributes = vec![attr("overwritten", was_present)]; Ok(Response { submessages: Vec::new(), messages: Vec::new(), @@ -111,7 +157,7 @@ pub(crate) fn try_remove_mixnode( // send bonded funds back to the bond owner let messages = vec![BankMsg::Send { to_address: info.sender.as_str().to_owned(), - amount: mixnode_bond.amount().to_vec(), + amount: vec![mixnode_bond.bond_amount()], } .into()]; @@ -197,15 +243,19 @@ pub(crate) fn try_add_gateway( let minimum_bond = read_state_params(deps.storage).minimum_gateway_bond; validate_gateway_bond(&info.funds, minimum_bond)?; - let bond = GatewayBond::new(info.funds, info.sender.clone(), gateway); + let mut bond = GatewayBond::new(info.funds[0].clone(), info.sender.clone(), gateway); - let attributes = vec![attr("overwritten", was_present)]; + // this might potentially require more gas if a significant number of delegations was there + let delegations_bucket = gateway_delegations_read(deps.storage, &bond.gateway.identity_key); + let existing_delegation = find_old_delegations(delegations_bucket)?; + bond.total_delegation = existing_delegation; let identity = bond.identity(); gateways(deps.storage).save(identity.as_bytes(), &bond)?; gateways_owners(deps.storage).save(sender_bytes, identity)?; increment_layer_count(deps.storage, Layer::Gateway)?; + let attributes = vec![attr("overwritten", was_present)]; Ok(Response { submessages: Vec::new(), messages: Vec::new(), @@ -232,7 +282,7 @@ pub(crate) fn try_remove_gateway( // send bonded funds back to the bond owner let messages = vec![BankMsg::Send { to_address: info.sender.as_str().to_owned(), - amount: gateway_bond.amount().to_vec(), + amount: vec![gateway_bond.bond_amount()], } .into()]; @@ -321,11 +371,19 @@ pub(crate) fn try_reward_mixnode( // optimisation for uptime being 0. No rewards will be given so just terminate here if uptime == 0 { - return Ok(Response::default()); + return Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", Uint128(0)), + attr("total delegation increase", Uint128(0)), + ], + data: None, + }); } // check if the bond even exists - let current_bond = match mixnodes(deps.storage).load(mix_identity.as_bytes()) { + let mut current_bond = match mixnodes_read(deps.storage).load(mix_identity.as_bytes()) { Ok(bond) => bond, Err(_) => { return Ok(Response { @@ -335,13 +393,27 @@ pub(crate) fn try_reward_mixnode( } }; - let reward = read_mixnode_epoch_reward_rate(deps.storage); - let scaled_reward = scale_reward_by_uptime(reward, uptime)?; + let reward_rate = read_mixnode_epoch_reward_rate(deps.storage); + let scaled_reward_rate = scale_reward_by_uptime(reward_rate, uptime)?; - increase_mixnode_bond(deps.storage, current_bond, scaled_reward)?; - increase_mix_delegated_stakes(deps.storage, mix_identity, scaled_reward)?; + let node_reward = current_bond.bond_amount.amount * scaled_reward_rate; + let total_delegation_reward = + increase_mix_delegated_stakes(deps.storage, &mix_identity, scaled_reward_rate)?; - Ok(Response::default()) + // update current bond with the reward given to the node and the delegators + current_bond.bond_amount.amount += node_reward; + current_bond.total_delegation.amount += total_delegation_reward; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; + + Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", node_reward), + attr("total delegation increase", total_delegation_reward), + ], + data: None, + }) } pub(crate) fn try_reward_gateway( @@ -357,8 +429,21 @@ pub(crate) fn try_reward_gateway( return Err(ContractError::Unauthorized); } + // optimisation for uptime being 0. No rewards will be given so just terminate here + if uptime == 0 { + return Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", Uint128(0)), + attr("total delegation increase", Uint128(0)), + ], + data: None, + }); + } + // check if the bond even exists - let current_bond = match gateways(deps.storage).load(gateway_identity.as_bytes()) { + let mut current_bond = match gateways(deps.storage).load(gateway_identity.as_bytes()) { Ok(bond) => bond, Err(_) => { return Ok(Response { @@ -368,13 +453,27 @@ pub(crate) fn try_reward_gateway( } }; - let reward = read_gateway_epoch_reward_rate(deps.storage); - let scaled_reward = scale_reward_by_uptime(reward, uptime)?; + let reward_rate = read_gateway_epoch_reward_rate(deps.storage); + let scaled_reward_rate = scale_reward_by_uptime(reward_rate, uptime)?; - increase_gateway_bond(deps.storage, current_bond, scaled_reward)?; - increase_gateway_delegated_stakes(deps.storage, gateway_identity, scaled_reward)?; + let node_reward = current_bond.bond_amount.amount * scaled_reward_rate; + let total_delegation_reward = + increase_gateway_delegated_stakes(deps.storage, &gateway_identity, scaled_reward_rate)?; - Ok(Response::default()) + // update current bond with the reward given to the node and the delegators + current_bond.bond_amount.amount += node_reward; + current_bond.total_delegation.amount += total_delegation_reward; + gateways(deps.storage).save(gateway_identity.as_bytes(), ¤t_bond)?; + + Ok(Response { + submessages: vec![], + messages: vec![], + attributes: vec![ + attr("bond increase", node_reward), + attr("total delegation increase", total_delegation_reward), + ], + data: None, + }) } fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> { @@ -409,22 +508,29 @@ pub(crate) fn try_delegate_to_mixnode( validate_delegation_stake(&info.funds)?; // check if the target node actually exists - if mixnodes_read(deps.storage) - .load(mix_identity.as_bytes()) - .is_err() - { - return Err(ContractError::MixNodeBondNotFound { - identity: mix_identity, - }); - } - - let mut bucket = mix_delegations(deps.storage, &mix_identity); - let sender_bytes = info.sender.as_bytes(); - match bucket.may_load(sender_bytes)? { - Some(existing_delegation) => { - bucket.save(sender_bytes, &(existing_delegation + info.funds[0].amount))? + let mut mixnodes_bucket = mixnodes(deps.storage); + let mut current_bond = match mixnodes_bucket.load(mix_identity.as_bytes()) { + Ok(bond) => bond, + Err(_) => { + return Err(ContractError::MixNodeBondNotFound { + identity: mix_identity, + }); } - None => bucket.save(sender_bytes, &info.funds[0].amount)?, + }; + + // update total_delegation of this node + current_bond.total_delegation.amount += info.funds[0].amount; + mixnodes_bucket.save(mix_identity.as_bytes(), ¤t_bond)?; + + let mut delegation_bucket = mix_delegations(deps.storage, &mix_identity); + let sender_bytes = info.sender.as_bytes(); + + // write the delegation + match delegation_bucket.may_load(sender_bytes)? { + Some(existing_delegation) => { + delegation_bucket.save(sender_bytes, &(existing_delegation + info.funds[0].amount))? + } + None => delegation_bucket.save(sender_bytes, &info.funds[0].amount)?, } Ok(Response::default()) @@ -449,6 +555,20 @@ pub(crate) fn try_remove_delegation_from_mixnode( } .into()]; + // update total_delegation of this node + let mut mixnodes_bucket = mixnodes(deps.storage); + // in some rare cases the mixnode bond might no longer exist as the node unbonded + // before delegation was removed. that is fine + if let Some(mut existing_bond) = mixnodes_bucket.may_load(mix_identity.as_bytes())? { + // we should NEVER underflow here, if we do, it means we have some serious error in our logic + existing_bond.total_delegation.amount = existing_bond + .total_delegation + .amount + .checked_sub(delegation) + .unwrap(); + mixnodes_bucket.save(mix_identity.as_bytes(), &existing_bond)?; + } + Ok(Response { submessages: Vec::new(), messages, @@ -472,22 +592,29 @@ pub(crate) fn try_delegate_to_gateway( validate_delegation_stake(&info.funds)?; // check if the target node actually exists - if gateways_read(deps.storage) - .load(gateway_identity.as_bytes()) - .is_err() - { - return Err(ContractError::GatewayBondNotFound { - identity: gateway_identity, - }); - } - - let mut bucket = gateway_delegations(deps.storage, &gateway_identity); - let sender_bytes = info.sender.as_bytes(); - match bucket.may_load(sender_bytes)? { - Some(existing_delegation) => { - bucket.save(sender_bytes, &(existing_delegation + info.funds[0].amount))? + let mut gateways_bucket = gateways(deps.storage); + let mut current_bond = match gateways_bucket.load(gateway_identity.as_bytes()) { + Ok(bond) => bond, + Err(_) => { + return Err(ContractError::GatewayBondNotFound { + identity: gateway_identity, + }); } - None => bucket.save(sender_bytes, &info.funds[0].amount)?, + }; + + // update total_delegation of this node + current_bond.total_delegation.amount += info.funds[0].amount; + gateways_bucket.save(gateway_identity.as_bytes(), ¤t_bond)?; + + let mut delegation_bucket = gateway_delegations(deps.storage, &gateway_identity); + let sender_bytes = info.sender.as_bytes(); + + // write the delegation + match delegation_bucket.may_load(sender_bytes)? { + Some(existing_delegation) => { + delegation_bucket.save(sender_bytes, &(existing_delegation + info.funds[0].amount))? + } + None => delegation_bucket.save(sender_bytes, &info.funds[0].amount)?, } Ok(Response::default()) @@ -512,6 +639,22 @@ pub(crate) fn try_remove_delegation_from_gateway( } .into()]; + // update total_delegation of this node + let mut gateways_bucket = gateways(deps.storage); + // in some rare cases the gateway bond might no longer exist as the node unbonded + // before delegation was removed. that is fine + if let Some(mut existing_bond) = + gateways_bucket.may_load(gateway_identity.as_bytes())? + { + // we should NEVER underflow here, if we do, it means we have some serious error in our logic + existing_bond.total_delegation.amount = existing_bond + .total_delegation + .amount + .checked_sub(delegation) + .unwrap(); + gateways_bucket.save(gateway_identity.as_bytes(), &existing_bond)?; + } + Ok(Response { submessages: Vec::new(), messages, @@ -546,8 +689,8 @@ pub mod tests { mix_node_fixture, }; use cosmwasm_std::testing::{mock_env, mock_info}; - use cosmwasm_std::{coins, from_binary, Addr, Uint128}; - use mixnet_contract::{LayerDistribution, PagedGatewayResponse, PagedResponse}; + use cosmwasm_std::{coin, coins, from_binary, Addr, Uint128}; + use mixnet_contract::{LayerDistribution, PagedGatewayResponse, PagedMixnodeResponse}; #[test] fn validating_mixnode_bond() { @@ -619,7 +762,7 @@ pub mod tests { }, ) .unwrap(); - let page: PagedResponse = from_binary(&res).unwrap(); + let page: PagedMixnodeResponse = from_binary(&res).unwrap(); assert_eq!(0, page.nodes.len()); // if we send enough funds @@ -645,7 +788,7 @@ pub mod tests { }, ) .unwrap(); - let page: PagedResponse = from_binary(&query_response).unwrap(); + let page: PagedMixnodeResponse = from_binary(&query_response).unwrap(); assert_eq!(1, page.nodes.len()); assert_eq!( &MixNode { @@ -906,7 +1049,10 @@ pub mod tests { attr("action", "unbond"), attr( "mixnode_bond", - format!("amount: {} {}, owner: fred", INITIAL_MIXNODE_BOND, DENOM), + format!( + "amount: {} {}, owner: fred, identity: fredsmixnode", + INITIAL_MIXNODE_BOND, DENOM + ), ), ]; @@ -1303,7 +1449,10 @@ pub mod tests { attr("address", "fred"), attr( "gateway_bond", - format!("amount: {} {}, owner: fred", INITIAL_GATEWAY_BOND, DENOM), + format!( + "amount: {} {}, owner: fred, identity: fredsgateway", + INITIAL_GATEWAY_BOND, DENOM + ), ), ]; @@ -1511,7 +1660,8 @@ pub mod tests { let initial_bond = 100_000000; let mixnode_bond = MixNodeBond { - amount: coins(initial_bond, DENOM), + bond_amount: coin(initial_bond, DENOM), + total_delegation: coin(0, DENOM), owner: node_owner.clone(), mix_node: MixNode { identity_key: node_identity.clone(), @@ -1523,31 +1673,49 @@ pub mod tests { .save(node_identity.as_bytes(), &mixnode_bond) .unwrap(); - let reward = read_mixnode_epoch_reward_rate(deps.as_ref().storage); + let reward_rate = read_mixnode_epoch_reward_rate(deps.as_ref().storage); + let expected_reward = Uint128(initial_bond) * reward_rate; // the node's bond is correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward - let expected_bond = Uint128(initial_bond) * reward + Uint128(initial_bond); + let expected_bond = expected_reward + Uint128(initial_bond); let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); assert_eq!( expected_bond, read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_reward), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); + // if node was 20% up, it will get 1/5th of epoch reward - let scaled_reward = scale_reward_by_uptime(reward, 20).unwrap(); - let expected_bond = expected_bond * scaled_reward + expected_bond; + let scaled_reward = scale_reward_by_uptime(reward_rate, 20).unwrap(); + let expected_reward = expected_bond * scaled_reward; + let expected_bond = expected_reward + expected_bond; let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); assert_eq!( expected_bond, read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() ); + + assert_eq!( + vec![ + attr("bond increase", expected_reward), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); } #[test] @@ -1571,7 +1739,8 @@ pub mod tests { let initial_bond = 100_000000; let gateway_bond = GatewayBond { - amount: coins(initial_bond, DENOM), + bond_amount: coin(initial_bond, DENOM), + total_delegation: coin(0, DENOM), owner: node_owner.clone(), gateway: Gateway { identity_key: node_identity.clone(), @@ -1583,31 +1752,49 @@ pub mod tests { .save(node_identity.as_bytes(), &gateway_bond) .unwrap(); - let reward = read_gateway_epoch_reward_rate(deps.as_ref().storage); + let reward_rate = read_gateway_epoch_reward_rate(deps.as_ref().storage); + let expected_reward = Uint128(initial_bond) * reward_rate; // the node's bond is correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward - let expected_bond = Uint128(initial_bond) * reward + Uint128(initial_bond); + let expected_bond = expected_reward + Uint128(initial_bond); let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); assert_eq!( expected_bond, read_gateway_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_reward), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); + // if node was 20% up, it will get 1/5th of epoch reward - let scaled_reward = scale_reward_by_uptime(reward, 20).unwrap(); - let expected_bond = expected_bond * scaled_reward + expected_bond; + let scaled_reward = scale_reward_by_uptime(reward_rate, 20).unwrap(); + let expected_reward = expected_bond * scaled_reward; + let expected_bond = expected_reward + expected_bond; let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); + let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); assert_eq!( expected_bond, read_gateway_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() ); + + assert_eq!( + vec![ + attr("bond increase", expected_reward), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); } #[cfg(test)] @@ -1683,20 +1870,29 @@ pub mod tests { let mixnode_owner = "bob"; let identity = add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + let delegation = coin(123, DENOM); assert!(try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender", &coins(123, DENOM)), + mock_info("sender", &vec![delegation.clone()]), identity.clone() ) .is_ok()); assert_eq!( - 123, + delegation.amount, mix_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is increased + assert_eq!( + delegation, + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) } #[test] @@ -1728,21 +1924,30 @@ pub mod tests { add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[])).unwrap(); let identity = add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + let delegation = coin(123, DENOM); assert!(try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender", &coins(123, DENOM)), + mock_info("sender", &vec![delegation.clone()]), identity.clone() ) .is_ok()); assert_eq!( - 123, + delegation.amount, mix_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is increased + assert_eq!( + delegation, + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) } #[test] @@ -1751,27 +1956,39 @@ pub mod tests { let mixnode_owner = "bob"; let identity = add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + let delegation1 = coin(100, DENOM); + let delegation2 = coin(50, DENOM); + try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender", &coins(100, DENOM)), + mock_info("sender", &vec![delegation1.clone()]), identity.clone(), ) .unwrap(); try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender", &coins(50, DENOM)), + mock_info("sender", &vec![delegation2.clone()]), identity.clone(), ) .unwrap(); assert_eq!( - 150, + delegation1.amount + delegation2.amount, mix_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is sum of both + assert_eq!( + delegation1.amount + delegation2.amount, + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -1847,19 +2064,32 @@ pub mod tests { let mixnode_owner = "bob"; let identity = add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + let delegation1 = coin(123, DENOM); + let delegation2 = coin(234, DENOM); + assert!(try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender1", &coins(123, DENOM)), + mock_info("sender1", &vec![delegation1.clone()]), identity.clone() ) .is_ok()); assert!(try_delegate_to_mixnode( deps.as_mut(), - mock_info("sender2", &coins(123, DENOM)), - identity + mock_info("sender2", &vec![delegation2.clone()]), + identity.clone() ) .is_ok()); + + // node's "total_delegation" is sum of both + assert_eq!( + delegation1.amount + delegation2.amount, + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -1950,6 +2180,16 @@ pub mod tests { .may_load(b"sender") .unwrap() .is_none()); + + // and total delegation is cleared + assert_eq!( + Uint128::zero(), + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -1991,6 +2231,48 @@ pub mod tests { .unwrap() .is_none()); } + + #[test] + fn total_delegation_is_preserved_if_only_some_undelegate() { + let mut deps = helpers::init_contract(); + let mixnode_owner = "bob"; + let identity = add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + let delegation1 = coin(123, DENOM); + let delegation2 = coin(234, DENOM); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender1", &vec![delegation1.clone()]), + identity.clone() + ) + .is_ok()); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender2", &vec![delegation2.clone()]), + identity.clone() + ) + .is_ok()); + + // sender1 undelegates + try_remove_delegation_from_mixnode( + deps.as_mut(), + mock_info("sender1", &[]), + identity.clone(), + ) + .unwrap(); + + // but total delegation should still equal to what sender2 sent + // node's "total_delegation" is sum of both + assert_eq!( + delegation2, + mixnodes_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) + } } #[test] @@ -2021,16 +2303,18 @@ pub mod tests { // the node's bond is correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward - let expected_bond = Uint128(initial_mix_bond) * reward + Uint128(initial_mix_bond); - let expected_delegation1 = - Uint128(initial_delegation1) * reward + Uint128(initial_delegation1); - let expected_delegation2 = - Uint128(initial_delegation2) * reward + Uint128(initial_delegation2); - let expected_delegation3 = - Uint128(initial_delegation3) * reward + Uint128(initial_delegation3); + let expected_mix_reward = Uint128(initial_mix_bond) * reward; + let expected_delegation1_reward = Uint128(initial_delegation1) * reward; + let expected_delegation2_reward = Uint128(initial_delegation2) * reward; + let expected_delegation3_reward = Uint128(initial_delegation3) * reward; + + let expected_bond = expected_mix_reward + Uint128(initial_mix_bond); + let expected_delegation1 = expected_delegation1_reward + Uint128(initial_delegation1); + let expected_delegation2 = expected_delegation2_reward + Uint128(initial_delegation2); + let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_mixnode(deps.as_mut(), info, identity.clone(), 100).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 100).unwrap(); assert_eq!( expected_bond, @@ -2058,15 +2342,34 @@ pub mod tests { .unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_mix_reward), + attr( + "total delegation increase", + expected_delegation1_reward + + expected_delegation2_reward + + expected_delegation3_reward + ), + ], + res.attributes + ); + // if node was 20% up, it will get 1/5th of epoch reward let scaled_reward = scale_reward_by_uptime(reward, 20).unwrap(); - let expected_bond = expected_bond * scaled_reward + expected_bond; - let expected_delegation1 = expected_delegation1 * scaled_reward + expected_delegation1; - let expected_delegation2 = expected_delegation2 * scaled_reward + expected_delegation2; - let expected_delegation3 = expected_delegation3 * scaled_reward + expected_delegation3; + + let expected_mix_reward = expected_bond * scaled_reward; + let expected_delegation1_reward = expected_delegation1 * scaled_reward; + let expected_delegation2_reward = expected_delegation2 * scaled_reward; + let expected_delegation3_reward = expected_delegation3 * scaled_reward; + + let expected_bond = expected_mix_reward + expected_bond; + let expected_delegation1 = expected_delegation1_reward + expected_delegation1; + let expected_delegation2 = expected_delegation2_reward + expected_delegation2; + let expected_delegation3 = expected_delegation3_reward + expected_delegation3; let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_mixnode(deps.as_mut(), info, identity.clone(), 20).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 20).unwrap(); assert_eq!( expected_bond, @@ -2094,9 +2397,22 @@ pub mod tests { .unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_mix_reward), + attr( + "total delegation increase", + expected_delegation1_reward + + expected_delegation2_reward + + expected_delegation3_reward + ), + ], + res.attributes + ); + // if the node was 0% up, nobody will get any rewards let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_mixnode(deps.as_mut(), info, identity.clone(), 0).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 0).unwrap(); assert_eq!( expected_bond, @@ -2123,6 +2439,14 @@ pub mod tests { .load("delegator3".as_bytes()) .unwrap() ); + + assert_eq!( + vec![ + attr("bond increase", Uint128(0)), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); } #[cfg(test)] @@ -2152,20 +2476,29 @@ pub mod tests { let gateway_owner = "bob"; let identity = add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + let delegation = coin(123, DENOM); assert!(try_delegate_to_gateway( deps.as_mut(), - mock_info("sender", &coins(123, DENOM)), + mock_info("sender", &vec![delegation.clone()]), identity.clone() ) .is_ok()); assert_eq!( - 123, + delegation.amount, gateway_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is increased + assert_eq!( + delegation, + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) } #[test] @@ -2196,21 +2529,30 @@ pub mod tests { add_gateway(gateway_owner, good_gateway_bond(), &mut deps); try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[])).unwrap(); let identity = add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + let delegation = coin(123, DENOM); assert!(try_delegate_to_gateway( deps.as_mut(), - mock_info("sender", &coins(123, DENOM)), + mock_info("sender", &vec![delegation.clone()]), identity.clone() ) .is_ok()); assert_eq!( - 123, + delegation.amount, gateway_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is increased + assert_eq!( + delegation, + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) } #[test] @@ -2219,27 +2561,39 @@ pub mod tests { let gateway_owner = "bob"; let identity = add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + let delegation1 = coin(100, DENOM); + let delegation2 = coin(50, DENOM); + try_delegate_to_gateway( deps.as_mut(), - mock_info("sender", &coins(100, DENOM)), + mock_info("sender", &vec![delegation1.clone()]), identity.clone(), ) .unwrap(); try_delegate_to_gateway( deps.as_mut(), - mock_info("sender", &coins(50, DENOM)), + mock_info("sender", &vec![delegation2.clone()]), identity.clone(), ) .unwrap(); assert_eq!( - 150, + delegation1.amount + delegation2.amount, gateway_delegations_read(&deps.storage, &identity) .load(b"sender") .unwrap() - .u128() ); + + // node's "total_delegation" is sum of both + assert_eq!( + delegation1.amount + delegation2.amount, + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -2315,19 +2669,32 @@ pub mod tests { let gateway_owner = "bob"; let identity = add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + let delegation1 = coin(123, DENOM); + let delegation2 = coin(234, DENOM); + assert!(try_delegate_to_gateway( deps.as_mut(), - mock_info("sender1", &coins(123, DENOM)), + mock_info("sender1", &vec![delegation1.clone()]), identity.clone() ) .is_ok()); assert!(try_delegate_to_gateway( deps.as_mut(), - mock_info("sender2", &coins(123, DENOM)), + mock_info("sender2", &vec![delegation2.clone()]), identity.clone() ) .is_ok()); + + // node's "total_delegation" is sum of both + assert_eq!( + delegation1.amount + delegation2.amount, + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -2417,6 +2784,16 @@ pub mod tests { .may_load(b"sender") .unwrap() .is_none()); + + // and total delegation is cleared + assert_eq!( + Uint128::zero(), + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + .amount + ) } #[test] @@ -2458,6 +2835,48 @@ pub mod tests { .unwrap() .is_none()); } + + #[test] + fn total_delegation_is_preserved_if_only_some_undelegate() { + let mut deps = helpers::init_contract(); + let gateway_owner = "bob"; + let identity = add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + let delegation1 = coin(123, DENOM); + let delegation2 = coin(234, DENOM); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender1", &vec![delegation1.clone()]), + identity.clone() + ) + .is_ok()); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender2", &vec![delegation2.clone()]), + identity.clone() + ) + .is_ok()); + + // sender1 undelegates + try_remove_delegation_from_gateway( + deps.as_mut(), + mock_info("sender1", &[]), + identity.clone(), + ) + .unwrap(); + + // but total delegation should still equal to what sender2 sent + // node's "total_delegation" is sum of both + assert_eq!( + delegation2, + gateways_read(&deps.storage) + .load(identity.as_bytes()) + .unwrap() + .total_delegation + ) + } } #[test] @@ -2488,16 +2907,18 @@ pub mod tests { // the node's bond is correctly increased and scaled by uptime // if node was 100% up, it will get full epoch reward - let expected_bond = Uint128(initial_gateway_bond) * reward + Uint128(initial_gateway_bond); - let expected_delegation1 = - Uint128(initial_delegation1) * reward + Uint128(initial_delegation1); - let expected_delegation2 = - Uint128(initial_delegation2) * reward + Uint128(initial_delegation2); - let expected_delegation3 = - Uint128(initial_delegation3) * reward + Uint128(initial_delegation3); + let expected_gateway_reward = Uint128(initial_gateway_bond) * reward; + let expected_delegation1_reward = Uint128(initial_delegation1) * reward; + let expected_delegation2_reward = Uint128(initial_delegation2) * reward; + let expected_delegation3_reward = Uint128(initial_delegation3) * reward; + + let expected_bond = expected_gateway_reward + Uint128(initial_gateway_bond); + let expected_delegation1 = expected_delegation1_reward + Uint128(initial_delegation1); + let expected_delegation2 = expected_delegation2_reward + Uint128(initial_delegation2); + let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_gateway(deps.as_mut(), info, identity.clone(), 100).unwrap(); + let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 100).unwrap(); assert_eq!( expected_bond, @@ -2525,15 +2946,34 @@ pub mod tests { .unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_gateway_reward), + attr( + "total delegation increase", + expected_delegation1_reward + + expected_delegation2_reward + + expected_delegation3_reward + ), + ], + res.attributes + ); + // if node was 20% up, it will get 1/5th of epoch reward let scaled_reward = scale_reward_by_uptime(reward, 20).unwrap(); - let expected_bond = expected_bond * scaled_reward + expected_bond; - let expected_delegation1 = expected_delegation1 * scaled_reward + expected_delegation1; - let expected_delegation2 = expected_delegation2 * scaled_reward + expected_delegation2; - let expected_delegation3 = expected_delegation3 * scaled_reward + expected_delegation3; + + let expected_gateway_reward = expected_bond * scaled_reward; + let expected_delegation1_reward = expected_delegation1 * scaled_reward; + let expected_delegation2_reward = expected_delegation2 * scaled_reward; + let expected_delegation3_reward = expected_delegation3 * scaled_reward; + + let expected_bond = expected_gateway_reward + expected_bond; + let expected_delegation1 = expected_delegation1_reward + expected_delegation1; + let expected_delegation2 = expected_delegation2_reward + expected_delegation2; + let expected_delegation3 = expected_delegation3_reward + expected_delegation3; let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_gateway(deps.as_mut(), info, identity.clone(), 20).unwrap(); + let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 20).unwrap(); assert_eq!( expected_bond, @@ -2561,9 +3001,22 @@ pub mod tests { .unwrap() ); + assert_eq!( + vec![ + attr("bond increase", expected_gateway_reward), + attr( + "total delegation increase", + expected_delegation1_reward + + expected_delegation2_reward + + expected_delegation3_reward + ), + ], + res.attributes + ); + // if the node was 0% up, nobody will get any rewards let info = mock_info(network_monitor_address.as_ref(), &[]); - try_reward_gateway(deps.as_mut(), info, identity.clone(), 0).unwrap(); + let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 0).unwrap(); assert_eq!( expected_bond, @@ -2590,5 +3043,65 @@ pub mod tests { .load("delegator3".as_bytes()) .unwrap() ); + + assert_eq!( + vec![ + attr("bond increase", Uint128(0)), + attr("total delegation increase", Uint128(0)), + ], + res.attributes + ); + } + + #[cfg(test)] + mod finding_old_delegations { + use super::*; + + #[test] + fn when_there_werent_any() { + let deps = helpers::init_contract(); + + let node_identity: IdentityKey = "nodeidentity".into(); + + let read_bucket = mix_delegations_read(&deps.storage, &node_identity); + let old_delegations = find_old_delegations(read_bucket).unwrap(); + + assert_eq!(Coin::new(0, DENOM), old_delegations); + } + + #[test] + fn when_some_existed() { + let num_delegations = vec![ + 1, + 5, + OLD_DELEGATIONS_CHUNK_SIZE - 1, + OLD_DELEGATIONS_CHUNK_SIZE, + OLD_DELEGATIONS_CHUNK_SIZE + 1, + OLD_DELEGATIONS_CHUNK_SIZE * 3, + OLD_DELEGATIONS_CHUNK_SIZE * 3 + 1, + ]; + + for total_delegations in num_delegations { + let mut deps = helpers::init_contract(); + + let node_identity: IdentityKey = "nodeidentity".into(); + + // delegate some stake + let mut write_bucket = mix_delegations(&mut deps.storage, &node_identity); + for i in 1..=total_delegations { + let delegator = Addr::unchecked(format!("delegator{}", i)); + let delegation = Uint128(i as u128); + write_bucket + .save(delegator.as_bytes(), &delegation) + .unwrap(); + } + + let read_bucket = mix_delegations_read(&deps.storage, &node_identity); + let old_delegations = find_old_delegations(read_bucket).unwrap(); + + let total_delegation = (1..=total_delegations as u128).into_iter().sum(); + assert_eq!(Coin::new(total_delegation, DENOM), old_delegations); + } + } } }