Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rpc: add getBlockhashLastValidSlot endpoint #10237

Closed
Show file tree
Hide file tree
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
Expand Up @@ -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:
Expand All @@ -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(),
Expand All @@ -643,7 +664,11 @@ impl RpcClient {
})?;
Ok(Response {
context,
value: (blockhash, fee_calculator),
value: (
blockhash,
fee_calculator,
blockhash_last_valid_slot.unwrap(),
),
})
}

Expand Down
2 changes: 2 additions & 0 deletions client/src/rpc_request.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum RpcRequest {
ValidatorExit,
GetAccountInfo,
GetBalance,
GetBlockhashLastValidSlot,
GetBlockTime,
GetClusterNodes,
GetConfirmedBlock,
Expand Down Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion client/src/thin_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
86 changes: 84 additions & 2 deletions core/src/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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>,
Expand Down Expand Up @@ -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>>,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand All @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
27 changes: 27 additions & 0 deletions docs/src/apps/jsonrpc-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
9 changes: 9 additions & 0 deletions runtime/src/bank.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

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