Skip to content
This repository has been archived by the owner on Jan 22, 2025. It is now read-only.

Rpc: add getBlockhashLastValidSlot endpoint #10237

Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 29 additions & 4 deletions client/src/rpc_client.rs
Original file line number Diff line number Diff line change
@@ -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::<Response<Option<Slot>>>(
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(),
),
})
}

2 changes: 2 additions & 0 deletions client/src/rpc_request.rs
Original file line number Diff line number Diff line change
@@ -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",
2 changes: 1 addition & 1 deletion client/src/thin_client.rs
Original file line number Diff line number Diff line change
@@ -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);
86 changes: 84 additions & 2 deletions core/src/rpc.rs
Original file line number Diff line number Diff line change
@@ -230,6 +230,15 @@ impl JsonRpcRequestProcessor {
)
}

fn get_blockhash_last_valid_slot(
&self,
blockhash: &Hash,
commitment: Option<CommitmentConfig>,
) -> RpcResponse<Option<Slot>> {
let bank = &*self.bank(commitment)?;
new_response(bank, bank.get_blockhash_last_valid_slot(blockhash))
}

pub fn confirm_transaction(
&self,
signature: Result<Signature>,
@@ -677,6 +686,10 @@ fn verify_signature(input: &str) -> Result<Signature> {
.map_err(|e| Error::invalid_params(format!("{:?}", e)))
}

fn verify_hash(input: &str) -> Result<Hash> {
Hash::from_str(input).map_err(|e| Error::invalid_params(format!("{:?}", e)))
}

#[derive(Clone)]
pub struct Meta {
pub request_processor: Arc<RwLock<JsonRpcRequestProcessor>>,
@@ -803,6 +816,14 @@ pub trait RpcSol {
#[rpc(meta, name = "getFeeRateGovernor")]
fn get_fee_rate_governor(&self, meta: Self::Metadata) -> RpcResponse<RpcFeeRateGovernor>;

#[rpc(meta, name = "getBlockhashLastValidSlot")]
fn get_blockhash_last_valid_slot(
&self,
meta: Self::Metadata,
blockhash: String,
commitment: Option<CommitmentConfig>,
) -> RpcResponse<Option<Slot>>;

#[rpc(meta, name = "getSignatureStatuses")]
fn get_signature_statuses(
&self,
@@ -1132,8 +1153,7 @@ impl RpcSol for RpcSolImpl {
blockhash: String,
) -> RpcResponse<Option<RpcFeeCalculator>> {
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<CommitmentConfig>,
) -> RpcResponse<Option<Slot>> {
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();
27 changes: 27 additions & 0 deletions docs/src/apps/jsonrpc-api.md
Original file line number Diff line number Diff line change
@@ -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: <string>`, query blockhash as a Base58 encoded string
* `<object>` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment)

#### Results:

The result will be an RpcResponse JSON object with `value` equal to either:

* `<null>` - blockhash is not present in blockhash queue, because it has expired or is not valid on this fork
* `<u64>` - 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.
9 changes: 9 additions & 0 deletions runtime/src/bank.rs
Original file line number Diff line number Diff line change
@@ -936,6 +936,15 @@ impl Bank {
&self.fee_rate_governor
}

pub fn get_blockhash_last_valid_slot(&self, blockhash: &Hash) -> Option<Slot> {
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;

10 changes: 10 additions & 0 deletions runtime/src/blockhash_queue.rs
Original file line number Diff line number Diff line change
@@ -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<u64> {
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 {