Feature/simple payments (#571)
* Calculating reward per epoch * Initial uptime-based rewards * Setting monitor address on init + minor cleanup * Ability to reward nodes through validator client * Preemptively updated client version * Using hardcoded monitor address * Removed unnecessary let binding * Comment typo * Changed validator-client package.json version * Checking for broadcast error * Added epoch_length to typescript client's stateparams * Setting default state on migration * Removed reward mixnode/gateway methods from the client api * api for executing custom contract methods * Removed whitespace leftover from the merge * Extra comments * Cleanup Co-authored-by: Dave Hrycyszyn <futurechimp@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
528eba2faa
commit
2c058bcbca
@@ -1,7 +1,7 @@
|
||||
import NetClient, { INetClient } from "./net-client";
|
||||
import { Gateway, GatewayBond, MixNode, MixNodeBond, SendRequest } from "./types";
|
||||
import { Bip39, Random } from "@cosmjs/crypto";
|
||||
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
|
||||
import { DirectSecp256k1HdWallet, EncodeObject } from "@cosmjs/proto-signing";
|
||||
import MixnodesCache from "./caches/mixnodes";
|
||||
import { buildFeeTable, coin, Coin, coins, StdFee } from "@cosmjs/launchpad";
|
||||
import {
|
||||
@@ -23,16 +23,18 @@ import {
|
||||
} from "./currency";
|
||||
import GatewaysCache from "./caches/gateways";
|
||||
import QueryClient, { IQueryClient } from "./query-client";
|
||||
import { nymGasLimits, nymGasPrice } from "./stargate-helper";
|
||||
import { BroadcastTxSuccess, isBroadcastTxFailure } from "@cosmjs/stargate";
|
||||
import { makeBankMsgSend } from "./utils";
|
||||
import { nymGasLimits, nymGasPrice } from "./stargate-helper";
|
||||
|
||||
export { coins, coin };
|
||||
export { Coin };
|
||||
export { displayAmountToNative, nativeCoinToDisplay, printableCoin, printableBalance, nativeToPrintable, MappedCoin, CoinMap }
|
||||
export { nymGasLimits, nymGasPrice }
|
||||
|
||||
export default class ValidatorClient {
|
||||
private readonly stakeDenom: string;
|
||||
// TODO: do those even still make sense since they can vary?
|
||||
private readonly defaultGatewayBondingStake: number = 100_000000
|
||||
private readonly defaultMixnodeBondingStake: number = 100_000000
|
||||
|
||||
@@ -364,7 +366,6 @@ export default class ValidatorClient {
|
||||
} else {
|
||||
throw new Error("Tried to update state params with a query client")
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// TODO: if we just keep a reference to the SigningCosmWasmClient somewhere we can probably go direct
|
||||
@@ -409,6 +410,18 @@ export default class ValidatorClient {
|
||||
}
|
||||
}
|
||||
|
||||
public async executeCustom(signerAddress: string, messages: readonly EncodeObject[], customFee: StdFee, memo?: string): Promise<BroadcastTxSuccess> {
|
||||
if (this.client instanceof NetClient) {
|
||||
const result = await this.client.signAndBroadcast(signerAddress, messages, customFee, memo);
|
||||
if (isBroadcastTxFailure(result)) {
|
||||
throw new Error(`Error when broadcasting tx ${result.transactionHash} at height ${result.height}. Code: ${result.code}; Raw log: ${result.rawLog}`)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
throw new Error("Tried to use executeCustom with a query client");
|
||||
}
|
||||
}
|
||||
|
||||
async upload(senderAddress: string, wasmCode: Uint8Array, meta?: UploadMeta, memo?: string): Promise<UploadResult> {
|
||||
if (this.client instanceof NetClient) {
|
||||
return this.client.upload(senderAddress, wasmCode, meta, memo).catch((err) => this.handleRequestFailure(err));
|
||||
@@ -477,6 +490,7 @@ export type GatewayOwnershipResponse = {
|
||||
}
|
||||
|
||||
export type StateParams = {
|
||||
epoch_length: number,
|
||||
// ideally I'd want to define those as `number` rather than `string`, but
|
||||
// rust-side they are defined as Uint128 and Decimal that don't have
|
||||
// native javascript representations and therefore are interpreted as strings after deserialization
|
||||
|
||||
@@ -1,26 +1,58 @@
|
||||
use crate::helpers::calculate_epoch_reward_rate;
|
||||
use crate::msg::{HandleMsg, InitMsg, MigrateMsg, QueryMsg};
|
||||
use crate::state::{config, State, StateParams};
|
||||
use crate::state::{State, StateParams};
|
||||
use crate::storage::config;
|
||||
use crate::{error::ContractError, queries, transactions};
|
||||
use cosmwasm_std::{
|
||||
to_binary, Decimal, Deps, DepsMut, Env, HandleResponse, InitResponse, MessageInfo,
|
||||
to_binary, Decimal, Deps, DepsMut, Env, HandleResponse, HumanAddr, InitResponse, MessageInfo,
|
||||
MigrateResponse, QueryResponse, Uint128,
|
||||
};
|
||||
|
||||
pub const INITIAL_DEFAULT_EPOCH_LENGTH: u32 = 2;
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a gateway
|
||||
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
/// Constant specifying minimum of coin required to bond a mixnode
|
||||
pub const INITIAL_MIXNODE_BOND: Uint128 = Uint128(100_000000);
|
||||
|
||||
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: Decimal = Decimal::one();
|
||||
|
||||
pub const INITIAL_GATEWAY_BOND_REWARD_RATE: Decimal = Decimal::one();
|
||||
// percentage annual increase. Given starting value of x, we expect to have 1.1x at the end of the year
|
||||
pub const INITIAL_MIXNODE_BOND_REWARD_RATE: u64 = 110;
|
||||
pub const INITIAL_GATEWAY_BOND_REWARD_RATE: u64 = 110;
|
||||
|
||||
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
|
||||
|
||||
const NETWORK_MONITOR_ADDRESS: &str = "hal1v9qauwdq5terag6uvfsdytcs2d0sdmfdq6e83g";
|
||||
|
||||
/// Constant specifying denomination of the coin used for bonding
|
||||
pub const DENOM: &str = "uhal";
|
||||
|
||||
fn default_initial_state(owner: HumanAddr) -> State {
|
||||
let mixnode_bond_reward_rate = Decimal::percent(INITIAL_MIXNODE_BOND_REWARD_RATE);
|
||||
let gateway_bond_reward_rate = Decimal::percent(INITIAL_GATEWAY_BOND_REWARD_RATE);
|
||||
|
||||
State {
|
||||
owner,
|
||||
network_monitor_address: NETWORK_MONITOR_ADDRESS.into(),
|
||||
params: StateParams {
|
||||
epoch_length: INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_bond_reward_rate,
|
||||
gateway_bond_reward_rate,
|
||||
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
|
||||
},
|
||||
mixnode_epoch_bond_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
mixnode_bond_reward_rate,
|
||||
),
|
||||
gateway_epoch_bond_reward: calculate_epoch_reward_rate(
|
||||
INITIAL_DEFAULT_EPOCH_LENGTH,
|
||||
gateway_bond_reward_rate,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
/// Instantiate the contract.
|
||||
///
|
||||
/// `deps` contains Storage, API and Querier
|
||||
@@ -32,19 +64,8 @@ pub fn init(
|
||||
info: MessageInfo,
|
||||
_msg: InitMsg,
|
||||
) -> Result<InitResponse, ContractError> {
|
||||
// TODO: to discuss with DH, should the initial state be set as it is right now, i.e.
|
||||
// using the defined constants, or should it rather be all based on whatever is sent
|
||||
// in `InitMsg`?
|
||||
let state = State {
|
||||
owner: info.sender,
|
||||
params: StateParams {
|
||||
minimum_mixnode_bond: INITIAL_MIXNODE_BOND,
|
||||
minimum_gateway_bond: INITIAL_GATEWAY_BOND,
|
||||
mixnode_bond_reward_rate: INITIAL_MIXNODE_BOND_REWARD_RATE,
|
||||
gateway_bond_reward_rate: INITIAL_GATEWAY_BOND_REWARD_RATE,
|
||||
mixnode_active_set_size: INITIAL_MIXNODE_ACTIVE_SET_SIZE,
|
||||
},
|
||||
};
|
||||
let state = default_initial_state(info.sender);
|
||||
|
||||
config(deps.storage).save(&state)?;
|
||||
Ok(InitResponse::default())
|
||||
}
|
||||
@@ -64,6 +85,12 @@ pub fn handle(
|
||||
HandleMsg::UpdateStateParams(params) => {
|
||||
transactions::try_update_state_params(deps, info, params)
|
||||
}
|
||||
HandleMsg::RewardMixnode { owner, uptime } => {
|
||||
transactions::try_reward_mixnode(deps, info, owner, uptime)
|
||||
}
|
||||
HandleMsg::RewardGateway { owner, uptime } => {
|
||||
transactions::try_reward_gateway(deps, info, owner, uptime)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,11 +115,15 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, Cont
|
||||
}
|
||||
|
||||
pub fn migrate(
|
||||
_deps: DepsMut,
|
||||
deps: DepsMut,
|
||||
_env: Env,
|
||||
_info: MessageInfo,
|
||||
info: MessageInfo,
|
||||
_msg: MigrateMsg,
|
||||
) -> Result<MigrateResponse, ContractError> {
|
||||
// set the state to the default
|
||||
let state = default_initial_state(info.sender);
|
||||
|
||||
config(deps.storage).save(&state)?;
|
||||
Ok(Default::default())
|
||||
}
|
||||
|
||||
|
||||
@@ -33,9 +33,21 @@ pub enum ContractError {
|
||||
#[error("Wrong coin denomination, you must send {}", DENOM)]
|
||||
WrongDenom {},
|
||||
|
||||
#[error("Received multiple coin types during bond")]
|
||||
MultipleDenoms,
|
||||
|
||||
#[error("No coin was sent for the bonding, you must send {}", DENOM)]
|
||||
NoBondFound,
|
||||
|
||||
#[error("The bond reward rate for mixnode was set to be lower than 1")]
|
||||
DecreasingMixnodeBondReward,
|
||||
|
||||
#[error("The bond reward rate for gateway was set to be lower than 1")]
|
||||
DecreasingGatewayBondReward,
|
||||
|
||||
#[error("The node had uptime larger than 100%")]
|
||||
UnexpectedUptime,
|
||||
|
||||
#[error("This address has already bonded a mixnode")]
|
||||
AlreadyOwnsMixnode,
|
||||
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
// Copyright 2021 Nym Technologies SA
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use crate::error::ContractError;
|
||||
use cosmwasm_std::{Decimal, Uint128};
|
||||
|
||||
// for time being completely ignore concept of a leap year and assume each year is exactly 365 days
|
||||
// i.e. 8760 hours
|
||||
const HOURS_IN_YEAR: u128 = 8760;
|
||||
|
||||
const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128);
|
||||
|
||||
// calculates value - 1
|
||||
fn decimal_sub_one(value: Decimal) -> Decimal {
|
||||
assert!(value >= Decimal::one());
|
||||
|
||||
let value_uint128 = value * DECIMAL_FRACTIONAL;
|
||||
let uint128_sub_one = (value_uint128 - DECIMAL_FRACTIONAL).unwrap();
|
||||
Decimal::from_ratio(uint128_sub_one, DECIMAL_FRACTIONAL)
|
||||
}
|
||||
|
||||
fn decimal_to_uint128(value: Decimal) -> Uint128 {
|
||||
value * DECIMAL_FRACTIONAL
|
||||
}
|
||||
|
||||
fn uint128_to_decimal(value: Uint128) -> Decimal {
|
||||
Decimal::from_ratio(value, DECIMAL_FRACTIONAL)
|
||||
}
|
||||
|
||||
pub(crate) fn calculate_epoch_reward_rate(
|
||||
epoch_length: u32,
|
||||
annual_reward_rate: Decimal,
|
||||
) -> Decimal {
|
||||
// this is more of a sanity check as the contract does not allow setting annual reward rates
|
||||
// to be lower than 1.
|
||||
debug_assert!(annual_reward_rate >= Decimal::one());
|
||||
|
||||
// converts reward rate, like 1.25 into the expected gain, like 0.25
|
||||
let annual_reward = decimal_sub_one(annual_reward_rate);
|
||||
// do a simple cross-multiplication:
|
||||
// `annual_reward` - `HOURS_IN_YEAR`
|
||||
// x - `epoch_length`
|
||||
//
|
||||
// x = `annual_reward` * `epoch_length` / `HOURS_IN_YEAR`
|
||||
|
||||
// converts reward, like 0.25 into 250000000000000000
|
||||
let annual_reward_uint128 = decimal_to_uint128(annual_reward);
|
||||
|
||||
// calculates `annual_reward_uint128` * `epoch_length` / `HOURS_IN_YEAR`
|
||||
let epoch_reward_uint128 = annual_reward_uint128.multiply_ratio(epoch_length, HOURS_IN_YEAR);
|
||||
|
||||
// note: this returns a % reward, like 0.05 rather than reward rate (like 1.05)
|
||||
uint128_to_decimal(epoch_reward_uint128)
|
||||
}
|
||||
|
||||
pub(crate) fn scale_reward_by_uptime(
|
||||
reward: Decimal,
|
||||
uptime: u32,
|
||||
) -> Result<Decimal, ContractError> {
|
||||
if uptime > 100 {
|
||||
return Err(ContractError::UnexpectedUptime);
|
||||
}
|
||||
let uptime_ratio = Decimal::from_ratio(uptime, 100u128);
|
||||
// if we do not convert into a more precise representation, we might end up with, for example,
|
||||
// reward 0.05 and uptime of 50% which would produce 0.50 * 0.05 = 0 (because of u128 representation)
|
||||
// and also the above would be impossible to compute as Mul<Decimal> for Decimal is not implemented
|
||||
//
|
||||
// but with the intermediate conversion, we would have
|
||||
// 0.50 * 50_000_000_000_000_000 = 25_000_000_000_000_000
|
||||
// which converted back would give us the proper 0.025
|
||||
let uptime_ratio_u128 = decimal_to_uint128(uptime_ratio);
|
||||
let scaled = reward * uptime_ratio_u128;
|
||||
Ok(uint128_to_decimal(scaled))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn calculating_epoch_reward_rate() {
|
||||
// 1.10
|
||||
let annual_reward_rate = Decimal::from_ratio(110u128, 100u128);
|
||||
|
||||
// if the epoch is (for some reason) exactly one year,
|
||||
// the reward rate should be unchanged
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(HOURS_IN_YEAR as u32, annual_reward_rate);
|
||||
// 0.10
|
||||
let expected = decimal_sub_one(annual_reward_rate);
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
|
||||
// 24 hours
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(24, annual_reward_rate);
|
||||
// 0.1 / 365
|
||||
let expected = Decimal::from_ratio(1u128, 3650u128);
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
|
||||
let expected_per_epoch_rate_excel = Decimal::from_str("0.000273972602739726").unwrap();
|
||||
assert_eq!(expected_per_epoch_rate_excel, per_epoch_rate);
|
||||
|
||||
// 1 hour
|
||||
let per_epoch_rate = calculate_epoch_reward_rate(1, annual_reward_rate);
|
||||
// 0.1 / 8760
|
||||
let expected = Decimal::from_ratio(1u128, 87600u128);
|
||||
assert_eq!(expected, per_epoch_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scaling_reward_by_uptime() {
|
||||
// 0.05
|
||||
let epoch_reward = Decimal::from_ratio(5u128, 100u128);
|
||||
|
||||
// scaling by 100 does nothing
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 100).unwrap();
|
||||
assert_eq!(epoch_reward, scaled);
|
||||
|
||||
// scaling by 0 makes the reward 0
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 0).unwrap();
|
||||
assert_eq!(Decimal::zero(), scaled);
|
||||
|
||||
// 50 halves it
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 50).unwrap();
|
||||
let expected = Decimal::from_ratio(25u128, 1000u128);
|
||||
assert_eq!(expected, scaled);
|
||||
|
||||
// 10 takes 1/10th
|
||||
let scaled = scale_reward_by_uptime(epoch_reward, 10).unwrap();
|
||||
let expected = Decimal::from_ratio(5u128, 1000u128);
|
||||
assert_eq!(expected, scaled);
|
||||
|
||||
// anything larger than 100 returns an error
|
||||
assert!(scale_reward_by_uptime(epoch_reward, 101).is_err())
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,10 @@
|
||||
pub mod contract;
|
||||
pub mod error;
|
||||
pub(crate) mod helpers;
|
||||
pub mod msg;
|
||||
pub mod queries;
|
||||
pub mod state;
|
||||
pub(crate) mod storage;
|
||||
pub mod support;
|
||||
pub mod transactions;
|
||||
|
||||
|
||||
@@ -10,11 +10,27 @@ pub struct InitMsg {}
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum HandleMsg {
|
||||
BondMixnode { mix_node: MixNode },
|
||||
BondMixnode {
|
||||
mix_node: MixNode,
|
||||
},
|
||||
UnbondMixnode {},
|
||||
BondGateway { gateway: Gateway },
|
||||
BondGateway {
|
||||
gateway: Gateway,
|
||||
},
|
||||
UnbondGateway {},
|
||||
UpdateStateParams(StateParams),
|
||||
|
||||
RewardMixnode {
|
||||
owner: HumanAddr,
|
||||
// percentage value in range 0-100
|
||||
uptime: u32,
|
||||
},
|
||||
|
||||
RewardGateway {
|
||||
owner: HumanAddr,
|
||||
// percentage value in range 0-100
|
||||
uptime: u32,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
// settings for pagination
|
||||
use crate::state::{config_read, gateways_read, mixnodes_read, StateParams, PREFIX_MIXNODES};
|
||||
use crate::state::StateParams;
|
||||
use crate::storage::{gateways_read, mixnodes_read, read_state_params};
|
||||
use cosmwasm_std::Deps;
|
||||
use cosmwasm_std::HumanAddr;
|
||||
use cosmwasm_std::Order;
|
||||
use cosmwasm_std::StdResult;
|
||||
use cosmwasm_storage::bucket_read;
|
||||
use mixnet_contract::{
|
||||
GatewayBond, GatewayOwnershipResponse, MixNodeBond, MixOwnershipResponse, PagedGatewayResponse,
|
||||
PagedResponse,
|
||||
@@ -21,19 +20,15 @@ pub fn query_mixnodes_paged(
|
||||
let limit = limit.unwrap_or(DEFAULT_LIMIT).min(MAX_LIMIT) as usize;
|
||||
let start = calculate_start_value(start_after);
|
||||
|
||||
let bucket = bucket_read::<MixNodeBond>(deps.storage, PREFIX_MIXNODES);
|
||||
let res = bucket
|
||||
let nodes = mixnodes_read(deps.storage)
|
||||
.range(start.as_deref(), None, Order::Ascending)
|
||||
.take(limit);
|
||||
let node_tuples = res.collect::<StdResult<Vec<(Vec<u8>, MixNodeBond)>>>()?;
|
||||
let nodes = node_tuples
|
||||
.into_iter()
|
||||
.map(|item| item.1)
|
||||
.collect::<Vec<_>>();
|
||||
.take(limit)
|
||||
.map(|res| res.map(|item| item.1))
|
||||
.collect::<StdResult<Vec<MixNodeBond>>>()?;
|
||||
|
||||
let start_next_after = nodes.last().map(|node| node.owner().clone());
|
||||
|
||||
let response = PagedResponse::new(nodes, limit, start_next_after);
|
||||
Ok(response)
|
||||
Ok(PagedResponse::new(nodes, limit, start_next_after))
|
||||
}
|
||||
|
||||
pub(crate) fn query_gateways_paged(
|
||||
@@ -78,6 +73,10 @@ pub(crate) fn query_owns_gateway(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn query_state_params(deps: Deps) -> StateParams {
|
||||
read_state_params(deps.storage)
|
||||
}
|
||||
|
||||
/// Adds a 0 byte to terminate the `start_after` value given. This allows CosmWasm
|
||||
/// to get the succeeding key as the start of the next page.
|
||||
fn calculate_start_value(
|
||||
@@ -90,17 +89,11 @@ fn calculate_start_value(
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn query_state_params(deps: Deps) -> StateParams {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
config_read(deps.storage).load().unwrap().params
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::state::{config, gateways, mixnodes, State};
|
||||
use crate::state::State;
|
||||
use crate::storage::{config, gateways, mixnodes};
|
||||
use crate::support::tests::helpers;
|
||||
use cosmwasm_std::Storage;
|
||||
|
||||
@@ -403,13 +396,17 @@ mod tests {
|
||||
|
||||
let dummy_state = State {
|
||||
owner: "someowner".into(),
|
||||
network_monitor_address: "monitor".into(),
|
||||
params: StateParams {
|
||||
epoch_length: 1,
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
|
||||
gateway_bond_reward_rate: "4.56".parse().unwrap(),
|
||||
mixnode_active_set_size: 1000,
|
||||
},
|
||||
mixnode_epoch_bond_reward: "1.23".parse().unwrap(),
|
||||
gateway_epoch_bond_reward: "4.56".parse().unwrap(),
|
||||
};
|
||||
|
||||
config(deps.as_mut().storage).save(&dummy_state).unwrap();
|
||||
|
||||
@@ -1,94 +1,25 @@
|
||||
use cosmwasm_std::{Decimal, HumanAddr, Storage, Uint128};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
use mixnet_contract::{GatewayBond, MixNodeBond};
|
||||
use cosmwasm_std::{Decimal, HumanAddr, Uint128};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
// Contract-level stuff
|
||||
|
||||
pub static CONFIG_KEY: &[u8] = b"config";
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct State {
|
||||
pub owner: HumanAddr, // only the owner account can update state
|
||||
|
||||
pub network_monitor_address: HumanAddr,
|
||||
pub params: StateParams,
|
||||
|
||||
// helper values to avoid having to recalculate them on every single payment operation
|
||||
pub mixnode_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
pub gateway_epoch_bond_reward: Decimal, // reward per epoch expressed as a decimal like 0.05
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
|
||||
pub struct StateParams {
|
||||
pub epoch_length: u32, // length of an epoch, expressed in hours
|
||||
|
||||
pub minimum_mixnode_bond: Uint128, // minimum amount a mixnode must bond to get into the system
|
||||
pub minimum_gateway_bond: Uint128, // minimum amount a gateway must bond to get into the system
|
||||
pub mixnode_bond_reward_rate: Decimal, // epoch reward rate, expressed as a decimal like 1.25
|
||||
pub gateway_bond_reward_rate: Decimal, // epoch reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub gateway_bond_reward_rate: Decimal, // annual reward rate, expressed as a decimal like 1.25
|
||||
pub mixnode_active_set_size: u32,
|
||||
}
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<State> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
// Mixnode-related stuff
|
||||
pub const PREFIX_MIXNODES: &[u8] = b"mixnodes";
|
||||
|
||||
pub fn mixnodes(storage: &mut dyn Storage) -> Bucket<MixNodeBond> {
|
||||
bucket(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<MixNodeBond> {
|
||||
bucket_read(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
// Gateway-related stuff
|
||||
|
||||
pub const PREFIX_GATEWAYS: &[u8] = b"gateways";
|
||||
|
||||
pub fn gateways(storage: &mut dyn Storage) -> Bucket<GatewayBond> {
|
||||
bucket(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
pub fn gateways_read(storage: &dyn Storage) -> ReadonlyBucket<GatewayBond> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::support::tests::helpers::{gateway_bond_fixture, mixnode_bond_fixture};
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = mixnode_bond_fixture();
|
||||
let bond2 = mixnode_bond_fixture();
|
||||
mixnodes(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
mixnodes(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = mixnodes_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = gateway_bond_fixture();
|
||||
let bond2 = gateway_bond_fixture();
|
||||
gateways(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
gateways(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = gateways_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = gateways_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,282 @@
|
||||
use crate::state::{State, StateParams};
|
||||
use cosmwasm_std::{Decimal, StdError, StdResult, Storage};
|
||||
use cosmwasm_storage::{
|
||||
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
|
||||
Singleton,
|
||||
};
|
||||
use mixnet_contract::{GatewayBond, MixNodeBond};
|
||||
|
||||
// Contract-level stuff
|
||||
const CONFIG_KEY: &[u8] = b"config";
|
||||
|
||||
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
|
||||
singleton(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub fn config_read(storage: &dyn Storage) -> ReadonlySingleton<State> {
|
||||
singleton_read(storage, CONFIG_KEY)
|
||||
}
|
||||
|
||||
pub(crate) fn read_state_params(storage: &dyn Storage) -> StateParams {
|
||||
// note: In any other case, I wouldn't have attempted to unwrap this result, but in here
|
||||
// if we fail to load the stored state we would already be in the undefined behaviour land,
|
||||
// so we better just blow up immediately.
|
||||
config_read(storage).load().unwrap().params
|
||||
}
|
||||
|
||||
pub(crate) fn read_mixnode_epoch_reward_rate(storage: &dyn Storage) -> Decimal {
|
||||
// same justification as in `read_state_params` for the unwrap
|
||||
config_read(storage)
|
||||
.load()
|
||||
.unwrap()
|
||||
.mixnode_epoch_bond_reward
|
||||
}
|
||||
|
||||
pub(crate) fn read_gateway_epoch_reward_rate(storage: &dyn Storage) -> Decimal {
|
||||
// same justification as in `read_state_params` for the unwrap
|
||||
config_read(storage)
|
||||
.load()
|
||||
.unwrap()
|
||||
.gateway_epoch_bond_reward
|
||||
}
|
||||
|
||||
// Mixnode-related stuff
|
||||
const PREFIX_MIXNODES: &[u8] = b"mixnodes";
|
||||
|
||||
pub fn mixnodes(storage: &mut dyn Storage) -> Bucket<MixNodeBond> {
|
||||
bucket(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
pub fn mixnodes_read(storage: &dyn Storage) -> ReadonlyBucket<MixNodeBond> {
|
||||
bucket_read(storage, PREFIX_MIXNODES)
|
||||
}
|
||||
|
||||
// helpers
|
||||
pub(crate) fn increase_mixnode_bond(
|
||||
storage: &mut dyn Storage,
|
||||
owner: &[u8],
|
||||
scaled_reward_rate: Decimal,
|
||||
) -> StdResult<()> {
|
||||
let mut bucket = mixnodes(storage);
|
||||
let mut node = bucket.load(owner)?;
|
||||
if node.amount.len() != 1 {
|
||||
return Err(StdError::generic_err(
|
||||
"mixnode seems to have been bonded with multiple coin types",
|
||||
));
|
||||
}
|
||||
|
||||
let reward = node.amount[0].amount * scaled_reward_rate;
|
||||
node.amount[0].amount += reward;
|
||||
bucket.save(owner, &node)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_mixnode_bond(
|
||||
storage: &dyn Storage,
|
||||
owner: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = mixnodes_read(storage);
|
||||
let node = bucket.load(owner)?;
|
||||
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)
|
||||
}
|
||||
|
||||
// Gateway-related stuff
|
||||
|
||||
const PREFIX_GATEWAYS: &[u8] = b"gateways";
|
||||
|
||||
pub fn gateways(storage: &mut dyn Storage) -> Bucket<GatewayBond> {
|
||||
bucket(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
pub fn gateways_read(storage: &dyn Storage) -> ReadonlyBucket<GatewayBond> {
|
||||
bucket_read(storage, PREFIX_GATEWAYS)
|
||||
}
|
||||
|
||||
// helpers
|
||||
pub(crate) fn increase_gateway_bond(
|
||||
storage: &mut dyn Storage,
|
||||
owner: &[u8],
|
||||
scaled_reward_rate: Decimal,
|
||||
) -> StdResult<()> {
|
||||
let mut bucket = gateways(storage);
|
||||
let mut node = bucket.load(owner)?;
|
||||
if node.amount.len() != 1 {
|
||||
return Err(StdError::generic_err(
|
||||
"gateway seems to have been bonded with multiple coin types",
|
||||
));
|
||||
}
|
||||
let reward = node.amount[0].amount * scaled_reward_rate;
|
||||
node.amount[0].amount += reward;
|
||||
bucket.save(owner, &node)
|
||||
}
|
||||
|
||||
// currently not used outside tests
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_gateway_bond(
|
||||
storage: &dyn Storage,
|
||||
owner: &[u8],
|
||||
) -> StdResult<cosmwasm_std::Uint128> {
|
||||
let bucket = gateways_read(storage);
|
||||
let node = bucket.load(owner)?;
|
||||
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)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::contract::DENOM;
|
||||
use crate::support::tests::helpers::{
|
||||
gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture,
|
||||
};
|
||||
use cosmwasm_std::testing::MockStorage;
|
||||
use cosmwasm_std::{coins, Uint128};
|
||||
|
||||
#[test]
|
||||
fn mixnode_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = mixnode_bond_fixture();
|
||||
let bond2 = mixnode_bond_fixture();
|
||||
mixnodes(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
mixnodes(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = mixnodes_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = mixnodes_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn gateway_single_read_retrieval() {
|
||||
let mut storage = MockStorage::new();
|
||||
let bond1 = gateway_bond_fixture();
|
||||
let bond2 = gateway_bond_fixture();
|
||||
gateways(&mut storage).save(b"bond1", &bond1).unwrap();
|
||||
gateways(&mut storage).save(b"bond2", &bond2).unwrap();
|
||||
|
||||
let res1 = gateways_read(&storage).load(b"bond1").unwrap();
|
||||
let res2 = gateways_read(&storage).load(b"bond2").unwrap();
|
||||
assert_eq!(bond1, res1);
|
||||
assert_eq!(bond2, res2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn increasing_mixnode_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner = b"owner";
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
// produces an error if target mixnode doesn't exist
|
||||
let res = increase_mixnode_bond(&mut storage, node_owner, reward);
|
||||
assert!(res.is_err());
|
||||
|
||||
// increases the reward appropriately if node exists
|
||||
let mixnode_bond = MixNodeBond {
|
||||
amount: coins(1000, DENOM),
|
||||
owner: std::str::from_utf8(node_owner).unwrap().into(),
|
||||
mix_node: mix_node_fixture(),
|
||||
};
|
||||
|
||||
mixnodes(&mut storage)
|
||||
.save(node_owner, &mixnode_bond)
|
||||
.unwrap();
|
||||
|
||||
increase_mixnode_bond(&mut storage, node_owner, reward).unwrap();
|
||||
let new_bond = read_mixnode_bond(&storage, node_owner).unwrap();
|
||||
assert_eq!(Uint128(1001), new_bond);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_mixnode_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner = b"owner";
|
||||
|
||||
// produces an error if target mixnode doesn't exist
|
||||
let res = read_mixnode_bond(&storage, node_owner);
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let mixnode_bond = MixNodeBond {
|
||||
amount: coins(bond_value, DENOM),
|
||||
owner: std::str::from_utf8(node_owner).unwrap().into(),
|
||||
mix_node: mix_node_fixture(),
|
||||
};
|
||||
|
||||
mixnodes(&mut storage)
|
||||
.save(node_owner, &mixnode_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_mixnode_bond(&storage, node_owner).unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn increasing_gateway_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner = b"owner";
|
||||
// 0.001
|
||||
let reward = Decimal::from_ratio(1u128, 1000u128);
|
||||
|
||||
// produces an error if target gateway doesn't exist
|
||||
let res = increase_gateway_bond(&mut storage, node_owner, reward);
|
||||
assert!(res.is_err());
|
||||
|
||||
// increases the reward appropriately if node exists
|
||||
let gateway_bond = GatewayBond {
|
||||
amount: coins(1000, DENOM),
|
||||
owner: std::str::from_utf8(node_owner).unwrap().into(),
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
gateways(&mut storage)
|
||||
.save(node_owner, &gateway_bond)
|
||||
.unwrap();
|
||||
|
||||
increase_gateway_bond(&mut storage, node_owner, reward).unwrap();
|
||||
let new_bond = read_gateway_bond(&storage, node_owner).unwrap();
|
||||
assert_eq!(Uint128(1001), new_bond);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reading_gateway_bond() {
|
||||
let mut storage = MockStorage::new();
|
||||
let node_owner = b"owner";
|
||||
|
||||
// produces an error if target mixnode doesn't exist
|
||||
let res = read_gateway_bond(&storage, node_owner);
|
||||
assert!(res.is_err());
|
||||
|
||||
// returns appropriate value otherwise
|
||||
let bond_value = 1000;
|
||||
|
||||
let gateway_bond = GatewayBond {
|
||||
amount: coins(1000, DENOM),
|
||||
owner: std::str::from_utf8(node_owner).unwrap().into(),
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
gateways(&mut storage)
|
||||
.save(node_owner, &gateway_bond)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
Uint128(bond_value),
|
||||
read_gateway_bond(&storage, node_owner).unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,10 +1,15 @@
|
||||
use crate::contract::DENOM;
|
||||
use crate::error::ContractError;
|
||||
use crate::queries::query_state_params;
|
||||
use crate::state::{
|
||||
config, config_read, gateways, gateways_read, mixnodes, mixnodes_read, StateParams,
|
||||
use crate::helpers::scale_reward_by_uptime;
|
||||
use crate::state::StateParams;
|
||||
use crate::storage::{
|
||||
config, config_read, gateways, gateways_read, increase_gateway_bond, increase_mixnode_bond,
|
||||
mixnodes, mixnodes_read, read_gateway_epoch_reward_rate, read_mixnode_epoch_reward_rate,
|
||||
read_state_params,
|
||||
};
|
||||
use cosmwasm_std::{
|
||||
attr, BankMsg, Coin, Decimal, DepsMut, Env, HandleResponse, HumanAddr, MessageInfo, Uint128,
|
||||
};
|
||||
use cosmwasm_std::{attr, BankMsg, Coin, DepsMut, Env, HandleResponse, MessageInfo, Uint128};
|
||||
use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond};
|
||||
|
||||
fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
|
||||
@@ -14,7 +19,7 @@ fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), Con
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
@@ -46,7 +51,7 @@ pub(crate) fn try_add_mixnode(
|
||||
return Err(ContractError::AlreadyOwnsGateway);
|
||||
}
|
||||
|
||||
let minimum_bond = query_state_params(deps.as_ref()).minimum_mixnode_bond;
|
||||
let minimum_bond = read_state_params(deps.storage).minimum_mixnode_bond;
|
||||
validate_mixnode_bond(&info.sent_funds, minimum_bond)?;
|
||||
|
||||
let bond = MixNodeBond::new(info.sent_funds, info.sender.clone(), mix_node);
|
||||
@@ -56,7 +61,6 @@ pub(crate) fn try_add_mixnode(
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
mixnodes(deps.storage).save(sender_bytes, &bond)?;
|
||||
@@ -106,7 +110,7 @@ fn validate_gateway_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), Con
|
||||
}
|
||||
|
||||
if bond.len() > 1 {
|
||||
// TODO: ask DH what would be an appropriate action here
|
||||
return Err(ContractError::MultipleDenoms);
|
||||
}
|
||||
|
||||
// check that the denomination is correct
|
||||
@@ -138,7 +142,7 @@ pub(crate) fn try_add_gateway(
|
||||
return Err(ContractError::AlreadyOwnsMixnode);
|
||||
}
|
||||
|
||||
let minimum_bond = query_state_params(deps.as_ref()).minimum_gateway_bond;
|
||||
let minimum_bond = read_state_params(deps.storage).minimum_gateway_bond;
|
||||
validate_gateway_bond(&info.sent_funds, minimum_bond)?;
|
||||
|
||||
let bond = GatewayBond::new(info.sent_funds, info.sender.clone(), gateway);
|
||||
@@ -148,7 +152,6 @@ pub(crate) fn try_add_gateway(
|
||||
.may_load(sender_bytes)?
|
||||
.is_some();
|
||||
|
||||
// TODO: do attributes also go back to the client or does this need to be put into `data`?
|
||||
let attributes = vec![attr("overwritten", was_present)];
|
||||
|
||||
gateways(deps.storage).save(sender_bytes, &bond)?;
|
||||
@@ -216,18 +219,71 @@ pub(crate) fn try_update_state_params(
|
||||
return Err(ContractError::Unauthorized);
|
||||
}
|
||||
|
||||
if params.mixnode_bond_reward_rate < Decimal::one() {
|
||||
return Err(ContractError::DecreasingMixnodeBondReward);
|
||||
}
|
||||
|
||||
if params.gateway_bond_reward_rate < Decimal::one() {
|
||||
return Err(ContractError::DecreasingGatewayBondReward);
|
||||
}
|
||||
|
||||
state.params = params;
|
||||
config(deps.storage).save(&state)?;
|
||||
|
||||
Ok(HandleResponse::default())
|
||||
}
|
||||
|
||||
pub(crate) fn try_reward_mixnode(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
node_owner: HumanAddr,
|
||||
uptime: u32,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let state = config_read(deps.storage).load().unwrap();
|
||||
|
||||
// check if this is executed by the monitor, if not reject the transaction
|
||||
if info.sender != state.network_monitor_address {
|
||||
return Err(ContractError::Unauthorized);
|
||||
}
|
||||
|
||||
let reward = read_mixnode_epoch_reward_rate(deps.storage);
|
||||
let scaled_reward = scale_reward_by_uptime(reward, uptime)?;
|
||||
|
||||
increase_mixnode_bond(deps.storage, node_owner.as_bytes(), scaled_reward)?;
|
||||
|
||||
Ok(HandleResponse::default())
|
||||
}
|
||||
|
||||
pub(crate) fn try_reward_gateway(
|
||||
deps: DepsMut,
|
||||
info: MessageInfo,
|
||||
gateway_owner: HumanAddr,
|
||||
uptime: u32,
|
||||
) -> Result<HandleResponse, ContractError> {
|
||||
let state = config_read(deps.storage).load().unwrap();
|
||||
|
||||
// check if this is executed by the owner, if not reject the transaction
|
||||
if info.sender != state.network_monitor_address {
|
||||
return Err(ContractError::Unauthorized);
|
||||
}
|
||||
|
||||
let reward = read_gateway_epoch_reward_rate(deps.storage);
|
||||
let scaled_reward = scale_reward_by_uptime(reward, uptime)?;
|
||||
|
||||
increase_gateway_bond(deps.storage, gateway_owner.as_bytes(), scaled_reward)?;
|
||||
|
||||
Ok(HandleResponse::default())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use super::*;
|
||||
use crate::contract::{handle, init, query, INITIAL_GATEWAY_BOND, INITIAL_MIXNODE_BOND};
|
||||
use crate::msg::{HandleMsg, InitMsg, QueryMsg};
|
||||
use crate::state::StateParams;
|
||||
use crate::storage::{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};
|
||||
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
|
||||
use cosmwasm_std::{coins, from_binary, Uint128};
|
||||
use mixnet_contract::{PagedGatewayResponse, PagedResponse};
|
||||
@@ -704,6 +760,7 @@ pub mod tests {
|
||||
let mut deps = helpers::init_contract();
|
||||
|
||||
let new_params = StateParams {
|
||||
epoch_length: 1,
|
||||
minimum_mixnode_bond: 123u128.into(),
|
||||
minimum_gateway_bond: 456u128.into(),
|
||||
mixnode_bond_reward_rate: "1.23".parse().unwrap(),
|
||||
@@ -725,4 +782,112 @@ pub mod tests {
|
||||
let current_state = config_read(deps.as_ref().storage).load().unwrap();
|
||||
assert_eq!(current_state.params, new_params)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewarding_mixnode() {
|
||||
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;
|
||||
|
||||
// errors out if executed by somebody else than network monitor
|
||||
let info = mock_info("not-the-monitor", &[]);
|
||||
let res = try_reward_mixnode(deps.as_mut(), info, "node-owner".into(), 100);
|
||||
assert_eq!(res, Err(ContractError::Unauthorized));
|
||||
|
||||
// errors out if the target owner hasn't bound any mixnodes
|
||||
let info = mock_info(network_monitor_address.clone(), &[]);
|
||||
let res = try_reward_mixnode(deps.as_mut(), info, "node-owner".into(), 100);
|
||||
assert!(res.is_err());
|
||||
|
||||
let initial_bond = 100_000000;
|
||||
let mixnode_bond = MixNodeBond {
|
||||
amount: coins(initial_bond, DENOM),
|
||||
owner: "node-owner".into(),
|
||||
mix_node: mix_node_fixture(),
|
||||
};
|
||||
|
||||
mixnodes(deps.as_mut().storage)
|
||||
.save(b"node-owner", &mixnode_bond)
|
||||
.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_bond) * reward + Uint128(initial_bond);
|
||||
|
||||
let info = mock_info(network_monitor_address.clone(), &[]);
|
||||
try_reward_mixnode(deps.as_mut(), info, "node-owner".into(), 100).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_bond,
|
||||
read_mixnode_bond(deps.as_ref().storage, b"node-owner").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 info = mock_info(network_monitor_address, &[]);
|
||||
try_reward_mixnode(deps.as_mut(), info, "node-owner".into(), 20).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_bond,
|
||||
read_mixnode_bond(deps.as_ref().storage, b"node-owner").unwrap()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rewarding_gateway() {
|
||||
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;
|
||||
|
||||
// errors out if executed by somebody else than network monitor
|
||||
let info = mock_info("not-the-monitor", &[]);
|
||||
let res = try_reward_gateway(deps.as_mut(), info, "node-owner".into(), 100);
|
||||
assert_eq!(res, Err(ContractError::Unauthorized));
|
||||
|
||||
// errors out if the target owner hasn't bound any mixnodes
|
||||
let info = mock_info(network_monitor_address.clone(), &[]);
|
||||
let res = try_reward_gateway(deps.as_mut(), info, "node-owner".into(), 100);
|
||||
assert!(res.is_err());
|
||||
|
||||
let initial_bond = 100_000000;
|
||||
let gateway_bond = GatewayBond {
|
||||
amount: coins(initial_bond, DENOM),
|
||||
owner: "node-owner".into(),
|
||||
gateway: gateway_fixture(),
|
||||
};
|
||||
|
||||
gateways(deps.as_mut().storage)
|
||||
.save(b"node-owner", &gateway_bond)
|
||||
.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_bond) * reward + Uint128(initial_bond);
|
||||
|
||||
let info = mock_info(network_monitor_address.clone(), &[]);
|
||||
try_reward_gateway(deps.as_mut(), info, "node-owner".into(), 100).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_bond,
|
||||
read_gateway_bond(deps.as_ref().storage, b"node-owner").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 info = mock_info(network_monitor_address, &[]);
|
||||
try_reward_gateway(deps.as_mut(), info, "node-owner".into(), 20).unwrap();
|
||||
|
||||
assert_eq!(
|
||||
expected_bond,
|
||||
read_gateway_bond(deps.as_ref().storage, b"node-owner").unwrap()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user