diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 48305bc3fd..3774b78cd4 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -38,7 +38,7 @@ pub struct RpcLargestAccountsConfig { #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] -pub struct RpcInflationConfig { +pub struct RpcStakeConfig { pub epoch: Option, #[serde(flatten)] pub commitment: Option, diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 1ba4c0ede0..9252c3620f 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -260,3 +260,20 @@ pub struct RpcSupply { pub non_circulating: u64, pub non_circulating_accounts: Vec, } + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub enum StakeActivationState { + Activating, + Active, + Deactivating, + Inactive, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcStakeActivation { + pub state: StakeActivationState, + pub active: u64, + pub inactive: u64, +} diff --git a/core/src/rpc.rs b/core/src/rpc.rs index b9a745a9af..002cb0ec41 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -29,15 +29,19 @@ use solana_ledger::{ use solana_perf::packet::PACKET_DATA_SIZE; use solana_runtime::{accounts::AccountAddressFilter, bank::Bank}; use solana_sdk::{ + account_utils::StateMut, clock::{Epoch, Slot, UnixTimestamp}, commitment_config::{CommitmentConfig, CommitmentLevel}, epoch_schedule::EpochSchedule, hash::Hash, pubkey::Pubkey, signature::Signature, + stake_history::StakeHistory, + sysvar::{stake_history, Sysvar}, timing::slot_duration_from_slots_per_year, transaction::{self, Transaction}, }; +use solana_stake_program::stake_state::StakeState; use solana_transaction_status::{ ConfirmedBlock, ConfirmedTransaction, TransactionEncoding, TransactionStatus, }; @@ -748,6 +752,67 @@ impl JsonRpcRequestProcessor { .get_first_available_block() .unwrap_or_default()) } + + pub fn get_stake_activation( + &self, + pubkey: &Pubkey, + config: Option, + ) -> Result { + let config = config.unwrap_or_default(); + let bank = self.bank(config.commitment)?; + let epoch = config.epoch.unwrap_or_else(|| bank.epoch()); + if bank.epoch().saturating_sub(epoch) > solana_sdk::stake_history::MAX_ENTRIES as u64 { + return Err(Error::invalid_params(format!( + "Invalid param: epoch {:?} is too far in the past", + epoch + ))); + } + if epoch > bank.epoch() { + return Err(Error::invalid_params(format!( + "Invalid param: epoch {:?} has not yet started", + epoch + ))); + } + + let stake_account = bank + .get_account(pubkey) + .ok_or_else(|| Error::invalid_params("Invalid param: account not found".to_string()))?; + let stake_state: StakeState = stake_account + .state() + .map_err(|_| Error::invalid_params("Invalid param: not a stake account".to_string()))?; + let delegation = stake_state.delegation().ok_or_else(|| { + Error::invalid_params("Invalid param: stake account has not been delegated".to_string()) + })?; + + let stake_history_account = bank + .get_account(&stake_history::id()) + .ok_or_else(Error::internal_error)?; + let stake_history = + StakeHistory::from_account(&stake_history_account).ok_or_else(Error::internal_error)?; + + let (active, activating, deactivating) = + delegation.stake_activating_and_deactivating(epoch, Some(&stake_history)); + let stake_activation_state = if deactivating > 0 { + StakeActivationState::Deactivating + } else if activating > 0 { + StakeActivationState::Activating + } else if active > 0 { + StakeActivationState::Active + } else { + StakeActivationState::Inactive + }; + let inactive_stake = match stake_activation_state { + StakeActivationState::Activating => activating, + StakeActivationState::Active => 0, + StakeActivationState::Deactivating => delegation.stake.saturating_sub(active), + StakeActivationState::Inactive => delegation.stake, + }; + Ok(RpcStakeActivation { + state: stake_activation_state, + active, + inactive: inactive_stake, + }) + } } fn verify_pubkey(input: String) -> Result { @@ -1057,6 +1122,14 @@ pub trait RpcSol { #[rpc(meta, name = "getFirstAvailableBlock")] fn get_first_available_block(&self, meta: Self::Metadata) -> Result; + + #[rpc(meta, name = "getStakeActivation")] + fn get_stake_activation( + &self, + meta: Self::Metadata, + pubkey_str: String, + config: Option, + ) -> Result; } pub struct RpcSolImpl; @@ -1610,6 +1683,20 @@ impl RpcSol for RpcSolImpl { fn get_first_available_block(&self, meta: Self::Metadata) -> Result { meta.get_first_available_block() } + + fn get_stake_activation( + &self, + meta: Self::Metadata, + pubkey_str: String, + config: Option, + ) -> Result { + debug!( + "get_stake_activation rpc request received: {:?}", + pubkey_str + ); + let pubkey = verify_pubkey(pubkey_str)?; + meta.get_stake_activation(&pubkey, config) + } } fn deserialize_bs58_transaction(bs58_transaction: String) -> Result<(Vec, Transaction)> { diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index 16c588829a..2b014d3197 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -42,6 +42,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getSlot](jsonrpc-api.md#getslot) * [getSlotLeader](jsonrpc-api.md#getslotleader) * [getSlotsPerSegment](jsonrpc-api.md#getslotspersegment) +* [getStakeActivation](jsonrpc-api.md#getstakeactivation) * [getStoragePubkeysForSlot](jsonrpc-api.md#getstoragepubkeysforslot) * [getStorageTurn](jsonrpc-api.md#getstorageturn) * [getStorageTurnRate](jsonrpc-api.md#getstorageturnrate) @@ -944,6 +945,41 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":1024,"id":1} ``` +### getStakeActivation + +Returns epoch activation information for a stake account + +#### Parameters: + +* `` - Pubkey of stake account to query, as base-58 encoded string +* `` - (optional) Configuration object containing the following optional fields: + * (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + * (optional) `epoch: ` - epoch for which to calculate activation details. If parameter not provided, defaults to current epoch. + +#### Results: + +The result will be a JSON object with the following fields: + +* `state: ` - stake active during the epoch +* `inactive: ` - stake inactive during the epoch + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getStakeActivation", "params": ["CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT"]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"active":197717120,"inactive":0,"state":"active"},"id":1} + +// Request with Epoch +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getStakeActivation", "params": ["CYRJWqiSjLitBAcRxPvWpgX3s5TvmN2SuRY3eEYypFvT", {"epoch": 4}]}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"active":124429280,"inactive":73287840,"state":"activating"},"id":1} +``` + ### getStoragePubkeysForSlot Returns the storage Pubkeys for a particular slot diff --git a/programs/stake/src/stake_state.rs b/programs/stake/src/stake_state.rs index 4be2c6d989..5fe73714b9 100644 --- a/programs/stake/src/stake_state.rs +++ b/programs/stake/src/stake_state.rs @@ -208,7 +208,7 @@ impl Delegation { } #[allow(clippy::comparison_chain)] - fn stake_activating_and_deactivating( + pub fn stake_activating_and_deactivating( &self, epoch: Epoch, history: Option<&StakeHistory>,