From 2e84cff877086bb3071bf7266f1a2cbbc7023e64 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Thu, 29 Feb 2024 17:42:14 +0100 Subject: [PATCH 1/2] feat(jsonrpc): implement query stakes method This method allows to query the stakes of the specified argument. The argument can contain: - A validator and a withdrawer - A validator - A withdrawer - Empty argument, uses the node's address as validator. The type of the argument is an address as a string. --- data_structures/src/staking/aux.rs | 14 ++ data_structures/src/staking/errors.rs | 69 +++++++- data_structures/src/staking/stake.rs | 19 ++- data_structures/src/staking/stakes.rs | 183 ++++++++++++++++++++-- node/src/actors/chain_manager/handlers.rs | 14 +- node/src/actors/json_rpc/api.rs | 63 +++++++- node/src/actors/messages.rs | 46 ++++++ 7 files changed, 387 insertions(+), 21 deletions(-) diff --git a/data_structures/src/staking/aux.rs b/data_structures/src/staking/aux.rs index 6898f8ecb..1ebb5c28c 100644 --- a/data_structures/src/staking/aux.rs +++ b/data_structures/src/staking/aux.rs @@ -1,3 +1,4 @@ +use std::fmt::{Debug, Display, Formatter}; use std::{rc::Rc, str::FromStr, sync::RwLock}; use failure::Error; @@ -98,6 +99,19 @@ where } } +impl
Display for StakeKey
+where + Address: Display, +{ + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!( + f, + "validator: {} withdrawer: {}", + self.validator, self.withdrawer + ) + } +} + /// Couples an amount of coins, a validator address and a withdrawer address together. This is meant to be used in /// `Stakes` as the index of the `by_coins` index. #[derive(Clone, Debug, Deserialize, Eq, Ord, PartialEq, PartialOrd, Serialize)] diff --git a/data_structures/src/staking/errors.rs b/data_structures/src/staking/errors.rs index 7b270a92e..869536de3 100644 --- a/data_structures/src/staking/errors.rs +++ b/data_structures/src/staking/errors.rs @@ -1,12 +1,25 @@ -use std::sync::PoisonError; - use crate::staking::aux::StakeKey; +use failure::Fail; +use std::{ + convert::From, + fmt::{Debug, Display}, + sync::PoisonError, +}; /// All errors related to the staking functionality. -#[derive(Debug, PartialEq)] -pub enum StakesError { +#[derive(Debug, PartialEq, Fail)] +pub enum StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ /// The amount of coins being staked or the amount that remains after unstaking is below the /// minimum stakeable amount. + #[fail( + display = "The amount of coins being staked ({}) or the amount that remains after unstaking is below the minimum stakeable amount ({})", + amount, minimum + )] AmountIsBelowMinimum { /// The number of coins being staked or remaining after staking. amount: Coins, @@ -14,6 +27,10 @@ pub enum StakesError { minimum: Coins, }, /// Tried to query `Stakes` for information that belongs to the past. + #[fail( + display = "Tried to query `Stakes` for information that belongs to the past. Query Epoch: {} Latest Epoch: {}", + epoch, latest + )] EpochInThePast { /// The Epoch being referred. epoch: Epoch, @@ -21,6 +38,10 @@ pub enum StakesError { latest: Epoch, }, /// An operation thrown an Epoch value that overflows. + #[fail( + display = "An operation thrown an Epoch value that overflows. Computed Epoch: {} Maximum Epoch: {}", + computed, maximum + )] EpochOverflow { /// The computed Epoch value. computed: u64, @@ -28,18 +49,56 @@ pub enum StakesError { maximum: Epoch, }, /// Tried to query for a stake entry that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry that is not registered in Stakes {}", + key + )] EntryNotFound { /// A validator and withdrawer address pair. key: StakeKey
, }, /// Tried to obtain a lock on a write-locked piece of data that is already locked. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] PoisonedLock, /// The authentication signature contained within a stake transaction is not valid for the given validator and /// withdrawer addresses. + #[fail( + display = "The authentication signature contained within a stake transaction is not valid for the given validator and withdrawer addresses" + )] InvalidAuthentication, + /// Tried to query for a stake entry by validator that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by validator ({}) that is not registered in Stakes", + validator + )] + ValidatorNotFound { + /// A validator address. + validator: Address, + }, + /// Tried to query for a stake entry by withdrawer that is not registered in `Stakes`. + #[fail( + display = "Tried to query for a stake entry by withdrawer ({}) that is not registered in Stakes", + withdrawer + )] + WithdrawerNotFound { + /// A withdrawer address. + withdrawer: Address, + }, + /// Tried to query for a stake entry without providing a validator or a withdrawer address. + #[fail( + display = "Tried to query a stake entry without providing a validator or a withdrawer address" + )] + EmptyQuery, } -impl From> for StakesError { +impl From> for StakesError +where + Address: Debug + Display + Sync + Send + 'static, + Coins: Debug + Display + Sync + Send + 'static, + Epoch: Debug + Display + Sync + Send + 'static, +{ fn from(_value: PoisonError) -> Self { StakesError::PoisonedLock } diff --git a/data_structures/src/staking/stake.rs b/data_structures/src/staking/stake.rs index 0915df2b6..ea1926da9 100644 --- a/data_structures/src/staking/stake.rs +++ b/data_structures/src/staking/stake.rs @@ -3,6 +3,7 @@ use std::{marker::PhantomData, ops::*}; use serde::{Deserialize, Serialize}; use super::prelude::*; +use std::fmt::{Debug, Display}; /// A data structure that keeps track of a staker's staked coins and the epochs for different /// capabilities. @@ -23,7 +24,7 @@ where impl Stake where - Address: Default, + Address: Default + Debug + Display + Sync + Send, Coins: Copy + From + PartialOrd @@ -31,8 +32,20 @@ where + Add + Sub + Mul - + Mul, - Epoch: Copy + Default + num_traits::Saturating + Sub + From, + + Mul + + Debug + + Display + + Send + + Sync, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Sync + + Send, Power: Add + Div, u64: From + From, { diff --git a/data_structures/src/staking/stakes.rs b/data_structures/src/staking/stakes.rs index 426df952f..a5e22a6f8 100644 --- a/data_structures/src/staking/stakes.rs +++ b/data_structures/src/staking/stakes.rs @@ -1,6 +1,6 @@ use std::{ collections::{btree_map::Entry, BTreeMap}, - fmt::Debug, + fmt::{Debug, Display}, ops::{Add, Div, Mul, Sub}, }; @@ -11,6 +11,47 @@ use crate::{chain::PublicKeyHash, get_environment, transaction::StakeTransaction use super::prelude::*; +/// Message for querying stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesKey { + /// Query stakes by validator address + Validator(Address), + /// Query stakes by withdrawer address + Withdrawer(Address), + /// Query stakes by validator and withdrawer addresses + Key(StakeKey
), +} + +impl
Default for QueryStakesKey
+where + Address: Default + Ord, +{ + fn default() -> Self { + QueryStakesKey::Validator(Address::default()) + } +} + +impl TryFrom<(Option, Option)> for QueryStakesKey
+where + Address: Default + Ord, + T: Into
, +{ + type Error = String; + fn try_from(val: (Option, Option)) -> Result { + match val { + (Some(validator), Some(withdrawer)) => Ok(QueryStakesKey::Key(StakeKey { + validator: validator.into(), + withdrawer: withdrawer.into(), + })), + (Some(validator), _) => Ok(QueryStakesKey::Validator(validator.into())), + (_, Some(withdrawer)) => Ok(QueryStakesKey::Withdrawer(withdrawer.into())), + _ => Err(String::from( + "Either a validator address, a withdrawer address or both must be provided.", + )), + } + } +} + /// The main data structure that provides the "stakes tracker" functionality. /// /// This structure holds indexes of stake entries. Because the entries themselves are reference @@ -24,6 +65,10 @@ where { /// A listing of all the stakers, indexed by their address. by_key: BTreeMap, SyncStake>, + /// A listing of all the stakers, indexed by validator. + by_validator: BTreeMap>, + /// A listing of all the stakers, indexed by withdrawer. + by_withdrawer: BTreeMap>, /// A listing of all the stakers, indexed by their coins and address. /// /// Because this uses a compound key to prevent duplicates, if we want to know which addresses @@ -39,7 +84,7 @@ where impl Stakes where - Address: Default, + Address: Default + Send + Sync + Display, Coins: Copy + Default + Ord @@ -49,9 +94,21 @@ where + Add + Sub + Mul - + Mul, - Address: Clone + Ord + 'static, - Epoch: Copy + Default + num_traits::Saturating + Sub + From, + + Mul + + Debug + + Send + + Sync + + Display, + Address: Clone + Ord + 'static + Debug, + Epoch: Copy + + Default + + num_traits::Saturating + + Sub + + From + + Debug + + Display + + Send + + Sync, Power: Copy + Default + Ord + Add + Div, u64: From + From, { @@ -78,12 +135,21 @@ where // Update the position of the staker in the `by_coins` index // If this staker was not indexed by coins, this will index it now - let key = CoinsAndAddresses { + let coins_and_addresses = CoinsAndAddresses { coins, addresses: key, }; - self.by_coins.remove(&key); - self.by_coins.insert(key, stake.clone()); + self.by_coins.remove(&coins_and_addresses); + self.by_coins + .insert(coins_and_addresses.clone(), stake.clone()); + + let validator_key = coins_and_addresses.clone().addresses.validator; + self.by_validator.remove(&validator_key); + self.by_validator.insert(validator_key, stake.clone()); + + let withdrawer_key = coins_and_addresses.addresses.withdrawer; + self.by_withdrawer.remove(&withdrawer_key); + self.by_withdrawer.insert(withdrawer_key, stake.clone()); Ok(stake.value.read()?.clone()) } @@ -228,6 +294,64 @@ where ..Default::default() } } + + /// Query stakes based on different keys. + pub fn query_stakes( + &mut self, + query: TIQSK, + ) -> StakingResult + where + TIQSK: TryInto>, + { + match query.try_into() { + Ok(QueryStakesKey::Key(key)) => self.query_by_key(key), + Ok(QueryStakesKey::Validator(validator)) => self.query_by_validator(validator), + Ok(QueryStakesKey::Withdrawer(withdrawer)) => self.query_by_withdrawer(withdrawer), + Err(_) => Err(StakesError::EmptyQuery), + } + } + + /// Query stakes by stake key. + #[inline(always)] + fn query_by_key(&self, key: StakeKey
) -> StakingResult { + Ok(self + .by_key + .get(&key) + .ok_or(StakesError::EntryNotFound { key })? + .value + .read()? + .coins) + } + + /// Query stakes by validator address. + #[inline(always)] + fn query_by_validator( + &self, + validator: Address, + ) -> StakingResult { + Ok(self + .by_validator + .get(&validator) + .ok_or(StakesError::ValidatorNotFound { validator })? + .value + .read()? + .coins) + } + + /// Query stakes by withdrawer address. + #[inline(always)] + fn query_by_withdrawer( + &self, + withdrawer: Address, + ) -> StakingResult { + Ok(self + .by_withdrawer + .get(&withdrawer) + .ok_or(StakesError::WithdrawerNotFound { withdrawer })? + .value + .read()? + .coins) + } } /// Adds stake, based on the data from a stake transaction. @@ -240,7 +364,15 @@ pub fn process_stake_transaction( epoch: Epoch, ) -> StakingResult<(), PublicKeyHash, Wit, Epoch> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Display + + Send + + Sync, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, @@ -279,7 +411,15 @@ pub fn process_stake_transactions<'a, Epoch, Power>( epoch: Epoch, ) -> Result<(), StakesError> where - Epoch: Copy + Default + Sub + num_traits::Saturating + From + Debug, + Epoch: Copy + + Default + + Sub + + num_traits::Saturating + + From + + Debug + + Send + + Sync + + Display, Power: Add + Copy + Default + Div + Ord + Debug, Wit: Mul, u64: From + From, @@ -591,4 +731,27 @@ mod tests { ] ); } + + #[test] + fn test_query_stakes() { + // First, lets create a setup with a few stakers + let mut stakes = Stakes::::with_minimum(5); + let alice = "Alice"; + let bob = "Bob"; + let charlie = "Charlie"; + let david = "David"; + let erin = "Erin"; + + let alice_charlie = (alice, charlie); + let bob_david = (bob, david); + let charlie_erin = (charlie, erin); + + stakes.add_stake(alice_charlie, 10, 0).unwrap(); + stakes.add_stake(bob_david, 20, 20).unwrap(); + stakes.add_stake(charlie_erin, 30, 30).unwrap(); + + let result = stakes.query_stakes(QueryStakesKey::Key(alice_charlie.into())); + + assert_eq!(result, Ok(10)) + } } diff --git a/node/src/actors/chain_manager/handlers.rs b/node/src/actors/chain_manager/handlers.rs index ec28d633f..eb489c60a 100644 --- a/node/src/actors/chain_manager/handlers.rs +++ b/node/src/actors/chain_manager/handlers.rs @@ -19,6 +19,7 @@ use witnet_data_structures::{ Hashable, NodeStats, PublicKeyHash, SuperBlockVote, SupplyInfo, }, error::{ChainInfoError, TransactionError::DataRequestNotFound}, + staking::errors::StakesError, transaction::{DRTransaction, StakeTransaction, Transaction, VTTransaction}, transaction_factory::{self, NodeBalance}, types::LastBeacon, @@ -37,7 +38,7 @@ use crate::{ GetDataRequestInfo, GetHighestCheckpointBeacon, GetMemoryTransaction, GetMempool, GetMempoolResult, GetNodeStats, GetReputation, GetReputationResult, GetSignalingInfo, GetState, GetSuperBlockVotes, GetSupplyInfo, GetUtxoInfo, IsConfirmedBlock, - PeersBeacons, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, + PeersBeacons, QueryStake, ReputationStats, Rewind, SendLastBeacon, SessionUnitResult, SetLastBeacon, SetPeersLimits, SignalingInfo, SnapshotExport, SnapshotImport, TryMineBlock, }, @@ -1356,6 +1357,17 @@ impl Handler for ChainManager { } } +impl Handler for ChainManager { + type Result = ::Result; + + fn handle(&mut self, msg: QueryStake, _ctx: &mut Self::Context) -> Self::Result { + // build address from public key hash + let stakes = self.chain_state.stakes.query_stakes(msg.key); + + stakes.map_err(StakesError::from).map_err(Into::into) + } +} + impl Handler for ChainManager { type Result = ResponseActFuture>; diff --git a/node/src/actors/json_rpc/api.rs b/node/src/actors/json_rpc/api.rs index 417bb0af6..76c4a1d6c 100644 --- a/node/src/actors/json_rpc/api.rs +++ b/node/src/actors/json_rpc/api.rs @@ -45,8 +45,8 @@ use crate::{ GetConsolidatedPeers, GetDataRequestInfo, GetEpoch, GetHighestCheckpointBeacon, GetItemBlock, GetItemSuperblock, GetItemTransaction, GetKnownPeers, GetMemoryTransaction, GetMempool, GetNodeStats, GetReputation, GetSignalingInfo, - GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, Rewind, - SnapshotExport, SnapshotImport, StakeAuthorization, + GetState, GetSupplyInfo, GetUtxoInfo, InitializePeers, IsConfirmedBlock, QueryStake, + QueryStakesParams, Rewind, SnapshotExport, SnapshotImport, StakeAuthorization, }, peers_manager::PeersManager, sessions_manager::SessionsManager, @@ -139,6 +139,9 @@ pub fn attach_regular_methods( Box::pin(signaling_info()) }); server.add_actix_method(system, "priority", |_params: Params| Box::pin(priority())); + server.add_actix_method(system, "queryStakes", |params: Params| { + Box::pin(query_stakes(params.parse())) + }); } /// Attach the sensitive JSON-RPC methods to a multi-transport server. @@ -2094,6 +2097,62 @@ pub async fn authorize_stake(params: Result) -> JsonRpcRe .await } +/// Param for query_stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesArgument { + /// To query by stake validator + Validator(String), + /// To query by stake withdrawer + Withdrawer(String), + /// To query by stake validator and withdrawer + Key((String, String)), +} + +/// Query the amount of nanowits staked by an address. +pub async fn query_stakes(params: Result, Error>) -> JsonRpcResult { + // Short-circuit if parameters are wrong + let params = params?; + + // If a withdrawer address is not specified, default to local node address + let key: QueryStakesParams = if let Some(address) = params { + match address { + QueryStakesArgument::Validator(validator) => QueryStakesParams::Validator( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + ), + QueryStakesArgument::Withdrawer(withdrawer) => QueryStakesParams::Withdrawer( + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + ), + QueryStakesArgument::Key((validator, withdrawer)) => QueryStakesParams::Key(( + PublicKeyHash::from_bech32(get_environment(), &validator) + .map_err(internal_error)?, + PublicKeyHash::from_bech32(get_environment(), &withdrawer) + .map_err(internal_error)?, + )), + } + } else { + let pk = signature_mngr::public_key().await.map_err(internal_error)?; + + QueryStakesParams::Validator(PublicKeyHash::from_public_key(&pk)) + }; + + ChainManager::from_registry() + .send(QueryStake { key }) + .map(|res| match res { + Ok(Ok(staked_amount)) => serde_json::to_value(staked_amount).map_err(internal_error), + Ok(Err(e)) => { + let err = internal_error_s(e); + Err(err) + } + Err(e) => { + let err = internal_error_s(e); + Err(err) + } + }) + .await +} + #[cfg(test)] mod mock_actix { use actix::{MailboxError, Message}; diff --git a/node/src/actors/messages.rs b/node/src/actors/messages.rs index 7cf75f511..fe605488d 100644 --- a/node/src/actors/messages.rs +++ b/node/src/actors/messages.rs @@ -27,6 +27,7 @@ use witnet_data_structures::{ }, fee::{deserialize_fee_backwards_compatible, Fee}, radon_report::RadonReport, + staking::{aux::StakeKey, stakes::QueryStakesKey}, transaction::{ CommitTransaction, DRTransaction, RevealTransaction, StakeTransaction, Transaction, VTTransaction, @@ -34,6 +35,7 @@ use witnet_data_structures::{ transaction_factory::NodeBalance, types::LastBeacon, utxo_pool::{UtxoInfo, UtxoSelectionStrategy}, + wit::Wit, }; use witnet_p2p::{ error::SessionsError, @@ -305,6 +307,50 @@ impl Message for StakeAuthorization { type Result = Result; } +/// Stake key for quering stakes +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] +pub enum QueryStakesParams { + /// To search by the validator public key hash + Validator(PublicKeyHash), + /// To search by the withdrawer public key hash + Withdrawer(PublicKeyHash), + /// To search by validator and withdrawer public key hashes + Key((PublicKeyHash, PublicKeyHash)), +} + +impl Default for QueryStakesParams { + fn default() -> Self { + QueryStakesParams::Validator(PublicKeyHash::default()) + } +} + +/// Message for querying stakes +#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub struct QueryStake { + /// stake key used to search the stake + pub key: QueryStakesParams, +} + +impl Message for QueryStake { + type Result = Result; +} + +impl
From for QueryStakesKey
+where + Address: Default + Ord + From, +{ + fn from(query: QueryStakesParams) -> Self { + match query { + QueryStakesParams::Key(key) => QueryStakesKey::Key(StakeKey { + validator: key.0.into(), + withdrawer: key.1.into(), + }), + QueryStakesParams::Validator(v) => QueryStakesKey::Validator(v.into()), + QueryStakesParams::Withdrawer(w) => QueryStakesKey::Withdrawer(w.into()), + } + } +} + /// Builds a `DataRequestTransaction` from a `DataRequestOutput` #[derive(Clone, Debug, Default, Hash, Eq, PartialEq, Serialize, Deserialize)] pub struct BuildDrt { From ef7bf95b2f5e1e7af351f658ff30ad9d4894e301 Mon Sep 17 00:00:00 2001 From: tommytrg Date: Wed, 6 Mar 2024 13:41:27 +0100 Subject: [PATCH 2/2] feat(CLI): implement method for querying stakes Please enter the commit message for your changes. Lines starting --- src/cli/node/json_rpc_client.rs | 32 +++++++++++++++++++++++++++++++- src/cli/node/with_node.rs | 14 ++++++++++++++ 2 files changed, 45 insertions(+), 1 deletion(-) diff --git a/src/cli/node/json_rpc_client.rs b/src/cli/node/json_rpc_client.rs index b022a14b4..cd345e139 100644 --- a/src/cli/node/json_rpc_client.rs +++ b/src/cli/node/json_rpc_client.rs @@ -41,7 +41,9 @@ use witnet_data_structures::{ }; use witnet_node::actors::{ chain_manager::run_dr_locally, - json_rpc::api::{AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult}, + json_rpc::api::{ + AddrType, GetBlockChainParams, GetTransactionOutput, PeersResult, QueryStakesArgument, + }, messages::{ AuthorizeStake, BuildDrt, BuildStakeParams, BuildStakeResponse, BuildVtt, GetBalanceTarget, GetReputationResult, MagicEither, SignalingInfo, StakeAuthorization, @@ -1826,6 +1828,34 @@ pub fn priority(addr: SocketAddr, json: bool) -> Result<(), failure::Error> { Ok(()) } +pub fn query_stakes( + addr: SocketAddr, + validator: Option, + withdrawer: Option, +) -> Result<(), failure::Error> { + let mut stream = start_client(addr)?; + + let params = match (validator, withdrawer) { + (Some(validator), Some(withdrawer)) => { + Some(QueryStakesArgument::Key((validator, withdrawer))) + } + (Some(validator), _) => Some(QueryStakesArgument::Validator(validator)), + (_, Some(withdrawer)) => Some(QueryStakesArgument::Withdrawer(withdrawer)), + (None, None) => None, + }; + + let response = send_request( + &mut stream, + &format!( + r#"{{"jsonrpc": "2.0","method": "queryStakes", "params": {}, "id": 1}}"#, + serde_json::to_string(¶ms).unwrap() + ), + )?; + log::info!("{}", response); + + Ok(()) +} + #[derive(Serialize, Deserialize)] struct SignatureWithData { address: String, diff --git a/src/cli/node/with_node.rs b/src/cli/node/with_node.rs index 567e32f91..0a45cbd3a 100644 --- a/src/cli/node/with_node.rs +++ b/src/cli/node/with_node.rs @@ -290,6 +290,11 @@ pub fn exec_cmd( Command::AuthorizeStake { node, withdrawer } => { rpc::authorize_st(node.unwrap_or(default_jsonrpc), withdrawer) } + Command::QueryStakes { + node, + withdrawer, + validator, + } => rpc::query_stakes(node.unwrap_or(default_jsonrpc), withdrawer, validator), } } @@ -785,6 +790,15 @@ pub enum Command { #[structopt(long = "withdrawer")] withdrawer: Option, }, + QueryStakes { + /// Socket address of the Witnet node to query + #[structopt(short = "n", long = "node")] + node: Option, + #[structopt(short = "v", long = "validator")] + validator: Option, + #[structopt(short = "w", long = "withdrawer")] + withdrawer: Option, + }, } #[derive(Debug, StructOpt)]