diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index 54f48130eb..05f90551a9 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -279,6 +279,70 @@ export default class ValidatorClient { } } + /** + * Delegates specified amount of stake to particular mixnode. + * + * @param mixOwner address of the owner of the node to which the delegation should be applied + * @param amount desired amount of coins to delegate to the node + */ + // requires coin type to ensure correct denomination ( + async delegateToMixnode(mixOwner: string, amount: Coin): Promise { + if (this.client instanceof NetClient) { + const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_mixnode: { mix_owner: mixOwner } }, `delegating to ${mixOwner}`, [amount]).catch((err) => this.handleRequestFailure(err)) + console.log(`account ${this.client.clientAddress} delegated ${amount} to mixnode owned by ${mixOwner}`); + return result; + } else { + throw new Error("Tried to delegate stake with a query client") + } + } + + /** + * Removes stake delegation from a particular mixnode. + * + * @param mixOwner address of the owner of the node from which the delegation should get removed + */ + async removeMixnodeDelegation(mixOwner: string): Promise { + if (this.client instanceof NetClient) { + const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_mixnode: { mix_owner: mixOwner } }).catch((err) => this.handleRequestFailure(err)) + console.log(`account ${this.client.clientAddress} removed delegation from mixnode owned by ${mixOwner}`); + return result; + } else { + throw new Error("Tried to remove stake delegation with a query client") + } + } + + /** + * Delegates specified amount of stake to particular gateway. + * + * @param gatewayOwner address of the owner of the node to which the delegation should be applied + * @param amount desired amount of coins to delegate to the node + */ + // requires coin type to ensure correct denomination ( + async delegateToGateway(gatewayOwner: string, amount: Coin): Promise { + if (this.client instanceof NetClient) { + const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { delegate_to_gateway: { gateway_owner: gatewayOwner } }, `delegating to ${gatewayOwner}`, [amount]).catch((err) => this.handleRequestFailure(err)) + console.log(`account ${this.client.clientAddress} delegated ${amount} to gateway owned by ${gatewayOwner}`); + return result; + } else { + throw new Error("Tried to delegate stake with a query client") + } + } + + /** + * Removes stake delegation from a particular gateway. + * + * @param gatewayOwner address of the owner of the node from which the delegation should get removed + */ + async removeGatewayDelegation(gatewayOwner: string): Promise { + if (this.client instanceof NetClient) { + const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { undelegate_from_gateway: { gateway_owner: gatewayOwner } }).catch((err) => this.handleRequestFailure(err)) + console.log(`account ${this.client.clientAddress} removed delegation from gateway owned by ${gatewayOwner}`); + return result; + } else { + throw new Error("Tried to remove stake delegation with a query client") + } + } + /** * Checks whether there is already a bonded mixnode associated with this client's address */ @@ -369,6 +433,76 @@ export default class ValidatorClient { } } + /** + * Gets list of all delegations towards particular mixnode. + * + * @param mixOwner address of the owner of the node to which the delegation was sent + */ + public async getMixDelegations(mixOwner: string): Promise { + // make this configurable somewhere + const limit = 500 + + let delegations: Delegation[] = []; + let response: PagedMixDelegationsResponse + let next: string | undefined = undefined; + for (;;) { + response = await this.client.getMixDelegations(this.contractAddress, mixOwner, limit, next) + delegations = delegations.concat(response.delegations) + next = response.start_next_after + // if `start_next_after` is not set, we're done + if (!next) { + break + } + } + + return delegations + } + + /** + * Checks value of delegation of given client towards particular mixnode. + * + * @param mixOwner address of the owner of the node to which the delegation was sent + * @param delegatorAddress address of the client who delegated the stake + */ + public getMixDelegation(mixOwner: string, delegatorAddress: string): Promise { + return this.client.getMixDelegation(this.contractAddress, mixOwner, delegatorAddress); + } + + /** + * Gets list of all delegations towards particular mixnode. + * + * @param gatewayOwner address of the owner of the gateway to which the delegation was sent + */ + public async getGatewayDelegations(gatewayOwner: string): Promise { + // make this configurable somewhere + const limit = 500 + + let delegations: Delegation[] = []; + let response: PagedGatewayDelegationsResponse + let next: string | undefined = undefined; + for (;;) { + response = await this.client.getGatewayDelegations(this.contractAddress, gatewayOwner, limit, next) + delegations = delegations.concat(response.delegations) + next = response.start_next_after + // if `start_next_after` is not set, we're done + if (!next) { + break + } + } + + return delegations + } + + /** + * Checks value of delegation of given client towards particular gateway. + * + * @param gatewayOwner address of the owner of the node to which the delegation was sent + * @param delegatorAddress address of the client who delegated the stake + */ + public getGatewayDelegation(gatewayOwner: string, delegatorAddress: string): Promise { + return this.client.getGatewayDelegation(this.contractAddress, gatewayOwner, delegatorAddress); + } + // TODO: if we just keep a reference to the SigningCosmWasmClient somewhere we can probably go direct // to it in the case of these methods below. @@ -501,3 +635,20 @@ export type StateParams = { gateway_bond_reward_rate: string, mixnode_active_set_size: number, } + +export type Delegation = { + owner: string, + amount: Coin, +} + +export type PagedMixDelegationsResponse = { + node_owner: string, + delegations: Delegation[], + start_next_after: string +} + +export type PagedGatewayDelegationsResponse = { + node_owner: string, + delegations: Delegation[], + start_next_after: string +} \ No newline at end of file diff --git a/clients/validator/src/net-client.ts b/clients/validator/src/net-client.ts index 99b7e64612..37e9898620 100644 --- a/clients/validator/src/net-client.ts +++ b/clients/validator/src/net-client.ts @@ -1,13 +1,14 @@ import { SigningCosmWasmClient, SigningCosmWasmClientOptions } from "@cosmjs/cosmwasm-stargate"; import { + Delegation, GatewayOwnershipResponse, - MixOwnershipResponse, - PagedGatewayResponse, + MixOwnershipResponse, PagedGatewayDelegationsResponse, + PagedGatewayResponse, PagedMixDelegationsResponse, PagedResponse, StateParams } from "./index"; import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing"; -import { Coin, GasPrice, StdFee } from "@cosmjs/launchpad"; +import { Coin, StdFee } from "@cosmjs/launchpad"; import { BroadcastTxResponse } from "@cosmjs/stargate" import { nymGasLimits, nymGasPrice } from "./stargate-helper" import { ExecuteResult, InstantiateOptions, InstantiateResult, MigrateResult, UploadMeta, UploadResult } from "@cosmjs/cosmwasm"; @@ -18,6 +19,10 @@ export interface INetClient { getBalance(address: string, stakeDenom: string): Promise; getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; getGateways(contractAddress: string, limit: number, start_after?: string): Promise; + getMixDelegations(contractAddress: string, mixOwner: string, limit: number, start_after?: string): Promise + getMixDelegation(contractAddress: string, mixOwner: string, delegatorAddress: string): Promise + getGatewayDelegations(contractAddress: string, gatewayOwner: string, limit: number, start_after?: string): Promise + getGatewayDelegation(contractAddress: string, gatewayOwner: string, delegatorAddress: string): Promise ownsMixNode(contractAddress: string, address: string): Promise; ownsGateway(contractAddress: string, address: string): Promise; getStateParams(contractAddress: string): Promise; @@ -82,6 +87,30 @@ export default class NetClient implements INetClient { } } + public getMixDelegations(contractAddress: string, mixOwner: 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_delegations: { mix_owner: mixOwner, limit } }); + } else { + return this.cosmClient.queryContractSmart(contractAddress, { get_mix_delegations: { mix_owner: mixOwner, limit, start_after } }); + } + } + + public getMixDelegation(contractAddress: string, mixOwner: string, delegatorAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { get_mix_delegation: { mix_owner: mixOwner, address: delegatorAddress } }); + } + + public getGatewayDelegations(contractAddress: string, gatewayOwner: 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_gateway_delegations: { gateway_owner: gatewayOwner, limit } }); + } else { + return this.cosmClient.queryContractSmart(contractAddress, { get_gateway_delegations: { gateway_owner: gatewayOwner, limit, start_after } }); + } + } + + public getGatewayDelegation(contractAddress: string, gatewayOwner: string, delegatorAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { get_gateway_delegation: { gateway_owner: gatewayOwner, address: delegatorAddress } }); + } + public ownsMixNode(contractAddress: string, address: string): Promise { return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } }); } diff --git a/clients/validator/src/query-client.ts b/clients/validator/src/query-client.ts index 3579173e1e..cc8cd4db66 100644 --- a/clients/validator/src/query-client.ts +++ b/clients/validator/src/query-client.ts @@ -1,9 +1,10 @@ import {Coin} from "@cosmjs/launchpad"; -import { CosmWasmClient, SigningCosmWasmClient } from "@cosmjs/cosmwasm-stargate"; +import { CosmWasmClient } from "@cosmjs/cosmwasm-stargate"; import { + Delegation, GatewayOwnershipResponse, - MixOwnershipResponse, - PagedGatewayResponse, + MixOwnershipResponse, PagedGatewayDelegationsResponse, + PagedGatewayResponse, PagedMixDelegationsResponse, PagedResponse, StateParams } from "./index"; @@ -12,6 +13,10 @@ export interface IQueryClient { getBalance(address: string, stakeDenom: string): Promise; getMixNodes(contractAddress: string, limit: number, start_after?: string): Promise; getGateways(contractAddress: string, limit: number, start_after?: string): Promise; + getMixDelegations(contractAddress: string, mixOwner: string, limit: number, start_after?: string): Promise + getMixDelegation(contractAddress: string, mixOwner: string, delegatorAddress: string): Promise + getGatewayDelegations(contractAddress: string, gatewayOwner: string, limit: number, start_after?: string): Promise + getGatewayDelegation(contractAddress: string, gatewayOwner: string, delegatorAddress: string): Promise ownsMixNode(contractAddress: string, address: string): Promise; ownsGateway(contractAddress: string, address: string): Promise; getStateParams(contractAddress: string): Promise; @@ -58,6 +63,30 @@ export default class QueryClient implements IQueryClient { } } + public getMixDelegations(contractAddress: string, mixOwner: 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_delegations: { mix_owner: mixOwner, limit } }); + } else { + return this.cosmClient.queryContractSmart(contractAddress, { get_mix_delegations: { mix_owner: mixOwner, limit, start_after } }); + } + } + + public getMixDelegation(contractAddress: string, mixOwner: string, delegatorAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { get_mix_delegation: { mix_owner: mixOwner, address: delegatorAddress } }); + } + + public getGatewayDelegations(contractAddress: string, gatewayOwner: 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_gateway_delegations: { gateway_owner: gatewayOwner, limit } }); + } else { + return this.cosmClient.queryContractSmart(contractAddress, { get_gateway_delegations: { gateway_owner: gatewayOwner, limit, start_after } }); + } + } + + public getGatewayDelegation(contractAddress: string, gatewayOwner: string, delegatorAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { get_gateway_delegation: { gateway_owner: gatewayOwner, address: delegatorAddress } }); + } + public ownsMixNode(contractAddress: string, address: string): Promise { return this.cosmClient.queryContractSmart(contractAddress, { owns_mixnode: { address } }); } diff --git a/common/mixnet-contract/src/delegation.rs b/common/mixnet-contract/src/delegation.rs new file mode 100644 index 0000000000..1ccd686b4e --- /dev/null +++ b/common/mixnet-contract/src/delegation.rs @@ -0,0 +1,79 @@ +// due to code generated by JsonSchema +#![allow(clippy::field_reassign_with_default)] + +use cosmwasm_std::{Coin, HumanAddr}; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::fmt::Display; + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct Delegation { + owner: HumanAddr, + amount: Coin, +} + +impl Delegation { + pub fn new(owner: HumanAddr, amount: Coin) -> Self { + Delegation { owner, amount } + } + + pub fn amount(&self) -> &Coin { + &self.amount + } + + pub fn owner(&self) -> &HumanAddr { + &self.owner + } +} + +impl Display for Delegation { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + write!( + f, + "{} {} delegated by {}", + self.amount.amount, self.amount.denom, self.owner + ) + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedMixDelegationsResponse { + pub node_owner: HumanAddr, + pub delegations: Vec, + pub start_next_after: Option, +} + +impl PagedMixDelegationsResponse { + pub fn new( + node_owner: HumanAddr, + delegations: Vec, + start_next_after: Option, + ) -> Self { + PagedMixDelegationsResponse { + node_owner, + delegations, + start_next_after, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedGatewayDelegationsResponse { + pub node_owner: HumanAddr, + pub delegations: Vec, + pub start_next_after: Option, +} + +impl PagedGatewayDelegationsResponse { + pub fn new( + node_owner: HumanAddr, + delegations: Vec, + start_next_after: Option, + ) -> Self { + PagedGatewayDelegationsResponse { + node_owner, + delegations, + start_next_after, + } + } +} diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index 04cc2ab09e..e95bb72861 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -1,9 +1,11 @@ use serde::{Deserialize, Serialize}; +mod delegation; mod gateway; mod mixnode; pub use cosmwasm_std::{Coin, HumanAddr}; +pub use delegation::{Delegation, PagedGatewayDelegationsResponse, PagedMixDelegationsResponse}; pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; pub use mixnode::{MixNode, MixNodeBond, MixOwnershipResponse, PagedResponse}; diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index cd205a0a50..6c0fa0814a 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -46,8 +46,27 @@ impl MixNode { } } +/* +By the way, I've also been thinking of some potential changes in the contract we could introduce once we have to make an incompatible upgrade (say to cosmwasm 0.14+). Is there some place I could write them down so that we would not forget about them? Should I make a GitHub issue, a google doc or maybe put it on GitLab? + + */ + #[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 + // - repurpose `host` field to hold either ip or hostname (WITHOUT port information) + // - introduce `mix_port` 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 owner: HumanAddr, pub mix_node: MixNode, diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index fdacd4eb3b..5d92872201 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -93,6 +93,18 @@ pub fn handle( HandleMsg::RewardGateway { owner, uptime } => { transactions::try_reward_gateway(deps, info, owner, uptime) } + HandleMsg::DelegateToMixnode { mix_owner } => { + transactions::try_delegate_to_mixnode(deps, info, mix_owner) + } + HandleMsg::UndelegateFromMixnode { mix_owner } => { + transactions::try_remove_delegation_from_mixnode(deps, info, env, mix_owner) + } + HandleMsg::DelegateToGateway { gateway_owner } => { + transactions::try_delegate_to_gateway(deps, info, gateway_owner) + } + HandleMsg::UndelegateFromGateway { gateway_owner } => { + transactions::try_remove_delegation_from_gateway(deps, info, env, gateway_owner) + } } } @@ -112,6 +124,37 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary(&queries::query_state_params(deps)), QueryMsg::LayerDistribution {} => to_binary(&queries::query_layer_distribution(deps)), + QueryMsg::GetMixDelegations { + mix_owner, + start_after, + limit, + } => to_binary(&queries::query_mixnode_delegations_paged( + deps, + mix_owner, + start_after, + limit, + )?), + QueryMsg::GetMixDelegation { mix_owner, address } => to_binary( + &queries::query_mixnode_delegation(deps, mix_owner, address)?, + ), + QueryMsg::GetGatewayDelegations { + gateway_owner, + start_after, + limit, + } => to_binary(&queries::query_gateway_delegations_paged( + deps, + gateway_owner, + start_after, + limit, + )?), + QueryMsg::GetGatewayDelegation { + gateway_owner, + address, + } => to_binary(&queries::query_gateway_delegation( + deps, + gateway_owner, + address, + )?), }; Ok(query_res?) diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index 9a66239cac..2bc080618b 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -33,7 +33,7 @@ pub enum ContractError { #[error("Wrong coin denomination, you must send {}", DENOM)] WrongDenom {}, - #[error("Received multiple coin types during bond")] + #[error("Received multiple coin types during staking")] MultipleDenoms, #[error("No coin was sent for the bonding, you must send {}", DENOM)] @@ -59,4 +59,13 @@ pub enum ContractError { #[error("Gateway with this identity already exists. Its owner is {owner:?}")] DuplicateGateway { owner: HumanAddr }, + + #[error("No funds were provided for the delegation")] + EmptyDelegation, + + #[error("Could not find any delegation information associated with mixnode owned by {mixnode_owner:?}")] + NoMixnodeDelegationFound { mixnode_owner: HumanAddr }, + + #[error("Could not find any delegation information associated with gateway owned by {gateway_owner:?}")] + NoGatewayDelegationFound { gateway_owner: HumanAddr }, } diff --git a/contracts/mixnet/src/msg.rs b/contracts/mixnet/src/msg.rs index fd4400a51a..876eecf246 100644 --- a/contracts/mixnet/src/msg.rs +++ b/contracts/mixnet/src/msg.rs @@ -20,6 +20,22 @@ pub enum HandleMsg { UnbondGateway {}, UpdateStateParams(StateParams), + DelegateToMixnode { + mix_owner: HumanAddr, + }, + + UndelegateFromMixnode { + mix_owner: HumanAddr, + }, + + DelegateToGateway { + gateway_owner: HumanAddr, + }, + + UndelegateFromGateway { + gateway_owner: HumanAddr, + }, + RewardMixnode { owner: HumanAddr, // percentage value in range 0-100 @@ -51,6 +67,24 @@ pub enum QueryMsg { address: HumanAddr, }, StateParams {}, + GetMixDelegations { + mix_owner: HumanAddr, + start_after: Option, + limit: Option, + }, + GetMixDelegation { + mix_owner: HumanAddr, + address: HumanAddr, + }, + GetGatewayDelegations { + gateway_owner: HumanAddr, + start_after: Option, + limit: Option, + }, + GetGatewayDelegation { + gateway_owner: HumanAddr, + address: HumanAddr, + }, LayerDistribution {}, } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index cd95c2e865..43cf8b7812 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -1,23 +1,35 @@ +use crate::contract::DENOM; +use crate::error::ContractError; use crate::state::StateParams; -use crate::storage::{gateways_read, mixnodes_read, read_layer_distribution, read_state_params}; +use crate::storage::{ + gateway_delegations_read, gateways_read, mix_delegations_read, mixnodes_read, + read_layer_distribution, read_state_params, +}; use cosmwasm_std::Deps; -use cosmwasm_std::HumanAddr; use cosmwasm_std::Order; use cosmwasm_std::StdResult; +use cosmwasm_std::{coin, HumanAddr}; use mixnet_contract::{ - GatewayBond, GatewayOwnershipResponse, LayerDistribution, MixNodeBond, MixOwnershipResponse, - PagedGatewayResponse, PagedResponse, + Delegation, GatewayBond, GatewayOwnershipResponse, LayerDistribution, MixNodeBond, + MixOwnershipResponse, PagedGatewayDelegationsResponse, PagedGatewayResponse, + PagedMixDelegationsResponse, PagedResponse, }; -const MAX_LIMIT: u32 = 100; -const DEFAULT_LIMIT: u32 = 50; +const BOND_PAGE_MAX_LIMIT: u32 = 100; +const BOND_PAGE_DEFAULT_LIMIT: u32 = 50; + +// currently the maximum limit before running into memory issue is somewhere between 1150 and 1200 +pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 750; +const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 500; pub fn query_mixnodes_paged( deps: Deps, start_after: Option, limit: Option, ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let limit = limit + .unwrap_or(BOND_PAGE_DEFAULT_LIMIT) + .min(BOND_PAGE_MAX_LIMIT) as usize; let start = calculate_start_value(start_after); let nodes = mixnodes_read(deps.storage) @@ -36,7 +48,9 @@ pub(crate) fn query_gateways_paged( start_after: Option, limit: Option, ) -> StdResult { - let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize; + let limit = limit + .unwrap_or(BOND_PAGE_DEFAULT_LIMIT) + .min(BOND_PAGE_MAX_LIMIT) as usize; let start = calculate_start_value(start_after); let nodes = gateways_read(deps.storage) @@ -93,13 +107,115 @@ fn calculate_start_value( }) } +pub(crate) fn query_mixnode_delegations_paged( + deps: Deps, + mix_owner: HumanAddr, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT) + .min(DELEGATION_PAGE_MAX_LIMIT) as usize; + let start = calculate_start_value(start_after); + + let delegations = mix_delegations_read(deps.storage, &mix_owner) + .range(start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|entry| { + Delegation::new( + HumanAddr::from(String::from_utf8(entry.0).unwrap()), + coin(entry.1.u128(), DENOM), + ) + }) + }) + .collect::>>()?; + + let start_next_after = delegations + .last() + .map(|delegation| delegation.owner().clone()); + + Ok(PagedMixDelegationsResponse::new( + mix_owner, + delegations, + start_next_after, + )) +} + +// queries for delegation value of given address for particular node +pub(crate) fn query_mixnode_delegation( + deps: Deps, + mix_owner: HumanAddr, + address: HumanAddr, +) -> Result { + match mix_delegations_read(deps.storage, &mix_owner).may_load(address.as_bytes())? { + Some(delegation_value) => Ok(Delegation::new( + address, + coin(delegation_value.u128(), DENOM), + )), + None => Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: mix_owner, + }), + } +} + +pub(crate) fn query_gateway_delegations_paged( + deps: Deps, + gateway_owner: HumanAddr, + start_after: Option, + limit: Option, +) -> StdResult { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT) + .min(DELEGATION_PAGE_MAX_LIMIT) as usize; + let start = calculate_start_value(start_after); + + let delegations = gateway_delegations_read(deps.storage, &gateway_owner) + .range(start.as_deref(), None, Order::Ascending) + .take(limit) + .map(|res| { + res.map(|entry| { + Delegation::new( + HumanAddr::from(String::from_utf8(entry.0).unwrap()), + coin(entry.1.u128(), DENOM), + ) + }) + }) + .collect::>>()?; + + let start_next_after = delegations + .last() + .map(|delegation| delegation.owner().clone()); + + Ok(PagedGatewayDelegationsResponse::new( + gateway_owner, + delegations, + start_next_after, + )) +} + +// queries for delegation value of given address for particular node +pub(crate) fn query_gateway_delegation( + deps: Deps, + gateway_owner: HumanAddr, + address: HumanAddr, +) -> Result { + match gateway_delegations_read(deps.storage, &gateway_owner).may_load(address.as_bytes())? { + Some(delegation_value) => Ok(Delegation::new( + address, + coin(delegation_value.u128(), DENOM), + )), + None => Err(ContractError::NoGatewayDelegationFound { gateway_owner }), + } +} + #[cfg(test)] mod tests { use super::*; use crate::state::State; - use crate::storage::{config, gateways, mixnodes}; + use crate::storage::{config, gateway_delegations, gateways, mix_delegations, mixnodes}; use crate::support::tests::helpers; - use cosmwasm_std::Storage; + use cosmwasm_std::{Storage, Uint128}; #[test] fn mixnodes_empty_on_init() { @@ -248,12 +364,12 @@ mod tests { fn gateways_paged_retrieval_has_default_limit() { let mut deps = helpers::init_contract(); let storage = deps.as_mut().storage; - store_n_gateway_fixtures(10 * DEFAULT_LIMIT, storage); + store_n_gateway_fixtures(10 * BOND_PAGE_DEFAULT_LIMIT, storage); // query without explicitly setting a limit let page1 = query_gateways_paged(deps.as_ref(), None, None).unwrap(); - assert_eq!(DEFAULT_LIMIT, page1.nodes.len() as u32); + assert_eq!(BOND_PAGE_DEFAULT_LIMIT, page1.nodes.len() as u32); } #[test] @@ -263,11 +379,11 @@ mod tests { store_n_gateway_fixtures(100, storage); // query with a crazily high limit in an attempt to use too many resources - let crazy_limit = 1000 * DEFAULT_LIMIT; + let crazy_limit = 1000 * BOND_PAGE_DEFAULT_LIMIT; let page1 = query_gateways_paged(deps.as_ref(), None, Option::from(crazy_limit)).unwrap(); // we default to a decent sized upper bound instead - let expected_limit = MAX_LIMIT; + let expected_limit = BOND_PAGE_MAX_LIMIT; assert_eq!(expected_limit, page1.nodes.len() as u32); } @@ -417,4 +533,422 @@ mod tests { assert_eq!(dummy_state.params, query_state_params(deps.as_ref())) } + + #[cfg(test)] + mod querying_for_mixnode_delegations_paged { + use super::*; + use crate::storage::mix_delegations; + use cosmwasm_std::Uint128; + + fn store_n_delegations(n: u32, storage: &mut dyn Storage, node_owner: &HumanAddr) { + for i in 0..n { + let address = format!("address{}", i); + mix_delegations(storage, node_owner) + .save(address.as_bytes(), &Uint128(42)) + .unwrap(); + } + } + + #[test] + fn retrieval_obeys_limits() { + let mut deps = helpers::init_contract(); + let limit = 2; + let node_owner: HumanAddr = "foo".into(); + store_n_delegations(100, &mut deps.storage, &node_owner); + + let page1 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner, + None, + Option::from(limit), + ) + .unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn retrieval_has_default_limit() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + store_n_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_owner, + ); + + // query without explicitly setting a limit + let page1 = + query_mixnode_delegations_paged(deps.as_ref(), node_owner, None, None).unwrap(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn retrieval_has_max_limit() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + store_n_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_owner, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT; + let page1 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner, + None, + Option::from(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DELEGATION_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.delegations.len() as u32); + } + + #[test] + fn pagination_works() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + mix_delegations(&mut deps.storage, &node_owner) + .save("1".as_bytes(), &Uint128(42)) + .unwrap(); + + let per_page = 2; + let page1 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + mix_delegations(&mut deps.storage, &node_owner) + .save("2".as_bytes(), &Uint128(42)) + .unwrap(); + + // page1 should have 2 results on it + let page1 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + mix_delegations(&mut deps.storage, &node_owner) + .save("3".as_bytes(), &Uint128(42)) + .unwrap(); + + // page1 still has 2 results + let page1 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + // retrieving the next page should start after the last key on this page + let start_after = HumanAddr::from("2"); + let page2 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner.clone(), + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + mix_delegations(&mut deps.storage, &node_owner) + .save("4".as_bytes(), &Uint128(42)) + .unwrap(); + + let start_after = HumanAddr::from("2"); + let page2 = query_mixnode_delegations_paged( + deps.as_ref(), + node_owner, + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } + } + + #[test] + fn mix_deletion_query_returns_current_delegation_value() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + mix_delegations(&mut deps.storage, &node_owner) + .save("foo".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Ok(Delegation::new(node_owner.clone(), coin(42, DENOM))), + query_mixnode_delegation(deps.as_ref(), node_owner, "foo".into()) + ) + } + + #[test] + fn mix_deletion_query_returns_error_if_delegation_doesnt_exist() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + assert_eq!( + Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: node_owner.clone(), + }), + query_mixnode_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ); + + // add delegation from a different address + mix_delegations(&mut deps.storage, &node_owner) + .save("bar".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: node_owner.clone(), + }), + query_mixnode_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ); + + // add delegation for a different node + mix_delegations(&mut deps.storage, &"differentnode".into()) + .save("foo".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: node_owner.clone(), + }), + query_mixnode_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ) + } + + #[cfg(test)] + mod querying_for_gateway_delegations_paged { + use super::*; + use crate::storage::gateway_delegations; + use cosmwasm_std::Uint128; + + fn store_n_delegations(n: u32, storage: &mut dyn Storage, node_owner: &HumanAddr) { + for i in 0..n { + let address = format!("address{}", i); + gateway_delegations(storage, node_owner) + .save(address.as_bytes(), &Uint128(42)) + .unwrap(); + } + } + + #[test] + fn retrieval_obeys_limits() { + let mut deps = helpers::init_contract(); + let limit = 2; + let node_owner: HumanAddr = "foo".into(); + store_n_delegations(100, &mut deps.storage, &node_owner); + + let page1 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner, + None, + Option::from(limit), + ) + .unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn retrieval_has_default_limit() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + store_n_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_owner, + ); + + // query without explicitly setting a limit + let page1 = + query_gateway_delegations_paged(deps.as_ref(), node_owner, None, None).unwrap(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn retrieval_has_max_limit() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + store_n_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_owner, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT; + let page1 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner, + None, + Option::from(crazy_limit), + ) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DELEGATION_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.delegations.len() as u32); + } + + #[test] + fn pagination_works() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + gateway_delegations(&mut deps.storage, &node_owner) + .save("1".as_bytes(), &Uint128(42)) + .unwrap(); + + let per_page = 2; + let page1 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + gateway_delegations(&mut deps.storage, &node_owner) + .save("2".as_bytes(), &Uint128(42)) + .unwrap(); + + // page1 should have 2 results on it + let page1 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + gateway_delegations(&mut deps.storage, &node_owner) + .save("3".as_bytes(), &Uint128(42)) + .unwrap(); + + // page1 still has 2 results + let page1 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner.clone(), + None, + Option::from(per_page), + ) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + // retrieving the next page should start after the last key on this page + let start_after = HumanAddr::from("2"); + let page2 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner.clone(), + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + gateway_delegations(&mut deps.storage, &node_owner) + .save("4".as_bytes(), &Uint128(42)) + .unwrap(); + + let start_after = HumanAddr::from("2"); + let page2 = query_gateway_delegations_paged( + deps.as_ref(), + node_owner, + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } + } + + #[test] + fn gateway_deletion_query_returns_current_delegation_value() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + gateway_delegations(&mut deps.storage, &node_owner) + .save("foo".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Ok(Delegation::new(node_owner.clone(), coin(42, DENOM))), + query_gateway_delegation(deps.as_ref(), node_owner, "foo".into()) + ) + } + + #[test] + fn gateway_deletion_query_returns_error_if_delegation_doesnt_exist() { + let mut deps = helpers::init_contract(); + let node_owner: HumanAddr = "foo".into(); + + assert_eq!( + Err(ContractError::NoGatewayDelegationFound { + gateway_owner: node_owner.clone(), + }), + query_gateway_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ); + + // add delegation from a different address + gateway_delegations(&mut deps.storage, &node_owner) + .save("bar".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Err(ContractError::NoGatewayDelegationFound { + gateway_owner: node_owner.clone(), + }), + query_gateway_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ); + + // add delegation for a different node + gateway_delegations(&mut deps.storage, &"differentnode".into()) + .save("foo".as_bytes(), &Uint128(42)) + .unwrap(); + + assert_eq!( + Err(ContractError::NoGatewayDelegationFound { + gateway_owner: node_owner.clone(), + }), + query_gateway_delegation(deps.as_ref(), node_owner.clone(), "foo".into()) + ) + } } diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 2593328a34..ccb007ee36 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -1,13 +1,35 @@ +use crate::queries; use crate::state::{State, StateParams}; -use cosmwasm_std::{Decimal, HumanAddr, StdError, StdResult, Storage}; +use cosmwasm_std::{Decimal, HumanAddr, Order, StdError, StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, Singleton, }; use mixnet_contract::{GatewayBond, LayerDistribution, MixNodeBond}; -// Contract-level stuff +// storage prefixes +// all of them must be unique and presumably not be a prefix od a different one +// keeping them as short as possible is also desirable as they are part of each stored key +// it's not as important for singletons, but is a nice optimisation for buckets + +// the legacy prefixes can't be changed without redeploying contract or doing fancy migration +// for time being let's leave them as they are, but when we're going to make incompatible +// contract changes, let's shorten all of them + +// singletons const CONFIG_KEY: &[u8] = b"config"; +const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; + +// buckets +const PREFIX_MIXNODES: &[u8] = b"mixnodes"; +const PREFIX_MIXNODES_OWNERS: &[u8] = b"mix-owners"; +const PREFIX_GATEWAYS: &[u8] = b"gateways"; +const PREFIX_GATEWAYS_OWNERS: &[u8] = b"gateway-owners"; + +const PREFIX_MIX_DELEGATION: &[u8] = b"md"; +const PREFIX_GATEWAY_DELEGATION: &[u8] = b"gd"; + +// Contract-level stuff pub fn config(storage: &mut dyn Storage) -> Singleton { singleton(storage, CONFIG_KEY) @@ -40,8 +62,6 @@ pub(crate) fn read_gateway_epoch_reward_rate(storage: &dyn Storage) -> Decimal { .gateway_epoch_bond_reward } -const LAYER_DISTRIBUTION_KEY: &[u8] = b"layers"; - pub fn layer_distribution(storage: &mut dyn Storage) -> Singleton { singleton(storage, LAYER_DISTRIBUTION_KEY) } @@ -127,7 +147,6 @@ pub fn decrement_layer_count(storage: &mut dyn Storage, layer: Layer) -> StdResu } // Mixnode-related stuff -const PREFIX_MIXNODES: &[u8] = b"mixnodes"; pub fn mixnodes(storage: &mut dyn Storage) -> Bucket { bucket(storage, PREFIX_MIXNODES) @@ -137,8 +156,6 @@ pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket { bucket_read(storage, PREFIX_MIXNODES) } -const PREFIX_MIXNODES_OWNERS: &[u8] = b"mix-owners"; - pub fn mixnodes_owners(storage: &mut dyn Storage) -> Bucket { bucket(storage, PREFIX_MIXNODES_OWNERS) } @@ -164,6 +181,90 @@ pub(crate) fn increase_mixnode_bond( mixnodes(storage).save(bond.owner.as_bytes(), &bond) } +pub(crate) fn increase_mix_delegated_stakes( + storage: &mut dyn Storage, + mix_owner: &HumanAddr, + scaled_reward_rate: Decimal, +) -> StdResult<()> { + let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + + let mut chunk_start: Option> = None; + loop { + // get `chunk_size` of delegations + let delegations_chunk = mix_delegations_read(storage, mix_owner) + .range(chunk_start.as_deref(), None, Order::Ascending) + .take(chunk_size) + .collect::>>()?; + + if delegations_chunk.is_empty() { + break; + } + + // append 0 byte to the last value to start with whatever is the next suceeding key + chunk_start = Some( + delegations_chunk + .last() + .unwrap() + .0 + .iter() + .cloned() + .chain(std::iter::once(0u8)) + .collect(), + ); + + // and for each of them increase the stake proportionally to the reward + for (delegator_address, amount) in delegations_chunk.into_iter() { + let reward = amount * scaled_reward_rate; + let new_amount = amount + reward; + mix_delegations(storage, mix_owner).save(&delegator_address, &new_amount)?; + } + } + + Ok(()) +} + +pub(crate) fn increase_gateway_delegated_stakes( + storage: &mut dyn Storage, + gateway_owner: &HumanAddr, + scaled_reward_rate: Decimal, +) -> StdResult<()> { + let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; + + let mut chunk_start: Option> = None; + loop { + // get `chunk_size` of delegations + let delegations_chunk = gateway_delegations_read(storage, gateway_owner) + .range(chunk_start.as_deref(), None, Order::Ascending) + .take(chunk_size) + .collect::>>()?; + + if delegations_chunk.is_empty() { + break; + } + + // append 0 byte to the last value to start with whatever is the next suceeding key + chunk_start = Some( + delegations_chunk + .last() + .unwrap() + .0 + .iter() + .cloned() + .chain(std::iter::once(0u8)) + .collect(), + ); + + // and for each of them increase the stake proportionally to the reward + for (delegator_address, amount) in delegations_chunk.into_iter() { + let reward = amount * scaled_reward_rate; + let new_amount = amount + reward; + gateway_delegations(storage, gateway_owner).save(&delegator_address, &new_amount)?; + } + } + + Ok(()) +} + // currently not used outside tests #[cfg(test)] pub(crate) fn read_mixnode_bond( @@ -182,8 +283,6 @@ pub(crate) fn read_mixnode_bond( // Gateway-related stuff -const PREFIX_GATEWAYS: &[u8] = b"gateways"; - pub fn gateways(storage: &mut dyn Storage) -> Bucket { bucket(storage, PREFIX_GATEWAYS) } @@ -192,8 +291,6 @@ pub fn gateways_read(storage: &dyn Storage) -> ReadonlyBucket { bucket_read(storage, PREFIX_GATEWAYS) } -const PREFIX_GATEWAYS_OWNERS: &[u8] = b"gateway-owners"; - pub fn gateways_owners(storage: &mut dyn Storage) -> Bucket { bucket(storage, PREFIX_GATEWAYS_OWNERS) } @@ -218,6 +315,41 @@ pub(crate) fn increase_gateway_bond( gateways(storage).save(bond.owner.as_bytes(), &bond) } +// delegation related +pub fn mix_delegations<'a>( + storage: &'a mut dyn Storage, + mix_owner: &'a HumanAddr, +) -> Bucket<'a, Uint128> { + Bucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_owner.as_bytes()]) +} + +pub fn mix_delegations_read<'a>( + storage: &'a dyn Storage, + mix_owner: &'a HumanAddr, +) -> ReadonlyBucket<'a, Uint128> { + ReadonlyBucket::multilevel(storage, &[PREFIX_MIX_DELEGATION, mix_owner.as_bytes()]) +} + +pub fn gateway_delegations<'a>( + storage: &'a mut dyn Storage, + gateway_owner: &'a HumanAddr, +) -> Bucket<'a, Uint128> { + Bucket::multilevel( + storage, + &[PREFIX_GATEWAY_DELEGATION, gateway_owner.as_bytes()], + ) +} + +pub fn gateway_delegations_read<'a>( + storage: &'a dyn Storage, + gateway_owner: &'a HumanAddr, +) -> ReadonlyBucket<'a, Uint128> { + ReadonlyBucket::multilevel( + storage, + &[PREFIX_GATEWAY_DELEGATION, gateway_owner.as_bytes()], + ) +} + // currently not used outside tests #[cfg(test)] pub(crate) fn read_gateway_bond( @@ -373,4 +505,204 @@ mod tests { read_gateway_bond(&storage, node_owner).unwrap() ); } + + #[cfg(test)] + mod increasing_mix_delegated_stakes { + use super::*; + use crate::queries::query_mixnode_delegations_paged; + use cosmwasm_std::testing::mock_dependencies; + + #[test] + fn when_there_are_no_delegations() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + increase_mix_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + // there are no 'new' delegations magically added + assert!( + query_mixnode_delegations_paged(deps.as_ref(), node_owner, None, None) + .unwrap() + .delegations + .is_empty() + ) + } + + #[test] + fn when_there_is_a_single_delegation() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + let delegator_address = HumanAddr::from("bob"); + mix_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + + increase_mix_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + assert_eq!( + Uint128(1001), + mix_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + + #[test] + fn when_there_are_multiple_delegations() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + for i in 0..100 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + mix_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + } + + increase_mix_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + for i in 0..100 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + assert_eq!( + Uint128(1001), + mix_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + } + + #[test] + fn when_there_are_more_delegations_than_page_size() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + mix_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + } + + increase_mix_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + assert_eq!( + Uint128(1001), + mix_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + } + } + + #[cfg(test)] + mod increasing_gateway_delegated_stakes { + use super::*; + use crate::queries::query_gateway_delegations_paged; + use cosmwasm_std::testing::mock_dependencies; + + #[test] + fn when_there_are_no_delegations() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + increase_gateway_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + // there are no 'new' delegations magically added + assert!( + query_gateway_delegations_paged(deps.as_ref(), node_owner, None, None) + .unwrap() + .delegations + .is_empty() + ) + } + + #[test] + fn when_there_is_a_single_delegation() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + let delegator_address = HumanAddr::from("bob"); + gateway_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + + increase_gateway_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + assert_eq!( + Uint128(1001), + gateway_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + + #[test] + fn when_there_are_multiple_delegations() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + for i in 0..100 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + gateway_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + } + + increase_gateway_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + for i in 0..100 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + assert_eq!( + Uint128(1001), + gateway_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + } + + #[test] + fn when_there_are_more_delegations_than_page_size() { + let mut deps = mock_dependencies(&[]); + let node_owner = HumanAddr::from("owner"); + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + gateway_delegations(&mut deps.storage, &node_owner) + .save(delegator_address.as_bytes(), &Uint128(1000)) + .unwrap(); + } + + increase_gateway_delegated_stakes(&mut deps.storage, &node_owner, reward).unwrap(); + + for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { + let delegator_address = HumanAddr::from(format!("address{}", i)); + assert_eq!( + Uint128(1001), + gateway_delegations_read(&mut deps.storage, &node_owner) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + } + } } diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 8c2de44c08..9549ebb0e0 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -3,13 +3,15 @@ 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, gateways, gateways_owners, gateways_owners_read, - gateways_read, increase_gateway_bond, increase_mixnode_bond, increment_layer_count, mixnodes, - mixnodes_owners, mixnodes_owners_read, mixnodes_read, read_gateway_epoch_reward_rate, + 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 cosmwasm_std::{ - attr, BankMsg, Coin, Decimal, DepsMut, Env, HandleResponse, HumanAddr, MessageInfo, Uint128, + attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, HandleResponse, HumanAddr, MessageInfo, + Uint128, }; use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond}; @@ -312,10 +314,16 @@ 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(HandleResponse::default()); + } + let reward = read_mixnode_epoch_reward_rate(deps.storage); let scaled_reward = scale_reward_by_uptime(reward, uptime)?; increase_mixnode_bond(deps.storage, current_bond, scaled_reward)?; + increase_mix_delegated_stakes(deps.storage, &node_owner, scaled_reward)?; Ok(HandleResponse::default()) } @@ -348,10 +356,158 @@ pub(crate) fn try_reward_gateway( let scaled_reward = scale_reward_by_uptime(reward, uptime)?; increase_gateway_bond(deps.storage, current_bond, scaled_reward)?; + increase_gateway_delegated_stakes(deps.storage, &gateway_owner, scaled_reward)?; Ok(HandleResponse::default()) } +fn validate_delegation_stake(delegation: &[Coin]) -> Result<(), ContractError> { + // check if anything was put as delegation + if delegation.is_empty() { + return Err(ContractError::EmptyDelegation); + } + + if delegation.len() > 1 { + return Err(ContractError::MultipleDenoms); + } + + // check that the denomination is correct + if delegation[0].denom != DENOM { + return Err(ContractError::WrongDenom {}); + } + + // check that we have provided a non-zero amount in the delegation + if delegation[0].amount.is_zero() { + return Err(ContractError::EmptyDelegation); + } + + Ok(()) +} + +pub(crate) fn try_delegate_to_mixnode( + deps: DepsMut, + info: MessageInfo, + mix_owner: HumanAddr, +) -> Result { + // check if the delegation contains any funds of the appropriate denomination + validate_delegation_stake(&info.sent_funds)?; + + // check if the target node actually exists + if mixnodes_read(deps.storage) + .load(mix_owner.as_bytes()) + .is_err() + { + return Err(ContractError::MixNodeBondNotFound {}); + } + + let mut bucket = mix_delegations(deps.storage, &mix_owner); + let sender_bytes = info.sender.as_bytes(); + match bucket.may_load(sender_bytes)? { + Some(existing_delegation) => bucket.save( + sender_bytes, + &(existing_delegation + info.sent_funds[0].amount), + )?, + None => bucket.save(sender_bytes, &info.sent_funds[0].amount)?, + } + + Ok(HandleResponse::default()) +} + +pub(crate) fn try_remove_delegation_from_mixnode( + deps: DepsMut, + info: MessageInfo, + env: Env, + mix_owner: HumanAddr, +) -> Result { + let mut delegation_bucket = mix_delegations(deps.storage, &mix_owner); + let sender_bytes = info.sender.as_bytes(); + match delegation_bucket.may_load(sender_bytes)? { + Some(delegation) => { + // remove delegation from the bucket + delegation_bucket.remove(sender_bytes); + + // send delegated funds back to the delegation owner + let messages = vec![BankMsg::Send { + from_address: env.contract.address, + to_address: info.sender, + amount: coins(delegation.u128(), DENOM), + } + .into()]; + + Ok(HandleResponse { + messages, + attributes: Vec::new(), + data: None, + }) + } + None => Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: mix_owner, + }), + } +} + +pub(crate) fn try_delegate_to_gateway( + deps: DepsMut, + info: MessageInfo, + gateway_owner: HumanAddr, +) -> Result { + // check if the delegation contains any funds of the appropriate denomination + validate_delegation_stake(&info.sent_funds)?; + + // check if the target node actually exists + if gateways_read(deps.storage) + .load(gateway_owner.as_bytes()) + .is_err() + { + return Err(ContractError::GatewayBondNotFound { + account: gateway_owner, + }); + } + + let mut bucket = gateway_delegations(deps.storage, &gateway_owner); + let sender_bytes = info.sender.as_bytes(); + match bucket.may_load(sender_bytes)? { + Some(existing_delegation) => bucket.save( + sender_bytes, + &(existing_delegation + info.sent_funds[0].amount), + )?, + None => bucket.save(sender_bytes, &info.sent_funds[0].amount)?, + } + + Ok(HandleResponse::default()) +} + +pub(crate) fn try_remove_delegation_from_gateway( + deps: DepsMut, + info: MessageInfo, + env: Env, + gateway_owner: HumanAddr, +) -> Result { + let mut delegation_bucket = gateway_delegations(deps.storage, &gateway_owner); + let sender_bytes = info.sender.as_bytes(); + match delegation_bucket.may_load(sender_bytes)? { + Some(delegation) => { + // remove delegation from the bucket + delegation_bucket.remove(sender_bytes); + + // send delegated funds back to the delegation owner + let messages = vec![BankMsg::Send { + from_address: env.contract.address, + to_address: info.sender, + amount: coins(delegation.u128(), DENOM), + } + .into()]; + + Ok(HandleResponse { + messages, + attributes: Vec::new(), + data: None, + }) + } + None => Err(ContractError::NoGatewayDelegationFound { gateway_owner }), + } +} + #[cfg(test)] pub mod tests { use super::*; @@ -363,8 +519,8 @@ pub mod tests { use crate::msg::{HandleMsg, InitMsg, QueryMsg}; use crate::state::StateParams; use crate::storage::{ - layer_distribution_read, read_gateway_bond, read_gateway_epoch_reward_rate, - read_mixnode_bond, + gateway_delegations, gateway_delegations_read, layer_distribution_read, + mix_delegations_read, read_gateway_bond, read_gateway_epoch_reward_rate, read_mixnode_bond, }; use crate::support::tests::helpers; use crate::support::tests::helpers::{gateway_fixture, mix_node_fixture}; @@ -1443,4 +1599,1016 @@ pub mod tests { read_gateway_bond(deps.as_ref().storage, b"node-owner").unwrap() ); } + + #[cfg(test)] + mod delegation_stake_validation { + use super::*; + use cosmwasm_std::coin; + + #[test] + fn stake_cant_be_empty() { + assert_eq!( + Err(ContractError::EmptyDelegation), + validate_delegation_stake(&[]) + ) + } + + #[test] + fn stake_must_have_single_coin_type() { + assert_eq!( + Err(ContractError::MultipleDenoms), + validate_delegation_stake(&[coin(123, DENOM), coin(123, "BTC"), coin(123, "DOGE")]) + ) + } + + #[test] + fn stake_coin_must_be_of_correct_type() { + assert_eq!( + Err(ContractError::WrongDenom {}), + validate_delegation_stake(&[coin(123, "DOGE")]) + ) + } + + #[test] + fn stake_coin_must_have_value_greater_than_zero() { + assert_eq!( + Err(ContractError::EmptyDelegation), + validate_delegation_stake(&[coin(0, DENOM)]) + ) + } + + #[test] + fn stake_can_have_any_positive_value() { + // this might change in the future, but right now an arbitrary (positive) value can be delegated + assert!(validate_delegation_stake(&[coin(1, DENOM)]).is_ok()); + assert!(validate_delegation_stake(&[coin(123, DENOM)]).is_ok()); + assert!(validate_delegation_stake(&[coin(10000000000, DENOM)]).is_ok()); + } + } + + #[cfg(test)] + mod mix_stake_delegation { + use super::*; + use crate::storage::mix_delegations_read; + use crate::support::tests::helpers::add_mixnode; + + #[test] + fn fails_if_node_doesnt_exist() { + let mut deps = helpers::init_contract(); + assert_eq!( + Err(ContractError::MixNodeBondNotFound {}), + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + "non-existent-node-owner".into() + ) + ); + } + + #[test] + fn succeeds_for_existing_node() { + let mut deps = helpers::init_contract(); + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + mixnode_owner.into() + ) + .is_ok()); + + assert_eq!( + 123, + mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn fails_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[]), env).unwrap(); + + assert_eq!( + Err(ContractError::MixNodeBondNotFound {}), + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + mixnode_owner.into() + ) + ); + } + + #[test] + fn succeeds_if_node_rebonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[]), env).unwrap(); + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + mixnode_owner.into() + ) + .is_ok()); + + assert_eq!( + 123, + mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_possible_for_an_already_delegated_node() { + let mut deps = helpers::init_contract(); + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(50, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + assert_eq!( + 150, + mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_disallowed_for_already_delegated_node_if_it_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[]), env).unwrap(); + + assert_eq!( + Err(ContractError::MixNodeBondNotFound {}), + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(50, DENOM)), + mixnode_owner.into() + ) + ); + } + + #[test] + fn is_allowed_for_multiple_nodes() { + let mut deps = helpers::init_contract(); + let mixnode_owner1 = "bob"; + let mixnode_owner2 = "fred"; + add_mixnode(mixnode_owner1, good_mixnode_bond(), &mut deps); + add_mixnode(mixnode_owner2, good_mixnode_bond(), &mut deps); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + mixnode_owner1.into() + ) + .is_ok()); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(42, DENOM)), + mixnode_owner2.into() + ) + .is_ok()); + + assert_eq!( + 123, + mix_delegations_read(&deps.storage, &mixnode_owner1.into()) + .load(b"sender") + .unwrap() + .u128() + ); + + assert_eq!( + 42, + mix_delegations_read(&deps.storage, &mixnode_owner2.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_allowed_by_multiple_users() { + let mut deps = helpers::init_contract(); + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender1", &coins(123, DENOM)), + mixnode_owner.into() + ) + .is_ok()); + + assert!(try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender2", &coins(123, DENOM)), + mixnode_owner.into() + ) + .is_ok()); + } + + #[test] + fn delegation_is_not_removed_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[]), env).unwrap(); + + assert_eq!( + 100, + mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + } + + #[cfg(test)] + mod removing_mix_stake_delegation { + use super::*; + use crate::storage::mix_delegations_read; + use crate::support::tests::helpers::add_mixnode; + + #[test] + fn fails_if_delegation_never_existed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + assert_eq!( + Err(ContractError::NoMixnodeDelegationFound { + mixnode_owner: mixnode_owner.into(), + }), + try_remove_delegation_from_mixnode( + deps.as_mut(), + mock_info("sender", &[]), + env, + mixnode_owner.into(), + ) + ); + } + + #[test] + fn succeeds_if_delegation_existed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + assert_eq!( + Ok(HandleResponse { + messages: vec![BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: "sender".into(), + amount: coins(100, DENOM), + } + .into()], + attributes: Vec::new(), + data: None, + }), + try_remove_delegation_from_mixnode( + deps.as_mut(), + mock_info("sender", &[]), + env, + mixnode_owner.into(), + ) + ); + + assert!(mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .may_load(b"sender") + .unwrap() + .is_none()); + } + + #[test] + fn succeeds_if_delegation_existed_even_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let mixnode_owner = "bob"; + add_mixnode(mixnode_owner, good_mixnode_bond(), &mut deps); + + try_delegate_to_mixnode( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + mixnode_owner.into(), + ) + .unwrap(); + + try_remove_mixnode(deps.as_mut(), mock_info(mixnode_owner, &[]), env.clone()).unwrap(); + + assert_eq!( + Ok(HandleResponse { + messages: vec![BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: "sender".into(), + amount: coins(100, DENOM), + } + .into()], + attributes: Vec::new(), + data: None, + }), + try_remove_delegation_from_mixnode( + deps.as_mut(), + mock_info("sender", &[]), + env, + mixnode_owner.into(), + ) + ); + + assert!(mix_delegations_read(&deps.storage, &mixnode_owner.into()) + .may_load(b"sender") + .unwrap() + .is_none()); + } + } + + #[test] + fn delegators_share_the_same_reward_rate_as_mix_nodes() { + let mut deps = helpers::init_contract(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let network_monitor_address = current_state.network_monitor_address; + + let initial_mix_bond = 100_000000; + let initial_delegation1 = 50000; // will see single digits rewards + let initial_delegation2 = 100; // won't see any rewards due to such a small delegation + let initial_delegation3 = 100000_000000; // will see big proper rewards + + let node_owner: HumanAddr = "node-owner".into(); + let mixnode_bond = MixNodeBond { + amount: coins(initial_mix_bond, DENOM), + owner: node_owner.clone(), + mix_node: mix_node_fixture(), + }; + + mixnodes(&mut deps.storage) + .save(b"node-owner", &mixnode_bond) + .unwrap(); + + mix_delegations(&mut deps.storage, &node_owner) + .save(b"delegator1", &Uint128(initial_delegation1)) + .unwrap(); + mix_delegations(&mut deps.storage, &node_owner) + .save(b"delegator2", &Uint128(initial_delegation2)) + .unwrap(); + mix_delegations(&mut deps.storage, &node_owner) + .save(b"delegator3", &Uint128(initial_delegation3)) + .unwrap(); + + let reward = read_mixnode_epoch_reward_rate(deps.as_ref().storage); + + // 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 info = mock_info(network_monitor_address.clone(), &[]); + try_reward_mixnode(deps.as_mut(), info, node_owner.clone(), 100).unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + + // 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 info = mock_info(network_monitor_address.clone(), &[]); + try_reward_mixnode(deps.as_mut(), info, node_owner.clone(), 20).unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + + // if the node was 0% up, nobody will get any rewards + let info = mock_info(network_monitor_address, &[]); + try_reward_mixnode(deps.as_mut(), info, node_owner.clone(), 0).unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + mix_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + } + + #[cfg(test)] + mod gateway_stake_delegation { + use super::*; + use crate::storage::gateway_delegations_read; + use crate::support::tests::helpers::add_gateway; + + #[test] + fn fails_if_node_doesnt_exist() { + let mut deps = helpers::init_contract(); + assert_eq!( + Err(ContractError::GatewayBondNotFound { + account: "non-existent-node-owner".into() + }), + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + "non-existent-node-owner".into() + ) + ); + } + + #[test] + fn succeeds_for_existing_node() { + let mut deps = helpers::init_contract(); + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + gateway_owner.into() + ) + .is_ok()); + + assert_eq!( + 123, + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn fails_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[]), env).unwrap(); + + assert_eq!( + Err(ContractError::GatewayBondNotFound { + account: gateway_owner.into() + }), + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + gateway_owner.into() + ) + ); + } + + #[test] + fn succeeds_if_node_rebonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[]), env).unwrap(); + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + gateway_owner.into() + ) + .is_ok()); + + assert_eq!( + 123, + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_possible_for_an_already_delegated_node() { + let mut deps = helpers::init_contract(); + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(50, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + assert_eq!( + 150, + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_disallowed_for_already_delegated_node_if_it_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[]), env).unwrap(); + + assert_eq!( + Err(ContractError::GatewayBondNotFound { + account: gateway_owner.into() + }), + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(50, DENOM)), + gateway_owner.into() + ) + ); + } + + #[test] + fn is_allowed_for_multiple_nodes() { + let mut deps = helpers::init_contract(); + let gateway_owner1 = "bob"; + let gateway_owner2 = "fred"; + add_gateway(gateway_owner1, good_gateway_bond(), &mut deps); + add_gateway(gateway_owner2, good_gateway_bond(), &mut deps); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(123, DENOM)), + gateway_owner1.into() + ) + .is_ok()); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(42, DENOM)), + gateway_owner2.into() + ) + .is_ok()); + + assert_eq!( + 123, + gateway_delegations_read(&deps.storage, &gateway_owner1.into()) + .load(b"sender") + .unwrap() + .u128() + ); + + assert_eq!( + 42, + gateway_delegations_read(&deps.storage, &gateway_owner2.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + + #[test] + fn is_allowed_by_multiple_users() { + let mut deps = helpers::init_contract(); + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender1", &coins(123, DENOM)), + gateway_owner.into() + ) + .is_ok()); + + assert!(try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender2", &coins(123, DENOM)), + gateway_owner.into() + ) + .is_ok()); + } + + #[test] + fn delegation_is_not_removed_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[]), env).unwrap(); + + assert_eq!( + 100, + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .load(b"sender") + .unwrap() + .u128() + ); + } + } + + #[cfg(test)] + mod removing_gateway_stake_delegation { + use super::*; + use crate::storage::gateway_delegations_read; + use crate::support::tests::helpers::add_gateway; + + #[test] + fn fails_if_delegation_never_existed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + assert_eq!( + Err(ContractError::NoGatewayDelegationFound { + gateway_owner: gateway_owner.into(), + }), + try_remove_delegation_from_gateway( + deps.as_mut(), + mock_info("sender", &[]), + env, + gateway_owner.into(), + ) + ); + } + + #[test] + fn succeeds_if_delegation_existed() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + assert_eq!( + Ok(HandleResponse { + messages: vec![BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: "sender".into(), + amount: coins(100, DENOM), + } + .into()], + attributes: Vec::new(), + data: None, + }), + try_remove_delegation_from_gateway( + deps.as_mut(), + mock_info("sender", &[]), + env, + gateway_owner.into(), + ) + ); + + assert!( + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .may_load(b"sender") + .unwrap() + .is_none() + ); + } + + #[test] + fn succeeds_if_delegation_existed_even_if_node_unbonded() { + let mut deps = helpers::init_contract(); + let env = mock_env(); + + let gateway_owner = "bob"; + add_gateway(gateway_owner, good_gateway_bond(), &mut deps); + + try_delegate_to_gateway( + deps.as_mut(), + mock_info("sender", &coins(100, DENOM)), + gateway_owner.into(), + ) + .unwrap(); + + try_remove_gateway(deps.as_mut(), mock_info(gateway_owner, &[]), env.clone()).unwrap(); + + assert_eq!( + Ok(HandleResponse { + messages: vec![BankMsg::Send { + from_address: env.contract.address.clone(), + to_address: "sender".into(), + amount: coins(100, DENOM), + } + .into()], + attributes: Vec::new(), + data: None, + }), + try_remove_delegation_from_gateway( + deps.as_mut(), + mock_info("sender", &[]), + env, + gateway_owner.into(), + ) + ); + + assert!( + gateway_delegations_read(&deps.storage, &gateway_owner.into()) + .may_load(b"sender") + .unwrap() + .is_none() + ); + } + } + + #[test] + fn delegators_share_the_same_reward_rate_as_gateways() { + let mut deps = helpers::init_contract(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let network_monitor_address = current_state.network_monitor_address; + + let initial_gateway_bond = 100_000000; + let initial_delegation1 = 50000; // will see single digits rewards + let initial_delegation2 = 100; // won't see any rewards due to such a small delegation + let initial_delegation3 = 100000_000000; // will see big proper rewards + + let node_owner: HumanAddr = "node-owner".into(); + let gateway_bond = GatewayBond { + amount: coins(initial_gateway_bond, DENOM), + owner: node_owner.clone(), + gateway: gateway_fixture(), + }; + + gateways(&mut deps.storage) + .save(b"node-owner", &gateway_bond) + .unwrap(); + + gateway_delegations(&mut deps.storage, &node_owner) + .save(b"delegator1", &Uint128(initial_delegation1)) + .unwrap(); + gateway_delegations(&mut deps.storage, &node_owner) + .save(b"delegator2", &Uint128(initial_delegation2)) + .unwrap(); + gateway_delegations(&mut deps.storage, &node_owner) + .save(b"delegator3", &Uint128(initial_delegation3)) + .unwrap(); + + let reward = read_gateway_epoch_reward_rate(deps.as_ref().storage); + + // 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 info = mock_info(network_monitor_address.clone(), &[]); + try_reward_gateway(deps.as_mut(), info, node_owner.clone(), 100).unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + + // 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 info = mock_info(network_monitor_address.clone(), &[]); + try_reward_gateway(deps.as_mut(), info, node_owner.clone(), 20).unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + + // if the node was 0% up, nobody will get any rewards + let info = mock_info(network_monitor_address, &[]); + try_reward_gateway(deps.as_mut(), info, node_owner.clone(), 0).unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, b"node-owner").unwrap() + ); + + assert_eq!( + expected_delegation1, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator1".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation2, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator2".as_bytes()) + .unwrap() + ); + + assert_eq!( + expected_delegation3, + gateway_delegations_read(deps.as_ref().storage, &node_owner) + .load("delegator3".as_bytes()) + .unwrap() + ); + } }