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:
Jędrzej Stuczyński
2021-04-29 19:22:14 +01:00
committed by GitHub
parent 528eba2faa
commit 2c058bcbca
10 changed files with 731 additions and 135 deletions
+17 -3
View File
@@ -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
+51 -20
View File
@@ -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())
}
+12
View File
@@ -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,
+146
View File
@@ -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())
}
}
+2
View File
@@ -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;
+18 -2
View File
@@ -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)]
+18 -21
View File
@@ -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();
+10 -79
View File
@@ -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);
}
}
+282
View File
@@ -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()
);
}
}
+175 -10
View File
@@ -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()
);
}
}