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

828 lines
28 KiB
Rust

use std::collections::HashMap;
use std::{str::FromStr, sync::Arc};
use anyhow::Context;
use itertools::Itertools;
use jsonrpsee::core::StringError;
use jsonrpsee::{
core::SubscriptionResult, server::ServerBuilder, DisconnectError, PendingSubscriptionSink,
};
use log::{debug, error, warn};
use prometheus::{opts, register_int_counter, IntCounter};
use solana_lite_rpc_core::types::BlockStream;
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::response::SlotInfo;
use solana_rpc_client_api::{
config::{
RpcBlockSubscribeConfig, RpcBlockSubscribeFilter, RpcBlocksConfigWrapper, RpcContextConfig,
RpcGetVoteAccountsConfig, RpcLeaderScheduleConfig, RpcProgramAccountsConfig,
RpcRequestAirdropConfig, RpcSignatureStatusConfig, RpcSignatureSubscribeConfig,
RpcSignaturesForAddressConfig, RpcTransactionLogsConfig, RpcTransactionLogsFilter,
},
response::{
Response as RpcResponse, RpcBlockhash, RpcConfirmedTransactionStatusWithSignature,
RpcContactInfo, RpcPerfSample, RpcPrioritizationFee, RpcResponseContext, RpcVersionInfo,
RpcVoteAccountStatus,
},
};
use solana_sdk::epoch_info::EpochInfo;
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey, slot_history::Slot};
use solana_transaction_status::{TransactionStatus, UiConfirmedBlock};
use tokio::net::ToSocketAddrs;
use tokio::sync::broadcast::error::RecvError::{Closed, Lagged};
use solana_lite_rpc_blockstore::history::History;
use solana_lite_rpc_core::{
encoding,
stores::{block_information_store::BlockInformation, data_cache::DataCache, tx_store::TxProps},
AnyhowJoinHandle,
};
use solana_lite_rpc_services::{
transaction_service::TransactionService, tx_sender::TXS_IN_CHANNEL,
};
use crate::{
configs::{IsBlockHashValidConfig, SendTransactionConfig},
jsonrpsee_subscrption_handler_sink::JsonRpseeSubscriptionHandlerSink,
rpc::LiteRpcServer,
};
use solana_lite_rpc_prioritization_fees::rpc_data::{
AccountPrioFeesStats, AccountPrioFeesUpdateMessage, PrioFeesStats, PrioFeesUpdateMessage,
};
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();
static ref RPC_SIGNATURE_SUBSCRIBE: IntCounter =
register_int_counter!(opts!("literpc_rpc_signature_subscribe", "RPC call to subscribe to signature")).unwrap();
static ref RPC_BLOCK_PRIOFEES_SUBSCRIBE: IntCounter =
register_int_counter!(opts!("literpc_rpc_block_priofees_subscribe", "RPC call to subscribe to block prio fees")).unwrap();
static ref RPC_ACCOUNT_PRIOFEES_SUBSCRIBE: IntCounter =
register_int_counter!(opts!("literpc_rpc_account_priofees_subscribe", "RPC call to subscribe to account prio fees")).unwrap();
}
/// A bridge between clients and tpu
#[allow(dead_code)]
pub struct LiteBridge {
data_cache: DataCache,
// should be removed
rpc_client: Arc<RpcClient>,
transaction_service: TransactionService,
history: History,
prio_fees_service: PrioFeesService,
account_priofees_service: AccountPrioService,
block_stream: BlockStream,
}
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,
block_stream: BlockStream,
) -> Self {
Self {
rpc_client,
data_cache,
transaction_service,
history,
prio_fees_service,
account_priofees_service,
block_stream,
}
}
/// List for `JsonRpc` requests
pub async fn start<T: ToSocketAddrs + std::fmt::Debug + 'static + Send + Clone>(
self,
http_addr: T,
ws_addr: T,
) -> anyhow::Result<()> {
let rpc = self.into_rpc();
let ws_server_handle = ServerBuilder::default()
.ws_only()
.build(ws_addr.clone())
.await?
.start(rpc.clone())?;
let http_server_handle = ServerBuilder::default()
.http_only()
.build(http_addr.clone())
.await?
.start(rpc)?;
let ws_server: AnyhowJoinHandle = tokio::spawn(async move {
log::info!("Websocket Server started at {ws_addr:?}");
ws_server_handle.stopped().await;
anyhow::bail!("Websocket server stopped");
});
let http_server: AnyhowJoinHandle = tokio::spawn(async move {
log::info!("HTTP Server started at {http_addr:?}");
http_server_handle.stopped().await;
anyhow::bail!("HTTP server stopped");
});
tokio::select! {
res = ws_server => {
anyhow::bail!("WebSocket server {res:?}");
},
res = http_server => {
anyhow::bail!("HTTP server {res:?}");
},
}
}
}
#[jsonrpsee::core::async_trait]
impl LiteRpcServer for LiteBridge {
async fn get_block(&self, _slot: u64) -> crate::rpc::Result<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"
todo!("get_block: decide where to look")
}
async fn get_blocks(
&self,
_start_slot: Slot,
_config: Option<RpcBlocksConfigWrapper>,
_commitment: Option<CommitmentConfig>,
) -> crate::rpc::Result<Vec<Slot>> {
todo!()
}
async fn get_signatures_for_address(
&self,
_address: String,
_config: Option<RpcSignaturesForAddressConfig>,
) -> crate::rpc::Result<Vec<RpcConfirmedTransactionStatusWithSignature>> {
todo!()
}
async fn get_cluster_nodes(&self) -> crate::rpc::Result<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>) -> crate::rpc::Result<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(commitment_config)
.await;
Ok(slot)
}
async fn get_block_height(&self, config: Option<RpcContextConfig>) -> crate::rpc::Result<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(commitment_config)
.await;
Ok(block_info.block_height)
}
async fn get_block_time(&self, slot: u64) -> crate::rpc::Result<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::core::Error::Custom(
"Unable to find block information in LiteRPC cache".to_string(),
)),
}
}
async fn get_first_available_block(&self) -> crate::rpc::Result<u64> {
todo!()
}
async fn get_latest_blockhash(
&self,
config: Option<RpcContextConfig>,
) -> crate::rpc::Result<RpcResponse<RpcBlockhash>> {
RPC_GET_LATEST_BLOCKHASH.inc();
let commitment_config = config
.map(|config| config.commitment.unwrap_or_default())
.unwrap_or_default();
let BlockInformation {
slot,
block_height,
blockhash,
..
} = self
.data_cache
.block_information_store
.get_latest_block(commitment_config)
.await;
log::trace!("glb {blockhash} {slot} {block_height}");
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value: RpcBlockhash {
blockhash,
last_valid_block_height: block_height + 150,
},
})
}
async fn is_blockhash_valid(
&self,
blockhash: String,
config: Option<IsBlockHashValidConfig>,
) -> crate::rpc::Result<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(&blockhash, commitment)
.await;
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value: is_valid,
})
}
async fn get_epoch_info(
&self,
config: Option<RpcContextConfig>,
) -> crate::rpc::Result<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_info(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>,
) -> crate::rpc::Result<Vec<RpcPerfSample>> {
todo!()
}
async fn get_signature_statuses(
&self,
sigs: Vec<String>,
_config: Option<RpcSignatureStatusConfig>,
) -> crate::rpc::Result<RpcResponse<Vec<Option<TransactionStatus>>>> {
RPC_GET_SIGNATURE_STATUSES.inc();
let sig_statuses = sigs
.iter()
.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_info(CommitmentConfig::finalized())
.await
.slot,
api_version: None,
},
value: sig_statuses,
})
}
async fn get_recent_prioritization_fees(
&self,
pubkey_strs: Vec<String>,
) -> crate::rpc::Result<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::core::Error::Custom(
"Some accounts are invalid".to_string(),
));
}
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>,
) -> crate::rpc::Result<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::core::Error::Custom(format!(
"Transaction too large, expected : {} transaction len {}",
expected_size,
tx.len()
)));
}
let raw_tx = match encoding.decode(tx) {
Ok(raw_tx) => raw_tx,
Err(err) => {
return Err(jsonrpsee::core::Error::Custom(err.to_string()));
}
};
match self
.transaction_service
.send_transaction(raw_tx, max_retries)
.await
{
Ok(sig) => {
TXS_IN_CHANNEL.inc();
Ok(sig)
}
Err(e) => Err(jsonrpsee::core::Error::Custom(e.to_string())),
}
}
fn get_version(&self) -> crate::rpc::Result<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>,
) -> crate::rpc::Result<String> {
RPC_REQUEST_AIRDROP.inc();
let pubkey = match Pubkey::from_str(&pubkey_str) {
Ok(pubkey) => pubkey,
Err(err) => {
return Err(jsonrpsee::core::Error::Custom(err.to_string()));
}
};
let airdrop_sig = match self
.rpc_client
.request_airdrop_with_config(&pubkey, lamports, config.unwrap_or_default())
.await
.context("failed to request airdrop")
{
Ok(airdrop_sig) => airdrop_sig.to_string(),
Err(err) => {
return Err(jsonrpsee::core::Error::Custom(err.to_string()));
}
};
if let Ok((_, block_height)) = self
.rpc_client
.get_latest_blockhash_with_commitment(CommitmentConfig::finalized())
.await
.context("failed to get latest blockhash")
{
self.data_cache.txs.insert(
airdrop_sig.clone(),
TxProps {
status: None,
last_valid_blockheight: block_height,
sent_by_lite_rpc: true,
},
);
}
Ok(airdrop_sig)
}
async fn program_subscribe(
&self,
_pending: PendingSubscriptionSink,
_pubkey_str: String,
_config: Option<RpcProgramAccountsConfig>,
) -> SubscriptionResult {
todo!()
}
async fn slot_subscribe(&self, pending: PendingSubscriptionSink) -> SubscriptionResult {
let sink = pending.accept().await?;
let mut block_stream = self.block_stream.resubscribe();
tokio::spawn(async move {
loop {
match block_stream.recv().await {
Ok(produced_block) => {
if !produced_block.commitment_config.is_processed() {
continue;
}
let slot_info = SlotInfo {
slot: produced_block.slot,
parent: produced_block.parent_slot,
root: 0,
};
let result_message = jsonrpsee::SubscriptionMessage::from_json(&slot_info);
match sink.send(result_message.unwrap()).await {
Ok(()) => {
// success
continue;
}
Err(DisconnectError(_subscription_message)) => {
debug!("Stopping subscription task on disconnect");
return;
}
};
}
Err(e) => match e {
Closed => {
break;
}
Lagged(_) => {
log::error!("Slot subscription stream lagged");
continue;
}
},
}
}
});
Ok(())
}
async fn block_subscribe(
&self,
_pending: PendingSubscriptionSink,
_filter: RpcBlockSubscribeFilter,
_config: Option<RpcBlockSubscribeConfig>,
) -> SubscriptionResult {
todo!()
}
async fn logs_subscribe(
&self,
_pending: PendingSubscriptionSink,
_filter: RpcTransactionLogsFilter,
_config: Option<RpcTransactionLogsConfig>,
) -> SubscriptionResult {
todo!()
}
// WARN: enable_received_notification: bool is ignored
async fn signature_subscribe(
&self,
pending: PendingSubscriptionSink,
signature: String,
config: RpcSignatureSubscribeConfig,
) -> SubscriptionResult {
RPC_SIGNATURE_SUBSCRIBE.inc();
let sink = pending.accept().await?;
let jsonrpsee_sink = JsonRpseeSubscriptionHandlerSink::new(sink);
self.data_cache.tx_subs.signature_subscribe(
signature,
config.commitment.unwrap_or_default(),
Arc::new(jsonrpsee_sink),
);
Ok(())
}
async fn slot_updates_subscribe(
&self,
_pending: PendingSubscriptionSink,
) -> SubscriptionResult {
todo!()
}
async fn vote_subscribe(&self, _pending: PendingSubscriptionSink) -> SubscriptionResult {
todo!()
}
async fn get_leader_schedule(
&self,
slot: Option<u64>,
config: Option<RpcLeaderScheduleConfig>,
) -> crate::rpc::Result<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,
) -> crate::rpc::Result<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| {
jsonrpsee::core::Error::Custom(format!("error during query processing:{err}"))
})
}
async fn get_vote_accounts(
&self,
_config: Option<RpcGetVoteAccountsConfig>,
) -> crate::rpc::Result<RpcVoteAccountStatus> {
todo!()
}
async fn get_latest_block_priofees(
&self,
method: Option<PrioritizationFeeCalculationMethod>,
) -> crate::rpc::Result<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
}
_ => {
return Err(jsonrpsee::core::Error::Custom(
"Invalid calculation method".to_string(),
))
}
};
match res {
Some((confirmation_slot, priofees)) => Ok(RpcResponse {
context: RpcResponseContext {
slot: confirmation_slot,
api_version: None,
},
value: priofees,
}),
None => Err(jsonrpsee::core::Error::Custom(
"No latest priofees stats available found".to_string(),
)),
}
}
// use websocket-tungstenite-retry->examples/consume_literpc_priofees.rs to test
async fn latest_block_priofees_subscribe(
&self,
pending: PendingSubscriptionSink,
) -> SubscriptionResult {
let sink = pending.accept().await?;
let mut block_fees_stream = self.prio_fees_service.block_fees_stream.subscribe();
tokio::spawn(async move {
RPC_BLOCK_PRIOFEES_SUBSCRIBE.inc();
'recv_loop: loop {
match block_fees_stream.recv().await {
Ok(PrioFeesUpdateMessage {
slot: confirmation_slot,
priofees_stats,
}) => {
let result_message =
jsonrpsee::SubscriptionMessage::from_json(&RpcResponse {
context: RpcResponseContext {
slot: confirmation_slot,
api_version: None,
},
value: priofees_stats,
});
match sink.send(result_message.unwrap()).await {
Ok(()) => {
// success
continue 'recv_loop;
}
Err(DisconnectError(_subscription_message)) => {
debug!("Stopping subscription task on disconnect");
return;
}
};
}
Err(Lagged(lagged)) => {
// this usually happens if there is one "slow receiver", see https://docs.rs/tokio/latest/tokio/sync/broadcast/index.html#lagging
warn!(
"subscriber laggs some({}) priofees update messages - continue",
lagged
);
continue 'recv_loop;
}
Err(Closed) => {
error!("failed to receive block, sender closed - aborting");
return;
}
}
}
});
Ok(())
}
async fn get_latest_account_priofees(
&self,
account: String,
method: Option<PrioritizationFeeCalculationMethod>,
) -> crate::rpc::Result<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::core::Error::Custom(
"Invalid calculation method".to_string(),
))
}
};
Ok(RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value,
})
} else {
Err(jsonrpsee::core::Error::Custom(
"Invalid account".to_string(),
))
}
}
async fn latest_account_priofees_subscribe(
&self,
pending: PendingSubscriptionSink,
account: String,
) -> SubscriptionResult {
let Ok(account) = Pubkey::from_str(&account) else {
return Err(StringError::from("Invalid account".to_string()));
};
let sink = pending.accept().await?;
let mut account_fees_stream = self
.account_priofees_service
.priofees_update_sender
.subscribe();
tokio::spawn(async move {
RPC_BLOCK_PRIOFEES_SUBSCRIBE.inc();
'recv_loop: loop {
match account_fees_stream.recv().await {
Ok(AccountPrioFeesUpdateMessage {
slot,
accounts_data,
}) => {
if let Some(account_data) = accounts_data.get(&account) {
let result_message =
jsonrpsee::SubscriptionMessage::from_json(&RpcResponse {
context: RpcResponseContext {
slot,
api_version: None,
},
value: account_data,
});
match sink.send(result_message.unwrap()).await {
Ok(()) => {
// success
continue 'recv_loop;
}
Err(DisconnectError(_subscription_message)) => {
debug!("Stopping subscription task on disconnect");
return;
}
};
}
}
Err(Lagged(lagged)) => {
// this usually happens if there is one "slow receiver", see https://docs.rs/tokio/latest/tokio/sync/broadcast/index.html#lagging
warn!(
"subscriber laggs some({}) priofees update messages - continue",
lagged
);
continue 'recv_loop;
}
Err(Closed) => {
error!("failed to receive block, sender closed - aborting");
return;
}
}
}
});
Ok(())
}
}