diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index a93a81d61044d6..e4b2286ec5a7aa 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -614,15 +614,16 @@ impl RpcClient { } pub fn get_recent_blockhash(&self) -> ClientResult<(Hash, FeeCalculator)> { - Ok(self + let (blockhash, fee_calculator, _blockhash_last_valid_slot) = self .get_recent_blockhash_with_commitment(CommitmentConfig::default())? - .value) + .value; + Ok((blockhash, fee_calculator)) } pub fn get_recent_blockhash_with_commitment( &self, commitment_config: CommitmentConfig, - ) -> RpcResult<(Hash, FeeCalculator)> { + ) -> RpcResult<(Hash, FeeCalculator, Slot)> { let Response { context, value: @@ -635,6 +636,26 @@ impl RpcClient { json!([commitment_config]), )?; + let Response { + context: _context, + value: blockhash_last_valid_slot, + } = self + .send::>>( + RpcRequest::GetBlockhashLastValidSlot, + json!([commitment_config]), + ) + .unwrap_or(Some(0)); // Provides backward compatibility for old nodes that do not support the getBlockhashLastValidSlot endpoint + + if blockhash_last_valid_slot.is_none() { + return Err(ClientError::new_with_request( + RpcError::RpcRequestError(format!( + "Recent blockhash no longer present in blockhash queue" + )) + .into(), + RpcRequest::GetBlockhashLastValidSlot, + )); + } + let blockhash = blockhash.parse().map_err(|_| { ClientError::new_with_request( RpcError::ParseError("Hash".to_string()).into(), @@ -643,7 +664,11 @@ impl RpcClient { })?; Ok(Response { context, - value: (blockhash, fee_calculator), + value: ( + blockhash, + fee_calculator, + blockhash_last_valid_slot.unwrap(), + ), }) } diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 2edd88ec100baa..0caa691fba6028 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -8,6 +8,7 @@ pub enum RpcRequest { ValidatorExit, GetAccountInfo, GetBalance, + GetBlockhashLastValidSlot, GetBlockTime, GetClusterNodes, GetConfirmedBlock, @@ -53,6 +54,7 @@ impl fmt::Display for RpcRequest { RpcRequest::ValidatorExit => "validatorExit", RpcRequest::GetAccountInfo => "getAccountInfo", RpcRequest::GetBalance => "getBalance", + RpcRequest::GetBlockhashLastValidSlot => "getBlockhashLastValidSlot", RpcRequest::GetBlockTime => "getBlockTime", RpcRequest::GetClusterNodes => "getClusterNodes", RpcRequest::GetConfirmedBlock => "getConfirmedBlock", diff --git a/client/src/thin_client.rs b/client/src/thin_client.rs index c5d35f1938ad70..036eef1d75f003 100644 --- a/client/src/thin_client.rs +++ b/client/src/thin_client.rs @@ -441,7 +441,7 @@ impl SyncClient for ThinClient { match recent_blockhash { Ok(Response { value, .. }) => { self.optimizer.report(index, duration_as_ms(&now.elapsed())); - Ok(value) + Ok((value.0, value.1)) } Err(e) => { self.optimizer.report(index, std::u64::MAX); diff --git a/core/src/rpc.rs b/core/src/rpc.rs index b696b89ef9b8e8..5867cc031da16f 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -230,6 +230,15 @@ impl JsonRpcRequestProcessor { ) } + fn get_blockhash_last_valid_slot( + &self, + blockhash: &Hash, + commitment: Option, + ) -> RpcResponse> { + let bank = &*self.bank(commitment)?; + new_response(bank, bank.get_blockhash_last_valid_slot(blockhash)) + } + pub fn confirm_transaction( &self, signature: Result, @@ -677,6 +686,10 @@ fn verify_signature(input: &str) -> Result { .map_err(|e| Error::invalid_params(format!("{:?}", e))) } +fn verify_hash(input: &str) -> Result { + Hash::from_str(input).map_err(|e| Error::invalid_params(format!("{:?}", e))) +} + #[derive(Clone)] pub struct Meta { pub request_processor: Arc>, @@ -803,6 +816,14 @@ pub trait RpcSol { #[rpc(meta, name = "getFeeRateGovernor")] fn get_fee_rate_governor(&self, meta: Self::Metadata) -> RpcResponse; + #[rpc(meta, name = "getBlockhashLastValidSlot")] + fn get_blockhash_last_valid_slot( + &self, + meta: Self::Metadata, + blockhash: String, + commitment: Option, + ) -> RpcResponse>; + #[rpc(meta, name = "getSignatureStatuses")] fn get_signature_statuses( &self, @@ -1132,8 +1153,7 @@ impl RpcSol for RpcSolImpl { blockhash: String, ) -> RpcResponse> { debug!("get_fee_calculator_for_blockhash rpc request received"); - let blockhash = - Hash::from_str(&blockhash).map_err(|e| Error::invalid_params(format!("{:?}", e)))?; + let blockhash = verify_hash(&blockhash)?; meta.request_processor .read() .unwrap() @@ -1148,6 +1168,20 @@ impl RpcSol for RpcSolImpl { .get_fee_rate_governor() } + fn get_blockhash_last_valid_slot( + &self, + meta: Self::Metadata, + blockhash: String, + commitment: Option, + ) -> RpcResponse> { + debug!("get_blockhash_last_valid_slot rpc request received"); + let blockhash = verify_hash(&blockhash)?; + meta.request_processor + .read() + .unwrap() + .get_blockhash_last_valid_slot(&blockhash, commitment) + } + fn get_signature_confirmation( &self, meta: Self::Metadata, @@ -2594,6 +2628,54 @@ pub mod tests { assert_eq!(expected, result); } + #[test] + fn test_rpc_get_blockhash_last_valid_slot() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { io, meta, bank, .. } = start_rpc_handler_with_tx(&bob_pubkey); + + let (blockhash, _fee_calculator) = bank.last_blockhash_with_fee_calculator(); + let valid_last_slot = bank.get_blockhash_last_valid_slot(&blockhash); + + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getBlockhashLastValidSlot","params":["{:?}"]}}"#, + blockhash + ); + let res = io.handle_request_sync(&req, meta.clone()); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":valid_last_slot, + }, + "id": 1 + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + + // Expired (non-existent) blockhash + let req = format!( + r#"{{"jsonrpc":"2.0","id":1,"method":"getBlockhashLastValidSlot","params":["{:?}"]}}"#, + Hash::default() + ); + let res = io.handle_request_sync(&req, meta); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":Value::Null, + }, + "id": 1 + }); + let expected: Response = + serde_json::from_value(expected).expect("expected response deserialization"); + let result: Response = serde_json::from_str(&res.expect("actual response")) + .expect("actual response deserialization"); + assert_eq!(expected, result); + } + #[test] fn test_rpc_fail_request_airdrop() { let bob_pubkey = Pubkey::new_rand(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index e1937490cab837..28046e75a75ea4 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -17,6 +17,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getAccountInfo](jsonrpc-api.md#getaccountinfo) * [getBalance](jsonrpc-api.md#getbalance) * [getBlockCommitment](jsonrpc-api.md#getblockcommitment) +* [getBlockhashLastValidSlot](jsonrpc-api.md#getblockhashlastvalidslot) * [getBlockTime](jsonrpc-api.md#getblocktime) * [getClusterNodes](jsonrpc-api.md#getclusternodes) * [getConfirmedBlock](jsonrpc-api.md#getconfirmedblock) @@ -213,6 +214,32 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":{"commitment":[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,10,32],"totalStake": 42},"id":1} ``` +### getBlockhashLastValidSlot + +Returns the last slot in which a blockhash will be valid + +#### Parameters: + +* `blockhash: `, query blockhash as a Base58 encoded string +* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` equal to either: + +* `` - blockhash is not present in blockhash queue, because it has expired or is not valid on this fork +* `` - last slot in which a blockhash will be valid + +#### Example: + +```bash +// Request +curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getBlockhashLastValidSlot","params":["GJxqhuxcgfn5Tcj6y3f8X4FeCDd2RQ6SnEMo1AAxrPRZ"]}' 127.0.0.1:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":221},"value":400},"id":1} +``` + ### getBlockTime Returns the estimated production time of a block. diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 0ba7695f6b810c..7e7f1999c07ae5 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -936,6 +936,15 @@ impl Bank { &self.fee_rate_governor } + pub fn get_blockhash_last_valid_slot(&self, blockhash: &Hash) -> Option { + let blockhash_queue = self.blockhash_queue.read().unwrap(); + // This calculation will need to be updated to consider epoch boundaries if BlockhashQueue + // length is made variable by epoch + blockhash_queue + .get_hash_age(blockhash) + .map(|age| self.slot + blockhash_queue.len() as u64 - age) + } + pub fn confirmed_last_blockhash(&self) -> (Hash, FeeCalculator) { const NUM_BLOCKHASH_CONFIRMATIONS: usize = 3; diff --git a/runtime/src/blockhash_queue.rs b/runtime/src/blockhash_queue.rs index 16020d1a83c0b3..be3eab19d32733 100644 --- a/runtime/src/blockhash_queue.rs +++ b/runtime/src/blockhash_queue.rs @@ -58,6 +58,12 @@ impl BlockhashQueue { .map(|age| self.hash_height - age.hash_height <= max_age as u64) } + pub fn get_hash_age(&self, hash: &Hash) -> Option { + self.ages + .get(hash) + .map(|age| self.hash_height - age.hash_height) + } + /// check if hash is valid #[cfg(test)] pub fn check_hash(&self, hash: Hash) -> bool { @@ -119,6 +125,10 @@ impl BlockhashQueue { .iter() .map(|(k, v)| recent_blockhashes::IterItem(v.hash_height, k, &v.fee_calculator)) } + + pub fn len(&self) -> usize { + self.max_age + } } #[cfg(test)] mod tests {