From 49a415414f70cbdb795768086580d6e30b0bcf70 Mon Sep 17 00:00:00 2001 From: Michael Vines Date: Wed, 28 Apr 2021 09:57:05 -0700 Subject: [PATCH] Add getBlockProduction RPC method --- client/src/rpc_client.rs | 12 ++ client/src/rpc_config.rs | 16 ++ client/src/rpc_request.rs | 2 + client/src/rpc_response.rs | 15 ++ core/src/rpc.rs | 166 +++++++++++++++++---- docs/src/developing/clients/jsonrpc-api.md | 106 +++++++++++++ 6 files changed, 288 insertions(+), 29 deletions(-) diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index bfbce0d082..9a94d31b2a 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -422,6 +422,18 @@ impl RpcClient { }) } + /// Get block production for the current epoch + pub fn get_block_production(&self) -> RpcResult { + self.send(RpcRequest::GetBlockProduction, Value::Null) + } + + pub fn get_block_production_with_config( + &self, + config: RpcBlockProductionConfig, + ) -> RpcResult { + self.send(RpcRequest::GetBlockProduction, json!(config)) + } + pub fn get_stake_activation( &self, stake_account: Pubkey, diff --git a/client/src/rpc_config.rs b/client/src/rpc_config.rs index 0826e35cc1..bb939d04ae 100644 --- a/client/src/rpc_config.rs +++ b/client/src/rpc_config.rs @@ -49,6 +49,22 @@ pub struct RpcLeaderScheduleConfig { pub commitment: Option, } +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcBlockProductionConfigRange { + pub first_slot: Slot, + pub last_slot: Option, +} + +#[derive(Debug, Default, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcBlockProductionConfig { + pub identity: Option, // validator identity, as a base-58 encoded string + pub range: Option, // current epoch if `None` + #[serde(flatten)] + pub commitment: Option, +} + #[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct RpcGetVoteAccountsConfig { diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index c76cbc3e6f..5bafad08cb 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -11,6 +11,7 @@ pub enum RpcRequest { DeregisterNode, GetAccountInfo, GetBalance, + GetBlockProduction, GetBlockTime, GetClusterNodes, GetConfirmedBlock, @@ -83,6 +84,7 @@ impl fmt::Display for RpcRequest { RpcRequest::DeregisterNode => "deregisterNode", RpcRequest::GetAccountInfo => "getAccountInfo", RpcRequest::GetBalance => "getBalance", + RpcRequest::GetBlockProduction => "getBlockProduction", RpcRequest::GetBlockTime => "getBlockTime", RpcRequest::GetClusterNodes => "getClusterNodes", RpcRequest::GetConfirmedBlock => "getConfirmedBlock", diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 3af12c4ed3..0b03d400c0 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -211,6 +211,21 @@ pub struct RpcContactInfo { /// Map of leader base58 identity pubkeys to the slot indices relative to the first epoch slot pub type RpcLeaderSchedule = HashMap>; +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RpcBlockProductionRange { + pub first_slot: Slot, + pub last_slot: Slot, +} + +#[derive(Serialize, Deserialize, Clone)] +#[serde(rename_all = "camelCase")] +pub struct RpcBlockProduction { + /// Map of leader base58 identity pubkeys to a tuple of `(number of leader slots, number of blocks produced)` + pub by_identity: HashMap, + pub range: RpcBlockProductionRange, +} + #[derive(Serialize, Deserialize, Clone)] #[serde(rename_all = "kebab-case")] pub struct RpcVersionInfo { diff --git a/core/src/rpc.rs b/core/src/rpc.rs index 75236b86f3..d3352f9106 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -603,6 +603,43 @@ impl JsonRpcRequestProcessor { self.bank(commitment).collector_id().to_string() } + fn get_slot_leaders( + &self, + commitment: Option, + start_slot: Slot, + limit: usize, + ) -> Result> { + let bank = self.bank(commitment); + + let (mut epoch, mut slot_index) = + bank.epoch_schedule().get_epoch_and_slot_index(start_slot); + + let mut slot_leaders = Vec::with_capacity(limit); + while slot_leaders.len() < limit { + if let Some(leader_schedule) = + self.leader_schedule_cache.get_epoch_leader_schedule(epoch) + { + slot_leaders.extend( + leader_schedule + .get_slot_leaders() + .iter() + .skip(slot_index as usize) + .take(limit.saturating_sub(slot_leaders.len())), + ); + } else { + return Err(Error::invalid_params(format!( + "Invalid slot range: leader schedule for epoch {} is unavailable", + epoch + ))); + } + + epoch += 1; + slot_index = 0; + } + + Ok(slot_leaders) + } + fn minimum_ledger_slot(&self) -> Result { match self.blockstore.slot_meta_iterator(0) { Ok(mut metas) => match metas.next() { @@ -2542,6 +2579,13 @@ pub mod rpc_full { config: Option, ) -> Result; + #[rpc(meta, name = "getBlockProduction")] + fn get_block_production( + &self, + meta: Self::Metadata, + config: Option, + ) -> Result>; + // SPL Token-specific RPC endpoints // See https://github.com/solana-labs/solana-program-library/releases/tag/token-v2.0.0 for // program details @@ -3101,35 +3145,11 @@ pub mod rpc_full { ))); } - let bank = meta.bank(None); - let (mut epoch, mut slot_index) = - bank.epoch_schedule().get_epoch_and_slot_index(start_slot); - - let mut slot_leaders = Vec::with_capacity(limit); - while slot_leaders.len() < limit { - if let Some(leader_schedule) = - meta.leader_schedule_cache.get_epoch_leader_schedule(epoch) - { - slot_leaders.extend( - leader_schedule - .get_slot_leaders() - .iter() - .skip(slot_index as usize) - .take(limit.saturating_sub(slot_leaders.len())) - .map(|pubkey| pubkey.to_string()), - ); - } else { - return Err(Error::invalid_params(format!( - "Invalid slot range: leader schedule for epoch {} is unavailable", - epoch - ))); - } - - epoch += 1; - slot_index = 0; - } - - Ok(slot_leaders) + Ok(meta + .get_slot_leaders(None, start_slot, limit)? + .into_iter() + .map(|identity| identity.to_string()) + .collect()) } fn minimum_ledger_slot(&self, meta: Self::Metadata) -> Result { @@ -3272,6 +3292,94 @@ pub mod rpc_full { Ok(meta.get_first_available_block()) } + fn get_block_production( + &self, + meta: Self::Metadata, + config: Option, + ) -> Result> { + debug!("get_block_production rpc request received"); + + let config = config.unwrap_or_default(); + let filter_by_identity = if let Some(ref identity) = config.identity { + Some(verify_pubkey(identity)?) + } else { + None + }; + + let bank = meta.bank(config.commitment); + let (first_slot, last_slot) = match config.range { + None => ( + bank.epoch_schedule().get_first_slot_in_epoch(bank.epoch()), + bank.slot(), + ), + Some(range) => { + let first_slot = range.first_slot; + let last_slot = range.last_slot.unwrap_or_else(|| bank.slot()); + if last_slot < first_slot { + return Err(Error::invalid_params(format!( + "lastSlot, {}, cannot be less than firstSlot, {}", + last_slot, first_slot + ))); + } + (first_slot, last_slot) + } + }; + + let slot_history = bank.get_slot_history(); + if first_slot < slot_history.oldest() { + return Err(Error::invalid_params(format!( + "firstSlot, {}, is too small; min {}", + first_slot, + slot_history.oldest() + ))); + } + if last_slot > slot_history.newest() { + return Err(Error::invalid_params(format!( + "lastSlot, {}, is too large; max {}", + last_slot, + slot_history.newest() + ))); + } + + let slot_leaders = meta.get_slot_leaders( + config.commitment, + first_slot, + last_slot.saturating_sub(first_slot) as usize + 1, // +1 because last_slot is inclusive + )?; + + let mut block_production: HashMap<_, (usize, usize)> = HashMap::new(); + + let mut slot = first_slot; + for identity in slot_leaders { + slot += 1; + if let Some(ref filter_by_identity) = filter_by_identity { + if identity != *filter_by_identity { + continue; + } + } + + let mut entry = block_production.entry(identity).or_default(); + if slot_history.check(slot) == solana_sdk::slot_history::Check::Found { + entry.1 += 1; // Increment blocks_produced + } + entry.0 += 1; // Increment leader_slots + } + + Ok(new_response( + &bank, + RpcBlockProduction { + by_identity: block_production + .into_iter() + .map(|(k, v)| (k.to_string(), v)) + .collect(), + range: RpcBlockProductionRange { + first_slot, + last_slot, + }, + }, + )) + } + fn get_stake_activation( &self, meta: Self::Metadata, diff --git a/docs/src/developing/clients/jsonrpc-api.md b/docs/src/developing/clients/jsonrpc-api.md index c46c4e904d..d9c8d05289 100644 --- a/docs/src/developing/clients/jsonrpc-api.md +++ b/docs/src/developing/clients/jsonrpc-api.md @@ -20,6 +20,7 @@ gives a convenient interface for the RPC methods. - [getAccountInfo](jsonrpc-api.md#getaccountinfo) - [getBalance](jsonrpc-api.md#getbalance) +- [getBlockProduction](jsonrpc-api.md#getblockproduction) - [getBlockCommitment](jsonrpc-api.md#getblockcommitment) - [getBlockTime](jsonrpc-api.md#getblocktime) - [getClusterNodes](jsonrpc-api.md#getclusternodes) @@ -680,6 +681,111 @@ The JSON structure of token balances is defined as a list of objects in the foll - `uiAmount: ` - Token amount as a float, accounting for decimals. **DEPRECATED** - `uiAmountString: ` - Token amount as a string, accounting for decimals. +### getBlockProduction + +Returns recent block production information from the current or previous epoch. + +#### Parameters: + +- `` - (optional) Configuration object containing the following optional fields: + - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + - (optional) `range: ` - Slot range to return block production for. If parameter not provided, defaults to current epoch. + - `firstSlot: ` - first slot to return block production information for (inclusive) + - (optional) `lastSlot: ` - last slot to return block production information for (inclusive). If parameter not provided, defaults to the highest slot + - (optional) `identity: ` - Only return results for this validator identity (base-58 encoded) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to: +- `` + - `byIdentity: ` - a dictionary of validator identities, + as base-58 encoded strings. Value is a two element array containing the + number of leader slots and the number of blocks produced. + - `range: ` - Slot range to return block production for. If parameter not provided, defaults to current epoch. + - `firstSlot: ` - first slot of the block production information (inclusive) + - `lastSlot: ` - last slot of block production information (inclusive) + +#### Example: + +Request: +```bash +curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d ' + {"jsonrpc":"2.0","id":1, "method":"getBlockProduction"} +' +``` + +Result: +```json +{ + "jsonrpc": "2.0", + "result": { + "context": { + "slot": 9887 + }, + "value": { + "byIdentity": { + "85iYT5RuzRTDgjyRa3cP8SYhM2j21fj7NhfJ3peu1DPr": [ + 9888, + 9886 + ] + }, + "range": { + "firstSlot": 0, + "lastSlot": 9887, + } + } + }, + "id": 1 +} +``` + +#### Example: + +Request: +```bash +curl http://localhost:8899 -X POST -H "Content-Type: application/json" -d ' + { + "jsonrpc": "2.0", + "id": 1, + "method": "getBlockProduction", + "params": [ + { + "identity": "85iYT5RuzRTDgjyRa3cP8SYhM2j21fj7NhfJ3peu1DPr", + "range": { + "firstSlot": 40, + "lastSlot": 50 + } + } + ] + } +' +``` + +Result: +```json +{ + "jsonrpc": "2.0", + "result": { + "context": { + "slot": 10102 + }, + "value": { + "byIdentity": { + "85iYT5RuzRTDgjyRa3cP8SYhM2j21fj7NhfJ3peu1DPr": [ + 11, + 11 + ] + }, + "range": { + "firstSlot": 50, + "lastSlot": 40 + } + } + }, + "id": 1 +} +``` + ### getConfirmedBlocks Returns a list of confirmed blocks between two slots