lite-rpc/lite-rpc/src/bridge.rs

707 lines
24 KiB
Rust

use itertools::Itertools;
use jsonrpsee::core::RpcResult;
use prometheus::{opts, register_int_counter, IntCounter};
use solana_account_decoder::UiAccount;
use solana_lite_rpc_accounts::account_service::AccountService;
use solana_lite_rpc_prioritization_fees::account_prio_service::AccountPrioService;
use solana_lite_rpc_prioritization_fees::prioritization_fee_calculation_method::PrioritizationFeeCalculationMethod;
use solana_rpc_client::nonblocking::rpc_client::RpcClient;
use solana_rpc_client_api::config::RpcAccountInfoConfig;
use solana_rpc_client_api::response::{OptionalContext, RpcKeyedAccount};
use solana_rpc_client_api::{
config::{
RpcBlocksConfigWrapper, RpcContextConfig, RpcGetVoteAccountsConfig,
RpcLeaderScheduleConfig, RpcProgramAccountsConfig, RpcRequestAirdropConfig,
RpcSignatureStatusConfig, RpcSignaturesForAddressConfig,
},
response::{
Response as RpcResponse, RpcBlockhash, RpcConfirmedTransactionStatusWithSignature,
RpcContactInfo, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext, RpcVersionInfo,
RpcVoteAccountStatus,
},
};
use solana_sdk::epoch_info::EpochInfo;
use solana_sdk::signature::Signature;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, slot_history::Slot};
use solana_transaction_status::{TransactionStatus, UiConfirmedBlock};
use std::collections::HashMap;
use std::str::FromStr;
use std::sync::Arc;
use solana_lite_rpc_blockstore::history::History;
use solana_lite_rpc_core::solana_utils::hash_from_str;
use solana_lite_rpc_core::{
encoding,
stores::{block_information_store::BlockInformation, data_cache::DataCache},
};
use solana_lite_rpc_services::{
transaction_service::TransactionService, tx_sender::TXS_IN_CHANNEL,
};
use crate::rpc_errors::RpcErrors;
use crate::{
configs::{IsBlockHashValidConfig, SendTransactionConfig},
rpc::LiteRpcServer,
};
use solana_lite_rpc_prioritization_fees::rpc_data::{AccountPrioFeesStats, PrioFeesStats};
use solana_lite_rpc_prioritization_fees::PrioFeesService;
lazy_static::lazy_static! {
static ref RPC_SEND_TX: IntCounter =
register_int_counter!(opts!("literpc_rpc_send_tx", "RPC call send transaction")).unwrap();
static ref RPC_GET_LATEST_BLOCKHASH: IntCounter =
register_int_counter!(opts!("literpc_rpc_get_latest_blockhash", "RPC call to get latest block hash")).unwrap();
static ref RPC_IS_BLOCKHASH_VALID: IntCounter =
register_int_counter!(opts!("literpc_rpc_is_blockhash_valid", "RPC call to check if blockhash is vali calld")).unwrap();
static ref RPC_GET_SIGNATURE_STATUSES: IntCounter =
register_int_counter!(opts!("literpc_rpc_get_signature_statuses", "RPC call to get signature statuses")).unwrap();
static ref RPC_GET_VERSION: IntCounter =
register_int_counter!(opts!("literpc_rpc_get_version", "RPC call to version")).unwrap();
static ref RPC_REQUEST_AIRDROP: IntCounter =
register_int_counter!(opts!("literpc_rpc_airdrop", "RPC call to request airdrop")).unwrap();
}
/// A bridge between clients and tpu
#[allow(dead_code)]
pub struct LiteBridge {
rpc_client: Arc<RpcClient>,
data_cache: DataCache,
transaction_service: TransactionService,
history: History,
prio_fees_service: PrioFeesService,
account_priofees_service: AccountPrioService,
accounts_service: Option<AccountService>,
}
impl LiteBridge {
pub fn new(
rpc_client: Arc<RpcClient>,
data_cache: DataCache,
transaction_service: TransactionService,
history: History,
prio_fees_service: PrioFeesService,
account_priofees_service: AccountPrioService,
accounts_service: Option<AccountService>,
) -> Self {
Self {
rpc_client,
data_cache,
transaction_service,
history,
prio_fees_service,
account_priofees_service,
accounts_service,
}
}
}
#[jsonrpsee::core::async_trait]
impl LiteRpcServer for LiteBridge {
async fn get_block(&self, _slot: u64) -> RpcResult<Option<UiConfirmedBlock>> {
// let block = self.blockstore.block_storage.query_block(slot).await;
// if block.is_ok() {
// // TO DO Convert to UIConfirmed Block
// Err(jsonrpsee::core::Error::HttpNotImplemented)
// } else {
// Ok(None)
// }
// TODO get_block might deserve different implementation based on whether we serve from "blockstore module" vs. from "send tx module"
// under progress
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_blocks(
&self,
_start_slot: Slot,
_config: Option<RpcBlocksConfigWrapper>,
_commitment: Option<CommitmentConfig>,
) -> RpcResult<Vec<Slot>> {
// under progress
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_signatures_for_address(
&self,
_address: String,
_config: Option<RpcSignaturesForAddressConfig>,
) -> RpcResult<Vec<RpcConfirmedTransactionStatusWithSignature>> {
// under progress
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_cluster_nodes(&self) -> RpcResult<Vec<RpcContactInfo>> {
Ok(self
.data_cache
.cluster_info
.cluster_nodes
.iter()
.map(|v| v.value().as_ref().clone())
.collect_vec())
}
async fn get_slot(&self, config: Option<RpcContextConfig>) -> RpcResult<Slot> {
let commitment_config = config
.map(|config| config.commitment.unwrap_or_default())
.unwrap_or_default();
let BlockInformation { slot, .. } = self
.data_cache
.block_information_store
.get_latest_block_information(commitment_config)
.await;
Ok(slot)
}
async fn get_block_height(&self, config: Option<RpcContextConfig>) -> RpcResult<u64> {
let commitment_config = config.map_or(CommitmentConfig::finalized(), |x| {
x.commitment.unwrap_or_default()
});
let block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment_config)
.await;
Ok(block_info.block_height)
}
async fn get_block_time(&self, slot: u64) -> RpcResult<u64> {
let block_info = self
.data_cache
.block_information_store
.get_block_info_by_slot(slot);
match block_info {
Some(info) => Ok(info.block_time),
None => Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into()),
}
}
async fn get_first_available_block(&self) -> RpcResult<u64> {
// under progress
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_latest_blockhash(
&self,
config: Option<RpcContextConfig>,
) -> RpcResult<RpcResponse<RpcBlockhash>> {
RPC_GET_LATEST_BLOCKHASH.inc();
let commitment_config = config
.map(|config| config.commitment.unwrap_or(CommitmentConfig::confirmed()))
.unwrap_or_default();
let BlockInformation {
slot,
block_height,
blockhash,
..
} = self
.data_cache
.block_information_store
.get_latest_block_information(commitment_config)
.await;
log::trace!("glb {blockhash} {slot} {block_height}");
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value: RpcBlockhash {
blockhash: blockhash.to_string(),
last_valid_block_height: block_height + 150,
},
})
}
async fn is_blockhash_valid(
&self,
blockhash: String,
config: Option<IsBlockHashValidConfig>,
) -> RpcResult<RpcResponse<bool>> {
RPC_IS_BLOCKHASH_VALID.inc();
let commitment = config.unwrap_or_default().commitment.unwrap_or_default();
let commitment = CommitmentConfig { commitment };
let (is_valid, slot) = self
.data_cache
.block_information_store
.is_blockhash_valid(
&hash_from_str(&blockhash).expect("valid blockhash"),
commitment,
)
.await;
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value: is_valid,
})
}
async fn get_epoch_info(&self, config: Option<RpcContextConfig>) -> RpcResult<EpochInfo> {
let commitment_config = config
.map(|config| config.commitment.unwrap_or_default())
.unwrap_or_default();
let block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment_config)
.await;
//TODO manage transaction_count of epoch info. Currently None.
let epoch_info = self
.data_cache
.get_current_epoch(commitment_config)
.await
.as_epoch_info(block_info.block_height, None);
Ok(epoch_info)
}
async fn get_recent_performance_samples(
&self,
limit: Option<usize>,
) -> RpcResult<Vec<RpcPerfSample>> {
// TODO: implement our own perofmrance samples from blockstream and slot stream
// For now just use normal rpc to get the data
self.rpc_client
.get_recent_performance_samples(limit)
.await
.map_err(|_| jsonrpsee::types::error::ErrorCode::InternalError.into())
}
async fn get_signature_statuses(
&self,
sigs: Vec<String>,
_config: Option<RpcSignatureStatusConfig>,
) -> RpcResult<RpcResponse<Vec<Option<TransactionStatus>>>> {
RPC_GET_SIGNATURE_STATUSES.inc();
let sig_statuses = sigs
.iter()
.map(|sig| Signature::from_str(sig).expect("signature must be valid"))
.map(|sig| self.data_cache.txs.get(&sig).and_then(|v| v.status))
.collect();
Ok(RpcResponse {
context: RpcResponseContext {
slot: self
.data_cache
.block_information_store
.get_latest_block_information(CommitmentConfig::finalized())
.await
.slot,
api_version: None,
},
value: sig_statuses,
})
}
async fn get_recent_prioritization_fees(
&self,
pubkey_strs: Vec<String>,
) -> RpcResult<Vec<RpcPrioritizationFee>> {
// This method will get the latest global and account prioritization fee stats and then send the maximum p75
const PERCENTILE: f32 = 0.75;
let accounts = pubkey_strs
.iter()
.filter_map(|pubkey| Pubkey::from_str(pubkey).ok())
.collect_vec();
if accounts.len() != pubkey_strs.len() {
// if lengths do not match it means some of the accounts are invalid
return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into());
}
let global_prio_fees = self.prio_fees_service.get_latest_priofees().await;
let max_p75 = global_prio_fees
.map(|(_, fees)| {
let fees = fees.get_percentile(PERCENTILE).unwrap_or_default();
std::cmp::max(fees.0, fees.1)
})
.unwrap_or_default();
let ret: Vec<RpcPrioritizationFee> = accounts
.iter()
.map(|account| {
let (slot, stats) = self.account_priofees_service.get_latest_stats(account);
let stat = stats
.all_stats
.get_percentile(PERCENTILE)
.unwrap_or_default();
RpcPrioritizationFee {
slot,
prioritization_fee: std::cmp::max(max_p75, std::cmp::max(stat.0, stat.1)),
}
})
.collect_vec();
Ok(ret)
}
async fn send_transaction(
&self,
tx: String,
send_transaction_config: Option<SendTransactionConfig>,
) -> RpcResult<String> {
RPC_SEND_TX.inc();
// Copied these constants from solana labs code
const MAX_BASE58_SIZE: usize = 1683;
const MAX_BASE64_SIZE: usize = 1644;
let SendTransactionConfig {
encoding,
max_retries,
} = send_transaction_config.unwrap_or_default();
let expected_size = match encoding {
encoding::BinaryEncoding::Base58 => MAX_BASE58_SIZE,
encoding::BinaryEncoding::Base64 => MAX_BASE64_SIZE,
};
if tx.len() > expected_size {
return Err(jsonrpsee::types::error::ErrorCode::OversizedRequest.into());
}
let raw_tx = match encoding.decode(tx) {
Ok(raw_tx) => raw_tx,
Err(_) => {
return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into());
}
};
match self
.transaction_service
.send_wire_transaction(raw_tx, max_retries)
.await
{
Ok(sig) => {
TXS_IN_CHANNEL.inc();
Ok(sig)
}
Err(_) => Err(jsonrpsee::types::error::ErrorCode::InternalError.into()),
}
}
fn get_version(&self) -> RpcResult<RpcVersionInfo> {
RPC_GET_VERSION.inc();
let version = solana_version::Version::default();
Ok(RpcVersionInfo {
solana_core: version.to_string(),
feature_set: Some(version.feature_set),
})
}
async fn request_airdrop(
&self,
_pubkey_str: String,
_lamports: u64,
_config: Option<RpcRequestAirdropConfig>,
) -> RpcResult<String> {
RPC_REQUEST_AIRDROP.inc();
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_leader_schedule(
&self,
slot: Option<u64>,
config: Option<RpcLeaderScheduleConfig>,
) -> RpcResult<Option<HashMap<String, Vec<usize>>>> {
//TODO verify leader identity.
let schedule = self
.data_cache
.leader_schedule
.read()
.await
.get_leader_schedule_for_slot(slot, config.and_then(|c| c.commitment), &self.data_cache)
.await;
Ok(schedule)
}
async fn get_slot_leaders(&self, start_slot: u64, limit: u64) -> RpcResult<Vec<Pubkey>> {
let epock_schedule = self.data_cache.epoch_data.get_epoch_schedule();
self.data_cache
.leader_schedule
.read()
.await
.get_slot_leaders(start_slot, limit, epock_schedule)
.await
.map_err(|err| {
log::error!("Error processing get leader schedule : {err:?}");
jsonrpsee::types::error::ErrorCode::InternalError.into()
})
}
async fn get_vote_accounts(
&self,
_config: Option<RpcGetVoteAccountsConfig>,
) -> RpcResult<RpcVoteAccountStatus> {
// under progress
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
async fn get_latest_block_priofees(
&self,
method: Option<PrioritizationFeeCalculationMethod>,
) -> RpcResult<RpcResponse<PrioFeesStats>> {
let method = method.unwrap_or_default();
let res = match method {
PrioritizationFeeCalculationMethod::Latest => {
self.prio_fees_service.get_latest_priofees().await
}
PrioritizationFeeCalculationMethod::LastNBlocks(nb) => {
self.prio_fees_service
.get_last_n_priofees_aggregate(nb)
.await
}
_ => {
// method is invalid
return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into());
}
};
match res {
Some((confirmation_slot, priofees)) => Ok(RpcResponse {
context: RpcResponseContext {
slot: confirmation_slot,
api_version: None,
},
value: priofees,
}),
None => Err(jsonrpsee::types::error::ErrorCode::InternalError.into()),
}
}
async fn get_latest_account_priofees(
&self,
account: String,
method: Option<PrioritizationFeeCalculationMethod>,
) -> RpcResult<RpcResponse<AccountPrioFeesStats>> {
if let Ok(account) = Pubkey::from_str(&account) {
let method = method.unwrap_or_default();
let (slot, value) = match method {
PrioritizationFeeCalculationMethod::Latest => {
self.account_priofees_service.get_latest_stats(&account)
}
PrioritizationFeeCalculationMethod::LastNBlocks(nb) => {
self.account_priofees_service.get_n_last_stats(&account, nb)
}
_ => return Err(jsonrpsee::types::error::ErrorCode::InternalError.into()),
};
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value,
})
} else {
// Account key is invalid
Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into())
}
}
async fn get_account_info(
&self,
pubkey_str: String,
config: Option<RpcAccountInfoConfig>,
) -> RpcResult<RpcResponse<Option<UiAccount>>> {
let Ok(pubkey) = Pubkey::from_str(&pubkey_str) else {
// pubkey is invalid
return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into());
};
if let Some(account_service) = &self.accounts_service {
let commitment = config
.as_ref()
.and_then(|x| x.commitment)
.unwrap_or_default();
let current_block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment)
.await;
match account_service.get_account(pubkey, config).await {
Ok((_, ui_account)) => Ok(RpcResponse {
context: RpcResponseContext {
slot: current_block_info.slot,
api_version: None,
},
value: ui_account,
}),
Err(_) => {
// account not found
Err(jsonrpsee::types::error::ErrorCode::ServerError(
RpcErrors::AccountNotFound as i32,
)
.into())
}
}
} else {
// accounts are disabled
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
}
async fn get_multiple_accounts(
&self,
pubkey_strs: Vec<String>,
config: Option<RpcAccountInfoConfig>,
) -> RpcResult<RpcResponse<Vec<Option<UiAccount>>>> {
let pubkeys = pubkey_strs
.iter()
.map(|key| Pubkey::from_str(key))
.collect_vec();
if pubkeys.iter().any(|res| res.is_err()) {
return Err(jsonrpsee::types::error::ErrorCode::InternalError.into());
};
if let Some(account_service) = &self.accounts_service {
let mut ui_accounts = vec![];
for pubkey in pubkeys {
match account_service
.get_account(pubkey.unwrap(), config.clone())
.await
{
Ok((_, ui_account)) => {
ui_accounts.push(ui_account);
}
Err(_) => {
ui_accounts.push(None);
}
}
}
let commitment = config
.as_ref()
.and_then(|x| x.commitment)
.unwrap_or_default();
let current_block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment)
.await;
assert_eq!(ui_accounts.len(), pubkey_strs.len());
Ok(RpcResponse {
context: RpcResponseContext {
slot: current_block_info.slot,
api_version: None,
},
value: ui_accounts,
})
} else {
// accounts are disabled
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
}
async fn get_program_accounts(
&self,
program_id_str: String,
config: Option<RpcProgramAccountsConfig>,
) -> RpcResult<OptionalContext<Vec<RpcKeyedAccount>>> {
let Ok(program_id) = Pubkey::from_str(&program_id_str) else {
return Err(jsonrpsee::types::error::ErrorCode::InternalError.into());
};
let with_context = config
.as_ref()
.map(|value| value.with_context.unwrap_or_default())
.unwrap_or_default();
let commitment: CommitmentConfig = config
.as_ref()
.and_then(|x| x.account_config.commitment)
.unwrap_or_default();
let current_block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment)
.await;
if let Some(account_service) = &self.accounts_service {
match account_service
.get_program_accounts(program_id, config)
.await
{
Ok((_, ui_account)) => {
if with_context {
Ok(OptionalContext::Context(RpcResponse {
context: RpcResponseContext {
slot: current_block_info.slot,
api_version: None,
},
value: ui_account,
}))
} else {
Ok(OptionalContext::NoContext(ui_account))
}
}
Err(_) => {
return Err(jsonrpsee::types::error::ErrorCode::ServerError(
RpcErrors::AccountNotFound as i32,
)
.into());
}
}
} else {
// accounts are disabled
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
}
async fn get_balance(
&self,
pubkey_str: String,
config: Option<RpcContextConfig>,
) -> RpcResult<RpcResponse<u64>> {
let Ok(pubkey) = Pubkey::from_str(&pubkey_str) else {
// pubkey is invalid
return Err(jsonrpsee::types::error::ErrorCode::InvalidParams.into());
};
let config = config.map(|x| RpcAccountInfoConfig {
encoding: None,
data_slice: None,
commitment: x.commitment,
min_context_slot: x.min_context_slot,
});
let commitment = config
.as_ref()
.and_then(|x| x.commitment)
.unwrap_or_default();
let current_block_info = self
.data_cache
.block_information_store
.get_latest_block_information(commitment)
.await;
if let Some(account_service) = &self.accounts_service {
match account_service.get_account(pubkey, config).await {
Ok((_, ui_account)) => Ok(RpcResponse {
context: RpcResponseContext {
slot: current_block_info.slot,
api_version: None,
},
value: ui_account.map(|x| x.lamports).unwrap_or_default(),
}),
Err(_) => {
// account not found
Err(jsonrpsee::types::error::ErrorCode::ServerError(
RpcErrors::AccountNotFound as i32,
)
.into())
}
}
} else {
// accounts are disabled
Err(jsonrpsee::types::error::ErrorCode::MethodNotFound.into())
}
}
}