From 2a98f7482dbfeb0ca2ef7317d6097de1910df1d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bogdan-=C8=98tefan=20Neac=C5=9Fu?= Date: Thu, 30 Sep 2021 15:04:33 +0300 Subject: [PATCH] Feature/migrate hidden delegations (#786) * Remove migration code * Added function to iterate over delegation of variable type * Add unit tests * Refactored some naming and reused mix/gateway functionality * Borrow bucket instead of move * Linked with existing delegations function * Migration of left-over delegations * Remove unused imports * Put a gateway test as well, next to the mix one * Expose queries for all delegations * Change break point * Added client side calls to the new queries * Fix clippy * Added pagination and read check tests * Fix gateway test from the last commit * Test functions for (de)serialization of identity and owner (in)to storage keys * Add delegation function unit test * Feature guard import * Changed UnpackedDelegation from type to struct * Remove mutable parameter and put start_after in returned value * Made all delegations into iterator for OOM safety * Fix clippy * Add test for delegations iterator size in memory * Change map with if let for ease of read * Use DENOM instead of hardcoded value --- .../validator-client/src/client.rs | 62 ++- .../validator-client/src/nymd/mod.rs | 47 ++- common/mixnet-contract/src/delegation.rs | 34 +- common/mixnet-contract/src/lib.rs | 5 +- common/mixnet-contract/src/msg.rs | 8 + contracts/mixnet/src/contract.rs | 40 +- contracts/mixnet/src/helpers.rs | 329 +++++++++++++++- contracts/mixnet/src/queries.rs | 357 ++++++++++++++++-- contracts/mixnet/src/storage.rs | 86 ++++- contracts/mixnet/src/transactions.rs | 108 +++--- explorer-api/src/mix_node/http.rs | 21 +- explorer-api/src/mix_nodes/mod.rs | 18 +- validator-api/src/nymd_client.rs | 4 +- 13 files changed, 995 insertions(+), 124 deletions(-) diff --git a/common/client-libs/validator-client/src/client.rs b/common/client-libs/validator-client/src/client.rs index da8a38965e..48535ba7d8 100644 --- a/common/client-libs/validator-client/src/client.rs +++ b/common/client-libs/validator-client/src/client.rs @@ -10,6 +10,8 @@ use mixnet_contract::StateParams; use crate::{validator_api, ValidatorClientError}; use coconut_interface::{BlindSignRequestBody, BlindedSignatureResponse, VerificationKeyResponse}; +#[cfg(feature = "nymd-client")] +use mixnet_contract::RawDelegationData; use mixnet_contract::{GatewayBond, MixNodeBond}; use url::Url; @@ -227,7 +229,7 @@ impl Client { Ok(gateways) } - pub async fn get_all_nymd_mixnode_delegations( + pub async fn get_all_nymd_single_mixnode_delegations( &self, identity: mixnet_contract::IdentityKey, ) -> Result, ValidatorClientError> @@ -257,6 +259,34 @@ impl Client { Ok(delegations) } + pub async fn get_all_nymd_mixnode_delegations( + &self, + ) -> Result>, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_all_mix_delegations_paged( + start_after.take(), + self.mixnode_delegations_page_limit, + ) + .await?; + delegations.append(&mut paged_response.delegations); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(delegations) + } + pub async fn get_all_nymd_reverse_mixnode_delegations( &self, delegation_owner: &cosmrs::AccountId, @@ -309,7 +339,7 @@ impl Client { Ok(delegations) } - pub async fn get_all_nymd_gateway_delegations( + pub async fn get_all_nymd_single_gateway_delegations( &self, identity: mixnet_contract::IdentityKey, ) -> Result, ValidatorClientError> @@ -339,6 +369,34 @@ impl Client { Ok(delegations) } + pub async fn get_all_nymd_gateway_delegations( + &self, + ) -> Result>, ValidatorClientError> + where + C: CosmWasmClient + Sync, + { + let mut delegations = Vec::new(); + let mut start_after = None; + loop { + let mut paged_response = self + .nymd + .get_all_gateway_delegations( + start_after.take(), + self.gateway_delegations_page_limit, + ) + .await?; + delegations.append(&mut paged_response.delegations); + + if let Some(start_after_res) = paged_response.start_next_after { + start_after = Some(start_after_res) + } else { + break; + } + } + + Ok(delegations) + } + pub async fn get_all_nymd_reverse_gateway_delegations( &self, delegation_owner: &cosmrs::AccountId, diff --git a/common/client-libs/validator-client/src/nymd/mod.rs b/common/client-libs/validator-client/src/nymd/mod.rs index 821ac8ee12..c05d77a51f 100644 --- a/common/client-libs/validator-client/src/nymd/mod.rs +++ b/common/client-libs/validator-client/src/nymd/mod.rs @@ -15,10 +15,10 @@ use cosmrs::rpc::{Error as TendermintRpcError, HttpClientUrl}; use cosmwasm_std::Coin; use mixnet_contract::{ Addr, Delegation, ExecuteMsg, Gateway, GatewayOwnershipResponse, IdentityKey, - LayerDistribution, MixNode, MixOwnershipResponse, PagedGatewayDelegationsResponse, - PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, - PagedReverseGatewayDelegationsResponse, PagedReverseMixDelegationsResponse, QueryMsg, - StateParams, + LayerDistribution, MixNode, MixOwnershipResponse, PagedAllDelegationsResponse, + PagedGatewayDelegationsResponse, PagedGatewayResponse, PagedMixDelegationsResponse, + PagedMixnodeResponse, PagedReverseGatewayDelegationsResponse, + PagedReverseMixDelegationsResponse, QueryMsg, RawDelegationData, StateParams, }; use serde::Serialize; use std::collections::HashMap; @@ -283,6 +283,25 @@ impl NymdClient { .await } + /// Gets list of all mixnode delegations on particular page. + pub async fn get_all_mix_delegations_paged( + &self, + // I really hate mixing cosmwasm and cosmos-sdk types here... + start_after: Option>, + page_limit: Option, + ) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetAllMixDelegations { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + /// Gets list of all the mixnodes on which a particular address delegated. pub async fn get_reverse_mix_delegations_paged( &self, @@ -321,7 +340,7 @@ impl NymdClient { .await } - /// Gets list of all delegations towards particular mixnode on particular page. + /// Gets list of all delegations towards particular gateway on particular page. pub async fn get_gateway_delegations( &self, gateway_identity: IdentityKey, @@ -341,6 +360,24 @@ impl NymdClient { .await } + /// Gets list of all gateway delegations on particular page. + pub async fn get_all_gateway_delegations( + &self, + start_after: Option>, + page_limit: Option, + ) -> Result, NymdError> + where + C: CosmWasmClient + Sync, + { + let request = QueryMsg::GetAllGatewayDelegations { + start_after, + limit: page_limit, + }; + self.client + .query_contract_smart(self.contract_address()?, &request) + .await + } + /// Gets list of all the gateways on which a particular address delegated. pub async fn get_reverse_gateway_delegations_paged( &self, diff --git a/common/mixnet-contract/src/delegation.rs b/common/mixnet-contract/src/delegation.rs index f0efe37297..65fc4c1dd2 100644 --- a/common/mixnet-contract/src/delegation.rs +++ b/common/mixnet-contract/src/delegation.rs @@ -7,7 +7,24 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::fmt::Display; -#[derive(Debug, Deserialize, PartialEq, Serialize)] +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct UnpackedDelegation { + pub owner: Addr, + pub node_identity: IdentityKey, + pub delegation_data: T, +} + +impl UnpackedDelegation { + pub fn new(owner: Addr, node_identity: IdentityKey, delegation_data: T) -> Self { + UnpackedDelegation { + owner, + node_identity, + delegation_data, + } + } +} + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] pub struct RawDelegationData { pub amount: Uint128, pub block_height: u64, @@ -144,3 +161,18 @@ impl PagedReverseGatewayDelegationsResponse { } } } + +#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, JsonSchema)] +pub struct PagedAllDelegationsResponse { + pub delegations: Vec>, + pub start_next_after: Option>, +} + +impl PagedAllDelegationsResponse { + pub fn new(delegations: Vec>, start_next_after: Option>) -> Self { + PagedAllDelegationsResponse { + delegations, + start_next_after, + } + } +} diff --git a/common/mixnet-contract/src/lib.rs b/common/mixnet-contract/src/lib.rs index a3ad1a7fc2..23295df1a2 100644 --- a/common/mixnet-contract/src/lib.rs +++ b/common/mixnet-contract/src/lib.rs @@ -9,8 +9,9 @@ mod types; pub use cosmwasm_std::{Addr, Coin}; pub use delegation::{ - Delegation, PagedGatewayDelegationsResponse, PagedMixDelegationsResponse, - PagedReverseGatewayDelegationsResponse, PagedReverseMixDelegationsResponse, RawDelegationData, + Delegation, PagedAllDelegationsResponse, PagedGatewayDelegationsResponse, + PagedMixDelegationsResponse, PagedReverseGatewayDelegationsResponse, + PagedReverseMixDelegationsResponse, RawDelegationData, UnpackedDelegation, }; pub use gateway::{Gateway, GatewayBond, GatewayOwnershipResponse, PagedGatewayResponse}; pub use mixnode::{Layer, MixNode, MixNodeBond, MixOwnershipResponse, PagedMixnodeResponse}; diff --git a/common/mixnet-contract/src/msg.rs b/common/mixnet-contract/src/msg.rs index 32cb96da86..15b4193369 100644 --- a/common/mixnet-contract/src/msg.rs +++ b/common/mixnet-contract/src/msg.rs @@ -75,6 +75,10 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + GetAllMixDelegations { + start_after: Option>, + limit: Option, + }, GetReverseMixDelegations { delegation_owner: Addr, start_after: Option, @@ -89,6 +93,10 @@ pub enum QueryMsg { start_after: Option, limit: Option, }, + GetAllGatewayDelegations { + start_after: Option>, + limit: Option, + }, GetReverseGatewayDelegations { delegation_owner: Addr, start_after: Option, diff --git a/contracts/mixnet/src/contract.rs b/contracts/mixnet/src/contract.rs index 5254e394ff..445911b941 100644 --- a/contracts/mixnet/src/contract.rs +++ b/contracts/mixnet/src/contract.rs @@ -1,7 +1,7 @@ // Copyright 2021 - Nym Technologies SA // SPDX-License-Identifier: Apache-2.0 -use crate::helpers::calculate_epoch_reward_rate; +use crate::helpers::{calculate_epoch_reward_rate, Delegations}; use crate::state::State; use crate::storage::{config, layer_distribution}; use crate::{error::ContractError, queries, transactions}; @@ -155,6 +155,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary( + &queries::query_all_mixnode_delegations_paged(deps, start_after, limit)?, + ), QueryMsg::GetReverseMixDelegations { delegation_owner, start_after, @@ -183,6 +186,9 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result to_binary( + &queries::query_all_gateway_delegations_paged(deps, start_after, limit)?, + ), QueryMsg::GetReverseGatewayDelegations { delegation_owner, start_after, @@ -207,7 +213,37 @@ pub fn query(deps: Deps, _env: Env, msg: QueryMsg) -> Result Result { +pub fn migrate(deps: DepsMut, env: Env, _msg: MigrateMsg) -> Result { + use crate::storage::{ + all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations, + mix_delegations, reverse_gateway_delegations, reverse_mix_delegations, + }; + use mixnet_contract::{RawDelegationData, UnpackedDelegation}; + + let mix_bucket = all_mix_delegations_read::(deps.storage); + let delegations = Delegations::new(mix_bucket).collect::>>(); + for delegation in delegations { + let owner = delegation.owner; + let node_identity = delegation.node_identity; + let amount = delegation.delegation_data; + let raw_delegation = RawDelegationData::new(amount, env.block.height); + mix_delegations(deps.storage, &node_identity).save(owner.as_bytes(), &raw_delegation)?; + reverse_mix_delegations(deps.storage, &owner).save(node_identity.as_bytes(), &())?; + } + + let gateway_bucket = all_gateway_delegations_read::(deps.storage); + let delegations = + Delegations::new(gateway_bucket).collect::>>(); + for delegation in delegations { + let owner = delegation.owner; + let node_identity = delegation.node_identity; + let amount = delegation.delegation_data; + let raw_delegation = RawDelegationData::new(amount, env.block.height); + gateway_delegations(deps.storage, &node_identity) + .save(owner.as_bytes(), &raw_delegation)?; + reverse_gateway_delegations(deps.storage, &owner).save(node_identity.as_bytes(), &())?; + } + Ok(Default::default()) } diff --git a/contracts/mixnet/src/helpers.rs b/contracts/mixnet/src/helpers.rs index 25c5896f80..0bb672dbad 100644 --- a/contracts/mixnet/src/helpers.rs +++ b/contracts/mixnet/src/helpers.rs @@ -2,7 +2,12 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::ContractError; -use cosmwasm_std::{Decimal, Uint128}; +use crate::transactions::OLD_DELEGATIONS_CHUNK_SIZE; +use cosmwasm_std::{Decimal, Order, StdError, StdResult, Uint128}; +use cosmwasm_storage::ReadonlyBucket; +use mixnet_contract::{Addr, IdentityKey, PagedAllDelegationsResponse, UnpackedDelegation}; +use serde::de::DeserializeOwned; +use serde::Serialize; use std::ops::Sub; // for time being completely ignore concept of a leap year and assume each year is exactly 365 days @@ -11,6 +16,9 @@ const HOURS_IN_YEAR: u128 = 8760; const DECIMAL_FRACTIONAL: Uint128 = Uint128(1_000_000_000_000_000_000u128); +// cosmwasm bucket internal value +const NAMESPACE_LENGTH: usize = 2; + fn decimal_to_uint128(value: Decimal) -> Uint128 { value * DECIMAL_FRACTIONAL } @@ -65,11 +73,185 @@ pub(crate) fn scale_reward_by_uptime( Ok(uint128_to_decimal(scaled)) } +// Extracts the node identity and owner of a delegation from the bytes used as +// key in the delegation buckets. +fn extract_identity_and_owner(bytes: Vec) -> StdResult<(Addr, IdentityKey)> { + if bytes.len() < NAMESPACE_LENGTH { + return Err(StdError::parse_err( + "mixnet_contract::types::IdentityKey", + "Invalid type", + )); + } + let identity_size = u16::from_be_bytes([bytes[0], bytes[1]]) as usize; + let identity_bytes: Vec = bytes + .iter() + .skip(NAMESPACE_LENGTH) + .take(identity_size) + .copied() + .collect(); + let identity = IdentityKey::from_utf8(identity_bytes) + .map_err(|_| StdError::parse_err("mixnet_contract::types::IdentityKey", "Invalid type"))?; + let owner_bytes: Vec = bytes + .iter() + .skip(NAMESPACE_LENGTH + identity_size) + .copied() + .collect(); + let owner = Addr::unchecked( + String::from_utf8(owner_bytes) + .map_err(|_| StdError::parse_err("cosmwasm_std::addresses::Addr", "Invalid type"))?, + ); + + Ok((owner, identity)) +} + +// currently not used outside tests +#[cfg(test)] +// Converts the node identity and owner of a delegation into the bytes used as +// key in the delegation buckets. +pub(crate) fn identity_and_owner_to_bytes(identity: &IdentityKey, owner: &Addr) -> Vec { + let mut bytes = u16::to_be_bytes(identity.len() as u16).to_vec(); + bytes.append(&mut identity.as_bytes().to_vec()); + bytes.append(&mut owner.as_bytes().to_vec()); + + bytes +} + +pub(crate) fn get_all_delegations_paged( + bucket: &ReadonlyBucket, + start_after: &Option>, + limit: usize, +) -> StdResult> +where + T: Serialize + DeserializeOwned, +{ + let delegations = bucket + .range(start_after.as_deref(), None, Order::Ascending) + .filter(|res| res.is_ok()) + .take(limit) + .map(|res| { + res.map(|entry| { + let (owner, identity) = extract_identity_and_owner(entry.0).expect("Invalid node identity or address used as key in bucket. The storage is corrupted!"); + UnpackedDelegation::new(owner, identity, entry.1) + }) + }) + .collect::>>>()?; + + let start_next_after = if let Some(Ok(last)) = bucket + .range(start_after.as_deref(), None, Order::Ascending) + .filter(|res| res.is_ok()) + .take(limit) + .last() + { + Some(last.0) + } else { + None + }; + + Ok(PagedAllDelegationsResponse::new( + delegations, + start_next_after, + )) +} + +pub struct Delegations<'a, T: Clone + Serialize + DeserializeOwned> { + delegations_bucket: ReadonlyBucket<'a, T>, + curr_delegations: Vec>, + curr_index: usize, + start_after: Option>, + last_page: bool, +} + +impl<'a, T: Clone + Serialize + DeserializeOwned> Delegations<'a, T> { + pub fn new(delegations_bucket: ReadonlyBucket<'a, T>) -> Self { + Delegations { + delegations_bucket, + curr_delegations: vec![], + curr_index: OLD_DELEGATIONS_CHUNK_SIZE, + start_after: None, + last_page: false, + } + } +} + +impl<'a, T: Clone + Serialize + DeserializeOwned> Iterator for Delegations<'a, T> { + type Item = UnpackedDelegation; + + fn next(&mut self) -> Option { + if self.curr_index == OLD_DELEGATIONS_CHUNK_SIZE && !self.last_page { + self.start_after = self.start_after.clone().map(|mut v: Vec| { + v.push(0); + v + }); + let delegations_paged = get_all_delegations_paged( + &self.delegations_bucket, + &self.start_after, + OLD_DELEGATIONS_CHUNK_SIZE, + ) + .ok()?; + self.curr_delegations = delegations_paged.delegations; + self.curr_index = 0; + self.start_after = delegations_paged.start_next_after; + if self.start_after.is_none() { + self.last_page = true; + } + } + if self.curr_index < self.curr_delegations.len() { + let ret = self.curr_delegations[self.curr_index].clone(); + self.curr_index += 1; + Some(ret) + } else { + None + } + } +} + #[cfg(test)] mod tests { use super::*; + use crate::queries::tests::store_n_mix_delegations; + use crate::storage::{ + all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations, + mix_delegations, + }; + use crate::support::tests::helpers; + use cosmwasm_std::testing::mock_dependencies; + use mixnet_contract::RawDelegationData; use std::str::FromStr; + #[test] + fn delegations_iterator() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + + store_n_mix_delegations( + 2 * OLD_DELEGATIONS_CHUNK_SIZE as u32, + &mut deps.storage, + &node_identity, + ); + let mix_bucket = all_mix_delegations_read::(&deps.storage); + let mut delegations = Delegations::new(mix_bucket); + assert!(delegations.curr_delegations.is_empty()); + assert_eq!(delegations.curr_index, OLD_DELEGATIONS_CHUNK_SIZE); + delegations.next().unwrap(); + assert_eq!( + delegations.curr_delegations.len(), + OLD_DELEGATIONS_CHUNK_SIZE + ); + assert_eq!(delegations.curr_index, 1); + for _ in 0..OLD_DELEGATIONS_CHUNK_SIZE { + delegations.next().unwrap(); + } + assert_eq!( + delegations.curr_delegations.len(), + OLD_DELEGATIONS_CHUNK_SIZE + ); + assert_eq!(delegations.curr_index, 1); + for _ in 0..OLD_DELEGATIONS_CHUNK_SIZE - 1 { + delegations.next().unwrap(); + } + assert!(delegations.next().is_none()); + } + #[test] fn calculating_epoch_reward_rate() { // 1.10 @@ -124,4 +306,149 @@ mod tests { // anything larger than 100 returns an error assert!(scale_reward_by_uptime(epoch_reward, 101).is_err()) } + + #[test] + fn identity_and_owner_deserialization() { + assert!(extract_identity_and_owner(vec![]).is_err()); + assert!(extract_identity_and_owner(vec![0]).is_err()); + let (owner, identity) = extract_identity_and_owner(vec![ + 0, 7, 109, 105, 120, 110, 111, 100, 101, 97, 108, 105, 99, 101, + ]) + .unwrap(); + assert_eq!(owner, "alice"); + assert_eq!(identity, "mixnode"); + } + + #[test] + fn identity_and_owner_serialization() { + let identity: IdentityKey = "gateway".into(); + let owner = Addr::unchecked("bob"); + assert_eq!( + vec![0, 7, 103, 97, 116, 101, 119, 97, 121, 98, 111, 98], + identity_and_owner_to_bytes(&identity, &owner) + ); + } + + #[test] + fn all_mix_delegations() { + let mut deps = mock_dependencies(&[]); + let node_identity1: IdentityKey = "foo1".into(); + let delegation_owner1 = Addr::unchecked("bar1"); + let node_identity2: IdentityKey = "foo2".into(); + let delegation_owner2 = Addr::unchecked("bar2"); + let raw_delegation = RawDelegationData::new(1000u128.into(), 42); + let mut start_after = None; + + mix_delegations(&mut deps.storage, &node_identity1) + .save(delegation_owner1.as_bytes(), &raw_delegation) + .unwrap(); + + let bucket = all_mix_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + start_after = response.start_next_after; + let delegations = response.delegations; + assert_eq!(delegations.len(), 1); + assert_eq!( + delegations[0], + UnpackedDelegation::new( + delegation_owner1.clone(), + node_identity1.clone(), + raw_delegation.clone() + ) + ); + + mix_delegations(&mut deps.storage, &node_identity2) + .save(delegation_owner2.as_bytes(), &raw_delegation) + .unwrap(); + + let bucket = all_mix_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + start_after = response.start_next_after; + let delegations = response.delegations; + assert_eq!(delegations.len(), 2); + assert_eq!( + delegations[1], + UnpackedDelegation::new( + delegation_owner2.clone(), + node_identity2.clone(), + raw_delegation.clone() + ) + ); + + mix_delegations(&mut deps.storage, &node_identity1).remove(delegation_owner1.as_bytes()); + + let bucket = all_mix_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + let delegations = response.delegations; + assert_eq!(delegations.len(), 1); + assert_eq!( + delegations[0], + UnpackedDelegation::new(delegation_owner2, node_identity2, raw_delegation.clone()), + ); + } + + #[test] + fn all_gateway_delegations() { + let mut deps = mock_dependencies(&[]); + let node_identity1: IdentityKey = "foo1".into(); + let delegation_owner1 = Addr::unchecked("bar1"); + let node_identity2: IdentityKey = "foo2".into(); + let delegation_owner2 = Addr::unchecked("bar2"); + let raw_delegation = RawDelegationData::new(1000u128.into(), 42); + let mut start_after = None; + + gateway_delegations(&mut deps.storage, &node_identity1) + .save(delegation_owner1.as_bytes(), &raw_delegation) + .unwrap(); + + let bucket = all_gateway_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + start_after = response.start_next_after; + let delegations = response.delegations; + assert_eq!(delegations.len(), 1); + assert_eq!( + delegations[0], + UnpackedDelegation::new( + delegation_owner1.clone(), + node_identity1.clone(), + raw_delegation.clone() + ) + ); + + gateway_delegations(&mut deps.storage, &node_identity2) + .save(delegation_owner2.as_bytes(), &raw_delegation) + .unwrap(); + + let bucket = all_gateway_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + start_after = response.start_next_after; + let delegations = response.delegations; + assert_eq!(delegations.len(), 2); + assert_eq!( + delegations[1], + UnpackedDelegation::new( + delegation_owner2.clone(), + node_identity2.clone(), + raw_delegation.clone() + ) + ); + + gateway_delegations(&mut deps.storage, &node_identity1) + .remove(delegation_owner1.as_bytes()); + + let bucket = all_gateway_delegations_read::(&deps.storage); + let response = + get_all_delegations_paged::(&bucket, &start_after, 10).unwrap(); + let delegations = response.delegations; + assert_eq!(delegations.len(), 1); + assert_eq!( + delegations[0], + UnpackedDelegation::new(delegation_owner2, node_identity2, raw_delegation.clone()), + ); + } } diff --git a/contracts/mixnet/src/queries.rs b/contracts/mixnet/src/queries.rs index 96b6f93fe8..0d2c3826aa 100644 --- a/contracts/mixnet/src/queries.rs +++ b/contracts/mixnet/src/queries.rs @@ -2,21 +2,21 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::ContractError; +use crate::helpers::get_all_delegations_paged; use crate::storage::{ - gateway_delegations_read, gateways_owners_read, gateways_read, mix_delegations_read, - mixnodes_owners_read, mixnodes_read, read_layer_distribution, read_state_params, - reverse_gateway_delegations_read, reverse_mix_delegations_read, + all_gateway_delegations_read, all_mix_delegations_read, gateway_delegations_read, + gateways_owners_read, gateways_read, mix_delegations_read, mixnodes_owners_read, mixnodes_read, + read_layer_distribution, read_state_params, reverse_gateway_delegations_read, + reverse_mix_delegations_read, }; use config::defaults::DENOM; -use cosmwasm_std::Deps; -use cosmwasm_std::Order; -use cosmwasm_std::StdResult; -use cosmwasm_std::{coin, Addr}; +use cosmwasm_std::{coin, Addr, Deps, Order, StdResult}; use mixnet_contract::{ Delegation, GatewayBond, GatewayOwnershipResponse, IdentityKey, LayerDistribution, MixNodeBond, - MixOwnershipResponse, PagedGatewayDelegationsResponse, PagedGatewayResponse, - PagedMixDelegationsResponse, PagedMixnodeResponse, PagedReverseGatewayDelegationsResponse, - PagedReverseMixDelegationsResponse, StateParams, + MixOwnershipResponse, PagedAllDelegationsResponse, PagedGatewayDelegationsResponse, + PagedGatewayResponse, PagedMixDelegationsResponse, PagedMixnodeResponse, + PagedReverseGatewayDelegationsResponse, PagedReverseMixDelegationsResponse, RawDelegationData, + StateParams, }; const BOND_PAGE_MAX_LIMIT: u32 = 100; @@ -24,7 +24,7 @@ const BOND_PAGE_DEFAULT_LIMIT: u32 = 50; // currently the maximum limit before running into memory issue is somewhere between 1150 and 1200 pub(crate) const DELEGATION_PAGE_MAX_LIMIT: u32 = 750; -const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 500; +pub(crate) const DELEGATION_PAGE_DEFAULT_LIMIT: u32 = 500; pub fn query_mixnodes_paged( deps: Deps, @@ -144,6 +144,23 @@ pub(crate) fn query_mixnode_delegations_paged( )) } +pub(crate) fn query_all_mixnode_delegations_paged( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult> { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT) + .min(DELEGATION_PAGE_MAX_LIMIT) as usize; + + let bucket = all_mix_delegations_read::(deps.storage); + let start = start_after.map(|mut v| { + v.push(0); + v + }); + get_all_delegations_paged::(&bucket, &start, limit) +} + pub(crate) fn query_reverse_mixnode_delegations_paged( deps: Deps, delegation_owner: Addr, @@ -230,6 +247,23 @@ pub(crate) fn query_gateway_delegations_paged( )) } +pub(crate) fn query_all_gateway_delegations_paged( + deps: Deps, + start_after: Option>, + limit: Option, +) -> StdResult> { + let limit = limit + .unwrap_or(DELEGATION_PAGE_DEFAULT_LIMIT) + .min(DELEGATION_PAGE_MAX_LIMIT) as usize; + + let bucket = all_gateway_delegations_read::(deps.storage); + let start = start_after.map(|mut v| { + v.push(0); + v + }); + get_all_delegations_paged::(&bucket, &start, limit) +} + pub(crate) fn query_reverse_gateway_delegations_paged( deps: Deps, delegation_owner: Addr, @@ -281,7 +315,7 @@ pub(crate) fn query_gateway_delegation( } #[cfg(test)] -mod tests { +pub(crate) mod tests { use super::*; use crate::state::State; use crate::storage::{config, gateway_delegations, gateways, mix_delegations, mixnodes}; @@ -656,25 +690,25 @@ mod tests { assert_eq!(dummy_state.params, query_state_params(deps.as_ref())) } + pub fn store_n_mix_delegations(n: u32, storage: &mut dyn Storage, node_identity: &IdentityKey) { + for i in 0..n { + let address = format!("address{}", i); + mix_delegations(storage, node_identity) + .save(address.as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + } + } + #[cfg(test)] mod querying_for_mixnode_delegations_paged { use super::*; - fn store_n_delegations(n: u32, storage: &mut dyn Storage, node_identity: &IdentityKey) { - for i in 0..n { - let address = format!("address{}", i); - mix_delegations(storage, node_identity) - .save(address.as_bytes(), &raw_delegation_fixture(42)) - .unwrap(); - } - } - #[test] fn retrieval_obeys_limits() { let mut deps = helpers::init_contract(); let limit = 2; let node_identity: IdentityKey = "foo".into(); - store_n_delegations(100, &mut deps.storage, &node_identity); + store_n_mix_delegations(100, &mut deps.storage, &node_identity); let page1 = query_mixnode_delegations_paged( deps.as_ref(), @@ -690,7 +724,7 @@ mod tests { fn retrieval_has_default_limit() { let mut deps = helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); - store_n_delegations( + store_n_mix_delegations( DELEGATION_PAGE_DEFAULT_LIMIT * 10, &mut deps.storage, &node_identity, @@ -709,7 +743,7 @@ mod tests { fn retrieval_has_max_limit() { let mut deps = helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); - store_n_delegations( + store_n_mix_delegations( DELEGATION_PAGE_DEFAULT_LIMIT * 10, &mut deps.storage, &node_identity, @@ -811,6 +845,129 @@ mod tests { } } + #[cfg(test)] + mod querying_for_all_mixnode_delegations_paged { + use super::*; + use crate::helpers::identity_and_owner_to_bytes; + + #[test] + fn retrieval_obeys_limits() { + let mut deps = helpers::init_contract(); + let limit = 2; + let node_identity: IdentityKey = "foo".into(); + store_n_mix_delegations(100, &mut deps.storage, &node_identity); + + let page1 = + query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(limit)) + .unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn retrieval_has_default_limit() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + store_n_mix_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + + // query without explicitly setting a limit + let page1 = query_all_mixnode_delegations_paged(deps.as_ref(), None, None).unwrap(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn retrieval_has_max_limit() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + store_n_mix_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT; + let page1 = + query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(crazy_limit)) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DELEGATION_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.delegations.len() as u32); + } + + #[test] + fn pagination_works() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + + mix_delegations(&mut deps.storage, &node_identity) + .save("1".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + let per_page = 2; + let page1 = + query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + mix_delegations(&mut deps.storage, &node_identity) + .save("2".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + // page1 should have 2 results on it + let page1 = + query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + mix_delegations(&mut deps.storage, &node_identity) + .save("3".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + // page1 still has 2 results + let page1 = + query_all_mixnode_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + // retrieving the next page should start after the last key on this page + let start_after = identity_and_owner_to_bytes(&node_identity, &Addr::unchecked("2")); + let page2 = query_all_mixnode_delegations_paged( + deps.as_ref(), + Option::from(start_after.clone()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + mix_delegations(&mut deps.storage, &node_identity) + .save("4".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + let page2 = query_all_mixnode_delegations_paged( + deps.as_ref(), + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } + } + #[test] fn mix_deletion_query_returns_current_delegation_value() { let mut deps = helpers::init_contract(); @@ -1047,26 +1204,30 @@ mod tests { } } + pub fn store_n_gateway_delegations( + n: u32, + storage: &mut dyn Storage, + node_identity: &IdentityKey, + ) { + for i in 0..n { + let address = format!("address{}", i); + gateway_delegations(storage, node_identity) + .save(address.as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + } + } + #[cfg(test)] mod querying_for_gateway_delegations_paged { use super::*; use crate::storage::gateway_delegations; - fn store_n_delegations(n: u32, storage: &mut dyn Storage, node_identity: &IdentityKey) { - for i in 0..n { - let address = format!("address{}", i); - gateway_delegations(storage, node_identity) - .save(address.as_bytes(), &raw_delegation_fixture(42)) - .unwrap(); - } - } - #[test] fn retrieval_obeys_limits() { let mut deps = helpers::init_contract(); let limit = 2; let node_identity: IdentityKey = "foo".into(); - store_n_delegations(100, &mut deps.storage, &node_identity); + store_n_gateway_delegations(100, &mut deps.storage, &node_identity); let page1 = query_gateway_delegations_paged( deps.as_ref(), @@ -1082,7 +1243,7 @@ mod tests { fn retrieval_has_default_limit() { let mut deps = helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); - store_n_delegations( + store_n_gateway_delegations( DELEGATION_PAGE_DEFAULT_LIMIT * 10, &mut deps.storage, &node_identity, @@ -1101,7 +1262,7 @@ mod tests { fn retrieval_has_max_limit() { let mut deps = helpers::init_contract(); let node_identity: IdentityKey = "foo".into(); - store_n_delegations( + store_n_gateway_delegations( DELEGATION_PAGE_DEFAULT_LIMIT * 10, &mut deps.storage, &node_identity, @@ -1278,6 +1439,130 @@ mod tests { ) } + #[cfg(test)] + mod querying_for_all_gateway_delegations_paged { + use super::*; + use crate::helpers::identity_and_owner_to_bytes; + use crate::storage::gateway_delegations; + + #[test] + fn retrieval_obeys_limits() { + let mut deps = helpers::init_contract(); + let limit = 2; + let node_identity: IdentityKey = "foo".into(); + store_n_gateway_delegations(100, &mut deps.storage, &node_identity); + + let page1 = + query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(limit)) + .unwrap(); + assert_eq!(limit, page1.delegations.len() as u32); + } + + #[test] + fn retrieval_has_default_limit() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + store_n_gateway_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + + // query without explicitly setting a limit + let page1 = query_all_gateway_delegations_paged(deps.as_ref(), None, None).unwrap(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT, + page1.delegations.len() as u32 + ); + } + + #[test] + fn retrieval_has_max_limit() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + store_n_gateway_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + + // query with a crazily high limit in an attempt to use too many resources + let crazy_limit = 1000 * DELEGATION_PAGE_DEFAULT_LIMIT; + let page1 = + query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(crazy_limit)) + .unwrap(); + + // we default to a decent sized upper bound instead + let expected_limit = DELEGATION_PAGE_MAX_LIMIT; + assert_eq!(expected_limit, page1.delegations.len() as u32); + } + + #[test] + fn pagination_works() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + + gateway_delegations(&mut deps.storage, &node_identity) + .save("1".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + let per_page = 2; + let page1 = + query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + + // page should have 1 result on it + assert_eq!(1, page1.delegations.len()); + + // save another + gateway_delegations(&mut deps.storage, &node_identity) + .save("2".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + // page1 should have 2 results on it + let page1 = + query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + gateway_delegations(&mut deps.storage, &node_identity) + .save("3".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + // page1 still has 2 results + let page1 = + query_all_gateway_delegations_paged(deps.as_ref(), None, Option::from(per_page)) + .unwrap(); + assert_eq!(2, page1.delegations.len()); + + // retrieving the next page should start after the last key on this page + let start_after = identity_and_owner_to_bytes(&node_identity, &Addr::unchecked("2")); + let page2 = query_all_gateway_delegations_paged( + deps.as_ref(), + Option::from(start_after.clone()), + Option::from(per_page), + ) + .unwrap(); + + assert_eq!(1, page2.delegations.len()); + + // save another one + gateway_delegations(&mut deps.storage, &node_identity) + .save("4".as_bytes(), &raw_delegation_fixture(42)) + .unwrap(); + + let page2 = query_all_gateway_delegations_paged( + deps.as_ref(), + Option::from(start_after), + Option::from(per_page), + ) + .unwrap(); + + // now we have 2 pages, with 2 results on the second page + assert_eq!(2, page2.delegations.len()); + } + } + #[cfg(test)] mod querying_for_reverse_gateway_delegations_paged { use super::*; diff --git a/contracts/mixnet/src/storage.rs b/contracts/mixnet/src/storage.rs index bb7b514c9e..21cd2c41da 100644 --- a/contracts/mixnet/src/storage.rs +++ b/contracts/mixnet/src/storage.rs @@ -13,6 +13,8 @@ use mixnet_contract::{ Addr, GatewayBond, IdentityKey, IdentityKeyRef, Layer, LayerDistribution, MixNodeBond, RawDelegationData, StateParams, }; +use serde::de::DeserializeOwned; +use serde::Serialize; // storage prefixes // all of them must be unique and presumably not be a prefix of a different one @@ -304,6 +306,13 @@ pub fn gateways_owners_read(storage: &dyn Storage) -> ReadonlyBucket(storage: &dyn Storage) -> ReadonlyBucket +where + T: Serialize + DeserializeOwned, +{ + bucket_read(storage, PREFIX_MIX_DELEGATION) +} + pub fn mix_delegations<'a>( storage: &'a mut dyn Storage, mix_identity: IdentityKeyRef, @@ -329,6 +338,13 @@ pub fn reverse_mix_delegations_read<'a>( ReadonlyBucket::multilevel(storage, &[PREFIX_REVERSE_MIX_DELEGATION, owner.as_bytes()]) } +pub fn all_gateway_delegations_read(storage: &dyn Storage) -> ReadonlyBucket +where + T: Serialize + DeserializeOwned, +{ + bucket_read(storage, PREFIX_GATEWAY_DELEGATION) +} + pub fn gateway_delegations<'a>( storage: &'a mut dyn Storage, gateway_identity: IdentityKeyRef, @@ -394,12 +410,13 @@ pub(crate) fn read_gateway_delegation( #[cfg(test)] mod tests { use super::*; + use crate::helpers::identity_and_owner_to_bytes; use crate::support::tests::helpers::{ gateway_bond_fixture, gateway_fixture, mix_node_fixture, mixnode_bond_fixture, raw_delegation_fixture, }; use config::defaults::DENOM; - use cosmwasm_std::testing::MockStorage; + use cosmwasm_std::testing::{mock_dependencies, MockStorage}; use cosmwasm_std::{coin, Addr, Uint128}; use mixnet_contract::{Gateway, MixNode}; @@ -466,6 +483,7 @@ mod tests { ); } + #[test] #[test] fn reading_gateway_bond() { let mut storage = MockStorage::new(); @@ -500,6 +518,72 @@ mod tests { ); } + #[test] + fn all_mixnode_delegations_read_retrieval() { + let mut deps = mock_dependencies(&[]); + let node_identity1: IdentityKey = "foo1".into(); + let delegation_owner1 = Addr::unchecked("bar1"); + let node_identity2: IdentityKey = "foo2".into(); + let delegation_owner2 = Addr::unchecked("bar2"); + let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000); + let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000); + + mix_delegations(&mut deps.storage, &node_identity1) + .save(delegation_owner1.as_bytes(), &raw_delegation1) + .unwrap(); + mix_delegations(&mut deps.storage, &node_identity2) + .save(delegation_owner2.as_bytes(), &raw_delegation2) + .unwrap(); + + let res1 = all_mix_delegations_read::(&deps.storage) + .load(&*identity_and_owner_to_bytes( + &node_identity1, + &delegation_owner1, + )) + .unwrap(); + let res2 = all_mix_delegations_read::(&deps.storage) + .load(&*identity_and_owner_to_bytes( + &node_identity2, + &delegation_owner2, + )) + .unwrap(); + assert_eq!(raw_delegation1, res1); + assert_eq!(raw_delegation2, res2); + } + + #[test] + fn all_gateway_delegations_read_retrieval() { + let mut deps = mock_dependencies(&[]); + let node_identity1: IdentityKey = "foo1".into(); + let delegation_owner1 = Addr::unchecked("bar1"); + let node_identity2: IdentityKey = "foo2".into(); + let delegation_owner2 = Addr::unchecked("bar2"); + let raw_delegation1 = RawDelegationData::new(1u128.into(), 1000); + let raw_delegation2 = RawDelegationData::new(2u128.into(), 2000); + + gateway_delegations(&mut deps.storage, &node_identity1) + .save(delegation_owner1.as_bytes(), &raw_delegation1) + .unwrap(); + gateway_delegations(&mut deps.storage, &node_identity2) + .save(delegation_owner2.as_bytes(), &raw_delegation2) + .unwrap(); + + let res1 = all_gateway_delegations_read::(&deps.storage) + .load(&*identity_and_owner_to_bytes( + &node_identity1, + &delegation_owner1, + )) + .unwrap(); + let res2 = all_gateway_delegations_read::(&deps.storage) + .load(&*identity_and_owner_to_bytes( + &node_identity2, + &delegation_owner2, + )) + .unwrap(); + assert_eq!(raw_delegation1, res1); + assert_eq!(raw_delegation2, res2); + } + #[cfg(test)] mod increasing_mix_delegated_stakes { use super::*; diff --git a/contracts/mixnet/src/transactions.rs b/contracts/mixnet/src/transactions.rs index 4a0660897a..7b2ec5c14c 100644 --- a/contracts/mixnet/src/transactions.rs +++ b/contracts/mixnet/src/transactions.rs @@ -2,84 +2,27 @@ // SPDX-License-Identifier: Apache-2.0 use crate::error::ContractError; -use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime}; +use crate::helpers::{calculate_epoch_reward_rate, scale_reward_by_uptime, Delegations}; use crate::queries; use crate::storage::*; use config::defaults::DENOM; use cosmwasm_std::{ - attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Order, Response, StdResult, - Uint128, + attr, coins, BankMsg, Coin, Decimal, DepsMut, Env, MessageInfo, Response, StdResult, Uint128, }; use cosmwasm_storage::ReadonlyBucket; use mixnet_contract::{ Gateway, GatewayBond, IdentityKey, Layer, MixNode, MixNodeBond, RawDelegationData, StateParams, }; -use serde::de::DeserializeOwned; -use serde::Serialize; -const OLD_DELEGATIONS_CHUNK_SIZE: usize = 500; +pub(crate) 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: -// 1. The mixnode/gateway bonds -// 2. Some addresses start to delegate to the node -// 3. The node unbonds -// 4. Some of the addresses that delegated in the past have not removed the delegation yet -// 5. The node rebonds with the same identity -pub fn delegations( - delegations_bucket: ReadonlyBucket, -) -> StdResult, T)>> { - // I think it's incredibly unlikely to ever read more than that - // but in case we do, we should guard ourselves against possible - // out of memory errors (wasm contracts can only allocate at most 2MB - // of RAM, so we don't want to box the entire iterator) - let mut delegations = Vec::new(); - // let mut total_delegation = Coin::new(0, DENOM); - let mut start = None; - loop { - let iterator = delegations_bucket - .range(start.as_deref(), None, Order::Ascending) - .take(OLD_DELEGATIONS_CHUNK_SIZE + 1); - - let mut iterated = 0; - - for result_tuple in iterator { - iterated += 1; - match result_tuple { - Ok((position, delegation)) => { - // We might skip some values due to deserializatio errors - if iterated > OLD_DELEGATIONS_CHUNK_SIZE { - // we reached start of next chunk, don't process it, mark it for the next iteration of the loop - start = Some(position); - continue; - } - delegations.push((position, delegation)) - } - Err(_e) => { - // Skip errors - continue; - } - } - } - - if iterated <= OLD_DELEGATIONS_CHUNK_SIZE { - // that was the final chunk - break; - } - } - - Ok(delegations) -} - fn total_delegations(delegations_bucket: ReadonlyBucket) -> StdResult { - match delegations(delegations_bucket) { - Ok(delegations) => Ok(Coin::new( - delegations.iter().fold(0, |acc, x| acc + x.1.amount.u128()), - "upunk", - )), - Err(e) => Err(e), - } + Ok(Coin::new( + Delegations::new(delegations_bucket) + .fold(0, |acc, x| acc + x.delegation_data.amount.u128()), + DENOM, + )) } fn validate_mixnode_bond(bond: &[Coin], minimum_bond: Uint128) -> Result<(), ContractError> { @@ -790,6 +733,7 @@ pub mod tests { INITIAL_MIXNODE_DELEGATION_REWARD_RATE, }; use crate::helpers::calculate_epoch_reward_rate; + use crate::queries::DELEGATION_PAGE_DEFAULT_LIMIT; use crate::storage::{ gateway_delegations, gateway_delegations_read, layer_distribution_read, mix_delegations_read, read_gateway_bond, read_gateway_epoch_bond_reward_rate, @@ -804,7 +748,9 @@ pub mod tests { use cosmwasm_std::{coin, coins, from_binary, Addr, Uint128}; use mixnet_contract::{ ExecuteMsg, LayerDistribution, PagedGatewayResponse, PagedMixnodeResponse, QueryMsg, + UnpackedDelegation, }; + use queries::tests::{store_n_gateway_delegations, store_n_mix_delegations}; #[test] fn validating_mixnode_bond() { @@ -3948,6 +3894,38 @@ pub mod tests { ); } + #[test] + fn multiple_page_delegations() { + let mut deps = helpers::init_contract(); + let node_identity: IdentityKey = "foo".into(); + + store_n_mix_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + let mix_bucket = all_mix_delegations_read::(&deps.storage); + let mix_delegations = + Delegations::new(mix_bucket).collect::>>(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + mix_delegations.len() as u32 + ); + + store_n_gateway_delegations( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + &mut deps.storage, + &node_identity, + ); + let gateway_bucket = all_gateway_delegations_read::(&deps.storage); + let gateway_delegations = Delegations::new(gateway_bucket) + .collect::>>(); + assert_eq!( + DELEGATION_PAGE_DEFAULT_LIMIT * 10, + gateway_delegations.len() as u32 + ); + } + #[cfg(test)] mod finding_old_delegations { use super::*; diff --git a/explorer-api/src/mix_node/http.rs b/explorer-api/src/mix_node/http.rs index 56cb5ba848..eeb737b16c 100644 --- a/explorer-api/src/mix_node/http.rs +++ b/explorer-api/src/mix_node/http.rs @@ -3,14 +3,20 @@ use rocket::serde::json::Json; use rocket::{Route, State}; use serde::Serialize; -use mixnet_contract::{Addr, Coin, Layer, MixNode}; +use mixnet_contract::{Addr, Coin, Layer, MixNode, RawDelegationData}; use crate::mix_node::models::{NodeDescription, NodeStats}; -use crate::mix_nodes::{get_mixnode_delegations, Location}; +use crate::mix_nodes::{get_mixnode_delegations, get_single_mixnode_delegations, Location}; use crate::state::ExplorerApiStateContext; pub fn mix_node_make_default_routes() -> Vec { - routes_with_openapi![get_delegations, get_description, get_stats, list] + routes_with_openapi![ + get_delegations, + get_all_delegations, + get_description, + get_stats, + list + ] } #[derive(Clone, Debug, Serialize, JsonSchema)] @@ -54,7 +60,14 @@ pub(crate) async fn list( #[openapi(tag = "mix_node")] #[get("//delegations")] pub(crate) async fn get_delegations(pubkey: &str) -> Json> { - Json(get_mixnode_delegations(pubkey).await) + Json(get_single_mixnode_delegations(pubkey).await) +} + +#[openapi(tag = "mix_node")] +#[get("/all_mix_delegations")] +pub(crate) async fn get_all_delegations( +) -> Json>> { + Json(get_mixnode_delegations().await) } #[openapi(tag = "mix_node")] diff --git a/explorer-api/src/mix_nodes/mod.rs b/explorer-api/src/mix_nodes/mod.rs index 2c82ef22d9..f04a2dbce0 100644 --- a/explorer-api/src/mix_nodes/mod.rs +++ b/explorer-api/src/mix_nodes/mod.rs @@ -8,7 +8,7 @@ use rocket::tokio::sync::RwLock; use serde::{Deserialize, Serialize}; use crate::mix_nodes::utils::map_2_letter_to_3_letter_country_code; -use mixnet_contract::{Delegation, MixNodeBond}; +use mixnet_contract::{Delegation, MixNodeBond, RawDelegationData, UnpackedDelegation}; use network_defaults::{ default_api_endpoints, default_nymd_endpoints, DEFAULT_MIXNET_CONTRACT_ADDRESS, }; @@ -166,10 +166,10 @@ pub(crate) async fn retrieve_mixnodes() -> Vec { bonds } -pub(crate) async fn get_mixnode_delegations(pubkey: &str) -> Vec { +pub(crate) async fn get_single_mixnode_delegations(pubkey: &str) -> Vec { let client = new_nymd_client(); let delegates = match client - .get_all_nymd_mixnode_delegations(pubkey.to_string()) + .get_all_nymd_single_mixnode_delegations(pubkey.to_string()) .await { Ok(result) => result, @@ -181,6 +181,18 @@ pub(crate) async fn get_mixnode_delegations(pubkey: &str) -> Vec { delegates } +pub(crate) async fn get_mixnode_delegations() -> Vec> { + let client = new_nymd_client(); + let delegates = match client.get_all_nymd_mixnode_delegations().await { + Ok(result) => result, + Err(e) => { + error!("Could not get all mix delegations: {:?}", e); + vec![] + } + }; + delegates +} + fn new_nymd_client() -> validator_client::Client { let mixnet_contract = DEFAULT_MIXNET_CONTRACT_ADDRESS.to_string(); let nymd_url = default_nymd_endpoints()[0].clone(); diff --git a/validator-api/src/nymd_client.rs b/validator-api/src/nymd_client.rs index f836d27358..870b0194ac 100644 --- a/validator-api/src/nymd_client.rs +++ b/validator-api/src/nymd_client.rs @@ -123,7 +123,7 @@ impl Client { self.0 .read() .await - .get_all_nymd_mixnode_delegations(identity) + .get_all_nymd_single_mixnode_delegations(identity) .await } @@ -137,7 +137,7 @@ impl Client { self.0 .read() .await - .get_all_nymd_gateway_delegations(identity) + .get_all_nymd_single_gateway_delegations(identity) .await }