diff --git a/clients/validator/src/currency.ts b/clients/validator/src/currency.ts index 221db062c4..7c5de65a9d 100644 --- a/clients/validator/src/currency.ts +++ b/clients/validator/src/currency.ts @@ -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; diff --git a/clients/validator/src/index.ts b/clients/validator/src/index.ts index e6538f39f4..3986cb1d3a 100644 --- a/clients/validator/src/index.ts +++ b/clients/validator/src/index.ts @@ -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 { 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 { + 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 { 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 { 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 { 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 { + 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, +} diff --git a/clients/validator/src/net-client.ts b/clients/validator/src/net-client.ts index e9b8e1a34d..7929d8cc6b 100644 --- a/clients/validator/src/net-client.ts +++ b/clients/validator/src/net-client.ts @@ -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; ownsMixNode(contractAddress: string, address: string): Promise; ownsGateway(contractAddress: string, address: string): Promise; + getStateParams(contractAddress: string): Promise; executeContract(senderAddress: string, contractAddress: string, handleMsg: Record, memo?: string, transferAmount?: readonly Coin[]): Promise; instantiate(senderAddress: string, codeId: number, initMsg: Record, label: string, options?: InstantiateOptions): Promise; sendTokens(senderAddress: string, recipientAddress: string, transferAmount: readonly Coin[], memo?: string): Promise; @@ -77,6 +84,10 @@ export default class NetClient implements INetClient { return this.cosmClient.getBalance(address, stakeDenom); } + public getStateParams(contractAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } }); + } + public executeContract(senderAddress: string, contractAddress: string, handleMsg: Record, memo?: string, transferAmount?: readonly Coin[]): Promise { return this.cosmClient.execute(senderAddress, contractAddress, handleMsg, memo, transferAmount); } diff --git a/clients/validator/src/query-client.ts b/clients/validator/src/query-client.ts index 51b6dc82b7..5d032d0876 100644 --- a/clients/validator/src/query-client.ts +++ b/clients/validator/src/query-client.ts @@ -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; @@ -8,6 +14,7 @@ export interface IQueryClient { getGateways(contractAddress: string, limit: number, start_after?: string): Promise; ownsMixNode(contractAddress: string, address: string): Promise; ownsGateway(contractAddress: string, address: string): Promise; + getStateParams(contractAddress: string): Promise; } /** @@ -57,4 +64,8 @@ export default class QueryClient implements IQueryClient { public getBalance(address: string, stakeDenom: string): Promise { return this.cosmClient.getBalance(address, stakeDenom); } + + public getStateParams(contractAddress: string): Promise { + return this.cosmClient.queryContractSmart(contractAddress, { state_params: { } }); + } } diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index a6fd4cc6b7..7801c1d46f 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -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 { - 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 { 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 { - 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 { - // 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 { - 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 { - 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 { - match msg { +pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result { + 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 { - 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::::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::::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 { - 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::::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::::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()); - } } diff --git a/contracts/mixnet/src/error.rs b/contracts/mixnet/src/error.rs index c2047f9d25..a128253ed2 100644 --- a/contracts/mixnet/src/error.rs +++ b/contracts/mixnet/src/error.rs @@ -28,7 +28,7 @@ pub enum ContractError { GatewayBondNotFound { account: HumanAddr }, #[error("Unauthorized")] - Unauthorized {}, + Unauthorized, #[error("Wrong coin denomination, you must send {}", DENOM)] WrongDenom {}, diff --git a/contracts/mixnet/src/lib.rs b/contracts/mixnet/src/lib.rs index 780f8c9852..cca2400889 100644 --- a/contracts/mixnet/src/lib.rs +++ b/contracts/mixnet/src/lib.rs @@ -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); diff --git a/contracts/mixnet/src/msg.rs b/contracts/mixnet/src/msg.rs index 1518dc6118..0ab0fa1a62 100644 --- a/contracts/mixnet/src/msg.rs +++ b/contracts/mixnet/src/msg.rs @@ -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)] diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 8fcd61d6fb..b3a8b1d901 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -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())) + } } diff --git a/contracts/mixnet/src/state.rs b/contracts/mixnet/src/state.rs index 2eb817aa7d..64682a46a7 100644 --- a/contracts/mixnet/src/state.rs +++ b/contracts/mixnet/src/state.rs @@ -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 { diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index 30bb81a03a..13d05d58b3 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -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, diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs new file mode 100644 index 0000000000..467bbb1f5c --- /dev/null +++ b/contracts/mixnet/src/transactions.rs @@ -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 { + 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 { + // 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 { + 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 { + 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 { + // 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 { + 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::::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::::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 { + 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::::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::::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) + } +}