diff --git a/common/mixnet-contract/src/gateway.rs b/common/mixnet-contract/src/gateway.rs index 7b9c816b4a..a6b6d91e7a 100644 --- a/common/mixnet-contract/src/gateway.rs +++ b/common/mixnet-contract/src/gateway.rs @@ -7,6 +7,8 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Display; +use crate::current_block_height; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct Gateway { pub host: String, @@ -24,15 +26,18 @@ pub struct GatewayBond { pub bond_amount: Coin, pub total_delegation: Coin, pub owner: Addr, + #[serde(default = "current_block_height")] + pub block_height: u64, pub gateway: Gateway, } impl GatewayBond { - pub fn new(bond_amount: Coin, owner: Addr, gateway: Gateway) -> Self { + pub fn new(bond_amount: Coin, owner: Addr, block_height: u64, gateway: Gateway) -> Self { GatewayBond { total_delegation: coin(0, &bond_amount.denom), bond_amount, owner, + block_height, gateway, } } diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index 73a3769e01..a3ad1a7fc2 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -16,3 +16,10 @@ pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayRe pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; pub use msg::{ExecuteMsg, InstantiateMsg, MigrateMsg, QueryMsg}; pub use types::{IdentityKey, IdentityKeyRef, LayerDistribution, SphinxKey, StateParams}; + +use std::sync::atomic::{AtomicU64, Ordering}; +pub static CURRENT_BLOCK_HEIGHT: AtomicU64 = AtomicU64::new(0); + +pub fn current_block_height() -> u64 { + CURRENT_BLOCK_HEIGHT.load(Ordering::Relaxed) +} diff --git a/common/mixnet-contract/src/mixnode.rs b/common/mixnet-contract/src/mixnode.rs index 9870516b93..7c20186b67 100644 --- a/common/mixnet-contract/src/mixnode.rs +++ b/common/mixnet-contract/src/mixnode.rs @@ -8,6 +8,8 @@ use serde::{Deserialize, Serialize}; use serde_repr::{Deserialize_repr, Serialize_repr}; use std::fmt::Display; +use crate::current_block_height; + #[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct MixNode { pub host: String, @@ -35,16 +37,25 @@ pub struct MixNodeBond { pub total_delegation: Coin, pub owner: Addr, pub layer: Layer, + #[serde(default = "current_block_height")] + pub block_height: u64, pub mix_node: MixNode, } impl MixNodeBond { - pub fn new(bond_amount: Coin, owner: Addr, layer: Layer, mix_node: MixNode) -> Self { + pub fn new( + bond_amount: Coin, + owner: Addr, + layer: Layer, + block_height: u64, + mix_node: MixNode, + ) -> Self { MixNodeBond { total_delegation: coin(0, &bond_amount.denom), bond_amount, owner, layer, + block_height, mix_node, } } diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 257f14324b..7dc7843723 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -94,18 +94,22 @@ pub fn execute( msg: ExecuteMsg, ) -> Result { match msg { - ExecuteMsg::BondMixnode { mix_node } => transactions::try_add_mixnode(deps, info, mix_node), + ExecuteMsg::BondMixnode { mix_node } => { + transactions::try_add_mixnode(deps, env, info, mix_node) + } ExecuteMsg::UnbondMixnode {} => transactions::try_remove_mixnode(deps, info), - ExecuteMsg::BondGateway { gateway } => transactions::try_add_gateway(deps, info, gateway), + ExecuteMsg::BondGateway { gateway } => { + transactions::try_add_gateway(deps, env, info, gateway) + } ExecuteMsg::UnbondGateway {} => transactions::try_remove_gateway(deps, info), ExecuteMsg::UpdateStateParams(params) => { transactions::try_update_state_params(deps, info, params) } ExecuteMsg::RewardMixnode { identity, uptime } => { - transactions::try_reward_mixnode(deps, info, identity, uptime) + transactions::try_reward_mixnode(deps, env, info, identity, uptime) } ExecuteMsg::RewardGateway { identity, uptime } => { - transactions::try_reward_gateway(deps, info, identity, uptime) + transactions::try_reward_gateway(deps, env, info, identity, uptime) } ExecuteMsg::DelegateToMixnode { mix_identity } => { transactions::try_delegate_to_mixnode(deps, env, info, mix_identity) @@ -201,7 +205,33 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Result { +pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result { + use crate::storage::{gateways, gateways_read, mixnodes, mixnodes_read}; + use cosmwasm_std::{Order, StdResult}; + use mixnet_contract::CURRENT_BLOCK_HEIGHT; + use mixnet_contract::{GatewayBond, MixNodeBond}; + use std::sync::atomic::Ordering; + + CURRENT_BLOCK_HEIGHT.store(env.block.height, Ordering::Relaxed); + + let bonds = mixnodes_read(deps.storage) + .range(None, None, Order::Ascending) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + for bond in bonds { + mixnodes(deps.storage).save(bond.identity().as_bytes(), &bond)?; + } + + let bonds = gateways_read(deps.storage) + .range(None, None, Order::Ascending) + .map(|res| res.map(|item| item.1)) + .collect::>>()?; + + for bond in bonds { + gateways(deps.storage).save(bond.identity().as_bytes(), &bond)?; + } + Ok(Default::default()) } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 535b3beb71..a9643a372e 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -290,7 +290,7 @@ mod tests { good_gateway_bond, good_mixnode_bond, raw_delegation_fixture, }; use crate::transactions; - use cosmwasm_std::testing::mock_info; + use cosmwasm_std::testing::{mock_env, mock_info}; use cosmwasm_std::{Addr, Storage}; use mixnet_contract::{Gateway, MixNode, RawDelegationData}; @@ -546,8 +546,13 @@ mod tests { identity_key: "bobsnode".into(), ..helpers::mix_node_fixture() }; - transactions::try_add_mixnode(deps.as_mut(), mock_info("bob", &good_mixnode_bond()), node) - .unwrap(); + transactions::try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info("bob", &good_mixnode_bond()), + node, + ) + .unwrap(); let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap(); assert!(!res.has_node); @@ -557,8 +562,13 @@ mod tests { identity_key: "fredsnode".into(), ..helpers::mix_node_fixture() }; - transactions::try_add_mixnode(deps.as_mut(), mock_info("fred", &good_mixnode_bond()), node) - .unwrap(); + transactions::try_add_mixnode( + deps.as_mut(), + mock_env(), + mock_info("fred", &good_mixnode_bond()), + node, + ) + .unwrap(); let res = query_owns_mixnode(deps.as_ref(), Addr::unchecked("fred")).unwrap(); assert!(res.has_node); @@ -583,8 +593,13 @@ mod tests { identity_key: "bobsnode".into(), ..helpers::gateway_fixture() }; - transactions::try_add_gateway(deps.as_mut(), mock_info("bob", &good_gateway_bond()), node) - .unwrap(); + transactions::try_add_gateway( + deps.as_mut(), + mock_env(), + mock_info("bob", &good_gateway_bond()), + node, + ) + .unwrap(); let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap(); assert!(!res.has_gateway); @@ -594,8 +609,13 @@ mod tests { identity_key: "fredsnode".into(), ..helpers::gateway_fixture() }; - transactions::try_add_gateway(deps.as_mut(), mock_info("fred", &good_gateway_bond()), node) - .unwrap(); + transactions::try_add_gateway( + deps.as_mut(), + mock_env(), + mock_info("fred", &good_gateway_bond()), + node, + ) + .unwrap(); let res = query_owns_gateway(deps.as_ref(), Addr::unchecked("fred")).unwrap(); assert!(res.has_gateway); diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index 3de3b08c9b..c038691389 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -3,6 +3,7 @@ use crate::queries; use crate::state::State; +use crate::transactions::MINIMUM_BLOCK_AGE_FOR_REWARDING; use cosmwasm_std::{Decimal, Order, StdResult, Storage, Uint128}; use cosmwasm_storage::{ bucket, bucket_read, singleton, singleton_read, Bucket, ReadonlyBucket, ReadonlySingleton, @@ -164,6 +165,7 @@ pub(crate) fn increase_mix_delegated_stakes( storage: &mut dyn Storage, mix_identity: IdentityKeyRef, scaled_reward_rate: Decimal, + reward_blockstamp: u64, ) -> StdResult { let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; @@ -193,11 +195,15 @@ pub(crate) fn increase_mix_delegated_stakes( ); // and for each of them increase the stake proportionally to the reward + // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created + // since they delegated for (delegator_address, mut delegation) in delegations_chunk.into_iter() { - let reward = delegation.amount * scaled_reward_rate; - delegation.amount += reward; - total_rewarded += reward; - mix_delegations(storage, mix_identity).save(&delegator_address, &delegation)?; + if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= reward_blockstamp { + let reward = delegation.amount * scaled_reward_rate; + delegation.amount += reward; + total_rewarded += reward; + mix_delegations(storage, mix_identity).save(&delegator_address, &delegation)?; + } } } @@ -208,6 +214,7 @@ pub(crate) fn increase_gateway_delegated_stakes( storage: &mut dyn Storage, gateway_identity: IdentityKeyRef, scaled_reward_rate: Decimal, + reward_blockstamp: u64, ) -> StdResult { let chunk_size = queries::DELEGATION_PAGE_MAX_LIMIT as usize; @@ -237,11 +244,16 @@ pub(crate) fn increase_gateway_delegated_stakes( ); // and for each of them increase the stake proportionally to the reward + // if at least `MINIMUM_BLOCK_AGE_FOR_REWARDING` blocks have been created + // since they delegated for (delegator_address, mut delegation) in delegations_chunk.into_iter() { - let reward = delegation.amount * scaled_reward_rate; - delegation.amount += reward; - total_rewarded += reward; - gateway_delegations(storage, gateway_identity).save(&delegator_address, &delegation)?; + if delegation.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= reward_blockstamp { + let reward = delegation.amount * scaled_reward_rate; + delegation.amount += reward; + total_rewarded += reward; + gateway_delegations(storage, gateway_identity) + .save(&delegator_address, &delegation)?; + } } } @@ -435,6 +447,7 @@ mod tests { total_delegation: coin(0, DENOM), owner: node_owner.clone(), layer: Layer::One, + block_height: 12_345, mix_node: MixNode { identity_key: node_identity.clone(), ..mix_node_fixture() @@ -468,6 +481,7 @@ mod tests { bond_amount: coin(bond_value, DENOM), total_delegation: coin(0, DENOM), owner: node_owner.clone(), + block_height: 12_345, gateway: Gateway { identity_key: node_identity.clone(), ..gateway_fixture() @@ -498,9 +512,13 @@ mod tests { // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); - let total_increase = - increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) - .unwrap(); + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + 42, + ) + .unwrap(); // there was no increase assert!(total_increase.is_zero()); @@ -518,6 +536,7 @@ mod tests { fn when_there_is_a_single_delegation() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -526,13 +545,17 @@ mod tests { mix_delegations(&mut deps.storage, &node_identity) .save( delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), 42), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), ) .unwrap(); - let total_increase = - increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) - .unwrap(); + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, + ) + .unwrap(); assert_eq!(Uint128(1), total_increase); @@ -545,10 +568,67 @@ mod tests { ) } + #[test] + fn when_there_is_a_single_delegation_depending_on_blockstamp() { + let mut deps = mock_dependencies(&[]); + let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; + + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + let delegator_address = Addr::unchecked("bob"); + mix_delegations(&mut deps.storage, &node_identity) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) + .unwrap(); + + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1, + ) + .unwrap(); + + // there was no increase + assert!(total_increase.is_zero()); + + // amount is not incremented + assert_eq!( + RawDelegationData::new(1000u128.into(), delegation_blockstamp), + mix_delegations_read(&mut deps.storage, &node_identity) + .load(delegator_address.as_bytes()) + .unwrap() + ); + + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING, + ) + .unwrap(); + + // there is an increase now, that the lock period has passed + assert_eq!(Uint128(1), total_increase); + + // amount is incremented + assert_eq!( + RawDelegationData::new(1001u128.into(), delegation_blockstamp), + mix_delegations_read(&mut deps.storage, &node_identity) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + #[test] fn when_there_are_multiple_delegations() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -556,13 +636,20 @@ mod tests { for i in 0..100 { let delegator_address = Addr::unchecked(format!("address{}", i)); mix_delegations(&mut deps.storage, &node_identity) - .save(delegator_address.as_bytes(), &raw_delegation_fixture(1000)) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) .unwrap(); } - let total_increase = - increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) - .unwrap(); + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, + ) + .unwrap(); assert_eq!(Uint128(100), total_increase); @@ -581,6 +668,7 @@ mod tests { fn when_there_are_more_delegations_than_page_size() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -588,13 +676,20 @@ mod tests { for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { let delegator_address = Addr::unchecked(format!("address{}", i)); mix_delegations(&mut deps.storage, &node_identity) - .save(delegator_address.as_bytes(), &raw_delegation_fixture(1000)) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) .unwrap(); } - let total_increase = - increase_mix_delegated_stakes(&mut deps.storage, node_identity.as_ref(), reward) - .unwrap(); + let total_increase = increase_mix_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, + ) + .unwrap(); assert_eq!( Uint128(queries::DELEGATION_PAGE_MAX_LIMIT as u128 * 10), @@ -696,6 +791,7 @@ mod tests { &mut deps.storage, node_identity.as_ref(), reward, + 42, ) .unwrap(); @@ -715,6 +811,7 @@ mod tests { fn when_there_is_a_single_delegation() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -723,7 +820,7 @@ mod tests { gateway_delegations(&mut deps.storage, &node_identity) .save( delegator_address.as_bytes(), - &RawDelegationData::new(1000u128.into(), 42), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), ) .unwrap(); @@ -731,6 +828,7 @@ mod tests { &mut deps.storage, node_identity.as_ref(), reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, ) .unwrap(); @@ -745,10 +843,67 @@ mod tests { ) } + #[test] + fn when_there_is_a_single_delegation_depending_on_blockstamp() { + let mut deps = mock_dependencies(&[]); + let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; + + // 0.001 + let reward = Decimal::from_ratio(1u128, 1000u128); + + let delegator_address = Addr::unchecked("bob"); + gateway_delegations(&mut deps.storage, &node_identity) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) + .unwrap(); + + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING - 1, + ) + .unwrap(); + + // there was no increase + assert!(total_increase.is_zero()); + + // amount is not incremented + assert_eq!( + RawDelegationData::new(1000u128.into(), delegation_blockstamp), + gateway_delegations_read(&mut deps.storage, &node_identity) + .load(delegator_address.as_bytes()) + .unwrap() + ); + + let total_increase = increase_gateway_delegated_stakes( + &mut deps.storage, + node_identity.as_ref(), + reward, + delegation_blockstamp + MINIMUM_BLOCK_AGE_FOR_REWARDING, + ) + .unwrap(); + + // there is an increase now, that the lock period has passed + assert_eq!(Uint128(1), total_increase); + + // amount is incremented + assert_eq!( + RawDelegationData::new(1001u128.into(), delegation_blockstamp), + gateway_delegations_read(&mut deps.storage, &node_identity) + .load(delegator_address.as_bytes()) + .unwrap() + ) + } + #[test] fn when_there_are_multiple_delegations() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -756,7 +911,10 @@ mod tests { for i in 0..100 { let delegator_address = Addr::unchecked(format!("address{}", i)); gateway_delegations(&mut deps.storage, &node_identity) - .save(delegator_address.as_bytes(), &raw_delegation_fixture(1000)) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) .unwrap(); } @@ -764,6 +922,7 @@ mod tests { &mut deps.storage, node_identity.as_ref(), reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, ) .unwrap(); @@ -784,6 +943,7 @@ mod tests { fn when_there_are_more_delegations_than_page_size() { let mut deps = mock_dependencies(&[]); let node_identity: IdentityKey = "nodeidentity".into(); + let delegation_blockstamp = 42; // 0.001 let reward = Decimal::from_ratio(1u128, 1000u128); @@ -791,7 +951,10 @@ mod tests { for i in 0..queries::DELEGATION_PAGE_MAX_LIMIT * 10 { let delegator_address = Addr::unchecked(format!("address{}", i)); gateway_delegations(&mut deps.storage, &node_identity) - .save(delegator_address.as_bytes(), &raw_delegation_fixture(1000)) + .save( + delegator_address.as_bytes(), + &RawDelegationData::new(1000u128.into(), delegation_blockstamp), + ) .unwrap(); } @@ -799,6 +962,7 @@ mod tests { &mut deps.storage, node_identity.as_ref(), reward, + delegation_blockstamp + 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING, ) .unwrap(); diff --git a/contracts/mixnet/src/support/tests.rs b/contracts/mixnet/src/support/tests.rs index fd1ac7fade..70b761e0fd 100644 --- a/contracts/mixnet/src/support/tests.rs +++ b/contracts/mixnet/src/support/tests.rs @@ -31,6 +31,7 @@ pub mod helpers { let key = format!("{}mixnode", sender); try_add_mixnode( deps.as_mut(), + mock_env(), info, MixNode { identity_key: key.clone(), @@ -67,6 +68,7 @@ pub mod helpers { let key = format!("{}gateway", sender); try_add_gateway( deps.as_mut(), + mock_env(), info, Gateway { identity_key: key.clone(), @@ -129,6 +131,7 @@ pub mod helpers { coin(50, DENOM), Addr::unchecked("foo"), Layer::One, + 12_345, mix_node, ) } @@ -156,7 +159,7 @@ pub mod helpers { identity_key: "identity".to_string(), version: "0.10.0".to_string(), }; - GatewayBond::new(coin(50, DENOM), Addr::unchecked("foo"), gateway) + GatewayBond::new(coin(50, DENOM), Addr::unchecked("foo"), 12_345, gateway) } pub fn raw_delegation_fixture(amount: u128) -> RawDelegationData { diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 7260f751aa..2e753ebf06 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -16,6 +16,7 @@ use mixnet_contract::{ }; const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; +pub(crate) const MINIMUM_BLOCK_AGE_FOR_REWARDING: u64 = 17280; // Looks for the total amount of delegations towards a particular node. // This function is used only in very specific circumstances: @@ -87,6 +88,7 @@ fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), Con pub(crate) fn try_add_mixnode( deps: DepsMut, + env: Env, info: MessageInfo, mix_node: MixNode, ) -> Result { @@ -126,7 +128,13 @@ pub(crate) fn try_add_mixnode( let layer_distribution = queries::query_layer_distribution(deps.as_ref()); let layer = layer_distribution.choose_with_fewest(); - let mut bond = MixNodeBond::new(info.funds[0].clone(), info.sender.clone(), layer, mix_node); + let mut bond = MixNodeBond::new( + info.funds[0].clone(), + info.sender.clone(), + layer, + env.block.height, + mix_node, + ); // this might potentially require more gas if a significant number of delegations was there let delegations_bucket = mix_delegations_read(deps.storage, &bond.mix_node.identity_key); @@ -216,6 +224,7 @@ fn validate_gateway_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), Con pub(crate) fn try_add_gateway( deps: DepsMut, + env: Env, info: MessageInfo, gateway: Gateway, ) -> Result { @@ -252,7 +261,12 @@ pub(crate) fn try_add_gateway( let minimum_bond = read_state_params(deps.storage).minimum_gateway_bond; validate_gateway_bond(&info.funds, minimum_bond)?; - let mut bond = GatewayBond::new(info.funds[0].clone(), info.sender.clone(), gateway); + let mut bond = GatewayBond::new( + info.funds[0].clone(), + info.sender.clone(), + env.block.height, + gateway, + ); // this might potentially require more gas if a significant number of delegations was there let delegations_bucket = gateway_delegations_read(deps.storage, &bond.gateway.identity_key); @@ -391,6 +405,7 @@ pub(crate) fn try_update_state_params( pub(crate) fn try_reward_mixnode( deps: DepsMut, + env: Env, info: MessageInfo, mix_identity: IdentityKey, uptime: u32, @@ -431,14 +446,22 @@ pub(crate) fn try_reward_mixnode( let bond_scaled_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; let delegation_scaled_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; - let node_reward = current_bond.bond_amount.amount * bond_scaled_reward_rate; - let total_delegation_reward = - increase_mix_delegated_stakes(deps.storage, &mix_identity, delegation_scaled_reward_rate)?; + let mut node_reward = Uint128(0); + let total_delegation_reward = increase_mix_delegated_stakes( + deps.storage, + &mix_identity, + delegation_scaled_reward_rate, + env.block.height, + )?; // update current bond with the reward given to the node and the delegators - current_bond.bond_amount.amount += node_reward; - current_bond.total_delegation.amount += total_delegation_reward; - mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; + // if it has been bonded for long enough + if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height { + node_reward = current_bond.bond_amount.amount * bond_scaled_reward_rate; + current_bond.bond_amount.amount += node_reward; + current_bond.total_delegation.amount += total_delegation_reward; + mixnodes(deps.storage).save(mix_identity.as_bytes(), ¤t_bond)?; + } Ok(Response { submessages: vec![], @@ -453,6 +476,7 @@ pub(crate) fn try_reward_mixnode( pub(crate) fn try_reward_gateway( deps: DepsMut, + env: Env, info: MessageInfo, gateway_identity: IdentityKey, uptime: u32, @@ -493,17 +517,22 @@ pub(crate) fn try_reward_gateway( let scaled_bond_reward_rate = scale_reward_by_uptime(bond_reward_rate, uptime)?; let scaled_delegation_reward_rate = scale_reward_by_uptime(delegation_reward_rate, uptime)?; - let node_reward = current_bond.bond_amount.amount * scaled_bond_reward_rate; + let mut node_reward = Uint128(0); let total_delegation_reward = increase_gateway_delegated_stakes( deps.storage, &gateway_identity, scaled_delegation_reward_rate, + env.block.height, )?; // update current bond with the reward given to the node and the delegators - current_bond.bond_amount.amount += node_reward; - current_bond.total_delegation.amount += total_delegation_reward; - gateways(deps.storage).save(gateway_identity.as_bytes(), ¤t_bond)?; + // if it has been bonded for long enough + if current_bond.block_height + MINIMUM_BLOCK_AGE_FOR_REWARDING <= env.block.height { + node_reward = current_bond.bond_amount.amount * scaled_bond_reward_rate; + current_bond.bond_amount.amount += node_reward; + current_bond.total_delegation.amount += total_delegation_reward; + gateways(deps.storage).save(gateway_identity.as_bytes(), ¤t_bond)?; + } Ok(Response { submessages: vec![], @@ -1082,6 +1111,7 @@ pub mod tests { let info = mock_info("fred", &good_mixnode_bond()); try_add_mixnode( deps.as_mut(), + mock_env(), info, MixNode { identity_key: "fredsmixnode".to_string(), @@ -1481,6 +1511,7 @@ pub mod tests { let info = mock_info("fred", &good_gateway_bond()); try_add_gateway( deps.as_mut(), + mock_env(), info, Gateway { identity_key: "fredsgateway".into(), @@ -1773,6 +1804,7 @@ pub mod tests { #[test] fn rewarding_mixnode() { let mut deps = helpers::init_contract(); + let mut env = mock_env(); let current_state = config(deps.as_mut().storage).load().unwrap(); let network_monitor_address = current_state.network_monitor_address; @@ -1781,12 +1813,13 @@ pub mod tests { // errors out if executed by somebody else than network monitor let info = mock_info("not-the-monitor", &[]); - let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 100); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100); assert_eq!(res, Err(ContractError::Unauthorized)); // returns bond not found attribute if the target owner hasn't bonded any mixnodes let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); assert_eq!(vec![attr("result", "bond not found")], res.attributes); let initial_bond = 100_000000; @@ -1796,6 +1829,7 @@ pub mod tests { total_delegation: coin(initial_delegation, DENOM), owner: node_owner.clone(), layer: Layer::One, + block_height: env.block.height, mix_node: MixNode { identity_key: node_identity.clone(), ..mix_node_fixture() @@ -1807,9 +1841,14 @@ pub mod tests { .unwrap(); mix_delegations(&mut deps.storage, &node_identity) - .save(b"delegator", &raw_delegation_fixture(initial_delegation)) + .save( + b"delegator", + &RawDelegationData::new(initial_delegation.into(), env.block.height), + ) .unwrap(); + env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; + let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); let delegation_reward_rate = read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); @@ -1822,7 +1861,8 @@ pub mod tests { let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); assert_eq!( expected_bond, @@ -1850,7 +1890,136 @@ pub mod tests { let expected_delegation = expected_delegation_reward + expected_delegation; let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 20) + .unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + } + + #[test] + fn rewarding_mixnode_blockstamp_based() { + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let network_monitor_address = current_state.network_monitor_address; + + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + let initial_bond = 100_000000; + let initial_delegation = 200_000000; + let mixnode_bond = MixNodeBond { + bond_amount: coin(initial_bond, DENOM), + total_delegation: coin(initial_delegation, DENOM), + owner: node_owner.clone(), + layer: Layer::One, + block_height: env.block.height, + mix_node: MixNode { + identity_key: node_identity.clone(), + ..mix_node_fixture() + }, + }; + + mixnodes(deps.as_mut().storage) + .save(node_identity.as_bytes(), &mixnode_bond) + .unwrap(); + + // delegation happens later, but not later enough + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + + mix_delegations(&mut deps.storage, &node_identity) + .save( + b"delegator", + &RawDelegationData::new(initial_delegation.into(), env.block.height), + ) + .unwrap(); + + let bond_reward_rate = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); + let delegation_reward_rate = + read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); + let scaled_bond_reward = scale_reward_by_uptime(bond_reward_rate, 100).unwrap(); + let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward_rate, 100).unwrap(); + + // no reward is due + let expected_bond_reward = Uint128(0); + let expected_delegation_reward = Uint128(0); + let expected_bond = expected_bond_reward + Uint128(initial_bond); + let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + + // reward can happen now, but only for bonded node + env.block.height += 1; + let expected_bond_reward = expected_bond * scaled_bond_reward; + let expected_delegation_reward = Uint128(0); + let expected_bond = expected_bond_reward + expected_bond; + let expected_delegation = expected_delegation_reward + expected_delegation; + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); + + assert_eq!( + expected_bond, + read_mixnode_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_mixnode_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + + // reward happens now, both for node owner and delegators + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + let expected_bond_reward = expected_bond * scaled_bond_reward; + let expected_delegation_reward = expected_delegation * scaled_delegation_reward; + let expected_bond = expected_bond_reward + expected_bond; + let expected_delegation = expected_delegation_reward + expected_delegation; + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_mixnode(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); assert_eq!( expected_bond, @@ -1873,6 +2042,7 @@ pub mod tests { #[test] fn rewarding_gateway() { let mut deps = helpers::init_contract(); + let mut env = mock_env(); let current_state = config(deps.as_mut().storage).load().unwrap(); let network_monitor_address = current_state.network_monitor_address; @@ -1881,12 +2051,13 @@ pub mod tests { // errors out if executed by somebody else than network monitor let info = mock_info("not-the-monitor", &[]); - let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 100); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100); assert_eq!(res, Err(ContractError::Unauthorized)); // returns bond not found attribute if the target owner hasn't bonded any gateways let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); assert_eq!(vec![attr("result", "bond not found")], res.attributes); let initial_bond = 100_000000; @@ -1895,6 +2066,7 @@ pub mod tests { bond_amount: coin(initial_bond, DENOM), total_delegation: coin(initial_delegation, DENOM), owner: node_owner.clone(), + block_height: env.block.height, gateway: Gateway { identity_key: node_identity.clone(), ..gateway_fixture() @@ -1909,6 +2081,8 @@ pub mod tests { .save(b"delegator", &raw_delegation_fixture(initial_delegation)) .unwrap(); + env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; + let bond_reward_rate = read_gateway_epoch_bond_reward_rate(deps.as_ref().storage); let delegation_reward_rate = read_gateway_epoch_delegation_reward_rate(deps.as_ref().storage); @@ -1921,7 +2095,8 @@ pub mod tests { let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 100).unwrap(); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); assert_eq!( expected_bond, @@ -1950,7 +2125,8 @@ pub mod tests { let expected_delegation = expected_delegation_reward + expected_delegation; let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, node_identity.clone(), 20).unwrap(); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 20) + .unwrap(); assert_eq!( expected_bond, @@ -1971,6 +2147,133 @@ pub mod tests { ); } + #[test] + fn rewarding_gateway_blockstamp_based() { + let mut deps = helpers::init_contract(); + let mut env = mock_env(); + let current_state = config(deps.as_mut().storage).load().unwrap(); + let network_monitor_address = current_state.network_monitor_address; + + let node_owner: Addr = Addr::unchecked("node-owner"); + let node_identity: IdentityKey = "nodeidentity".into(); + + let initial_bond = 100_000000; + let initial_delegation = 200_000000; + let gateway_bond = GatewayBond { + bond_amount: coin(initial_bond, DENOM), + total_delegation: coin(initial_delegation, DENOM), + owner: node_owner.clone(), + block_height: env.block.height, + gateway: Gateway { + identity_key: node_identity.clone(), + ..gateway_fixture() + }, + }; + + gateways(deps.as_mut().storage) + .save(node_identity.as_bytes(), &gateway_bond) + .unwrap(); + + // delegation happens later, but not later enough + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + + gateway_delegations(&mut deps.storage, &node_identity) + .save( + b"delegator", + &RawDelegationData::new(initial_delegation.into(), env.block.height), + ) + .unwrap(); + + let bond_reward_rate = read_gateway_epoch_bond_reward_rate(deps.as_ref().storage); + let delegation_reward_rate = + read_gateway_epoch_delegation_reward_rate(deps.as_ref().storage); + let scaled_bond_reward = scale_reward_by_uptime(bond_reward_rate, 100).unwrap(); + let scaled_delegation_reward = scale_reward_by_uptime(delegation_reward_rate, 100).unwrap(); + + // no reward is due + let expected_bond_reward = Uint128(0); + let expected_delegation_reward = Uint128(0); + let expected_bond = expected_bond_reward + Uint128(initial_bond); + let expected_delegation = expected_delegation_reward + Uint128(initial_delegation); + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_gateway_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + + // reward can happen now, but only for bonded node + env.block.height += 1; + let expected_bond_reward = expected_bond * scaled_bond_reward; + let expected_delegation_reward = Uint128(0); + let expected_bond = expected_bond_reward + expected_bond; + let expected_delegation = expected_delegation_reward + expected_delegation; + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_gateway_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + + // reward happens now, both for node owner and delegators + env.block.height += MINIMUM_BLOCK_AGE_FOR_REWARDING - 1; + let expected_bond_reward = expected_bond * scaled_bond_reward; + let expected_delegation_reward = expected_delegation * scaled_delegation_reward; + let expected_bond = expected_bond_reward + expected_bond; + let expected_delegation = expected_delegation_reward + expected_delegation; + + let info = mock_info(network_monitor_address.as_ref(), &[]); + let res = try_reward_gateway(deps.as_mut(), env.clone(), info, node_identity.clone(), 100) + .unwrap(); + + assert_eq!( + expected_bond, + read_gateway_bond(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + assert_eq!( + expected_delegation, + read_gateway_delegation(deps.as_ref().storage, node_identity.as_bytes()).unwrap() + ); + + assert_eq!( + vec![ + attr("bond increase", expected_bond_reward), + attr("total delegation increase", expected_delegation_reward), + ], + res.attributes + ); + } + #[cfg(test)] mod delegation_stake_validation { use super::*; @@ -2623,6 +2926,7 @@ pub mod tests { #[test] fn delegators_on_mix_node_reward_rate() { let mut deps = helpers::init_contract(); + let mut env = mock_env(); let current_state = config(deps.as_mut().storage).load().unwrap(); let network_monitor_address = current_state.network_monitor_address; @@ -2635,15 +2939,26 @@ pub mod tests { let identity = add_mixnode(node_owner, good_mixnode_bond(), &mut deps); mix_delegations(&mut deps.storage, &identity) - .save(b"delegator1", &raw_delegation_fixture(initial_delegation1)) + .save( + b"delegator1", + &RawDelegationData::new(initial_delegation1.into(), env.block.height), + ) .unwrap(); mix_delegations(&mut deps.storage, &identity) - .save(b"delegator2", &raw_delegation_fixture(initial_delegation2)) + .save( + b"delegator2", + &RawDelegationData::new(initial_delegation2.into(), env.block.height), + ) .unwrap(); mix_delegations(&mut deps.storage, &identity) - .save(b"delegator3", &raw_delegation_fixture(initial_delegation3)) + .save( + b"delegator3", + &RawDelegationData::new(initial_delegation3.into(), env.block.height), + ) .unwrap(); + env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; + let bond_reward = read_mixnode_epoch_bond_reward_rate(deps.as_ref().storage); let delegation_reward = read_mixnode_epoch_delegation_reward_rate(deps.as_ref().storage); @@ -2660,7 +2975,8 @@ pub mod tests { let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 100).unwrap(); + let res = + try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 100).unwrap(); assert_eq!( expected_bond, @@ -2719,7 +3035,8 @@ pub mod tests { let expected_delegation3 = expected_delegation3_reward + expected_delegation3; let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 20).unwrap(); + let res = + try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 20).unwrap(); assert_eq!( expected_bond, @@ -2765,7 +3082,8 @@ pub mod tests { // if the node was 0% up, nobody will get any rewards let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_mixnode(deps.as_mut(), info, identity.clone(), 0).unwrap(); + let res = + try_reward_mixnode(deps.as_mut(), env.clone(), info, identity.clone(), 0).unwrap(); assert_eq!( expected_bond, @@ -3411,6 +3729,7 @@ pub mod tests { #[test] fn delegators_on_gateway_reward_rate() { let mut deps = helpers::init_contract(); + let mut env = mock_env(); let current_state = config(deps.as_mut().storage).load().unwrap(); let network_monitor_address = current_state.network_monitor_address; @@ -3432,6 +3751,8 @@ pub mod tests { .save(b"delegator3", &raw_delegation_fixture(initial_delegation3)) .unwrap(); + env.block.height += 2 * MINIMUM_BLOCK_AGE_FOR_REWARDING; + let bond_reward = read_gateway_epoch_bond_reward_rate(deps.as_ref().storage); let delegation_reward = read_gateway_epoch_delegation_reward_rate(deps.as_ref().storage); @@ -3448,7 +3769,8 @@ pub mod tests { let expected_delegation3 = expected_delegation3_reward + Uint128(initial_delegation3); let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 100).unwrap(); + let res = + try_reward_gateway(deps.as_mut(), env.clone(), info, identity.clone(), 100).unwrap(); assert_eq!( expected_bond, @@ -3507,7 +3829,8 @@ pub mod tests { let expected_delegation3 = expected_delegation3_reward + expected_delegation3; let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 20).unwrap(); + let res = + try_reward_gateway(deps.as_mut(), env.clone(), info, identity.clone(), 20).unwrap(); assert_eq!( expected_bond, @@ -3553,7 +3876,8 @@ pub mod tests { // if the node was 0% up, nobody will get any rewards let info = mock_info(network_monitor_address.as_ref(), &[]); - let res = try_reward_gateway(deps.as_mut(), info, identity.clone(), 0).unwrap(); + let res = + try_reward_gateway(deps.as_mut(), env.clone(), info, identity.clone(), 0).unwrap(); assert_eq!( expected_bond, @@ -3651,6 +3975,7 @@ pub mod tests { for owner in ["alice", "bob"] { try_add_mixnode( deps.as_mut(), + mock_env(), mock_info(owner, &good_mixnode_bond()), MixNode { identity_key: owner.to_string(),