A pull request for discussion about contract state variables (#563)

* A pull request for discussion about contract state variables

* Updated contract state struct

* Introduced transaction to update the contract state

* Moved transactions to separate file

* Corrected return type on query

* Corrected query match

* Added test for state params query

* Test for state params update

* Removed unused imports

* Helper functions to query for state params

* Removed hardcoded inside bond, old mix bond value

* Helper function to update contract state

* Changed typescript StateParam from numbers to strings

* Somehow unresolved post-merge issues in tests

* Introduced additional helpers for the validator client to use the new contract features

* Using the state-specified bond values

Co-authored-by: Jędrzej Stuczyński <jedrzej.stuczynski@gmail.com>
This commit is contained in:
Dave Hrycyszyn
2021-04-09 13:10:01 +01:00
committed by GitHub
parent b6ba8f8555
commit 5652eb7ee3
12 changed files with 847 additions and 637 deletions
+5
View File
@@ -30,6 +30,11 @@ export function printableBalanceToNative(amountToDisplay: string): string {
return decimalAmount.atomics;
}
// reciprocal of `printableBalanceToNative`, takes, for example 10000000 and returns 10
export function nativeToPrintable(nativeValue: string): string {
return Decimal.fromAtomics(nativeValue, 6).toString()
}
export interface MappedCoin {
readonly denom: string;
readonly fractionalDigits: number;
+61 -11
View File
@@ -5,18 +5,35 @@ import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import MixnodesCache from "./caches/mixnodes";
import { coin, Coin, coins } from "@cosmjs/launchpad";
import { BroadcastTxResponse } from "@cosmjs/stargate"
import { ExecuteResult, InstantiateOptions, InstantiateResult, MigrateResult, UploadMeta, UploadResult } from "@cosmjs/cosmwasm";
import { CoinMap, displayAmountToNative, MappedCoin, nativeCoinToDisplay, printableBalance, printableCoin } from "./currency";
import {
ExecuteResult,
InstantiateOptions,
InstantiateResult,
MigrateResult,
UploadMeta,
UploadResult
} from "@cosmjs/cosmwasm";
import {
CoinMap,
displayAmountToNative,
MappedCoin,
nativeCoinToDisplay,
printableBalance,
printableCoin,
nativeToPrintable
} from "./currency";
import GatewaysCache from "./caches/gateways";
import QueryClient, { IQueryClient } from "./query-client";
export { coins, coin };
export { Coin };
export { displayAmountToNative, nativeCoinToDisplay, printableCoin, printableBalance, MappedCoin, CoinMap }
export { displayAmountToNative, nativeCoinToDisplay, printableCoin, printableBalance, nativeToPrintable, MappedCoin, CoinMap }
export default class ValidatorClient {
private readonly stakeDenom: string;
private readonly gatewayBondingStake: number = 1000_000000
private readonly defaultGatewayBondingStake: number = 100_000000
private readonly defaultMixnodeBondingStake: number = 100_000000
url: string;
private readonly client: INetClient | IQueryClient
private mixNodesCache: MixnodesCache;
@@ -84,7 +101,7 @@ export default class ValidatorClient {
*/
static async mnemonicToAddress(mnemonic: string): Promise<string> {
const wallet = await ValidatorClient.buildWallet(mnemonic);
const [{ address }] = await wallet.getAccounts()
const [{address}] = await wallet.getAccounts()
return address
}
@@ -96,6 +113,11 @@ export default class ValidatorClient {
return this.client.getBalance(address, this.stakeDenom);
}
async getStateParams(): Promise<StateParams> {
return this.client.getStateParams(this.contractAddress)
}
/**
* Get or refresh the list of mixnodes in the network.
*
@@ -118,11 +140,20 @@ export default class ValidatorClient {
}
/**
* Announce a mixnode, paying a fee.
*/
* Generate a minimum gateway bond required to create a fresh mixnode.
*
* @returns a `Coin` instance containing minimum amount of coins to stake a gateway.
*/
minimumMixnodeBond = (): Coin => {
return coin(this.defaultMixnodeBondingStake, this.stakeDenom)
}
/**
* Announce a mixnode, paying a fee.
*/
async bond(mixNode: MixNode): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const bond = [{ amount: "1000000000", denom: this.stakeDenom }];
const bond = [this.minimumMixnodeBond()];
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_mixnode: { mix_node: mixNode } }, "adding mixnode", bond);
console.log(`account ${this.client.clientAddress} added mixnode with ${mixNode.host}`);
return result;
@@ -195,7 +226,7 @@ export default class ValidatorClient {
* @returns a `Coin` instance containing minimum amount of coins to stake a gateway.
*/
minimumGatewayBond = (): Coin => {
return coin(this.gatewayBondingStake, this.stakeDenom)
return coin(this.defaultGatewayBondingStake, this.stakeDenom)
}
/**
@@ -204,7 +235,7 @@ export default class ValidatorClient {
async bondGateway(gateway: Gateway): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const bond = this.minimumGatewayBond()
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { bond_gateway: { gateway: gateway } }, "adding gateway", [bond]);
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {bond_gateway: {gateway: gateway}}, "adding gateway", [bond]);
console.log(`account ${this.client.clientAddress} added gateway with ${gateway.mix_host}`);
return result;
} else {
@@ -217,7 +248,7 @@ export default class ValidatorClient {
*/
async unbondGateway(): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, { unbond_gateway: {} })
const result = await this.client.executeContract(this.client.clientAddress, this.contractAddress, {unbond_gateway: {}})
console.log(`account ${this.client.clientAddress} unbonded gateway`);
return result;
} else {
@@ -225,6 +256,14 @@ export default class ValidatorClient {
}
}
async updateStateParams(newParams: StateParams): Promise<ExecuteResult> {
if (this.client instanceof NetClient) {
return await this.client.executeContract(this.client.clientAddress, this.contractAddress, {update_state_params: newParams}, "updating contract state");
} 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
// to it in the case of these methods below.
@@ -306,3 +345,14 @@ export type GatewayOwnershipResponse = {
address: string,
has_gateway: boolean,
}
export type StateParams = {
// 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
minimum_mixnode_bond: string,
minimum_gateway_bond: string,
mixnode_bond_reward_rate: string,
gateway_bond_reward_rate: string,
mixnode_active_set_size: number,
}
+12 -1
View File
@@ -1,5 +1,11 @@
import { SigningCosmWasmClient, SigningCosmWasmClientOptions } from "@cosmjs/cosmwasm-stargate";
import {GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayResponse, PagedResponse} from "./index";
import {
GatewayOwnershipResponse,
MixOwnershipResponse,
PagedGatewayResponse,
PagedResponse,
StateParams
} from "./index";
import { DirectSecp256k1HdWallet } from "@cosmjs/proto-signing";
import { Coin, GasPrice } from "@cosmjs/launchpad";
import { BroadcastTxResponse } from "@cosmjs/stargate"
@@ -14,6 +20,7 @@ export interface INetClient {
getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse>;
ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse>;
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
getStateParams(contractAddress: string): Promise<StateParams>;
executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult>;
instantiate(senderAddress: string, codeId: number, initMsg: Record<string, unknown>, label: string, options?: InstantiateOptions): Promise<InstantiateResult>;
sendTokens(senderAddress: string, recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise<BroadcastTxResponse>;
@@ -77,6 +84,10 @@ export default class NetClient implements INetClient {
return this.cosmClient.getBalance(address, stakeDenom);
}
public getStateParams(contractAddress: string): Promise<StateParams> {
return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } });
}
public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record<string, unknown>, memo?: string, transferAmount?: readonly Coin[]): Promise<ExecuteResult> {
return this.cosmClient.execute(senderAddress, contractAddress, handleMsg, memo, transferAmount);
}
+12 -1
View File
@@ -1,6 +1,12 @@
import {Coin} from "@cosmjs/launchpad";
import {CosmWasmClient} from "@cosmjs/cosmwasm-stargate";
import {GatewayOwnershipResponse, MixOwnershipResponse, PagedGatewayResponse, PagedResponse} from "./index";
import {
GatewayOwnershipResponse,
MixOwnershipResponse,
PagedGatewayResponse,
PagedResponse,
StateParams
} from "./index";
export interface IQueryClient {
getBalance(address: string, stakeDenom: string): Promise<Coin | null>;
@@ -8,6 +14,7 @@ export interface IQueryClient {
getGateways(contractAddress: string, limit: number, start_after?: string): Promise<PagedGatewayResponse>;
ownsMixNode(contractAddress: string, address: string): Promise<MixOwnershipResponse>;
ownsGateway(contractAddress: string, address: string): Promise<GatewayOwnershipResponse>;
getStateParams(contractAddress: string): Promise<StateParams>;
}
/**
@@ -57,4 +64,8 @@ export default class QueryClient implements IQueryClient {
public getBalance(address: string, stakeDenom: string): Promise<Coin | null> {
return this.cosmClient.getBalance(address, stakeDenom);
}
public getStateParams(contractAddress: string): Promise<StateParams> {
return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } });
}
}
+46 -615
View File
@@ -1,20 +1,22 @@
use crate::msg::{HandleMsg, InitMsg, MigrateMsg, QueryMsg};
use crate::queries::{
query_gateways_paged, query_mixnodes_paged, query_owns_gateway, query_owns_mixnode,
};
use crate::state::{config, gateways, gateways_read, State};
use crate::{error::ContractError, state::mixnodes, state::mixnodes_read};
use crate::state::{config, State, StateParams};
use crate::{error::ContractError, queries, transactions};
use cosmwasm_std::{
attr, to_binary, BankMsg, Binary, Coin, Deps, DepsMut, Env, HandleResponse, InitResponse,
MessageInfo, MigrateResponse, StdResult, Uint128,
to_binary, Decimal, Deps, DepsMut, Env, HandleResponse, InitResponse, MessageInfo,
MigrateResponse, QueryResponse, Uint128,
};
use mixnet_contract::{Gateway, GatewayBond, MixNode, MixNodeBond};
/// Constant specifying minimum of coin required to bond a gateway
const GATEWAY_BOND: Uint128 = Uint128(100_000000);
pub const INITIAL_GATEWAY_BOND: Uint128 = Uint128(100_000000);
/// Constant specifying minimum of coin required to bond a mixnode
const MIXNODE_BOND: Uint128 = Uint128(100_000000);
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();
pub const INITIAL_MIXNODE_ACTIVE_SET_SIZE: u32 = 100;
/// Constant specifying denomination of the coin used for bonding
pub const DENOM: &str = "uhal";
@@ -30,7 +32,19 @@ pub fn init(
info: MessageInfo,
_msg: InitMsg,
) -> Result<InitResponse, ContractError> {
let state = State { owner: info.sender };
// 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,
},
};
config(deps.storage).save(&state)?;
Ok(InitResponse::default())
}
@@ -43,201 +57,34 @@ pub fn handle(
msg: HandleMsg,
) -> Result<HandleResponse, ContractError> {
match msg {
HandleMsg::BondMixnode { mix_node } => try_add_mixnode(deps, info, mix_node),
HandleMsg::UnbondMixnode {} => try_remove_mixnode(deps, info, env),
HandleMsg::BondGateway { gateway } => try_add_gateway(deps, info, gateway),
HandleMsg::UnbondGateway {} => try_remove_gateway(deps, info, env),
}
}
fn validate_mixnode_bond(bond: &[Coin]) -> Result<(), ContractError> {
// check if anything was put as bond
if bond.is_empty() {
return Err(ContractError::NoBondFound);
}
if bond.len() > 1 {
// TODO: ask DH what would be an appropriate action here
}
// check that the denomination is correct
if bond[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have at least MIXNODE_BOND coins in our bond
if bond[0].amount < MIXNODE_BOND {
return Err(ContractError::InsufficientMixNodeBond {
received: bond[0].amount.into(),
minimum: GATEWAY_BOND.into(),
});
}
Ok(())
}
pub fn try_add_mixnode(
deps: DepsMut,
info: MessageInfo,
mix_node: MixNode,
) -> Result<HandleResponse, ContractError> {
validate_mixnode_bond(&info.sent_funds)?;
let bond = MixNodeBond::new(info.sent_funds, info.sender.clone(), mix_node);
let sender_bytes = info.sender.as_bytes();
let was_present = mixnodes_read(deps.storage)
.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)?;
Ok(HandleResponse {
messages: vec![],
attributes,
data: None,
})
}
fn try_remove_mixnode(
deps: DepsMut,
info: MessageInfo,
env: Env,
) -> Result<HandleResponse, ContractError> {
// find the bond, return ContractError::MixNodeBondNotFound if it doesn't exist
let mixnode_bond = match mixnodes_read(deps.storage).may_load(info.sender.as_bytes())? {
None => return Err(ContractError::MixNodeBondNotFound {}),
Some(bond) => bond,
};
// send bonded funds back to the bond owner
let messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender.clone(),
amount: mixnode_bond.amount().to_vec(),
}
.into()];
// remove the bond from the list of bonded mixnodes
mixnodes(deps.storage).remove(info.sender.as_bytes());
// log our actions
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
fn validate_gateway_bond(bond: &[Coin]) -> Result<(), ContractError> {
// check if anything was put as bond
if bond.is_empty() {
return Err(ContractError::NoBondFound);
}
if bond.len() > 1 {
// TODO: ask DH what would be an appropriate action here
}
// check that the denomination is correct
if bond[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have at least 100 coins in our bond
if bond[0].amount < GATEWAY_BOND {
return Err(ContractError::InsufficientGatewayBond {
received: bond[0].amount.into(),
minimum: GATEWAY_BOND.into(),
});
}
Ok(())
}
pub(crate) fn try_add_gateway(
deps: DepsMut,
info: MessageInfo,
gateway: Gateway,
) -> Result<HandleResponse, ContractError> {
validate_gateway_bond(&info.sent_funds)?;
let bond = GatewayBond::new(info.sent_funds, info.sender.clone(), gateway);
let sender_bytes = info.sender.as_bytes();
let was_present = gateways_read(deps.storage)
.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)?;
Ok(HandleResponse {
messages: vec![],
attributes,
data: None,
})
}
fn try_remove_gateway(
deps: DepsMut,
info: MessageInfo,
env: Env,
) -> Result<HandleResponse, ContractError> {
let sender_bytes = info.sender.as_bytes();
// find the bond, return ContractError::GatewayBondNotFound if it doesn't exist
let gateway_bond = match gateways_read(deps.storage).may_load(sender_bytes)? {
None => {
return Err(ContractError::GatewayBondNotFound {
account: info.sender,
});
HandleMsg::BondMixnode { mix_node } => transactions::try_add_mixnode(deps, info, mix_node),
HandleMsg::UnbondMixnode {} => transactions::try_remove_mixnode(deps, info, env),
HandleMsg::BondGateway { gateway } => transactions::try_add_gateway(deps, info, gateway),
HandleMsg::UnbondGateway {} => transactions::try_remove_gateway(deps, info, env),
HandleMsg::UpdateStateParams(params) => {
transactions::try_update_state_params(deps, info, params)
}
Some(bond) => bond,
};
// send bonded funds back to the bond owner
let messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender.clone(),
amount: gateway_bond.amount().to_vec(),
}
.into()];
// remove the bond from the list of bonded gateways
gateways(deps.storage).remove(sender_bytes);
// log our actions
let attributes = vec![
attr("action", "unbond"),
attr("address", info.sender),
attr("gateway_bond", gateway_bond),
];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> StdResult<Binary> {
match msg {
pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result<QueryResponse, ContractError> {
let query_res = match msg {
QueryMsg::GetMixNodes { start_after, limit } => {
to_binary(&query_mixnodes_paged(deps, start_after, limit)?)
to_binary(&queries::query_mixnodes_paged(deps, start_after, limit)?)
}
QueryMsg::GetGateways { limit, start_after } => {
to_binary(&query_gateways_paged(deps, start_after, limit)?)
to_binary(&queries::query_gateways_paged(deps, start_after, limit)?)
}
QueryMsg::OwnsMixnode { address } => to_binary(&query_owns_mixnode(deps, address)?),
QueryMsg::OwnsGateway { address } => to_binary(&query_owns_gateway(deps, address)?),
}
QueryMsg::OwnsMixnode { address } => {
to_binary(&queries::query_owns_mixnode(deps, address)?)
}
QueryMsg::OwnsGateway { address } => {
to_binary(&queries::query_owns_gateway(deps, address)?)
}
QueryMsg::StateParams {} => to_binary(&queries::query_state_params(deps)),
};
Ok(query_res?)
}
pub fn migrate(
@@ -252,11 +99,10 @@ pub fn migrate(
#[cfg(test)]
pub mod tests {
use super::*;
use crate::support::tests::helpers;
use crate::support::tests::helpers::*;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary};
use mixnet_contract::{PagedGatewayResponse, PagedResponse};
use mixnet_contract::PagedResponse;
#[test]
fn initialize_contract() {
@@ -287,419 +133,4 @@ pub mod tests {
query_contract_balance(env.contract.address, deps)
);
}
fn good_mixnode_bond() -> Vec<Coin> {
vec![Coin {
denom: DENOM.to_string(),
amount: MIXNODE_BOND,
}]
}
#[test]
fn validating_mixnode_bond() {
// you must send SOME funds
let result = validate_mixnode_bond(&[]);
assert_eq!(result, Err(ContractError::NoBondFound));
// you must send at least 100 coins...
let mut bond = good_mixnode_bond();
bond[0].amount = (MIXNODE_BOND - Uint128(1)).unwrap();
let result = validate_mixnode_bond(&bond);
assert_eq!(
result,
Err(ContractError::InsufficientMixNodeBond {
received: Into::<u128>::into(MIXNODE_BOND) - 1,
minimum: MIXNODE_BOND.into(),
})
);
// more than that is still fine
let mut bond = good_mixnode_bond();
bond[0].amount = MIXNODE_BOND + Uint128(1);
let result = validate_mixnode_bond(&bond);
assert!(result.is_ok());
// it must be sent in the defined denom!
let mut bond = good_mixnode_bond();
bond[0].denom = "baddenom".to_string();
let result = validate_mixnode_bond(&bond);
assert_eq!(result, Err(ContractError::WrongDenom {}));
let mut bond = good_mixnode_bond();
bond[0].denom = "foomp".to_string();
let result = validate_mixnode_bond(&bond);
assert_eq!(result, Err(ContractError::WrongDenom {}));
}
#[test]
fn mixnode_add() {
let mut deps = helpers::init_contract();
// if we don't send enough funds
let insufficient_bond = Into::<u128>::into(MIXNODE_BOND) - 1;
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
let msg = HandleMsg::BondMixnode {
mix_node: helpers::mix_node_fixture(),
};
// we are informed that we didn't send enough funds
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::InsufficientMixNodeBond {
received: insufficient_bond,
minimum: GATEWAY_BOND.into(),
})
);
// no mixnode was inserted into the topology
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&res).unwrap();
assert_eq!(0, page.nodes.len());
// if we send enough funds
let info = mock_info("anyone", &coins(1000_000000, DENOM));
let msg = HandleMsg::BondMixnode {
mix_node: helpers::mix_node_fixture(),
};
// we get back a message telling us everything was OK
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
assert!(handle_response.is_ok());
// we can query topology and the new node is there
let query_response = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&query_response).unwrap();
assert_eq!(1, page.nodes.len());
assert_eq!(&helpers::mix_node_fixture(), page.nodes[0].mix_node());
// if there was already a mixnode bonded by particular user
let info = mock_info("foomper", &good_mixnode_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
let info = mock_info("foomper", &good_mixnode_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get a log message about it (TODO: does it get back to the user?)
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
}
#[test]
fn mixnode_remove() {
let env = mock_env();
let mut deps = mock_dependencies(&[]);
let msg = InitMsg {};
let info = mock_info("creator", &[]);
init(deps.as_mut(), env.clone(), info, msg).unwrap();
// try un-registering when no nodes exist yet
let info = mock_info("anyone", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
// we're told that there is no node for our address
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
// let's add a node owned by bob
helpers::add_mixnode("bob", coins(1000_000000, DENOM), &mut deps);
// attempt to un-register fred's node, which doesn't exist
let info = mock_info("fred", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
// bob's node is still there
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&res).unwrap();
let first_node = &page.nodes[0];
assert_eq!(1, page.nodes.len());
assert_eq!("bob", first_node.owner());
// add a node owned by fred
let fred_bond = good_mixnode_bond();
helpers::add_mixnode("fred", fred_bond.clone(), &mut deps);
// let's make sure we now have 2 nodes:
assert_eq!(2, helpers::get_mix_nodes(&mut deps).len());
// un-register fred's node
let info = mock_info("fred", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
// we should see log messages come back showing an unbond message
let expected_attributes = vec![
attr("action", "unbond"),
attr(
"mixnode_bond",
format!("amount: {} {}, owner: fred", MIXNODE_BOND, DENOM),
),
];
// we should see a funds transfer from the contract back to fred
let expected_messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender,
amount: fred_bond,
}
.into()];
// run the handler and check that we got back the correct results
let expected = HandleResponse {
messages: expected_messages,
attributes: expected_attributes,
data: None,
};
assert_eq!(remove_fred, expected);
// only 1 node now exists, owned by bob:
let mix_node_bonds = helpers::get_mix_nodes(&mut deps);
assert_eq!(1, mix_node_bonds.len());
assert_eq!("bob", mix_node_bonds[0].owner());
}
fn good_gateway_bond() -> Vec<Coin> {
vec![Coin {
denom: DENOM.to_string(),
amount: GATEWAY_BOND,
}]
}
#[test]
fn validating_gateway_bond() {
// you must send SOME funds
let result = validate_gateway_bond(&[]);
assert_eq!(result, Err(ContractError::NoBondFound));
// you must send at least 100 coins...
let mut bond = good_gateway_bond();
bond[0].amount = (GATEWAY_BOND - Uint128(1)).unwrap();
let result = validate_gateway_bond(&bond);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: Into::<u128>::into(GATEWAY_BOND) - 1,
minimum: GATEWAY_BOND.into(),
})
);
// more than that is still fine
let mut bond = good_gateway_bond();
bond[0].amount = GATEWAY_BOND + Uint128(1);
let result = validate_gateway_bond(&bond);
assert!(result.is_ok());
// it must be sent in the defined denom!
let mut bond = good_gateway_bond();
bond[0].denom = "baddenom".to_string();
let result = validate_gateway_bond(&bond);
assert_eq!(result, Err(ContractError::WrongDenom {}));
let mut bond = good_gateway_bond();
bond[0].denom = "foomp".to_string();
let result = validate_gateway_bond(&bond);
assert_eq!(result, Err(ContractError::WrongDenom {}));
}
#[test]
fn gateway_add() {
let mut deps = helpers::init_contract();
// if we fail validation (by say not sending enough funds
let insufficient_bond = Into::<u128>::into(GATEWAY_BOND) - 1;
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we are informed that we didn't send enough funds
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: insufficient_bond,
minimum: GATEWAY_BOND.into(),
})
);
// make sure no gateway was inserted into the topology
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&res).unwrap();
assert_eq!(0, page.nodes.len());
// if we send enough funds
let info = mock_info("anyone", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get back a message telling us everything was OK
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
assert!(handle_response.is_ok());
// we can query topology and the new node is there
let query_response = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
assert_eq!(1, page.nodes.len());
assert_eq!(&helpers::gateway_fixture(), page.nodes[0].gateway());
// if there was already a gateway bonded by particular user
let info = mock_info("foomper", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
let info = mock_info("foomper", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get a log message about it (TODO: does it get back to the user?)
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
// Is that right? Think about this, not sure yet.
}
#[test]
fn gateway_remove() {
let env = mock_env();
let mut deps = mock_dependencies(&[]);
let msg = InitMsg {};
let info = mock_info("creator", &[]);
init(deps.as_mut(), env.clone(), info, msg).unwrap();
// try unbond when no nodes exist yet
let info = mock_info("anyone", &[]);
let msg = HandleMsg::UnbondGateway {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
// we're told that there is no node for our address
assert_eq!(
result,
Err(ContractError::GatewayBondNotFound {
account: "anyone".into()
})
);
// let's add a node owned by bob
helpers::add_gateway("bob", good_gateway_bond(), &mut deps);
// attempt to unbond fred's node, which doesn't exist
let info = mock_info("fred", &[]);
let msg = HandleMsg::UnbondGateway {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::GatewayBondNotFound {
account: "fred".into()
})
);
// bob's node is still there
let nodes = helpers::get_gateways(&mut deps);
assert_eq!(1, nodes.len());
let first_node = &nodes[0];
assert_eq!("bob", first_node.owner());
// add a node owned by fred
let fred_bond = good_gateway_bond();
helpers::add_gateway("fred", fred_bond.clone(), &mut deps);
// let's make sure we now have 2 nodes:
assert_eq!(2, helpers::get_gateways(&mut deps).len());
// unbond fred's node
let info = mock_info("fred", &[]);
let msg = HandleMsg::UnbondGateway {};
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
// we should see log messages come back showing an unbond message
let expected_attributes = vec![
attr("action", "unbond"),
attr("address", "fred"),
attr(
"gateway_bond",
format!("amount: {} {}, owner: fred", GATEWAY_BOND, DENOM),
),
];
// we should see a funds transfer from the contract back to fred
let expected_messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender,
amount: fred_bond,
}
.into()];
// run the handler and check that we got back the correct results
let expected = HandleResponse {
messages: expected_messages,
attributes: expected_attributes,
data: None,
};
assert_eq!(remove_fred, expected);
// only 1 node now exists, owned by bob:
let gateway_bonds = helpers::get_gateways(&mut deps);
assert_eq!(1, gateway_bonds.len());
assert_eq!("bob", gateway_bonds[0].owner());
}
}
+1 -1
View File
@@ -28,7 +28,7 @@ pub enum ContractError {
GatewayBondNotFound { account: HumanAddr },
#[error("Unauthorized")]
Unauthorized {},
Unauthorized,
#[error("Wrong coin denomination, you must send {}", DENOM)]
WrongDenom {},
+1
View File
@@ -4,6 +4,7 @@ pub mod msg;
pub mod queries;
pub mod state;
pub mod support;
pub mod transactions;
#[cfg(target_arch = "wasm32")]
cosmwasm_std::create_entry_points_with_migration!(contract);
+3
View File
@@ -1,3 +1,4 @@
use crate::state::StateParams;
use cosmwasm_std::HumanAddr;
use mixnet_contract::{Gateway, MixNode};
use schemars::JsonSchema;
@@ -13,6 +14,7 @@ pub enum HandleMsg {
UnbondMixnode {},
BondGateway { gateway: Gateway },
UnbondGateway {},
UpdateStateParams(StateParams),
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
@@ -32,6 +34,7 @@ pub enum QueryMsg {
OwnsGateway {
address: HumanAddr,
},
StateParams {},
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
+29 -2
View File
@@ -1,5 +1,5 @@
// settings for pagination
use crate::state::{gateways_read, mixnodes_read, PREFIX_MIXNODES};
use crate::state::{config_read, gateways_read, mixnodes_read, StateParams, PREFIX_MIXNODES};
use cosmwasm_std::Deps;
use cosmwasm_std::HumanAddr;
use cosmwasm_std::Order;
@@ -90,10 +90,17 @@ 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::{gateways, mixnodes};
use crate::state::{config, gateways, mixnodes, State};
use crate::support::tests::helpers;
use cosmwasm_std::Storage;
@@ -389,4 +396,24 @@ mod tests {
let res = query_owns_gateway(deps.as_ref(), "fred".into()).unwrap();
assert!(!res.has_gateway);
}
#[test]
fn query_for_contract_state_works() {
let mut deps = helpers::init_contract();
let dummy_state = State {
owner: "someowner".into(),
params: StateParams {
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,
},
};
config(deps.as_mut().storage).save(&dummy_state).unwrap();
assert_eq!(dummy_state.params, query_state_params(deps.as_ref()))
}
}
+13 -2
View File
@@ -1,4 +1,4 @@
use cosmwasm_std::{HumanAddr, Storage};
use cosmwasm_std::{Decimal, HumanAddr, Storage, Uint128};
use cosmwasm_storage::{
bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton,
Singleton,
@@ -13,7 +13,18 @@ pub static CONFIG_KEY: &[u8] = b"config";
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct State {
pub owner: HumanAddr,
pub owner: HumanAddr, // only the owner account can update state
pub params: StateParams,
}
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, JsonSchema)]
pub struct StateParams {
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_active_set_size: u32,
}
pub fn config(storage: &mut dyn Storage) -> Singleton<State> {
+3 -4
View File
@@ -1,11 +1,12 @@
#[cfg(test)]
pub mod helpers {
use super::*;
use crate::contract::init;
use crate::contract::query;
use crate::contract::try_add_mixnode;
use crate::contract::{init, try_add_gateway};
use crate::contract::DENOM;
use crate::msg::InitMsg;
use crate::msg::QueryMsg;
use crate::transactions::{try_add_gateway, try_add_mixnode};
use cosmwasm_std::coins;
use cosmwasm_std::from_binary;
use cosmwasm_std::testing::mock_dependencies;
@@ -22,8 +23,6 @@ pub mod helpers {
Gateway, GatewayBond, MixNode, MixNodeBond, PagedGatewayResponse, PagedResponse,
};
use crate::contract::DENOM;
pub fn add_mixnode(
pubkey: &str,
stake: Vec<Coin>,
+661
View File
@@ -0,0 +1,661 @@
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 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> {
// check if anything was put as bond
if bond.is_empty() {
return Err(ContractError::NoBondFound);
}
if bond.len() > 1 {
// TODO: ask DH what would be an appropriate action here
}
// check that the denomination is correct
if bond[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have at least MIXNODE_BOND coins in our bond
if bond[0].amount < minimum_bond {
return Err(ContractError::InsufficientMixNodeBond {
received: bond[0].amount.into(),
minimum: minimum_bond.into(),
});
}
Ok(())
}
pub(crate) fn try_add_mixnode(
deps: DepsMut,
info: MessageInfo,
mix_node: MixNode,
) -> Result<HandleResponse, ContractError> {
let minimum_bond = query_state_params(deps.as_ref()).minimum_mixnode_bond;
validate_mixnode_bond(&info.sent_funds, minimum_bond)?;
let bond = MixNodeBond::new(info.sent_funds, info.sender.clone(), mix_node);
let sender_bytes = info.sender.as_bytes();
let was_present = mixnodes_read(deps.storage)
.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)?;
Ok(HandleResponse {
messages: vec![],
attributes,
data: None,
})
}
pub(crate) fn try_remove_mixnode(
deps: DepsMut,
info: MessageInfo,
env: Env,
) -> Result<HandleResponse, ContractError> {
// find the bond, return ContractError::MixNodeBondNotFound if it doesn't exist
let mixnode_bond = match mixnodes_read(deps.storage).may_load(info.sender.as_bytes())? {
None => return Err(ContractError::MixNodeBondNotFound {}),
Some(bond) => bond,
};
// send bonded funds back to the bond owner
let messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender.clone(),
amount: mixnode_bond.amount().to_vec(),
}
.into()];
// remove the bond from the list of bonded mixnodes
mixnodes(deps.storage).remove(info.sender.as_bytes());
// log our actions
let attributes = vec![attr("action", "unbond"), attr("mixnode_bond", mixnode_bond)];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
fn validate_gateway_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> {
// check if anything was put as bond
if bond.is_empty() {
return Err(ContractError::NoBondFound);
}
if bond.len() > 1 {
// TODO: ask DH what would be an appropriate action here
}
// check that the denomination is correct
if bond[0].denom != DENOM {
return Err(ContractError::WrongDenom {});
}
// check that we have at least 100 coins in our bond
if bond[0].amount < minimum_bond {
return Err(ContractError::InsufficientGatewayBond {
received: bond[0].amount.into(),
minimum: minimum_bond.into(),
});
}
Ok(())
}
pub(crate) fn try_add_gateway(
deps: DepsMut,
info: MessageInfo,
gateway: Gateway,
) -> Result<HandleResponse, ContractError> {
let minimum_bond = query_state_params(deps.as_ref()).minimum_gateway_bond;
validate_gateway_bond(&info.sent_funds, minimum_bond)?;
let bond = GatewayBond::new(info.sent_funds, info.sender.clone(), gateway);
let sender_bytes = info.sender.as_bytes();
let was_present = gateways_read(deps.storage)
.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)?;
Ok(HandleResponse {
messages: vec![],
attributes,
data: None,
})
}
pub(crate) fn try_remove_gateway(
deps: DepsMut,
info: MessageInfo,
env: Env,
) -> Result<HandleResponse, ContractError> {
let sender_bytes = info.sender.as_bytes();
// find the bond, return ContractError::GatewayBondNotFound if it doesn't exist
let gateway_bond = match gateways_read(deps.storage).may_load(sender_bytes)? {
None => {
return Err(ContractError::GatewayBondNotFound {
account: info.sender,
});
}
Some(bond) => bond,
};
// send bonded funds back to the bond owner
let messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender.clone(),
amount: gateway_bond.amount().to_vec(),
}
.into()];
// remove the bond from the list of bonded gateways
gateways(deps.storage).remove(sender_bytes);
// log our actions
let attributes = vec![
attr("action", "unbond"),
attr("address", info.sender),
attr("gateway_bond", gateway_bond),
];
Ok(HandleResponse {
messages,
attributes,
data: None,
})
}
pub(crate) fn try_update_state_params(
deps: DepsMut,
info: MessageInfo,
params: StateParams,
) -> Result<HandleResponse, ContractError> {
// 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.
let mut state = config_read(deps.storage).load().unwrap();
// check if this is executed by the owner, if not reject the transaction
if info.sender != state.owner {
return Err(ContractError::Unauthorized);
}
state.params = params;
config(deps.storage).save(&state)?;
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::support::tests::helpers;
use cosmwasm_std::testing::{mock_dependencies, mock_env, mock_info};
use cosmwasm_std::{coins, from_binary, Uint128};
use mixnet_contract::{PagedGatewayResponse, PagedResponse};
fn good_mixnode_bond() -> Vec<Coin> {
vec![Coin {
denom: DENOM.to_string(),
amount: INITIAL_MIXNODE_BOND,
}]
}
#[test]
fn validating_mixnode_bond() {
// you must send SOME funds
let result = validate_mixnode_bond(&[], INITIAL_MIXNODE_BOND);
assert_eq!(result, Err(ContractError::NoBondFound));
// you must send at least 100 coins...
let mut bond = good_mixnode_bond();
bond[0].amount = (INITIAL_MIXNODE_BOND - Uint128(1)).unwrap();
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
assert_eq!(
result,
Err(ContractError::InsufficientMixNodeBond {
received: Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1,
minimum: INITIAL_MIXNODE_BOND.into(),
})
);
// more than that is still fine
let mut bond = good_mixnode_bond();
bond[0].amount = INITIAL_MIXNODE_BOND + Uint128(1);
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
assert!(result.is_ok());
// it must be sent in the defined denom!
let mut bond = good_mixnode_bond();
bond[0].denom = "baddenom".to_string();
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
assert_eq!(result, Err(ContractError::WrongDenom {}));
let mut bond = good_mixnode_bond();
bond[0].denom = "foomp".to_string();
let result = validate_mixnode_bond(&bond, INITIAL_MIXNODE_BOND);
assert_eq!(result, Err(ContractError::WrongDenom {}));
}
#[test]
fn mixnode_add() {
let mut deps = helpers::init_contract();
// if we don't send enough funds
let insufficient_bond = Into::<u128>::into(INITIAL_MIXNODE_BOND) - 1;
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
let msg = HandleMsg::BondMixnode {
mix_node: helpers::mix_node_fixture(),
};
// we are informed that we didn't send enough funds
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::InsufficientMixNodeBond {
received: insufficient_bond,
minimum: INITIAL_GATEWAY_BOND.into(),
})
);
// no mixnode was inserted into the topology
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&res).unwrap();
assert_eq!(0, page.nodes.len());
// if we send enough funds
let info = mock_info("anyone", &coins(1000_000000, DENOM));
let msg = HandleMsg::BondMixnode {
mix_node: helpers::mix_node_fixture(),
};
// we get back a message telling us everything was OK
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
assert!(handle_response.is_ok());
// we can query topology and the new node is there
let query_response = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&query_response).unwrap();
assert_eq!(1, page.nodes.len());
assert_eq!(&helpers::mix_node_fixture(), page.nodes[0].mix_node());
// if there was already a mixnode bonded by particular user
let info = mock_info("foomper", &good_mixnode_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
let info = mock_info("foomper", &good_mixnode_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get a log message about it (TODO: does it get back to the user?)
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
// adding another node from another account, but with the same IP, should fail (or we would have a weird state). Is that right? Think about this, not sure yet.
// if we attempt to register a second node from the same address, should we get an error? It would probably be polite.
}
#[test]
fn mixnode_remove() {
let env = mock_env();
let mut deps = mock_dependencies(&[]);
let msg = InitMsg {};
let info = mock_info("creator", &[]);
init(deps.as_mut(), env.clone(), info, msg).unwrap();
// try un-registering when no nodes exist yet
let info = mock_info("anyone", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
// we're told that there is no node for our address
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
// let's add a node owned by bob
helpers::add_mixnode("bob", coins(1000_000000, DENOM), &mut deps);
// attempt to un-register fred's node, which doesn't exist
let info = mock_info("fred", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(result, Err(ContractError::MixNodeBondNotFound {}));
// bob's node is still there
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetMixNodes {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedResponse = from_binary(&res).unwrap();
let first_node = &page.nodes[0];
assert_eq!(1, page.nodes.len());
assert_eq!("bob", first_node.owner());
// add a node owned by fred
let fred_bond = good_mixnode_bond();
helpers::add_mixnode("fred", fred_bond.clone(), &mut deps);
// let's make sure we now have 2 nodes:
assert_eq!(2, helpers::get_mix_nodes(&mut deps).len());
// un-register fred's node
let info = mock_info("fred", &coins(999_9999, DENOM));
let msg = HandleMsg::UnbondMixnode {};
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
// we should see log messages come back showing an unbond message
let expected_attributes = vec![
attr("action", "unbond"),
attr(
"mixnode_bond",
format!("amount: {} {}, owner: fred", INITIAL_MIXNODE_BOND, DENOM),
),
];
// we should see a funds transfer from the contract back to fred
let expected_messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender,
amount: fred_bond,
}
.into()];
// run the handler and check that we got back the correct results
let expected = HandleResponse {
messages: expected_messages,
attributes: expected_attributes,
data: None,
};
assert_eq!(remove_fred, expected);
// only 1 node now exists, owned by bob:
let mix_node_bonds = helpers::get_mix_nodes(&mut deps);
assert_eq!(1, mix_node_bonds.len());
assert_eq!("bob", mix_node_bonds[0].owner());
}
fn good_gateway_bond() -> Vec<Coin> {
vec![Coin {
denom: DENOM.to_string(),
amount: INITIAL_GATEWAY_BOND,
}]
}
#[test]
fn validating_gateway_bond() {
// you must send SOME funds
let result = validate_gateway_bond(&[], INITIAL_GATEWAY_BOND);
assert_eq!(result, Err(ContractError::NoBondFound));
// you must send at least 100 coins...
let mut bond = good_gateway_bond();
bond[0].amount = (INITIAL_GATEWAY_BOND - Uint128(1)).unwrap();
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1,
minimum: INITIAL_GATEWAY_BOND.into(),
})
);
// more than that is still fine
let mut bond = good_gateway_bond();
bond[0].amount = INITIAL_GATEWAY_BOND + Uint128(1);
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
assert!(result.is_ok());
// it must be sent in the defined denom!
let mut bond = good_gateway_bond();
bond[0].denom = "baddenom".to_string();
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
assert_eq!(result, Err(ContractError::WrongDenom {}));
let mut bond = good_gateway_bond();
bond[0].denom = "foomp".to_string();
let result = validate_gateway_bond(&bond, INITIAL_GATEWAY_BOND);
assert_eq!(result, Err(ContractError::WrongDenom {}));
}
#[test]
fn gateway_add() {
let mut deps = helpers::init_contract();
// if we fail validation (by say not sending enough funds
let insufficient_bond = Into::<u128>::into(INITIAL_GATEWAY_BOND) - 1;
let info = mock_info("anyone", &coins(insufficient_bond, DENOM));
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we are informed that we didn't send enough funds
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::InsufficientGatewayBond {
received: insufficient_bond,
minimum: INITIAL_GATEWAY_BOND.into(),
})
);
// make sure no gateway was inserted into the topology
let res = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&res).unwrap();
assert_eq!(0, page.nodes.len());
// if we send enough funds
let info = mock_info("anyone", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get back a message telling us everything was OK
let handle_response = handle(deps.as_mut(), mock_env(), info, msg);
assert!(handle_response.is_ok());
// we can query topology and the new node is there
let query_response = query(
deps.as_ref(),
mock_env(),
QueryMsg::GetGateways {
start_after: None,
limit: Option::from(2),
},
)
.unwrap();
let page: PagedGatewayResponse = from_binary(&query_response).unwrap();
assert_eq!(1, page.nodes.len());
assert_eq!(&helpers::gateway_fixture(), page.nodes[0].gateway());
// if there was already a gateway bonded by particular user
let info = mock_info("foomper", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", false));
let info = mock_info("foomper", &good_gateway_bond());
let msg = HandleMsg::BondGateway {
gateway: helpers::gateway_fixture(),
};
// we get a log message about it (TODO: does it get back to the user?)
let handle_response = handle(deps.as_mut(), mock_env(), info, msg).unwrap();
assert_eq!(handle_response.attributes[0], attr("overwritten", true));
// adding another node from another account, but with the same IP, should fail (or we would have a weird state).
// Is that right? Think about this, not sure yet.
}
#[test]
fn gateway_remove() {
let env = mock_env();
let mut deps = mock_dependencies(&[]);
let msg = InitMsg {};
let info = mock_info("creator", &[]);
init(deps.as_mut(), env.clone(), info, msg).unwrap();
// try unbond when no nodes exist yet
let info = mock_info("anyone", &[]);
let msg = HandleMsg::UnbondGateway {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
// we're told that there is no node for our address
assert_eq!(
result,
Err(ContractError::GatewayBondNotFound {
account: "anyone".into()
})
);
// let's add a node owned by bob
helpers::add_gateway("bob", good_gateway_bond(), &mut deps);
// attempt to unbond fred's node, which doesn't exist
let info = mock_info("fred", &[]);
let msg = HandleMsg::UnbondGateway {};
let result = handle(deps.as_mut(), mock_env(), info, msg);
assert_eq!(
result,
Err(ContractError::GatewayBondNotFound {
account: "fred".into()
})
);
// bob's node is still there
let nodes = helpers::get_gateways(&mut deps);
assert_eq!(1, nodes.len());
let first_node = &nodes[0];
assert_eq!("bob", first_node.owner());
// add a node owned by fred
let fred_bond = good_gateway_bond();
helpers::add_gateway("fred", fred_bond.clone(), &mut deps);
// let's make sure we now have 2 nodes:
assert_eq!(2, helpers::get_gateways(&mut deps).len());
// unbond fred's node
let info = mock_info("fred", &[]);
let msg = HandleMsg::UnbondGateway {};
let remove_fred = handle(deps.as_mut(), mock_env(), info.clone(), msg).unwrap();
// we should see log messages come back showing an unbond message
let expected_attributes = vec![
attr("action", "unbond"),
attr("address", "fred"),
attr(
"gateway_bond",
format!("amount: {} {}, owner: fred", INITIAL_GATEWAY_BOND, DENOM),
),
];
// we should see a funds transfer from the contract back to fred
let expected_messages = vec![BankMsg::Send {
from_address: env.contract.address,
to_address: info.sender,
amount: fred_bond,
}
.into()];
// run the handler and check that we got back the correct results
let expected = HandleResponse {
messages: expected_messages,
attributes: expected_attributes,
data: None,
};
assert_eq!(remove_fred, expected);
// only 1 node now exists, owned by bob:
let gateway_bonds = helpers::get_gateways(&mut deps);
assert_eq!(1, gateway_bonds.len());
assert_eq!("bob", gateway_bonds[0].owner());
}
#[test]
fn updating_state_params() {
let mut deps = helpers::init_contract();
let new_params = StateParams {
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,
};
// cannot be updated from non-owner account
let info = mock_info("not-the-creator", &[]);
let res = try_update_state_params(deps.as_mut(), info, new_params.clone());
assert_eq!(res, Err(ContractError::Unauthorized));
// but works fine from the creator account
let info = mock_info("creator", &[]);
let res = try_update_state_params(deps.as_mut(), info, new_params.clone());
assert_eq!(res, Ok(HandleResponse::default()));
// and the state is actually updated
let current_state = config_read(deps.as_ref().storage).load().unwrap();
assert_eq!(current_state.params, new_params)
}
}