diff --git a/client/src/rpc_client.rs b/client/src/rpc_client.rs index a93a81d61..3739d5682 100644 --- a/client/src/rpc_client.rs +++ b/client/src/rpc_client.rs @@ -614,26 +614,46 @@ impl RpcClient { } pub fn get_recent_blockhash(&self) -> ClientResult<(Hash, FeeCalculator)> { - Ok(self + let (blockhash, fee_calculator, _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)> { - let Response { + ) -> RpcResult<(Hash, FeeCalculator, Slot)> { + let (context, blockhash, fee_calculator, last_valid_slot) = if let Ok(Response { + context, + value: + RpcFees { + blockhash, + fee_calculator, + last_valid_slot, + }, + }) = + self.send::>(RpcRequest::GetFees, json!([commitment_config])) + { + (context, blockhash, fee_calculator, last_valid_slot) + } else if let Ok(Response { context, value: RpcBlockhashFeeCalculator { blockhash, fee_calculator, }, - } = self.send::>( + }) = self.send::>( RpcRequest::GetRecentBlockhash, json!([commitment_config]), - )?; + ) { + (context, blockhash, fee_calculator, 0) + } else { + return Err(ClientError::new_with_request( + RpcError::ParseError("RpcBlockhashFeeCalculator or RpcFees".to_string()).into(), + RpcRequest::GetRecentBlockhash, + )); + }; let blockhash = blockhash.parse().map_err(|_| { ClientError::new_with_request( @@ -643,7 +663,7 @@ impl RpcClient { })?; Ok(Response { context, - value: (blockhash, fee_calculator), + value: (blockhash, fee_calculator, last_valid_slot), }) } diff --git a/client/src/rpc_request.rs b/client/src/rpc_request.rs index 2edd88ec1..e20fa4de1 100644 --- a/client/src/rpc_request.rs +++ b/client/src/rpc_request.rs @@ -16,15 +16,17 @@ pub enum RpcRequest { GetConfirmedTransaction, GetEpochInfo, GetEpochSchedule, + GetFeeCalculatorForBlockhash, + GetFeeRateGovernor, + GetFees, GetGenesisHash, GetIdentity, GetInflation, GetLargestAccounts, GetLeaderSchedule, + GetMinimumBalanceForRentExemption, GetProgramAccounts, GetRecentBlockhash, - GetFeeCalculatorForBlockhash, - GetFeeRateGovernor, GetSignatureStatuses, GetSlot, GetSlotLeader, @@ -37,13 +39,12 @@ pub enum RpcRequest { GetTransactionCount, GetVersion, GetVoteAccounts, + MinimumLedgerSlot, RegisterNode, RequestAirdrop, SendTransaction, SimulateTransaction, SignVote, - GetMinimumBalanceForRentExemption, - MinimumLedgerSlot, } impl fmt::Display for RpcRequest { @@ -61,15 +62,17 @@ impl fmt::Display for RpcRequest { RpcRequest::GetConfirmedTransaction => "getConfirmedTransaction", RpcRequest::GetEpochInfo => "getEpochInfo", RpcRequest::GetEpochSchedule => "getEpochSchedule", + RpcRequest::GetFeeCalculatorForBlockhash => "getFeeCalculatorForBlockhash", + RpcRequest::GetFeeRateGovernor => "getFeeRateGovernor", + RpcRequest::GetFees => "getFees", RpcRequest::GetGenesisHash => "getGenesisHash", RpcRequest::GetIdentity => "getIdentity", RpcRequest::GetInflation => "getInflation", RpcRequest::GetLargestAccounts => "getLargestAccounts", RpcRequest::GetLeaderSchedule => "getLeaderSchedule", + RpcRequest::GetMinimumBalanceForRentExemption => "getMinimumBalanceForRentExemption", RpcRequest::GetProgramAccounts => "getProgramAccounts", RpcRequest::GetRecentBlockhash => "getRecentBlockhash", - RpcRequest::GetFeeCalculatorForBlockhash => "getFeeCalculatorForBlockhash", - RpcRequest::GetFeeRateGovernor => "getFeeRateGovernor", RpcRequest::GetSignatureStatuses => "getSignatureStatuses", RpcRequest::GetSlot => "getSlot", RpcRequest::GetSlotLeader => "getSlotLeader", @@ -82,13 +85,12 @@ impl fmt::Display for RpcRequest { RpcRequest::GetTransactionCount => "getTransactionCount", RpcRequest::GetVersion => "getVersion", RpcRequest::GetVoteAccounts => "getVoteAccounts", + RpcRequest::MinimumLedgerSlot => "minimumLedgerSlot", RpcRequest::RegisterNode => "registerNode", RpcRequest::RequestAirdrop => "requestAirdrop", RpcRequest::SendTransaction => "sendTransaction", RpcRequest::SimulateTransaction => "simulateTransaction", RpcRequest::SignVote => "signVote", - RpcRequest::GetMinimumBalanceForRentExemption => "getMinimumBalanceForRentExemption", - RpcRequest::MinimumLedgerSlot => "minimumLedgerSlot", }; write!(f, "{}", method) diff --git a/client/src/rpc_response.rs b/client/src/rpc_response.rs index 1fbed4e7a..e3267a750 100644 --- a/client/src/rpc_response.rs +++ b/client/src/rpc_response.rs @@ -35,6 +35,14 @@ pub struct RpcBlockhashFeeCalculator { pub fee_calculator: FeeCalculator, } +#[derive(Serialize, Deserialize, Clone, Debug)] +#[serde(rename_all = "camelCase")] +pub struct RpcFees { + pub blockhash: String, + pub fee_calculator: FeeCalculator, + pub last_valid_slot: Slot, +} + #[derive(Serialize, Deserialize, Clone, Debug)] #[serde(rename_all = "camelCase")] pub struct RpcFeeCalculator { diff --git a/client/src/thin_client.rs b/client/src/thin_client.rs index c5d35f193..036eef1d7 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 b696b89ef..934b25d9c 100644 --- a/core/src/rpc.rs +++ b/core/src/rpc.rs @@ -207,6 +207,22 @@ impl JsonRpcRequestProcessor { ) } + fn get_fees(&self, commitment: Option) -> RpcResponse { + let bank = &*self.bank(commitment)?; + let (blockhash, fee_calculator) = bank.confirmed_last_blockhash(); + let last_valid_slot = bank + .get_blockhash_last_valid_slot(&blockhash) + .expect("bank blockhash queue should contain blockhash"); + new_response( + bank, + RpcFees { + blockhash: blockhash.to_string(), + fee_calculator, + last_valid_slot, + }, + ) + } + fn get_fee_calculator_for_blockhash( &self, blockhash: &Hash, @@ -793,6 +809,13 @@ pub trait RpcSol { commitment: Option, ) -> RpcResponse; + #[rpc(meta, name = "getFees")] + fn get_fees( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse; + #[rpc(meta, name = "getFeeCalculatorForBlockhash")] fn get_fee_calculator_for_blockhash( &self, @@ -1126,6 +1149,15 @@ impl RpcSol for RpcSolImpl { .get_recent_blockhash(commitment) } + fn get_fees( + &self, + meta: Self::Metadata, + commitment: Option, + ) -> RpcResponse { + debug!("get_fees rpc request received"); + meta.request_processor.read().unwrap().get_fees(commitment) + } + fn get_fee_calculator_for_blockhash( &self, meta: Self::Metadata, @@ -1571,6 +1603,7 @@ pub mod tests { get_tmp_ledger_path, }; use solana_sdk::{ + clock::MAX_RECENT_BLOCKHASHES, fee_calculator::DEFAULT_BURN_PERCENT, hash::{hash, Hash}, instruction::InstructionError, @@ -2517,6 +2550,38 @@ pub mod tests { assert_eq!(expected, result); } + #[test] + fn test_rpc_get_fees() { + let bob_pubkey = Pubkey::new_rand(); + let RpcHandler { + io, + meta, + blockhash, + .. + } = start_rpc_handler_with_tx(&bob_pubkey); + + let req = r#"{"jsonrpc":"2.0","id":1,"method":"getFees"}"#; + let res = io.handle_request_sync(&req, meta); + let expected = json!({ + "jsonrpc": "2.0", + "result": { + "context":{"slot":0}, + "value":{ + "blockhash": blockhash.to_string(), + "feeCalculator": { + "lamportsPerSignature": 0, + }, + "lastValidSlot": MAX_RECENT_BLOCKHASHES, + }}, + "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_get_fee_calculator_for_blockhash() { let bob_pubkey = Pubkey::new_rand(); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index d0cd6e522..67c5834b3 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -27,6 +27,7 @@ To interact with a Solana node inside a JavaScript application, use the [solana- * [getEpochSchedule](jsonrpc-api.md#getepochschedule) * [getFeeCalculatorForBlockhash](jsonrpc-api.md#getfeecalculatorforblockhash) * [getFeeRateGovernor](jsonrpc-api.md#getfeerategovernor) +* [getFees](jsonrpc-api.md#getfees) * [getFirstAvailableBlock](jsonrpc-api.md#getfirstavailableblock) * [getGenesisHash](jsonrpc-api.md#getgenesishash) * [getIdentity](jsonrpc-api.md#getidentity) @@ -538,6 +539,34 @@ curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "m {"jsonrpc":"2.0","result":{"context":{"slot":54},"value":{"feeRateGovernor":{"burnPercent":50,"maxLamportsPerSignature":100000,"minLamportsPerSignature":5000,"targetLamportsPerSignature":10000,"targetSignaturesPerSlot":20000}}},"id":1} ``` +### getFees + +Returns a recent block hash from the ledger, a fee schedule that can be used to +compute the cost of submitting a transaction using it, and the last slot in +which the blockhash will be valid. + +#### Parameters: + +* `` - (optional) [Commitment](jsonrpc-api.md#configuring-state-commitment) + +#### Results: + +The result will be an RpcResponse JSON object with `value` set to a JSON object with the following fields: + +* `blockhash: ` - a Hash as base-58 encoded string +* `feeCalculator: ` - FeeCalculator object, the fee schedule for this block hash +* `lastValidSlot: ` - 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":"getFees"}' http://localhost:8899 + +// Result +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"blockhash":"CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR","feeCalculator":{lamportsPerSignature":5000},"lastValidSlot":297}},"id":1} +``` + ### getFirstAvailableBlock Returns the slot of the lowest confirmed block that has not been purged from the ledger @@ -765,7 +794,7 @@ An RpcResponse containing a JSON object consisting of a string blockhash and Fee curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc":"2.0","id":1, "method":"getRecentBlockhash"}' http://localhost:8899 // Result -{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"blockhash":"CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR","feeCalculator":{"burnPercent":50,"lamportsPerSignature":5000,"maxLamportsPerSignature":100000,"minLamportsPerSignature":5000,"targetLamportsPerSignature":10000,"targetSignaturesPerSlot":20000}}},"id":1} +{"jsonrpc":"2.0","result":{"context":{"slot":1},"value":{"blockhash":"CSymwgTNX1j3E4qhKfJAUE41nBWEwXufoYryPbkde5RR","feeCalculator":{"lamportsPerSignature":5000}}},"id":1} ``` ### getSignatureStatuses diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 0ba7695f6..7e7f1999c 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 16020d1a8..be3eab19d 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 {