├── .env.sample ├── .gitignore ├── Batch_Txn_Result.md ├── Cargo.lock ├── Cargo.toml ├── README.md ├── rust-toolchain.toml └── src ├── chain_state.rs ├── connection_cache_client.rs ├── main.rs ├── rpc.rs ├── rpc_server.rs ├── store.rs ├── tpu_next_client.rs ├── utils.rs └── vendor ├── mod.rs └── solana_rpc.rs /.env.sample: -------------------------------------------------------------------------------- 1 | RPC_URL=http://api.mainnet-beta.solana.com 2 | WS_URL=ws://api.mainnet-beta.solana.com 3 | ADDRESS=127.0.0.1:7000 4 | IDENTITY_KEYPAIR_FILE=/ubuntu/.config/solana/id.json 5 | GRPC_URL=http://yellostone-rpc:10200 6 | MAX_RETRIES=5 7 | NUM_CONNECTIONS=5 8 | SKIP_CHECK_TRANSACTION_AGE=false 9 | WORKER_CHANNEL_SIZE=64 10 | MAX_RECONNECT_ATTEMPTS=5 11 | LOOKAHEAD_SLOTS=8 12 | USE_TPU_CLIENT_NEXT=true 13 | PROMETHEUS_ADDR=0.0.0.0:10025 14 | RETRY_INTERVAL_SECONDS=1 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env -------------------------------------------------------------------------------- /Batch_Txn_Result.md: -------------------------------------------------------------------------------- 1 | # Iris Batch Txn Result 2 | 3 | ### Bench methodology 4 | - Bench was between `sendTransaction` and `sendTransactionBatch` 5 | - in this we send 10000 txn 6 | - for `sendTransaction` , 5 txn were sent at burst per second for 2000 times 7 | - for `sendTransactionBatch` , batch of 5 txn were sent per second for 2000 times 8 | - both were sent at the same time 9 | 10 | ### Bench Result 11 | - txn in a batch where relatively faster and consistently landing in a same slot then the txn sent in a burst 12 | - the avg slot difference between txn landed in the batch was `1.7` and median was `0` 13 | - the avg slot difference between txn landed in the burst was `2.11` and median was `2` 14 | 15 | 16 | ### Example 17 | sendBatchTransaction: 18 | 19 | | Batch_no | S.No | Txn signature | slot_landed | 20 | | -------- | ---- | ---------------------------------------------------------------------------------------- | ----------- | 21 | | **0** | 0 | 2Bv59L5eiLiLs13dTxVTZe3WVdjtouVfteW9KQ81Kv4MmCoxmVwzZmDqzBnLDtfs2ZVFdUKcoFxSzx8pChuLGDye | 312275048 | 22 | | **0** | 1 | bAxuDKnJXDBDfsaBQg3RqUkwNkT2puJtDWmYHjc1WseF7CtRzh3Nnsy1TJnW2YfK3S5euKAKC4t8qVTL61mKPtG | 312275048 | 23 | | **0** | 2 | weHMfSEBs9jTuduAQ4mkc5Uoucx21GN4fCGJttxDpP2rBRZ7aFGLqJrx5MzAN7fjhDQKkuutRKbcU1u6UXWYTHH | 312275048 | 24 | | **0** | 3 | 2L6Tjsv7m9QgjfW8Rei9sLBbPYsnYrnkTx3P2ZyZTHotUngrbNGgAjGfGsGWxXesL7WxiwdFBZxzgEmLWeBZB9iG | 312275048 | 25 | | **0** | 4 | 2WvSX2nqzxtYD55Ki5f49mQy9f1VsViBgCqD5wHYz8S1XDTmhgG1gVqGjUHFojZVoTwfir6vbrNf6Hb9FkNERJrK | 312275048 | 26 | | **1** | 5 | gvCCTwrfQamTa8RptzXYkrWafRLQfbCMSxhMtJUQ5hhY7KMy9x6Yym9waMzsHERp4yXV5HocX5cJBMRBdbv7fEw | 312275051 | 27 | | **1** | 6 | 4GHLy9j5rKvMyoYucBMRrar4rGZDxk2kJkcHKn3KQcuKwAwHxEFXGoLv5Yn2ccGNn59t2w3C8C8ukYsnSHfzN6Aw | 312275051 | 28 | | **1** | 7 | 64et1PzP9hqtpAfQ7Tp8S1r5CsMuSKtH4b4YkwhsUVdmjrUkyHbxWsB5C2t8JwHkRbUnyJKTJJtqmXg7YGmLYX5X | 312275051 | 29 | | **1** | 8 | 5cdHuJXWo2YA74ST7uayemVZgiPt7ePC1rLxqGPdnY57oueJ5WQAeVbUkuYrWGjQDbKMEbgARUhqSrgXTCApk52r | 312275051 | 30 | | **1** | 9 | 5C1NGtvvi7XasjNbc8vzyifWyTg7bucMcFiJgiDk8VaG7tJD4fevgHwhjee3dyQ5it2oC5aK9ag2eu5vKNKk5GvL | 312275051 | 31 | 32 | 33 | sendTransaction : 34 | 35 | | Batch_no | S.No | Txn signature | slot_landed | 36 | | -------- | ---- | ---------------------------------------------------------------------------------------- | ----------- | 37 | | **0** | 0 | incjRom9Gv5CVZ1v5nDWKNRgyQoNbpdHLhSwy96VVZvtFhhnZ7NPdWWU8BxXv1FYe9kfi5pzNeGAocwAy39Cj1o | 312275048 | 38 | | **0** | 1 | 3uGhDoztzhkvVRvNpbNUo71SLbbCYw1N2hKcbd3dcBsHvzyqaJtkGRLnNZUU9uagGPLDZ8AWeAKSFw4aLxFkr981 | 312275048 | 39 | | **0** | 2 | 42v2Rr7ScTg4goQWcvD2hi5Nx6hodo7HVNwC7dEdfn7R4G68SVbuuGVN1kTSQkmni7EUfypvQjCLXZj5qK6H2EeE | 312275048 | 40 | | **0** | 3 | gMGNFBY9coutXTzNusNFsrnaGy3k5onCjtXfRW21cVK5Rt2AYpbigNQkbWgjtPBS7EMJxduh1EDtHMmJvZ14SnR | 312275048 | 41 | | **0** | 4 | 4pUUyLs9s1Hunz4yEUme7U59yJEu8eNWyRMMokZxdNeayYqAcvMphvym2VuXQWki8UEvSdi43Bdv73a8fSv1VzB2 | 312275051 | 42 | | **1** | 5 | 5qikkNrtg12GgeU9L5GsybRvtyH12EqGKueaiEHZSPJikzV9bXGxzLTjARdZL23nfExWbvjDdiVbG6Cw5aCmLRph | 312275051 | 43 | | **1** | 6 | 4ZW3wrnsXRnQpQa4GwFCgs9EKoBfdbyakc9uJzadY6JXoWC9FkgTTx4fW59fLcrd2SoCZjK8Pi4ApUKxfk2r98we | 312275051 | 44 | | **1** | 7 | 2FJqM1UKTfVqtVENfNZjLjgoSvmXdVsbAzpTDKF1aZ7RMFXUHTvZ3AqJCWXL9D3GAYLSXNsUhv9RFVQQ1w4MXbqF | 312275051 | 45 | | **1** | 8 | 5UDvgC7CzJzgaaG7pDT3QcaFgEVv1yZmwR3JEssyM81mgDYqd5n9jeWTZZJVtRCQZf6SUf7TUzAxAXrDT6gYqgxB | 312275052 | 46 | | **1** | 9 | 66o6zv1vETvYyKrdLsmEtcB3qEZgnEkmQs1GPqEbtg3on6pZYat76M1hxcMXn77x3QehxrsnxrY8bP9Rm5qG94wo | 312275051 | 47 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "iris" 3 | version = "0.1.0" 4 | edition = "2021" 5 | 6 | [dependencies] 7 | solana-tpu-client-next = { git = "https://github.com/anza-xyz/agave.git", default-features = false } 8 | solana-client = { git = "https://github.com/anza-xyz/agave.git" } 9 | solana-transaction-status = { git = "https://github.com/anza-xyz/agave.git", default-features = false } 10 | solana-sdk = { git = "https://github.com/anza-xyz/agave.git", default-features = false } 11 | solana-rpc-client-api ={ git = "https://github.com/anza-xyz/agave.git" } 12 | solana-connection-cache = { git = "https://github.com/anza-xyz/agave.git", default-features = false } 13 | 14 | yellowstone-grpc-client = { git = "https://github.com/rpcpool/yellowstone-grpc.git", tag = "v4.0.0+solana.2.1.1" } 15 | yellowstone-grpc-proto = { git = "https://github.com/rpcpool/yellowstone-grpc.git", tag = "v4.0.0+solana.2.1.1", default-features = false } 16 | 17 | jsonrpsee = {version = "=0.24.5", features = ["server", "http-client", "macros"]} 18 | tokio = "1.40.0" 19 | env_logger = "0.11.5" 20 | tokio-util = "0.7.12" 21 | dashmap = "5.5.3" 22 | bincode = "1.3.3" 23 | figment = {version = "0.10.19", features = ["env"]} 24 | serde = { version = "1.0.188", features = ["derive"] } 25 | anyhow = "1.0.91" 26 | tracing = "0.1.40" 27 | tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } 28 | dotenv = "0.15.0" 29 | base64 = "0.22.1" 30 | log = "0.4.22" 31 | metrics = "0.24.0" 32 | metrics-exporter-prometheus = "0.16.0" 33 | futures-util = "0.3.31" 34 | rustls = { version = "0.23.17", features = ["ring"] } 35 | rand = "0.8.5" 36 | 37 | [patch.crates-io] 38 | solana-program = { git = "https://github.com/anza-xyz/agave.git" } 39 | solana-curve25519 = { git = "https://github.com/anza-xyz/agave.git" } 40 | solana-zk-sdk = { git = "https://github.com/anza-xyz/agave.git" } 41 | solana-zk-token-sdk = { git = "https://github.com/anza-xyz/agave.git" } 42 | 43 | [package.metadata.docs.rs] 44 | targets = ["x86_64-unknown-linux-gnu"] 45 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # iris-rs 2 | A fast and lightweight solana transaction sender, based on amazing previous works like [atlas](https://github.com/helius-labs/atlas-txn-sender) and agave's [tpu-client-next](https://github.com/anza-xyz/agave/blob/master/tpu-client-next) 3 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | channel = "1.81.0" 3 | -------------------------------------------------------------------------------- /src/chain_state.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{generate_random_string, ChainStateClient}; 2 | use dashmap::DashMap; 3 | use futures_util::{SinkExt, StreamExt}; 4 | use log::error; 5 | use metrics::gauge; 6 | use solana_client::nonblocking::pubsub_client::PubsubClient; 7 | use solana_rpc_client_api::config::{RpcBlockSubscribeConfig, RpcBlockSubscribeFilter}; 8 | use solana_rpc_client_api::response::SlotUpdate; 9 | use solana_sdk::commitment_config::CommitmentConfig; 10 | use solana_sdk::signature::Signature; 11 | use solana_transaction_status::TransactionDetails::Signatures; 12 | use solana_transaction_status::UiTransactionEncoding::Base64; 13 | use std::collections::HashMap; 14 | use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; 15 | use std::sync::Arc; 16 | use std::time::Duration; 17 | use tokio::runtime::Handle; 18 | use tokio::task::JoinHandle; 19 | use tokio::time::timeout; 20 | use tracing::{debug, info}; 21 | use yellowstone_grpc_client::GeyserGrpcClient; 22 | use yellowstone_grpc_proto::geyser::{CommitmentLevel, SubscribeRequestFilterBlocks}; 23 | use yellowstone_grpc_proto::prelude::subscribe_update::UpdateOneof; 24 | use yellowstone_grpc_proto::prelude::{ 25 | SubscribeRequest, SubscribeRequestPing, 26 | }; 27 | 28 | const RETRY_INTERVAL: u64 = 1000; 29 | const MAX_RETRIES: usize = 5; 30 | 31 | macro_rules! ping_request { 32 | () => { 33 | SubscribeRequest { 34 | ping: Some(SubscribeRequestPing { id: 1 }), 35 | ..Default::default() 36 | } 37 | }; 38 | } 39 | 40 | macro_rules! block_subscribe_request { 41 | () => { 42 | SubscribeRequest { 43 | blocks: HashMap::from_iter(vec![( 44 | generate_random_string(20).to_string(), 45 | SubscribeRequestFilterBlocks { 46 | account_include: vec![], 47 | include_transactions: Some(true), 48 | include_accounts: Some(false), 49 | include_entries: Some(false), 50 | }, 51 | )]), 52 | commitment: Some(CommitmentLevel::Confirmed as i32), 53 | ..Default::default() 54 | } 55 | }; 56 | } 57 | 58 | //Signature and slot number the transaction was confirmed 59 | type SignatureStore = DashMap; 60 | 61 | pub struct ChainStateWsClient { 62 | slot: Arc, 63 | // signature -> (block_time, slot) 64 | signature_store: Arc, 65 | thread_hdls: Vec>, 66 | } 67 | 68 | impl ChainStateWsClient { 69 | pub fn new( 70 | runtime: Handle, 71 | shutdown: Arc, 72 | retain_slot_count: u64, 73 | ws_client: Arc, 74 | grpc_url: Option, 75 | ) -> Self { 76 | let current_slot = Arc::new(AtomicU64::new(0)); 77 | let signature_store = Arc::new(DashMap::new()); 78 | let mut hdl = Vec::new(); 79 | let block_listener_hdl = if let Some(grpc_url) = grpc_url { 80 | spawn_grpc_block_listener( 81 | runtime.clone(), 82 | shutdown.clone(), 83 | signature_store.clone(), 84 | retain_slot_count, 85 | grpc_url, 86 | ) 87 | } else { 88 | spawn_ws_block_listener( 89 | runtime.clone(), 90 | shutdown.clone(), 91 | signature_store.clone(), 92 | retain_slot_count, 93 | ws_client.clone(), 94 | ) 95 | }; 96 | hdl.push(block_listener_hdl); 97 | hdl.push(spawn_ws_slot_listener( 98 | runtime.clone(), 99 | shutdown, 100 | current_slot.clone(), 101 | ws_client, 102 | )); 103 | 104 | Self { 105 | slot: current_slot, 106 | signature_store, 107 | thread_hdls: hdl, 108 | } 109 | } 110 | } 111 | 112 | impl ChainStateClient for ChainStateWsClient { 113 | fn get_slot(&self) -> u64 { 114 | self.slot.load(Ordering::Relaxed) 115 | } 116 | 117 | fn confirm_signature_status(&self, signature: &str) -> Option { 118 | self.signature_store.get(signature).map(|v| *v) 119 | } 120 | } 121 | fn spawn_ws_block_listener( 122 | runtime: Handle, 123 | shutdown: Arc, 124 | signature_store: Arc, 125 | retain_slot_count: u64, 126 | ws_client: Arc, 127 | ) -> JoinHandle<()> { 128 | runtime.spawn(async move { 129 | let config = Some(RpcBlockSubscribeConfig { 130 | commitment: Some(CommitmentConfig::confirmed()), 131 | encoding: Some(Base64), 132 | transaction_details: Some(Signatures), 133 | show_rewards: Some(false), 134 | max_supported_transaction_version: Some(0), 135 | }); 136 | info!("Subscribing to ws block updates"); 137 | match ws_client 138 | .block_subscribe(RpcBlockSubscribeFilter::All, config) 139 | .await 140 | { 141 | Ok((mut stream, unsub)) => { 142 | while let Some(block) = stream.next().await { 143 | debug!("Block update"); 144 | gauge!("iris_current_block").set(block.value.slot as f64); 145 | if shutdown.load(Ordering::Relaxed) { 146 | break; 147 | } 148 | let block_update = block.value; 149 | if let Some(block) = block_update.block { 150 | let slot = block_update.slot; 151 | let _block_time = block.block_time; 152 | debug!("Block update: {:?}", slot); 153 | if let Some(signatures) = block.signatures { 154 | for signature in signatures { 155 | signature_store.insert(signature, slot); 156 | } 157 | } 158 | // remove old signatures to prevent leak of memory < slot - retain_slot_count 159 | signature_store.retain(|_, v| *v > slot - retain_slot_count); 160 | gauge!("iris_signature_store_size").set(signature_store.len() as f64); 161 | } 162 | } 163 | error!("Block stream ended unexpectedly!!"); 164 | drop(stream); 165 | unsub().await; 166 | //critical error 167 | shutdown.store(true, Ordering::Relaxed); 168 | } 169 | Err(e) => { 170 | error!("Error subscribing to block updates {:?}", e); 171 | shutdown.store(true, Ordering::Relaxed); 172 | return; 173 | } 174 | } 175 | }) 176 | } 177 | 178 | fn spawn_ws_slot_listener( 179 | runtime: Handle, 180 | shutdown: Arc, 181 | current_slot: Arc, 182 | ws_client: Arc, 183 | ) -> JoinHandle<()> { 184 | runtime.spawn(async move { 185 | let subscription = ws_client.slot_updates_subscribe().await.unwrap(); 186 | let (mut stream, unsub) = subscription; 187 | while let Some(slot_update) = stream.next().await { 188 | if shutdown.load(Ordering::Relaxed) { 189 | break; 190 | } 191 | let slot = match slot_update { 192 | SlotUpdate::FirstShredReceived { slot, .. } => slot, 193 | SlotUpdate::Completed { slot, .. } => slot.saturating_add(1), 194 | _ => continue, 195 | }; 196 | debug!("Slot update: {}", slot); 197 | gauge!("iris_current_slot").set(slot as f64); 198 | current_slot.store(slot, Ordering::SeqCst); 199 | } 200 | error!("Slot stream ended unexpectedly!!"); 201 | drop(stream); 202 | unsub().await; 203 | shutdown.store(true, Ordering::Relaxed); 204 | }) 205 | } 206 | 207 | fn spawn_grpc_block_listener( 208 | runtime: Handle, 209 | shutdown: Arc, 210 | signature_store: Arc, 211 | retain_slot_count: u64, 212 | endpoint: String, 213 | ) -> JoinHandle<()> { 214 | let max_retries = 10; 215 | runtime.spawn(async move { 216 | let mut retries = 0; 217 | loop { 218 | retries += 1; 219 | if retries > max_retries { 220 | error!("Max retries reached, shutting down geyser grpc block listener"); 221 | shutdown.store(true, Ordering::Relaxed); 222 | return; 223 | } 224 | 225 | let client = GeyserGrpcClient::build_from_shared(endpoint.clone()); 226 | if let Err(e) = client { 227 | error!("Error creating geyser grpc client: {:?}", e); 228 | tokio::time::sleep(Duration::from_secs(2)).await; 229 | continue; 230 | } 231 | 232 | let client = client.unwrap() 233 | .max_encoding_message_size(64 * 1024 * 1024) 234 | .max_decoding_message_size(64 * 1024 * 1024); 235 | 236 | let connection = client.connect().await; 237 | if let Err(e) = connection { 238 | error!("Error connecting to geyser grpc: {:?}", e); 239 | tokio::time::sleep(Duration::from_secs(2)).await; 240 | continue; 241 | } 242 | 243 | let subscription = connection.unwrap().subscribe().await; 244 | if let Err(e) = subscription { 245 | error!("Error subscribing to geyser grpc: {:?}", e); 246 | tokio::time::sleep(Duration::from_secs(2)).await; 247 | continue; 248 | } 249 | 250 | let (mut grpc_tx, mut grpc_rx) = subscription.unwrap(); 251 | info!("Subscribing to grpc block updates.."); 252 | if let Err(e) = grpc_tx.send(block_subscribe_request!()).await { 253 | error!("Error sending subscription request: {:?}", e); 254 | tokio::time::sleep(Duration::from_secs(2)).await; 255 | continue; 256 | } 257 | 'event_loop: while let Ok(Some(update)) = timeout(Duration::from_secs(60), grpc_rx.next()).await { 258 | retries = 0; 259 | if shutdown.load(Ordering::Relaxed) { 260 | return; 261 | } 262 | match update { 263 | Ok(message) => match message.update_oneof { 264 | Some(UpdateOneof::Block(block)) => { 265 | let slot = block.slot; 266 | debug!("Block update: {:?}", slot); 267 | for transaction in block.transactions { 268 | let signature = Signature::try_from(transaction.signature) 269 | .expect("Invalid signature"); 270 | signature_store.insert(signature.to_string(), slot); 271 | } 272 | // remove old signatures to prevent leak of memory < slot - retain_slot_count 273 | signature_store.retain(|_, v| *v > slot - retain_slot_count); 274 | gauge!("iris_signature_store_size").set(signature_store.len() as f64); 275 | } 276 | Some(UpdateOneof::Ping(_)) => { 277 | if let Err(e) = grpc_tx.send(ping_request!()).await { 278 | error!("Error sending ping: {}", e); 279 | break 'event_loop; 280 | } 281 | } 282 | Some(UpdateOneof::Pong(_)) => {} 283 | _ => { 284 | debug!("Unknown message type"); 285 | } 286 | }, 287 | Err(e) => { 288 | error!("Error block updates subscription {:?}", e); 289 | tokio::time::sleep(Duration::from_secs(2)).await; 290 | break 'event_loop; 291 | } 292 | } 293 | } 294 | } 295 | }) 296 | } 297 | -------------------------------------------------------------------------------- /src/connection_cache_client.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{CreateClient, SendTransactionClient}; 2 | use log::{info, warn}; 3 | use metrics::counter; 4 | use solana_client::connection_cache::ConnectionCache; 5 | use solana_connection_cache::nonblocking::client_connection::ClientConnection; 6 | use solana_sdk::signature::Keypair; 7 | use solana_tpu_client_next::leader_updater::LeaderUpdater; 8 | use std::net::{IpAddr, Ipv4Addr}; 9 | use std::sync::{Arc, Mutex}; 10 | use std::time::Duration; 11 | use tokio::runtime::Handle; 12 | use tokio::time::timeout; 13 | use tracing::error; 14 | 15 | const MAX_RETRIES: u32 = 10; 16 | 17 | pub struct ConnectionCacheClient { 18 | runtime: Handle, 19 | connection_cache: Arc, 20 | leader_updater: Mutex>, 21 | lookahead_slots: u64, 22 | } 23 | 24 | impl SendTransactionClient for ConnectionCacheClient { 25 | fn send_transaction(&self, wire_transaction: Vec) { 26 | counter!("iris_tx_send_to_connection_cache").increment(1); 27 | let leaders = { 28 | let leaders_lock = self.leader_updater.lock(); 29 | leaders_lock 30 | .unwrap() 31 | .next_leaders(self.lookahead_slots as usize) 32 | }; 33 | for leader in leaders { 34 | let connection_cache = self.connection_cache.clone(); 35 | let wire_transaction = wire_transaction.clone(); 36 | self.runtime.spawn(async move { 37 | for _ in 0..MAX_RETRIES { 38 | let conn = connection_cache.get_nonblocking_connection(&leader); 39 | if let Ok(e) = timeout( 40 | Duration::from_millis(500), 41 | conn.send_data(&wire_transaction), 42 | ) 43 | .await 44 | { 45 | if let Err(e) = e { 46 | error!( 47 | "Failed to send transaction to leader TRANSPORT_ERROR {:?}: {:?}", 48 | leader, e 49 | ); 50 | counter!("iris_error", "type" => "cannot_send_to_leader").increment(1); 51 | } else { 52 | info!("Successfully sent transaction to leader: {:?}", leader); 53 | break; 54 | } 55 | } else { 56 | warn!( 57 | "Failed to send transaction to leader TIMEOUT: {:?}:", 58 | leader 59 | ); 60 | } 61 | } 62 | }); 63 | } 64 | } 65 | 66 | fn send_transaction_batch(&self, wire_transaction: Vec>) { 67 | unimplemented!() 68 | } 69 | } 70 | 71 | impl CreateClient for ConnectionCacheClient { 72 | fn create_client( 73 | runtime: Handle, 74 | leader_updater: Box, 75 | lookahead_slots: u64, 76 | validator_identity: Keypair, 77 | ) -> Self { 78 | let connection_cache = Arc::new(ConnectionCache::new_with_client_options( 79 | "iris", 80 | 24, 81 | None, // created if none specified 82 | Some((&validator_identity, IpAddr::V4(Ipv4Addr::new(0, 0, 0, 0)))), 83 | None, // not used as far as I can tell 84 | )); 85 | Self { 86 | runtime, 87 | connection_cache, 88 | leader_updater: Mutex::new(leader_updater), 89 | lookahead_slots, 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | #![warn(unused_crate_dependencies)] 2 | 3 | use crate::chain_state::ChainStateWsClient; 4 | use crate::connection_cache_client::ConnectionCacheClient; 5 | use crate::rpc::IrisRpcServer; 6 | use crate::rpc_server::IrisRpcServerImpl; 7 | use crate::tpu_next_client::TpuClientNextSender; 8 | use crate::utils::{ChainStateClient, CreateClient, SendTransactionClient}; 9 | use anyhow::anyhow; 10 | use figment::providers::Env; 11 | use figment::Figment; 12 | use jsonrpsee::server::ServerBuilder; 13 | use metrics_exporter_prometheus::PrometheusBuilder; 14 | use rustls::crypto::CryptoProvider; 15 | use serde::{Deserialize, Serialize}; 16 | use solana_client::nonblocking::pubsub_client::PubsubClient; 17 | use solana_client::nonblocking::rpc_client::RpcClient; 18 | use solana_sdk::signature::{read_keypair_file, Keypair}; 19 | use solana_tpu_client_next::leader_updater::create_leader_updater; 20 | use std::fmt::Debug; 21 | use std::net::SocketAddr; 22 | use std::process; 23 | use std::sync::atomic::AtomicBool; 24 | use std::sync::Arc; 25 | use std::time::Duration; 26 | use tokio::runtime::Handle; 27 | use tracing::info; 28 | use tracing_subscriber::EnvFilter; 29 | 30 | mod chain_state; 31 | mod connection_cache_client; 32 | mod rpc; 33 | mod rpc_server; 34 | mod store; 35 | mod tpu_next_client; 36 | mod utils; 37 | mod vendor; 38 | 39 | #[derive(Debug, Serialize, Deserialize)] 40 | pub struct Config { 41 | rpc_url: String, 42 | ws_url: String, 43 | address: SocketAddr, 44 | identity_keypair_file: Option, 45 | grpc_url: Option, 46 | max_retries: u32, 47 | //The number of connections to be maintained by the scheduler. 48 | num_connections: usize, 49 | //Whether to skip checking the transaction blockhash expiration. 50 | skip_check_transaction_age: bool, 51 | //The size of the channel used to transmit transaction batches to the worker tasks. 52 | worker_channel_size: usize, 53 | //The maximum number of reconnection attempts allowed in case of connection failure. 54 | max_reconnect_attempts: usize, 55 | //The number of slots to look ahead during the leader estimation procedure. 56 | //Determines how far into the future leaders are estimated, 57 | //allowing connections to be established with those leaders in advance. 58 | lookahead_slots: u64, 59 | use_tpu_client_next: bool, 60 | prometheus_addr: SocketAddr, 61 | retry_interval_seconds: u32, 62 | } 63 | 64 | fn default_true() -> bool { 65 | true 66 | } 67 | 68 | #[tokio::main] 69 | async fn main() -> anyhow::Result<()> { 70 | //for some reason ths is required to make rustls work 71 | CryptoProvider::install_default(rustls::crypto::ring::default_provider()) 72 | .expect("Failed to install default crypto provider"); 73 | 74 | dotenv::dotenv().ok(); 75 | env_logger::init(); 76 | //setup tracing 77 | tracing::subscriber::set_global_default( 78 | tracing_subscriber::fmt() 79 | .with_env_filter(EnvFilter::from_env("RUST_LOG")) 80 | .finish(), 81 | ) 82 | .expect("Failed to set up tracing"); 83 | 84 | //read config from env variables 85 | let config: Config = Figment::new().merge(Env::raw()).extract().unwrap(); 86 | info!("config: {:?}", config); 87 | 88 | let identity_keypair = config 89 | .identity_keypair_file 90 | .as_ref() 91 | .map(|file| read_keypair_file(file).expect("Failed to read identity keypair file")) 92 | .unwrap_or(Keypair::new()); 93 | 94 | let _metrics = PrometheusBuilder::new() 95 | .with_http_listener(config.prometheus_addr) 96 | .install() 97 | .expect("failed to install recorder/exporter"); 98 | 99 | let shutdown = Arc::new(AtomicBool::new(false)); 100 | let rpc = Arc::new(RpcClient::new(config.rpc_url.to_owned())); 101 | info!("creating leader updater..."); 102 | let leader_updater = create_leader_updater(rpc.clone(), config.ws_url.to_owned(), None) 103 | .await 104 | .map_err(|e| anyhow!(e))?; 105 | info!("leader updater created"); 106 | let txn_store = Arc::new(store::TransactionStoreImpl::new()); 107 | 108 | let tx_client: Arc = if config.use_tpu_client_next { 109 | log::info!("Using TpuClientNextSender"); 110 | Arc::new(TpuClientNextSender::create_client( 111 | Handle::current(), 112 | leader_updater, 113 | config.lookahead_slots, 114 | identity_keypair, 115 | )) 116 | } else { 117 | log::info!("Using ConnectionCacheClient"); 118 | Arc::new(ConnectionCacheClient::create_client( 119 | Handle::current(), 120 | leader_updater, 121 | config.lookahead_slots, 122 | identity_keypair, 123 | )) 124 | }; 125 | let ws_client = PubsubClient::new(&config.ws_url) 126 | .await 127 | .expect("Failed to connect to websocket"); 128 | 129 | let chain_state: Arc = Arc::new(ChainStateWsClient::new( 130 | Handle::current(), 131 | shutdown.clone(), 132 | 800, // around 4 mins 133 | Arc::new(ws_client), 134 | config.grpc_url, 135 | )); 136 | let iris = IrisRpcServerImpl::new( 137 | tx_client, 138 | txn_store, 139 | chain_state, 140 | Duration::from_secs(config.retry_interval_seconds as u64), 141 | shutdown.clone(), 142 | config.max_retries, 143 | ); 144 | 145 | let server = ServerBuilder::default() 146 | .max_request_body_size(15_000_000) 147 | .max_connections(1_000_000) 148 | .build(config.address) 149 | .await?; 150 | 151 | info!("server starting in {:?}", config.address); 152 | let server_hdl = server.start(iris.into_rpc()); 153 | //exit when shutdown is triggered 154 | while !shutdown.load(std::sync::atomic::Ordering::Relaxed) { 155 | tokio::time::sleep(Duration::from_secs(1)).await; 156 | } 157 | server_hdl.stop()?; 158 | server_hdl.stopped().await; 159 | process::exit(1); 160 | } 161 | -------------------------------------------------------------------------------- /src/rpc.rs: -------------------------------------------------------------------------------- 1 | use jsonrpsee::core::RpcResult; 2 | use jsonrpsee::proc_macros::rpc; 3 | use solana_rpc_client_api::config::RpcSendTransactionConfig; 4 | 5 | #[rpc(server)] 6 | pub trait IrisRpc { 7 | #[method(name = "health")] 8 | async fn health(&self) -> String; 9 | #[method(name = "sendTransaction")] 10 | async fn send_transaction( 11 | &self, 12 | txn: String, 13 | params: RpcSendTransactionConfig, 14 | ) -> RpcResult; 15 | 16 | #[method(name = "sendTransactionBatch")] 17 | async fn send_transaction_batch( 18 | &self, 19 | txns: Vec, 20 | params: RpcSendTransactionConfig, 21 | ) -> RpcResult>; 22 | } 23 | -------------------------------------------------------------------------------- /src/rpc_server.rs: -------------------------------------------------------------------------------- 1 | use crate::rpc::IrisRpcServer; 2 | use crate::store::{TransactionData, TransactionStore}; 3 | use crate::utils::{ChainStateClient, SendTransactionClient}; 4 | use crate::vendor::solana_rpc::decode_and_deserialize; 5 | use jsonrpsee::core::{async_trait, RpcResult}; 6 | use jsonrpsee::types::error::INVALID_PARAMS_CODE; 7 | use jsonrpsee::types::ErrorObjectOwned; 8 | use metrics::{counter, gauge, histogram}; 9 | use solana_client::rpc_client::SerializableTransaction; 10 | use solana_rpc_client_api::config::RpcSendTransactionConfig; 11 | use solana_sdk::transaction::VersionedTransaction; 12 | use solana_transaction_status::UiTransactionEncoding; 13 | use std::sync::atomic::AtomicBool; 14 | use std::sync::Arc; 15 | use std::time::Duration; 16 | use tracing::info; 17 | 18 | pub struct IrisRpcServerImpl { 19 | txn_sender: Arc, 20 | store: Arc, 21 | chain_state: Arc, 22 | retry_interval: Duration, 23 | max_retries: u32, 24 | } 25 | 26 | pub fn invalid_request(reason: &str) -> ErrorObjectOwned { 27 | ErrorObjectOwned::owned( 28 | INVALID_PARAMS_CODE, 29 | format!("Invalid Request: {reason}"), 30 | None::, 31 | ) 32 | } 33 | 34 | impl IrisRpcServerImpl { 35 | pub fn new( 36 | txn_sender: Arc, 37 | store: Arc, 38 | chain_state: Arc, 39 | retry_interval: Duration, 40 | shutdown: Arc, 41 | max_retries: u32, 42 | ) -> Self { 43 | let client = IrisRpcServerImpl { 44 | txn_sender, 45 | store, 46 | chain_state, 47 | retry_interval, 48 | max_retries, 49 | }; 50 | client.spawn_retry_transactions_loop(shutdown); 51 | client 52 | } 53 | 54 | fn spawn_retry_transactions_loop(&self, shutdown: Arc) { 55 | let store = self.store.clone(); 56 | let chain_state = self.chain_state.clone(); 57 | let txn_sender = self.txn_sender.clone(); 58 | let retry_interval = self.retry_interval; 59 | 60 | tokio::spawn(async move { 61 | loop { 62 | if shutdown.load(std::sync::atomic::Ordering::Relaxed) { 63 | break; 64 | } 65 | 66 | let transactions_map = store.get_transactions(); 67 | let mut transactions_to_remove = vec![]; 68 | let mut transactions_to_send = vec![]; 69 | gauge!("iris_retry_transactions").set(transactions_map.len() as f64); 70 | 71 | for mut txn in transactions_map.iter_mut() { 72 | if let Some(slot) = chain_state.confirm_signature_status(&txn.key()) { 73 | info!( 74 | "Transaction confirmed at slot: {slot} latency {:}", 75 | slot.saturating_sub(txn.slot) 76 | ); 77 | counter!("iris_txn_landed").increment(1); 78 | histogram!("iris_txn_slot_latency") 79 | .record(slot.saturating_sub(txn.slot) as f64); 80 | transactions_to_remove.push(txn.key().clone()); 81 | } 82 | //check if transaction has been in the store for too long 83 | if txn.value().sent_at.elapsed() > Duration::from_secs(60) { 84 | transactions_to_remove.push(txn.key().clone()); 85 | } 86 | //check if max retries has been reached 87 | if txn.retry_count == 0usize { 88 | transactions_to_remove.push(txn.key().clone()); 89 | } 90 | if txn.retry_count > 0usize { 91 | transactions_to_send.push(txn.wire_transaction.clone()); 92 | } 93 | txn.retry_count = txn.retry_count.saturating_sub(1); 94 | } 95 | 96 | gauge!("iris_transactions_removed").increment(transactions_to_remove.len() as f64); 97 | for signature in transactions_to_remove { 98 | store.remove_transaction(signature); 99 | } 100 | 101 | info!( 102 | "retrying {} transactions", 103 | transactions_to_send.iter().len() 104 | ); 105 | for batches in transactions_to_send.chunks(10) { 106 | txn_sender.send_transaction_batch(batches.to_vec()); 107 | } 108 | 109 | tokio::time::sleep(retry_interval).await; 110 | } 111 | }); 112 | } 113 | } 114 | #[async_trait] 115 | impl IrisRpcServer for IrisRpcServerImpl { 116 | async fn health(&self) -> String { 117 | "Ok(1.2)".to_string() 118 | } 119 | 120 | async fn send_transaction( 121 | &self, 122 | txn: String, 123 | params: RpcSendTransactionConfig, 124 | ) -> RpcResult { 125 | info!("Received transaction on rpc connection loop"); 126 | if self.store.has_signature(&txn) { 127 | counter!("iris_error", "type" => "duplicate_transaction").increment(1); 128 | return Err(invalid_request("duplicate transaction")); 129 | } 130 | counter!("iris_txn_total_transactions").increment(1); 131 | let encoding = params.encoding.unwrap_or(UiTransactionEncoding::Base58); 132 | if !params.skip_preflight { 133 | counter!("iris_error", "type" => "preflight_check").increment(1); 134 | return Err(invalid_request("running preflight check is not supported")); 135 | } 136 | let binary_encoding = encoding.into_binary_encoding().ok_or_else(|| { 137 | counter!("iris_error", "type" => "invalid_encoding").increment(1); 138 | invalid_request(&format!( 139 | "unsupported encoding: {encoding}. Supported encodings: base58, base64" 140 | )) 141 | })?; 142 | let (wire_transaction, versioned_transaction) = 143 | match decode_and_deserialize::(txn, binary_encoding) { 144 | Ok((wire_transaction, versioned_transaction)) => { 145 | (wire_transaction, versioned_transaction) 146 | } 147 | Err(e) => { 148 | counter!("iris_error", "type" => "cannot_decode_transaction").increment(1); 149 | return Err(invalid_request(&e.to_string())); 150 | } 151 | }; 152 | let signature = versioned_transaction.get_signature().to_string(); 153 | info!("processing transaction with signature: {signature}"); 154 | let slot = self.chain_state.get_slot(); 155 | let transaction = TransactionData::new( 156 | wire_transaction, 157 | versioned_transaction, 158 | slot, 159 | params.max_retries.unwrap_or(self.max_retries as usize), 160 | ); 161 | // add to store 162 | self.store.add_transaction(transaction.clone()); 163 | self.txn_sender 164 | .send_transaction(transaction.wire_transaction); 165 | Ok(signature) 166 | } 167 | 168 | async fn send_transaction_batch( 169 | &self, 170 | batch: Vec, 171 | params: RpcSendTransactionConfig, 172 | ) -> RpcResult> { 173 | if batch.len() > 10 { 174 | counter!("iris_error", "type" => "batch_size_exceeded").increment(1); 175 | return Err(invalid_request("batch size exceeded")); 176 | } 177 | counter!("iris_txn_total_batches").increment(1); 178 | let mut wired_transactions = Vec::new(); 179 | let mut signatures = Vec::new(); 180 | for txn in batch { 181 | if self.store.has_signature(&txn) { 182 | counter!("iris_error", "type" => "duplicate_transaction_in_batch").increment(1); 183 | return Err(invalid_request("duplicate transaction")); 184 | } 185 | let encoding = params.encoding.unwrap_or(UiTransactionEncoding::Base58); 186 | if !params.skip_preflight { 187 | counter!("iris_error", "type" => "preflight_check").increment(1); 188 | return Err(invalid_request("running preflight check is not supported")); 189 | } 190 | let binary_encoding = encoding.into_binary_encoding().ok_or_else(|| { 191 | counter!("iris_error", "type" => "invalid_encoding").increment(1); 192 | invalid_request(&format!( 193 | "unsupported encoding: {encoding}. Supported encodings: base58, base64" 194 | )) 195 | })?; 196 | let (wire_transaction, versioned_transaction) = 197 | match decode_and_deserialize::(txn, binary_encoding) { 198 | Ok((wire_transaction, versioned_transaction)) => { 199 | (wire_transaction, versioned_transaction) 200 | } 201 | Err(e) => { 202 | counter!("iris_error", "type" => "cannot_decode_transaction").increment(1); 203 | return Err(invalid_request(&e.to_string())); 204 | } 205 | }; 206 | let signature = versioned_transaction.get_signature().to_string(); 207 | let slot = self.chain_state.get_slot(); 208 | let transaction = TransactionData::new( 209 | wire_transaction, 210 | versioned_transaction, 211 | slot, 212 | params.max_retries.unwrap_or(self.max_retries as usize), 213 | ); 214 | // add to store 215 | self.store.add_transaction(transaction.clone()); 216 | wired_transactions.push(transaction.wire_transaction); 217 | signatures.push(signature); 218 | } 219 | self.txn_sender.send_transaction_batch(wired_transactions); 220 | Ok(signatures) 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/store.rs: -------------------------------------------------------------------------------- 1 | use dashmap::DashMap; 2 | use solana_sdk::transaction::VersionedTransaction; 3 | use std::sync::Arc; 4 | use std::time::Instant; 5 | use tracing::error; 6 | 7 | #[derive(Clone, Debug)] 8 | pub struct TransactionData { 9 | pub wire_transaction: Vec, 10 | pub versioned_transaction: VersionedTransaction, 11 | pub sent_at: Instant, 12 | pub slot: u64, 13 | pub retry_count: usize, 14 | } 15 | 16 | impl TransactionData { 17 | pub fn new( 18 | wire_transaction: Vec, 19 | versioned_transaction: VersionedTransaction, 20 | slot: u64, 21 | retry_count: usize, 22 | ) -> Self { 23 | Self { 24 | wire_transaction, 25 | versioned_transaction, 26 | sent_at: Instant::now(), 27 | slot, 28 | retry_count, 29 | } 30 | } 31 | } 32 | 33 | pub trait TransactionStore: Send + Sync { 34 | fn add_transaction(&self, transaction: TransactionData); 35 | fn get_signatures(&self) -> Vec; 36 | fn remove_transaction(&self, signature: String) -> Option; 37 | fn get_transactions(&self) -> Arc>; 38 | fn has_signature(&self, signature: &str) -> bool; 39 | } 40 | 41 | pub struct TransactionStoreImpl { 42 | transactions: Arc>, 43 | } 44 | 45 | impl TransactionStoreImpl { 46 | pub fn new() -> Self { 47 | let transaction_store = Self { 48 | transactions: Arc::new(DashMap::new()), 49 | }; 50 | transaction_store 51 | } 52 | } 53 | 54 | impl TransactionStore for TransactionStoreImpl { 55 | fn add_transaction(&self, transaction: TransactionData) { 56 | if let Some(signature) = get_signature(&transaction) { 57 | if self.transactions.contains_key(&signature) { 58 | return; 59 | } 60 | self.transactions.insert(signature.to_string(), transaction); 61 | } else { 62 | error!("Transaction has no signatures"); 63 | } 64 | } 65 | fn get_signatures(&self) -> Vec { 66 | let signatures = self 67 | .transactions 68 | .iter() 69 | .map(|t| get_signature(&t).unwrap()) 70 | .collect(); 71 | signatures 72 | } 73 | fn remove_transaction(&self, signature: String) -> Option { 74 | let transaction = self.transactions.remove(&signature); 75 | transaction.map_or(None, |t| Some(t.1)) 76 | } 77 | fn get_transactions(&self) -> Arc> { 78 | self.transactions.clone() 79 | } 80 | fn has_signature(&self, signature: &str) -> bool { 81 | self.transactions.contains_key(signature) 82 | } 83 | } 84 | 85 | pub fn get_signature(transaction: &TransactionData) -> Option { 86 | transaction 87 | .versioned_transaction 88 | .signatures 89 | .get(0) 90 | .map(|s| s.to_string()) 91 | } 92 | -------------------------------------------------------------------------------- /src/tpu_next_client.rs: -------------------------------------------------------------------------------- 1 | use crate::utils::{CreateClient, SendTransactionClient}; 2 | use metrics::counter; 3 | use solana_sdk::signature::Keypair; 4 | use solana_tpu_client_next::connection_workers_scheduler::{ 5 | ConnectionWorkersSchedulerConfig, Fanout, 6 | }; 7 | use solana_tpu_client_next::leader_updater::LeaderUpdater; 8 | use solana_tpu_client_next::transaction_batch::TransactionBatch; 9 | use solana_tpu_client_next::ConnectionWorkersScheduler; 10 | use std::net::{Ipv4Addr, SocketAddr}; 11 | use tokio::runtime::Handle; 12 | use tokio_util::sync::CancellationToken; 13 | use tracing::error; 14 | 15 | pub struct TpuClientNextSender { 16 | runtime: Handle, 17 | sender: tokio::sync::mpsc::Sender, 18 | cancel: CancellationToken, 19 | } 20 | 21 | impl CreateClient for TpuClientNextSender { 22 | fn create_client( 23 | runtime: Handle, 24 | leader_info: Box, 25 | leader_forward_count: u64, 26 | validator_identity: Keypair, 27 | ) -> Self { 28 | spawn_tpu_client_send_txs( 29 | runtime, 30 | leader_info, 31 | leader_forward_count, 32 | validator_identity, 33 | ) 34 | } 35 | } 36 | 37 | fn spawn_tpu_client_send_txs( 38 | runtime_handle: Handle, 39 | leader_info: Box, 40 | leader_forward_count: u64, 41 | validator_identity: Keypair, 42 | ) -> TpuClientNextSender { 43 | let (sender, receiver) = tokio::sync::mpsc::channel(16); 44 | let cancel = CancellationToken::new(); 45 | let _handle = runtime_handle.spawn({ 46 | let cancel = cancel.clone(); 47 | async move { 48 | let config = ConnectionWorkersSchedulerConfig { 49 | bind: SocketAddr::new(Ipv4Addr::new(0, 0, 0, 0).into(), 0), 50 | stake_identity: Some(validator_identity), 51 | // to match MAX_CONNECTIONS from ConnectionCache 52 | num_connections: 1024, 53 | skip_check_transaction_age: true, 54 | worker_channel_size: 128, 55 | max_reconnect_attempts: 4, 56 | leaders_fanout: Fanout { 57 | connect: leader_forward_count as usize, 58 | send: leader_forward_count as usize, 59 | }, 60 | }; 61 | let _scheduler = tokio::spawn(ConnectionWorkersScheduler::run( 62 | config, 63 | leader_info, 64 | receiver, 65 | cancel.clone(), 66 | )); 67 | } 68 | }); 69 | TpuClientNextSender { 70 | runtime: runtime_handle, 71 | sender, 72 | cancel, 73 | } 74 | } 75 | 76 | impl SendTransactionClient for TpuClientNextSender { 77 | fn send_transaction(&self, wire_transaction: Vec) { 78 | self.send_transaction_batch(vec![wire_transaction]); 79 | } 80 | 81 | fn send_transaction_batch(&self, wire_transactions: Vec>) { 82 | counter!("iris_tx_send_to_tpu_client_next").increment(wire_transactions.len() as u64); 83 | let txn_batch = TransactionBatch::new(wire_transactions); 84 | let sender = self.sender.clone(); 85 | self.runtime.spawn(async move { 86 | if let Err(e) = sender.send(txn_batch).await { 87 | error!("Failed to send transaction: {:?}", e); 88 | counter!("iris_error", "type" => "cannot_send_local").increment(1); 89 | } 90 | }); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | use rand::distributions::Alphanumeric; 2 | use rand::Rng; 3 | use solana_sdk::signature::Keypair; 4 | use solana_tpu_client_next::leader_updater::LeaderUpdater; 5 | use tokio::runtime::Handle; 6 | 7 | pub trait SendTransactionClient: Send + Sync { 8 | fn send_transaction(&self, txn: Vec); 9 | fn send_transaction_batch(&self, wire_transaction: Vec>); 10 | } 11 | 12 | pub trait ChainStateClient: Send + Sync { 13 | fn get_slot(&self) -> u64; 14 | fn confirm_signature_status(&self, signature: &str) -> Option; 15 | } 16 | pub trait CreateClient: SendTransactionClient { 17 | fn create_client( 18 | maybe_runtime: Handle, 19 | leader_updater: Box, 20 | leader_forward_count: u64, 21 | validator_identity: Keypair, 22 | ) -> Self; 23 | } 24 | 25 | pub fn generate_random_string(len: usize) -> String { 26 | rand::thread_rng() 27 | .sample_iter(&Alphanumeric) 28 | .take(len) 29 | .map(char::from) 30 | .collect() 31 | } 32 | -------------------------------------------------------------------------------- /src/vendor/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod solana_rpc; 2 | -------------------------------------------------------------------------------- /src/vendor/solana_rpc.rs: -------------------------------------------------------------------------------- 1 | use crate::rpc_server::invalid_request; 2 | use base64::prelude::BASE64_STANDARD; 3 | use base64::Engine; 4 | use bincode::Options; 5 | use jsonrpsee::core::RpcResult; 6 | use solana_sdk::bs58; 7 | use solana_sdk::packet::PACKET_DATA_SIZE; 8 | use solana_transaction_status::TransactionBinaryEncoding; 9 | use std::any::type_name; 10 | 11 | const MAX_BASE58_SIZE: usize = 1683; // Golden, bump if PACKET_DATA_SIZE changes 12 | const MAX_BASE64_SIZE: usize = 1644; // Golden, bump if PACKET_DATA_SIZE changes 13 | pub fn decode_and_deserialize( 14 | encoded: String, 15 | encoding: TransactionBinaryEncoding, 16 | ) -> RpcResult<(Vec, T)> 17 | where 18 | T: serde::de::DeserializeOwned, 19 | { 20 | let wire_output = match encoding { 21 | TransactionBinaryEncoding::Base58 => { 22 | if encoded.len() > MAX_BASE58_SIZE { 23 | return Err(invalid_request(&format!( 24 | "base58 encoded {} too large: {} bytes (max: encoded/raw {}/{})", 25 | type_name::(), 26 | encoded.len(), 27 | MAX_BASE58_SIZE, 28 | PACKET_DATA_SIZE, 29 | ))); 30 | } 31 | bs58::decode(encoded) 32 | .into_vec() 33 | .map_err(|e| invalid_request(&format!("invalid base58 encoding: {e:?}")))? 34 | } 35 | TransactionBinaryEncoding::Base64 => { 36 | if encoded.len() > MAX_BASE64_SIZE { 37 | return Err(invalid_request(&format!( 38 | "base64 encoded {} too large: {} bytes (max: encoded/raw {}/{})", 39 | type_name::(), 40 | encoded.len(), 41 | MAX_BASE64_SIZE, 42 | PACKET_DATA_SIZE, 43 | ))); 44 | } 45 | BASE64_STANDARD 46 | .decode(encoded) 47 | .map_err(|e| invalid_request(&format!("invalid base64 encoding: {e:?}")))? 48 | } 49 | }; 50 | if wire_output.len() > PACKET_DATA_SIZE { 51 | return Err(invalid_request(&format!( 52 | "decoded {} too large: {} bytes (max: {} bytes)", 53 | type_name::(), 54 | wire_output.len(), 55 | PACKET_DATA_SIZE 56 | ))); 57 | } 58 | bincode::options() 59 | .with_limit(PACKET_DATA_SIZE as u64) 60 | .with_fixint_encoding() 61 | .allow_trailing_bytes() 62 | .deserialize_from(&wire_output[..]) 63 | .map_err(|err| { 64 | invalid_request(&format!( 65 | "failed to deserialize {}: {}", 66 | type_name::(), 67 | &err.to_string() 68 | )) 69 | }) 70 | .map(|output| (wire_output, output)) 71 | } 72 | --------------------------------------------------------------------------------