diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index dbf8f1c06b..c1cb3c5e0d 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -90,4 +90,4 @@ jobs: # title: 'Bump version to ${{ steps.parse_info.outputs.next_version }}', # head: 'version-bump-${{ steps.parse_info.outputs.next_version }}', # base: '${{ steps.parse_info.outputs.target_branch }}' -# }) \ No newline at end of file +# }) diff --git a/.gitmodules b/.gitmodules index e31fc7fccd..79348345d3 100644 --- a/.gitmodules +++ b/.gitmodules @@ -7,3 +7,6 @@ [submodule "jito-protos/protos"] path = jito-protos/protos url = https://github.com/jito-labs/mev-protos.git +[submodule "jito-protos/bam-protos"] + path = jito-protos/bam-protos + url = git@github.com:jito-labs/bam-protos.git diff --git a/Cargo.lock b/Cargo.lock index 3b0e78e95f..453ef832a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7597,6 +7597,7 @@ dependencies = [ "serde_json", "serial_test", "slab", + "smallvec", "solana-accounts-db", "solana-address-lookup-table-program", "solana-bloom", diff --git a/accounts-db/benches/bench_lock_accounts.rs b/accounts-db/benches/bench_lock_accounts.rs index cffd9becce..117a8097a2 100644 --- a/accounts-db/benches/bench_lock_accounts.rs +++ b/accounts-db/benches/bench_lock_accounts.rs @@ -85,7 +85,7 @@ fn bench_entry_lock_accounts(c: &mut Criterion) { for batch in &transaction_batches { let results = accounts.lock_accounts(black_box(batch.iter()), MAX_TX_ACCOUNT_LOCKS); - accounts.unlock_accounts(batch.iter().zip(&results)); + accounts.unlock_accounts(batch.iter().zip(&results), false); } }) }); diff --git a/accounts-db/src/accounts.rs b/accounts-db/src/accounts.rs index 5f01ff533d..f7da37ebcf 100644 --- a/accounts-db/src/accounts.rs +++ b/accounts-db/src/accounts.rs @@ -25,8 +25,9 @@ use { }, solana_transaction_context::TransactionAccount, std::{ + cell::RefCell, cmp::Reverse, - collections::{BinaryHeap, HashSet}, + collections::{BinaryHeap, HashMap, HashSet}, ops::RangeBounds, sync::{ atomic::{AtomicUsize, Ordering}, @@ -35,6 +36,11 @@ use { }, }; +thread_local! { + /// Reusable deduped locks for batched account locking. + static REUSABLE_DEDUPED_LOCKS: RefCell> = RefCell::new(HashMap::new()); +} + pub type PubkeyAccountSlot = (Pubkey, AccountSharedData, Slot); pub struct TransactionAccountLocksIterator<'a, T: SVMMessage> { @@ -570,7 +576,7 @@ impl Accounts { .map(|_| TransactionAccountLocksIterator::new(tx)) }) .collect(); - self.lock_accounts_inner(tx_account_locks_results, None, None) + self.lock_accounts_inner(tx_account_locks_results, None, None, false) } #[must_use] @@ -581,6 +587,7 @@ impl Accounts { tx_account_lock_limit: usize, additional_read_locks: Option<&HashSet>, additional_write_locks: Option<&HashSet>, + batched: bool, ) -> Vec> { // Validate the account locks, then get iterator if successful validation. let tx_account_locks_results: Vec> = txs @@ -595,6 +602,7 @@ impl Accounts { tx_account_locks_results, additional_read_locks, additional_write_locks, + batched, ) } @@ -604,7 +612,15 @@ impl Accounts { tx_account_locks_results: Vec>>, additional_read_locks: Option<&HashSet>, additional_write_locks: Option<&HashSet>, + batched: bool, ) -> Vec> { + if batched { + return self.lock_accounts_batched( + tx_account_locks_results, + additional_read_locks, + additional_write_locks, + ); + } let account_locks = &mut self.account_locks.lock().unwrap(); tx_account_locks_results .into_iter() @@ -619,15 +635,97 @@ impl Accounts { .collect() } + /// Lock accounts in batches, deduplicating locks across transactions. + /// Returns a vec with all transaction locks associated with the + /// first transaction result, and `Ok(())` for all other transactions (If successful). + fn lock_accounts_batched( + &self, + tx_account_locks_results: Vec>>, + additional_read_locks: Option<&HashSet>, + additional_write_locks: Option<&HashSet>, + ) -> Vec> { + if let Some(err) = tx_account_locks_results.iter().find_map(|res| { + if let Err(err) = res { + Some(err.clone()) + } else { + None + } + }) { + return tx_account_locks_results + .into_iter() + .map(|_| Err(err.clone())) + .collect(); + } + + let len = tx_account_locks_results.len(); + let result = REUSABLE_DEDUPED_LOCKS.with_borrow_mut(|deduped_locks| { + deduped_locks.clear(); + Self::get_deduped_batch_locks( + deduped_locks, + tx_account_locks_results.into_iter().map(|res| res.unwrap()), + ); + let account_locks = &mut self.account_locks.lock().unwrap(); + account_locks.try_lock_accounts( + deduped_locks + .iter() + .map(|(pubkey, is_writable)| (pubkey, *is_writable)), + additional_read_locks, + additional_write_locks, + ) + }); + + (0..len).map(|_| result.clone()).collect() + } + + /// Deduplicate the locks across all transactions in a batch; promoting read-only locks to writable + /// if a writable lock exists for the same pubkey. + fn get_deduped_batch_locks<'a>( + deduped_locks: &mut HashMap, + tx_account_locks: impl Iterator< + Item = TransactionAccountLocksIterator<'a, impl SVMMessage + 'a>, + >, + ) { + for tx_account_locks in tx_account_locks { + for (pubkey, is_writable) in tx_account_locks.accounts_with_is_writable() { + if let Some(existing) = deduped_locks.get(pubkey) { + if *existing && !is_writable { + continue; + } + } + deduped_locks.insert(*pubkey, is_writable); + } + } + } + /// Once accounts are unlocked, new transactions that modify that state can enter the pipeline pub fn unlock_accounts<'a, Tx: SVMMessage + 'a>( &self, txs_and_results: impl Iterator)> + Clone, + batched: bool, ) { if !txs_and_results.clone().any(|(_, res)| res.is_ok()) { return; } + if batched { + REUSABLE_DEDUPED_LOCKS.with_borrow_mut(|deduped_locks| { + deduped_locks.clear(); + Self::get_deduped_batch_locks( + deduped_locks, + txs_and_results + .clone() + .map(|(tx, _)| TransactionAccountLocksIterator::new(tx)), + ); + let mut account_locks = self.account_locks.lock().unwrap(); + account_locks.unlock_accounts( + deduped_locks + .iter() + .map(|(pubkey, is_writable)| (pubkey, *is_writable)), + ); + }); + return; + } + let mut account_locks = self.account_locks.lock().unwrap(); debug!("bank unlock accounts"); for (tx, res) in txs_and_results { @@ -991,7 +1089,7 @@ mod tests { let txs = vec![new_sanitized_tx(&[&keypair], message, Hash::default())]; let results = accounts.lock_accounts(txs.iter(), MAX_TX_ACCOUNT_LOCKS); assert_eq!(results, vec![Ok(())]); - accounts.unlock_accounts(txs.iter().zip(&results)); + accounts.unlock_accounts(txs.iter().zip(&results), false); } // Disallow over MAX_TX_ACCOUNT_LOCKS @@ -1089,8 +1187,8 @@ mod tests { .unwrap() .is_locked_readonly(&keypair1.pubkey())); - accounts.unlock_accounts(iter::once(&tx).zip(&results0)); - accounts.unlock_accounts(txs.iter().zip(&results1)); + accounts.unlock_accounts(iter::once(&tx).zip(&results0), false); + accounts.unlock_accounts(txs.iter().zip(&results1), false); let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; let message = Message::new_with_compiled_instructions( 1, @@ -1171,7 +1269,7 @@ mod tests { counter_clone.clone().fetch_add(1, Ordering::Release); } } - accounts_clone.unlock_accounts(txs.iter().zip(&results)); + accounts_clone.unlock_accounts(txs.iter().zip(&results), false); if exit_clone.clone().load(Ordering::Relaxed) { break; } @@ -1187,7 +1285,7 @@ mod tests { thread::sleep(time::Duration::from_millis(50)); assert_eq!(counter_value, counter_clone.clone().load(Ordering::Acquire)); } - accounts_arc.unlock_accounts(txs.iter().zip(&results)); + accounts_arc.unlock_accounts(txs.iter().zip(&results), false); thread::sleep(time::Duration::from_millis(50)); } exit.store(true, Ordering::Relaxed); @@ -1321,6 +1419,7 @@ mod tests { MAX_TX_ACCOUNT_LOCKS, None, None, + false, ); assert_eq!( @@ -1638,4 +1737,89 @@ mod tests { )); } } + + #[test] + fn test_batched_locking() { + let keypair0 = Keypair::new(); + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + let keypair3 = Keypair::new(); + + let account0 = AccountSharedData::new(1, 0, &Pubkey::default()); + let account1 = AccountSharedData::new(2, 0, &Pubkey::default()); + let account2 = AccountSharedData::new(3, 0, &Pubkey::default()); + let account3 = AccountSharedData::new(4, 0, &Pubkey::default()); + + let accounts_db = AccountsDb::new_single_for_tests(); + let accounts = Accounts::new(Arc::new(accounts_db)); + accounts.store_for_tests(0, &keypair0.pubkey(), &account0); + accounts.store_for_tests(0, &keypair1.pubkey(), &account1); + accounts.store_for_tests(0, &keypair2.pubkey(), &account2); + accounts.store_for_tests(0, &keypair3.pubkey(), &account3); + + let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; + let message = Message::new_with_compiled_instructions( + 1, + 0, + 2, + vec![keypair1.pubkey(), keypair0.pubkey(), native_loader::id()], + Hash::default(), + instructions, + ); + let tx0 = new_sanitized_tx(&[&keypair1], message, Hash::default()); + let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; + let message = Message::new_with_compiled_instructions( + 1, + 0, + 2, + vec![keypair2.pubkey(), keypair0.pubkey(), native_loader::id()], + Hash::default(), + instructions, + ); + let tx1 = new_sanitized_tx(&[&keypair2], message, Hash::default()); + let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; + let message = Message::new_with_compiled_instructions( + 1, + 0, + 2, + vec![keypair3.pubkey(), keypair0.pubkey(), native_loader::id()], + Hash::default(), + instructions, + ); + let tx2 = new_sanitized_tx(&[&keypair3], message, Hash::default()); + let txs = vec![tx0, tx1, tx2]; + + let qos_results = vec![Ok(()), Ok(()), Ok(())]; + + let results = accounts.lock_accounts_with_results( + txs.iter(), + qos_results.into_iter(), + MAX_TX_ACCOUNT_LOCKS, + None, + None, + true, + ); + + assert_eq!( + results, + vec![ + Ok(()), // Read-only account (keypair0) can be referenced multiple times + Ok(()), // Read-only account (keypair0) can be referenced multiple times + Ok(()), // Read-only account (keypair0) can be referenced multiple times + ], + ); + + // verify that keypair0 read-only locked + assert!(accounts + .account_locks + .lock() + .unwrap() + .is_locked_readonly(&keypair0.pubkey())); + // verify that keypair2 (for tx1) is write-locked (2 txns referencing it) + assert!(accounts + .account_locks + .lock() + .unwrap() + .is_locked_write(&keypair2.pubkey())); + } } diff --git a/banking-bench/src/main.rs b/banking-bench/src/main.rs index 3ad81eac1b..bc23373c75 100644 --- a/banking-bench/src/main.rs +++ b/banking-bench/src/main.rs @@ -507,6 +507,8 @@ fn main() { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); // This is so that the signal_receiver does not go out of scope after the closure. diff --git a/core/Cargo.toml b/core/Cargo.toml index 494f685f3f..aa40c2563b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -80,6 +80,7 @@ serde = { workspace = true } serde_bytes = { workspace = true } serde_derive = { workspace = true } slab = { workspace = true } +smallvec = { workspace = true } solana-accounts-db = { workspace = true } solana-bloom = { workspace = true } solana-builtins-default-costs = { workspace = true } @@ -132,6 +133,7 @@ solana-version = { workspace = true } solana-vote = { workspace = true } solana-vote-program = { workspace = true } solana-wen-restart = { workspace = true } +spl-memo = { workspace = true, features = ["no-entrypoint"] } strum = { workspace = true, features = ["derive"] } strum_macros = { workspace = true } sys-info = { workspace = true } diff --git a/core/benches/banking_stage.rs b/core/benches/banking_stage.rs index fab11c1959..73e59a7887 100644 --- a/core/benches/banking_stage.rs +++ b/core/benches/banking_stage.rs @@ -339,6 +339,8 @@ fn bench_banking( HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); let chunk_len = verified.len() / CHUNKS; diff --git a/core/benches/consumer.rs b/core/benches/consumer.rs index f6a85b8637..c2bee835e7 100644 --- a/core/benches/consumer.rs +++ b/core/benches/consumer.rs @@ -177,6 +177,7 @@ fn bench_process_and_record_transactions(bencher: &mut Bencher, batch_size: usiz transaction_iter.next().unwrap(), 0, &|_| 0, + false, ); assert!(summary .execute_and_commit_transactions_output diff --git a/core/src/bam_connection.rs b/core/src/bam_connection.rs new file mode 100644 index 0000000000..eda127c33c --- /dev/null +++ b/core/src/bam_connection.rs @@ -0,0 +1,366 @@ +// Maintains a connection to the BAM Node and handles sending and receiving messages +// Keeps track of last received heartbeat 'behind the scenes' and will mark itself as unhealthy if no heartbeat is received + +use { + crate::bam_dependencies::v0_to_versioned_proto, + futures::{channel::mpsc, SinkExt, StreamExt}, + jito_protos::proto::{ + bam_api::{ + bam_node_api_client::BamNodeApiClient, start_scheduler_message_v0::Msg, + start_scheduler_response::VersionedMsg, start_scheduler_response_v0::Resp, + AuthChallengeRequest, ConfigRequest, ConfigResponse, StartSchedulerMessage, + StartSchedulerMessageV0, StartSchedulerResponse, StartSchedulerResponseV0, + }, + bam_types::{AtomicTxnBatch, AuthProof, ValidatorHeartBeat}, + }, + solana_gossip::cluster_info::ClusterInfo, + solana_sdk::{signature::Keypair, signer::Signer}, + std::sync::{ + atomic::{AtomicBool, AtomicU64, Ordering::Relaxed}, + Arc, Mutex, + }, + thiserror::Error, + tokio::time::{interval, timeout}, +}; + +pub struct BamConnection { + config: Arc>>, + background_task: tokio::task::JoinHandle<()>, + is_healthy: Arc, + url: String, + exit: Arc, +} + +impl BamConnection { + /// Try to initialize a connection to the BAM Node; if it is not possible to connect, it will return an error. + pub async fn try_init( + url: String, + cluster_info: Arc, + batch_sender: crossbeam_channel::Sender, + outbound_receiver: crossbeam_channel::Receiver, + ) -> Result { + let backend_endpoint = tonic::transport::Endpoint::from_shared(url.clone())?; + let connection_timeout = std::time::Duration::from_secs(5); + + let channel = timeout(connection_timeout, backend_endpoint.connect()).await??; + + let mut validator_client = BamNodeApiClient::new(channel); + + let (outbound_sender, outbound_receiver_internal) = mpsc::channel(100_000); + let outbound_stream = + tonic::Request::new(outbound_receiver_internal.map(|req: StartSchedulerMessage| req)); + let inbound_stream = validator_client + .start_scheduler_stream(outbound_stream) + .await + .map_err(|e| { + error!("Failed to start scheduler stream: {:?}", e); + TryInitError::StreamStartError(e) + })? + .into_inner(); + + let metrics = Arc::new(BamConnectionMetrics::default()); + let is_healthy = Arc::new(AtomicBool::new(true)); + let config = Arc::new(Mutex::new(None)); + + let exit = Arc::new(AtomicBool::new(false)); + let background_task = tokio::spawn(Self::connection_task( + exit.clone(), + inbound_stream, + outbound_sender, + validator_client, + config.clone(), + batch_sender, + cluster_info, + metrics.clone(), + is_healthy.clone(), + outbound_receiver, + )); + + Ok(Self { + config, + background_task, + is_healthy, + url, + exit, + }) + } + + #[allow(clippy::too_many_arguments)] + async fn connection_task( + exit: Arc, + mut inbound_stream: tonic::Streaming, + mut outbound_sender: mpsc::Sender, + mut validator_client: BamNodeApiClient, + config: Arc>>, + batch_sender: crossbeam_channel::Sender, + cluster_info: Arc, + metrics: Arc, + is_healthy: Arc, + outbound_receiver: crossbeam_channel::Receiver, + ) { + let mut last_heartbeat = std::time::Instant::now(); + let mut heartbeat_interval = interval(std::time::Duration::from_secs(5)); + heartbeat_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut metrics_and_health_check_interval = interval(std::time::Duration::from_secs(1)); + metrics_and_health_check_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + let mut outbound_tick_interval = interval(std::time::Duration::from_millis(1)); + outbound_tick_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Burst); + + // Create auth proof + let Some(auth_proof) = Self::prepare_auth_proof(&mut validator_client, cluster_info).await + else { + error!("Failed to prepare auth response"); + is_healthy.store(false, Relaxed); + return; + }; + + // Send it as first message + let start_message = StartSchedulerMessageV0 { + msg: Some(Msg::AuthProof(auth_proof)), + }; + if outbound_sender + .send(v0_to_versioned_proto(start_message)) + .await + .inspect_err(|_| { + error!("Failed to send initial auth proof message"); + }) + .is_err() + { + error!("Outbound sender channel closed before sending initial auth proof message"); + return; + } + + let builder_config_task = tokio::spawn(Self::refresh_config_task( + exit.clone(), + config.clone(), + validator_client.clone(), + metrics.clone(), + )); + while !exit.load(Relaxed) { + tokio::select! { + _ = heartbeat_interval.tick() => { + let _ = outbound_sender.try_send(v0_to_versioned_proto(StartSchedulerMessageV0 { + msg: Some(Msg::HeartBeat(ValidatorHeartBeat {})), + })); + metrics.heartbeat_sent.fetch_add(1, Relaxed); + } + _ = metrics_and_health_check_interval.tick() => { + const TIMEOUT_DURATION: std::time::Duration = std::time::Duration::from_secs(6); + let is_healthy_now = last_heartbeat.elapsed() < TIMEOUT_DURATION; + is_healthy.store(is_healthy_now, Relaxed); + if !is_healthy_now { + metrics + .unhealthy_connection_count + .fetch_add(1, Relaxed); + } + + metrics.report(); + } + inbound = inbound_stream.message() => { + let inbound = match inbound { + Ok(Some(msg)) => msg, + Ok(None) => { + error!("Inbound stream closed"); + break; + } + Err(e) => { + error!("Failed to receive message from inbound stream: {:?}", e); + break; + } + }; + + let Some(VersionedMsg::V0(inbound)) = inbound.versioned_msg else { + error!("Received unsupported versioned message: {:?}", inbound); + break; + }; + + match inbound { + StartSchedulerResponseV0 { resp: Some(Resp::HeartBeat(_)), .. } => { + last_heartbeat = std::time::Instant::now(); + metrics.heartbeat_received.fetch_add(1, Relaxed); + } + StartSchedulerResponseV0 { resp: Some(Resp::AtomicTxnBatch(batch)), .. } => { + let _ = batch_sender.try_send(batch).inspect_err(|_| { + error!("Failed to send bundle to receiver"); + }); + metrics.bundle_received.fetch_add(1, Relaxed); + } + _ => {} + } + } + _ = outbound_tick_interval.tick() => { + while let Ok(outbound) = outbound_receiver.try_recv() { + match outbound.msg.as_ref() { + Some(Msg::LeaderState(_)) => { + metrics.leaderstate_sent.fetch_add(1, Relaxed); + } + Some(Msg::AtomicTxnBatchResult(_)) => { + metrics.bundleresult_sent.fetch_add(1, Relaxed); + } + _ => {} + } + let _ = outbound_sender.try_send(v0_to_versioned_proto(outbound)).inspect_err(|_| { + error!("Failed to send outbound message"); + }); + metrics.outbound_sent.fetch_add(1, Relaxed); + } + } + } + } + is_healthy.store(false, Relaxed); + let _ = builder_config_task.await.ok(); + } + + async fn refresh_config_task( + exit: Arc, + config: Arc>>, + mut validator_client: BamNodeApiClient, + metrics: Arc, + ) { + let mut interval = interval(std::time::Duration::from_secs(1)); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + while !exit.load(Relaxed) { + tokio::select! { + _ = interval.tick() => { + let request = tonic::Request::new(ConfigRequest {}); + match validator_client.get_builder_config(request).await { + Ok(response) => { + let resp_config = response.into_inner(); + *config.lock().unwrap() = Some(resp_config); + metrics.builder_config_received.fetch_add(1, Relaxed); + } + Err(e) => { + error!("Failed to get config: {:?}", e); + } + } + } + } + } + } + + fn sign_message(keypair: &Keypair, message: &[u8]) -> Option { + let slot_signature = keypair.try_sign_message(message).ok()?; + let slot_signature = slot_signature.to_string(); + Some(slot_signature) + } + + pub fn is_healthy(&mut self) -> bool { + self.is_healthy.load(Relaxed) + } + + pub fn get_latest_config(&self) -> Option { + self.config.lock().unwrap().clone() + } + + pub fn url(&self) -> &str { + &self.url + } + + async fn prepare_auth_proof( + validator_client: &mut BamNodeApiClient, + cluster_info: Arc, + ) -> Option { + let request = tonic::Request::new(AuthChallengeRequest {}); + let Ok(resp) = validator_client.get_auth_challenge(request).await else { + error!("Failed to get auth challenge"); + return None; + }; + + let resp = resp.into_inner(); + let challenge_to_sign = resp.challenge_to_sign; + let challenge_bytes = challenge_to_sign.as_bytes(); + + let signature = Self::sign_message(cluster_info.keypair().as_ref(), challenge_bytes)?; + + Some(AuthProof { + challenge_to_sign, + validator_pubkey: cluster_info.keypair().pubkey().to_string(), + signature, + }) + } +} + +impl Drop for BamConnection { + fn drop(&mut self) { + self.is_healthy.store(false, Relaxed); + self.exit.store(true, Relaxed); + std::thread::sleep(std::time::Duration::from_millis(10)); + self.background_task.abort(); + } +} + +#[derive(Default)] +struct BamConnectionMetrics { + bundle_received: AtomicU64, + heartbeat_received: AtomicU64, + builder_config_received: AtomicU64, + + unhealthy_connection_count: AtomicU64, + + leaderstate_sent: AtomicU64, + bundleresult_sent: AtomicU64, + heartbeat_sent: AtomicU64, + outbound_sent: AtomicU64, +} + +impl BamConnectionMetrics { + pub fn report(&self) { + datapoint_info!( + "bam_connection-metrics", + ( + "bundle_received", + self.bundle_received.swap(0, Relaxed) as i64, + i64 + ), + ( + "heartbeat_received", + self.heartbeat_received.swap(0, Relaxed) as i64, + i64 + ), + ( + "builder_config_received", + self.builder_config_received.swap(0, Relaxed) as i64, + i64 + ), + ( + "unhealthy_connection_count", + self.unhealthy_connection_count.swap(0, Relaxed) as i64, + i64 + ), + ( + "leaderstate_sent", + self.leaderstate_sent.swap(0, Relaxed) as i64, + i64 + ), + ( + "bundleresult_sent", + self.bundleresult_sent.swap(0, Relaxed) as i64, + i64 + ), + ( + "heartbeat_sent", + self.heartbeat_sent.swap(0, Relaxed) as i64, + i64 + ), + ( + "outbound_sent", + self.outbound_sent.swap(0, Relaxed) as i64, + i64 + ), + ); + } +} + +#[derive(Error, Debug)] +pub enum TryInitError { + #[error("In leader slot")] + MidLeaderSlotError, + #[error("Invalid URI")] + EndpointConnectError(#[from] tonic::transport::Error), + #[error("Connection timeout")] + ConnectionTimeout(#[from] tokio::time::error::Elapsed), + #[error("Stream start error")] + StreamStartError(#[from] tonic::Status), +} diff --git a/core/src/bam_dependencies.rs b/core/src/bam_dependencies.rs new file mode 100644 index 0000000000..85f93b7513 --- /dev/null +++ b/core/src/bam_dependencies.rs @@ -0,0 +1,35 @@ +/// Dependencies that are needed for the BAM (Jito Scheduler Service) to function. +/// All-in-one for convenience. +use std::sync::{atomic::AtomicBool, Arc, Mutex}; +use { + crate::proxy::block_engine_stage::BlockBuilderFeeInfo, + jito_protos::proto::{ + bam_api::{ + start_scheduler_message::VersionedMsg, StartSchedulerMessage, StartSchedulerMessageV0, + }, + bam_types::AtomicTxnBatch, + }, + solana_gossip::cluster_info::ClusterInfo, + solana_pubkey::Pubkey, +}; + +#[derive(Clone)] +pub struct BamDependencies { + pub bam_enabled: Arc, + + pub batch_sender: crossbeam_channel::Sender, + pub batch_receiver: crossbeam_channel::Receiver, + + pub outbound_sender: crossbeam_channel::Sender, + pub outbound_receiver: crossbeam_channel::Receiver, + + pub cluster_info: Arc, + pub block_builder_fee_info: Arc>, + pub bam_node_pubkey: Arc>, +} + +pub fn v0_to_versioned_proto(v0: StartSchedulerMessageV0) -> StartSchedulerMessage { + StartSchedulerMessage { + versioned_msg: Some(VersionedMsg::V0(v0)), + } +} diff --git a/core/src/bam_manager.rs b/core/src/bam_manager.rs new file mode 100644 index 0000000000..14718d681f --- /dev/null +++ b/core/src/bam_manager.rs @@ -0,0 +1,255 @@ +/// Facilitates the BAM sub-system in the validator: +/// - Tries to connect to BAM +/// - Sends leader state to BAM +/// - Updates TPU config +/// - Updates block builder fee info +/// - Sets `bam_enabled` flag that is used everywhere +use std::{ + net::{Ipv4Addr, SocketAddr, SocketAddrV4}, + str::FromStr, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, Mutex, RwLock, + }, +}; +use { + crate::{ + bam_connection::BamConnection, + bam_dependencies::BamDependencies, + bam_payment::{BamPaymentSender, COMMISSION_PERCENTAGE}, + proxy::block_engine_stage::BlockBuilderFeeInfo, + }, + jito_protos::proto::{ + bam_api::{start_scheduler_message_v0::Msg, ConfigResponse, StartSchedulerMessageV0}, + bam_types::{LeaderState, Socket}, + }, + solana_gossip::cluster_info::ClusterInfo, + solana_poh::poh_recorder::PohRecorder, + solana_pubkey::Pubkey, + solana_runtime::bank::Bank, +}; + +pub struct BamManager { + thread: std::thread::JoinHandle<()>, +} + +impl BamManager { + pub fn new( + exit: Arc, + bam_url: Arc>>, + dependencies: BamDependencies, + poh_recorder: Arc>, + ) -> Self { + Self { + thread: std::thread::spawn(move || { + Self::run(exit, bam_url, dependencies, poh_recorder) + }), + } + } + + fn run( + exit: Arc, + bam_url: Arc>>, + dependencies: BamDependencies, + poh_recorder: Arc>, + ) { + let runtime = tokio::runtime::Builder::new_multi_thread() + .worker_threads(8) + .enable_all() + .build() + .unwrap(); + + let start = std::time::Instant::now(); + const GRACE_PERIOD_DURATION: std::time::Duration = std::time::Duration::from_secs(10); + let mut in_startup_grace_period = true; + + let mut current_connection = None; + let mut cached_builder_config = None; + let mut payment_sender = + BamPaymentSender::new(exit.clone(), poh_recorder.clone(), dependencies.clone()); + + while !exit.load(Ordering::Relaxed) { + // Check if we are in the startup grace period + if in_startup_grace_period && start.elapsed() > GRACE_PERIOD_DURATION { + in_startup_grace_period = false; + } + + // Update if bam is enabled and sleep for a while before checking again + // While in grace period, we allow BAM to be enabled even if no connection is established + dependencies.bam_enabled.store( + in_startup_grace_period + || (current_connection.is_some() && cached_builder_config.is_some()), + Ordering::Relaxed, + ); + + // If no connection then try to create a new one + if current_connection.is_none() { + let url = bam_url.lock().unwrap().clone(); + if let Some(url) = url { + let result = runtime.block_on(BamConnection::try_init( + url, + dependencies.cluster_info.clone(), + dependencies.batch_sender.clone(), + dependencies.outbound_receiver.clone(), + )); + match result { + Ok(connection) => { + current_connection = Some(connection); + info!("BAM connection established"); + // Sleep to let heartbeat come in + std::thread::sleep(std::time::Duration::from_secs(2)); + } + Err(e) => { + error!("Failed to connect to BAM: {}", e); + } + } + } + } + + let Some(connection) = current_connection.as_mut() else { + std::thread::sleep(std::time::Duration::from_secs(1)); + continue; + }; + + // Check if connection is healthy; if no then disconnect + if !connection.is_healthy() { + current_connection = None; + cached_builder_config = None; + warn!("BAM connection lost"); + continue; + } + + // Check if url changed; if yes then disconnect + let url = bam_url.lock().unwrap().clone(); + if Some(connection.url().to_string()) != url { + current_connection = None; + cached_builder_config = None; + info!("BAM URL changed"); + continue; + } + + // Check if block builder info has changed + if let Some(builder_config) = connection.get_latest_config() { + if Some(&builder_config) != cached_builder_config.as_ref() { + Self::update_tpu_config(Some(&builder_config), &dependencies.cluster_info); + Self::update_block_engine_key_and_commission( + Some(&builder_config), + &dependencies.block_builder_fee_info, + ); + Self::update_bam_recipient_and_commission( + &builder_config, + &dependencies.bam_node_pubkey, + ); + cached_builder_config = Some(builder_config); + } + } + + // Send leader state if we are in a leader slot + if let Some(bank_start) = poh_recorder.read().unwrap().bank_start() { + if bank_start.should_working_bank_still_be_processing_txs() { + let leader_state = Self::generate_leader_state(&bank_start.working_bank); + payment_sender.send_slot(leader_state.slot); + let _ = dependencies + .outbound_sender + .try_send(StartSchedulerMessageV0 { + msg: Some(Msg::LeaderState(leader_state)), + }); + } + } + + // Sleep for a short duration to avoid busy-waiting + std::thread::sleep(std::time::Duration::from_millis(5)); + } + + payment_sender + .join() + .expect("Failed to join payment sender thread"); + } + + fn generate_leader_state(bank: &Bank) -> LeaderState { + let max_block_cu = bank.read_cost_tracker().unwrap().block_cost_limit(); + let consumed_block_cu = bank.read_cost_tracker().unwrap().block_cost(); + let slot_cu_budget = max_block_cu.saturating_sub(consumed_block_cu) as u32; + LeaderState { + slot: bank.slot(), + tick: (bank.tick_height() % bank.ticks_per_slot()) as u32, + slot_cu_budget, + } + } + + fn get_sockaddr(info: Option<&Socket>) -> Option { + let info = info?; + let Socket { ip, port } = info; + Some(SocketAddr::V4(SocketAddrV4::new( + Ipv4Addr::from_str(ip).ok()?, + *port as u16, + ))) + } + + fn update_tpu_config(config: Option<&ConfigResponse>, cluster_info: &Arc) { + let Some(tpu_info) = config.and_then(|c| c.bam_config.as_ref()) else { + return; + }; + + if let Some(tpu) = Self::get_sockaddr(tpu_info.tpu_sock.as_ref()) { + let _ = cluster_info.set_tpu(tpu); + } + if let Some(tpu_fwd) = Self::get_sockaddr(tpu_info.tpu_fwd_sock.as_ref()) { + let _ = cluster_info.set_tpu_forwards(tpu_fwd); + } + } + + fn update_block_engine_key_and_commission( + config: Option<&ConfigResponse>, + block_builder_fee_info: &Arc>, + ) { + let Some(builder_info) = config.and_then(|c| c.block_engine_config.as_ref()) else { + return; + }; + + let Some(pubkey) = Pubkey::from_str(&builder_info.builder_pubkey).ok() else { + error!( + "Failed to parse builder pubkey: {}", + builder_info.builder_pubkey + ); + block_builder_fee_info.lock().unwrap().block_builder = Pubkey::default(); + return; + }; + + let commission = builder_info.builder_commission; + let mut block_builder_fee_info = block_builder_fee_info.lock().unwrap(); + block_builder_fee_info.block_builder = pubkey; + block_builder_fee_info.block_builder_commission = commission; + } + + fn update_bam_recipient_and_commission( + config: &ConfigResponse, + prio_fee_recipient_pubkey: &Arc>, + ) -> bool { + let Some(bam_info) = config.bam_config.as_ref() else { + return false; + }; + + if bam_info.commission_bps != COMMISSION_PERCENTAGE.saturating_mul(100) { + error!( + "BAM commission bps mismatch: expected {}, got {}", + COMMISSION_PERCENTAGE, bam_info.commission_bps + ); + return false; + } + + let Some(pubkey) = Pubkey::from_str(&bam_info.prio_fee_recipient_pubkey).ok() else { + return false; + }; + + prio_fee_recipient_pubkey + .lock() + .unwrap() + .clone_from(&pubkey); + true + } + + pub fn join(self) -> std::thread::Result<()> { + self.thread.join() + } +} diff --git a/core/src/bam_payment.rs b/core/src/bam_payment.rs new file mode 100644 index 0000000000..d293e4ddad --- /dev/null +++ b/core/src/bam_payment.rs @@ -0,0 +1,267 @@ +/// Simple payment sender for BAM. Will send payments to the BAM node +/// for the slots it was connected to as a leader. +/// It will calculate the payment amount based on the fees collected in that slot. +/// The payment is sent as a transfer transaction with a memo indicating the slot. +/// The payment is sent with a 1% commission. +use { + crate::bam_dependencies::BamDependencies, + solana_client::rpc_client::RpcClient, + solana_ledger::blockstore::{Blockstore, BlockstoreError}, + solana_poh::poh_recorder::PohRecorder, + solana_pubkey::Pubkey, + solana_sdk::{ + clock::Slot, commitment_config::CommitmentConfig, compute_budget::ComputeBudgetInstruction, + signature::Keypair, signer::Signer, transaction::VersionedTransaction, + }, + std::{ + collections::BTreeSet, + sync::{Arc, RwLock}, + time::Instant, + }, +}; + +pub const COMMISSION_PERCENTAGE: u64 = 1; // 1% commission +const LOCALHOST: &str = "http://localhost:8899"; + +pub struct BamPaymentSender { + thread: std::thread::JoinHandle<()>, + slot_sender: crossbeam_channel::Sender, + previous_slot: u64, +} + +impl BamPaymentSender { + pub fn new( + exit: Arc, + poh_recorder: Arc>, + dependencies: BamDependencies, + ) -> Self { + let (slot_sender, slot_receiver) = crossbeam_channel::bounded(10_000); + Self { + thread: std::thread::spawn(move || { + Self::run(exit, slot_receiver, poh_recorder, dependencies); + }), + slot_sender, + previous_slot: 0, + } + } + + fn run( + exit: Arc, + slot_receiver: crossbeam_channel::Receiver, + poh_recorder: Arc>, + dependencies: BamDependencies, + ) { + let mut leader_slots_for_payment = BTreeSet::new(); + let blockstore = poh_recorder.read().unwrap().get_blockstore(); + const DURATION_BETWEEN_PAYMENTS: std::time::Duration = std::time::Duration::from_secs(30); + let mut last_payment_time = Instant::now(); + + while !exit.load(std::sync::atomic::Ordering::Relaxed) { + // Receive new potentially new slots + while let Ok(slot) = slot_receiver.try_recv() { + leader_slots_for_payment.insert(slot); // Will dedup + } + + let now = Instant::now(); + if now.duration_since(last_payment_time).as_secs() < DURATION_BETWEEN_PAYMENTS.as_secs() + { + std::thread::sleep(std::time::Duration::from_millis(100)); + continue; + } + last_payment_time = now; + + // Create batch + let current_slot = poh_recorder.read().unwrap().get_current_slot(); + let batch = Self::create_batch(&blockstore, &leader_slots_for_payment, current_slot); + if batch.is_empty() { + continue; + } + + // Try to send + if Self::send_batch(&batch, &dependencies) { + for (slot, _) in batch.iter() { + leader_slots_for_payment.remove(slot); + } + info!("Payment sent successfully for slots: {:?}", batch); + } + info!("slots_unpaid={:?}", leader_slots_for_payment); + } + + let final_batch = Self::create_batch( + &blockstore, + &leader_slots_for_payment, + poh_recorder.read().unwrap().get_current_slot(), + ); + Self::send_batch(&final_batch, &dependencies); + warn!( + "BamPaymentSender thread exiting, final batch: {:?} remaining slots: {:?}", + final_batch, leader_slots_for_payment + ); + } + + pub fn send_batch(batch: &[(u64, u64)], dependencies: &BamDependencies) -> bool { + let total_payment = batch.iter().map(|(_, amount)| *amount).sum::(); + if total_payment == 0 { + return true; + } + + let payment_pubkey = *dependencies.bam_node_pubkey.lock().unwrap(); + let rpc_url = dependencies + .cluster_info + .my_contact_info() + .rpc() + .map_or_else(|| LOCALHOST.to_string(), |rpc| rpc.to_string()); + + let ((lowest_slot, _), (highest_slot, _)) = (batch.first().unwrap(), batch.last().unwrap()); + + info!( + "Sending payment for {} slots, range: ({}, {}), total payment: {}", + batch.len(), + lowest_slot, + highest_slot, + total_payment + ); + let Some(blockhash) = Self::get_latest_blockhash(&rpc_url) else { + error!("Failed to get latest blockhash, skipping payment"); + return false; + }; + let batch_txn = Self::create_transfer_transaction( + dependencies.cluster_info.keypair().as_ref(), + blockhash, + payment_pubkey, + total_payment, + *lowest_slot, + *highest_slot, + ); + + Self::payment_successful(&rpc_url, &batch_txn, *lowest_slot, *highest_slot) + } + + pub fn send_slot(&mut self, slot: Slot) -> bool { + if slot <= self.previous_slot { + return false; + } + self.previous_slot = slot; + self.slot_sender.try_send(slot).is_ok() + } + + pub fn join(self) -> std::thread::Result<()> { + self.thread.join() + } + + fn create_batch( + blockstore: &Blockstore, + leader_slots_for_payment: &BTreeSet, + current_slot: u64, + ) -> Vec<(u64, u64)> { + let mut batch = vec![]; + for slot in leader_slots_for_payment.iter() { + if current_slot.saturating_sub(*slot) < 32 { + continue; + } + let Some(payment_amount) = Self::calculate_payment_amount(blockstore, *slot) else { + break; + }; + + batch.push((*slot, payment_amount)); + } + batch + } + + fn get_latest_blockhash(rpc_url: &str) -> Option { + let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); + rpc_client.get_latest_blockhash().ok() + } + + fn payment_successful( + rpc_url: &str, + txn: &VersionedTransaction, + lowest_slot: Slot, + highest_slot: Slot, + ) -> bool { + // Send it via RpcClient (loopback to the same node) + let rpc_client = RpcClient::new_with_commitment(rpc_url, CommitmentConfig::confirmed()); + if let Err(err) = rpc_client.send_and_confirm_transaction(txn) { + error!( + "Failed to send payment transaction for slot range ({}, {}): {}", + lowest_slot, highest_slot, err + ); + false + } else { + info!( + "Payment for slot range ({}, {}) sent successfully; signature: {:?}", + lowest_slot, + highest_slot, + txn.signatures.first() + ); + true + } + } + + pub fn calculate_payment_amount(blockstore: &Blockstore, slot: u64) -> Option { + let result = blockstore.get_rooted_block(slot, false); + if result.is_err() && !matches!(result, Err(BlockstoreError::SlotNotRooted)) { + error!("Failed to get block for slot {}: {:?}", slot, result); + return Some(0); + } + + let Ok(block) = result else { + return None; + }; + + const BASE_FEE_LAMPORT_PER_SIGNATURE: u64 = 5_000; + Some( + block + .transactions + .iter() + .map(|tx| { + let fee = tx.meta.fee; + let base_fee = BASE_FEE_LAMPORT_PER_SIGNATURE + .saturating_mul(tx.transaction.signatures.len() as u64); + fee.saturating_sub(base_fee) + }) + .sum::() + .saturating_mul(COMMISSION_PERCENTAGE) + .saturating_div(100), + ) + } + + pub fn create_transfer_transaction( + keypair: &Keypair, + blockhash: solana_sdk::hash::Hash, + destination_pubkey: Pubkey, + lamports: u64, + lowest_slot: u64, + highest_slot: u64, + ) -> VersionedTransaction { + // Create transfer instruction + let transfer_ix = solana_sdk::system_instruction::transfer( + &keypair.pubkey(), + &destination_pubkey, + lamports, + ); + + // Create memo instruction + let memo = format!("bam_pay=({}, {})", lowest_slot, highest_slot); + let memo_ix = spl_memo::build_memo(memo.as_bytes(), &[&keypair.pubkey()]); + + // Set compute unit price + let compute_unit_price_ix = ComputeBudgetInstruction::set_compute_unit_price(10_000); + let compute_unit_limit_ix = ComputeBudgetInstruction::set_compute_unit_limit(50_000); + + let payer = keypair; + + let tx = solana_sdk::transaction::Transaction::new_signed_with_payer( + &[ + compute_unit_price_ix, + compute_unit_limit_ix, + transfer_ix, + memo_ix, + ], + Some(&payer.pubkey()), + &[payer], + blockhash, + ); + VersionedTransaction::from(tx) + } +} diff --git a/core/src/banking_simulation.rs b/core/src/banking_simulation.rs index d1a61d275f..8bf14883d2 100644 --- a/core/src/banking_simulation.rs +++ b/core/src/banking_simulation.rs @@ -843,6 +843,8 @@ impl BankingSimulator { collections::HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); let (&_slot, &raw_base_event_time) = freeze_time_by_slot diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index f80d7b6270..e0e9bd4684 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -17,6 +17,7 @@ use { unprocessed_transaction_storage::UnprocessedTransactionStorage, }, crate::{ + bam_dependencies::BamDependencies, banking_stage::{ consume_worker::ConsumeWorker, packet_deserializer::PacketDeserializer, @@ -29,6 +30,7 @@ use { validator::{BlockProductionMethod, TransactionStructure}, }, agave_banking_stage_ingress_types::BankingPacketReceiver, + consumer::TipProcessingDependencies, crossbeam_channel::{unbounded, Receiver, RecvTimeoutError, Sender}, histogram::Histogram, solana_client::connection_cache::ConnectionCache, @@ -41,20 +43,23 @@ use { bank::Bank, bank_forks::BankForks, prioritization_fee_cache::PrioritizationFeeCache, vote_sender_types::ReplayVoteSender, }, - solana_sdk::{pubkey::Pubkey, timing::AtomicInterval}, + solana_runtime_transaction::runtime_transaction::RuntimeTransaction, + solana_sdk::{pubkey::Pubkey, timing::AtomicInterval, transaction::SanitizedTransaction}, std::{ cmp, collections::HashSet, env, ops::Deref, sync::{ - atomic::{AtomicU64, AtomicUsize, Ordering}, + atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering}, Arc, RwLock, }, thread::{self, Builder, JoinHandle}, time::{Duration, Instant}, }, transaction_scheduler::{ + bam_receive_and_buffer::BamReceiveAndBuffer, + bam_scheduler::BamScheduler, greedy_scheduler::{GreedyScheduler, GreedySchedulerConfig}, prio_graph_scheduler::PrioGraphSchedulerConfig, receive_and_buffer::{ @@ -376,6 +381,8 @@ impl BankingStage { bundle_account_locker: BundleAccountLocker, // callback function for compute space reservation for BundleStage block_cost_limit_block_cost_limit_reservation_cb: impl Fn(&Bank) -> u64 + Clone + Send + 'static, + tip_processing_dependencies: Option, + bam_dependencies: Option, ) -> Self { Self::new_num_threads( block_production_method, @@ -396,6 +403,8 @@ impl BankingStage { blacklisted_accounts, bundle_account_locker, block_cost_limit_block_cost_limit_reservation_cb, + tip_processing_dependencies, + bam_dependencies, ) } @@ -419,6 +428,8 @@ impl BankingStage { blacklisted_accounts: HashSet, bundle_account_locker: BundleAccountLocker, block_cost_limit_reservation_cb: impl Fn(&Bank) -> u64 + Clone + Send + 'static, + tip_processing_dependencies: Option, + bam_dependencies: Option, ) -> Self { match block_production_method { BlockProductionMethod::CentralScheduler @@ -446,6 +457,8 @@ impl BankingStage { blacklisted_accounts, bundle_account_locker, block_cost_limit_reservation_cb, + tip_processing_dependencies, + bam_dependencies, ) } } @@ -471,6 +484,8 @@ impl BankingStage { blacklisted_accounts: HashSet, bundle_account_locker: BundleAccountLocker, block_cost_limit_reservation_cb: impl Fn(&Bank) -> u64 + Clone + Send + 'static, + tip_processing_dependencies: Option, + bam_dependencies: Option, ) -> Self { assert!(num_threads >= MIN_TOTAL_THREADS); // Single thread to generate entries from many banks. @@ -557,6 +572,8 @@ impl BankingStage { blacklisted_accounts.clone(), bundle_account_locker.clone(), block_cost_limit_reservation_cb.clone(), + tip_processing_dependencies.clone(), + bam_dependencies, ); } TransactionStructure::View => { @@ -581,6 +598,8 @@ impl BankingStage { blacklisted_accounts.clone(), bundle_account_locker.clone(), block_cost_limit_reservation_cb.clone(), + tip_processing_dependencies.clone(), + bam_dependencies, ); } } @@ -606,6 +625,8 @@ impl BankingStage { blacklisted_accounts: HashSet, bundle_account_locker: BundleAccountLocker, block_cost_limit_reservation_cb: impl Fn(&Bank) -> u64 + Clone + Send + 'static, + tip_processing_dependencies: Option, + bam_dependencies: Option, ) { // Create channels for communication between scheduler and workers let num_workers = (num_threads).saturating_sub(NUM_VOTE_PROCESSING_THREADS); @@ -655,24 +676,37 @@ impl BankingStage { }); // Spawn the central scheduler thread + let bam_enabled = bam_dependencies + .as_ref() + .map(|bam| bam.bam_enabled.clone()) + .unwrap_or(Arc::new(AtomicBool::new(false))); + let scheduler_worker_senders = work_senders.clone(); + let scheduler_finished_work_receiver = finished_work_receiver.clone(); + let scheduler_decision_maker = decision_maker.clone(); + let scheduler_blacklisted_accounts = blacklisted_accounts.clone(); + let scheduler_bank_forks = bank_forks.clone(); + let scheduler_worker_metrics = worker_metrics.clone(); + let scheduler_bam_enabled = bam_enabled.clone(); if use_greedy_scheduler { bank_thread_hdls.push( Builder::new() .name("solBnkTxSched".to_string()) .spawn(move || { let scheduler = GreedyScheduler::new( - work_senders, - finished_work_receiver, + scheduler_worker_senders, + scheduler_finished_work_receiver, GreedySchedulerConfig::default(), ); let scheduler_controller = SchedulerController::new( - decision_maker.clone(), + scheduler_decision_maker, receive_and_buffer, - bank_forks, + scheduler_bank_forks, scheduler, - worker_metrics, + scheduler_worker_metrics, forwarder, - blacklisted_accounts.clone(), + scheduler_blacklisted_accounts, + false, + scheduler_bam_enabled, ); match scheduler_controller.run() { @@ -691,18 +725,103 @@ impl BankingStage { .name("solBnkTxSched".to_string()) .spawn(move || { let scheduler = PrioGraphScheduler::new( - work_senders, - finished_work_receiver, + scheduler_worker_senders, + scheduler_finished_work_receiver, PrioGraphSchedulerConfig::default(), ); + let scheduler_controller = SchedulerController::new( + scheduler_decision_maker, + receive_and_buffer, + scheduler_bank_forks, + scheduler, + scheduler_worker_metrics, + forwarder, + scheduler_blacklisted_accounts, + false, + scheduler_bam_enabled, + ); + + match scheduler_controller.run() { + Ok(_) => {} + Err(SchedulerError::DisconnectedRecvChannel(_)) => {} + Err(SchedulerError::DisconnectedSendChannel(_)) => { + warn!("Unexpected worker disconnect from scheduler") + } + } + }) + .unwrap(), + ); + } + + if let Some(bam_dependencies) = bam_dependencies { + // Spawn BAM workers + // Create channels for communication between scheduler and workers + let num_workers = num_threads; + let (work_senders, work_receivers): (Vec>, Vec>) = + (0..num_workers).map(|_| unbounded()).unzip(); + let (finished_work_sender, finished_work_receiver) = unbounded(); + + // Spawn the worker threads + let mut worker_metrics = Vec::with_capacity(num_workers as usize); + for (index, work_receiver) in work_receivers.into_iter().enumerate() { + let id = (index as u32) + .saturating_add(NUM_VOTE_PROCESSING_THREADS) + .saturating_add(num_workers); + let consume_worker = ConsumeWorker::new( + id, + work_receiver, + Consumer::new_with_maybe_tip_processing( + committer.clone(), + poh_recorder.read().unwrap().new_recorder(), + QosService::new(id), + log_messages_bytes_limit, + blacklisted_accounts.clone(), + bundle_account_locker.clone(), + tip_processing_dependencies.clone(), + ), + finished_work_sender.clone(), + poh_recorder.read().unwrap().new_leader_bank_notifier(), + ); + + worker_metrics.push(consume_worker.metrics_handle()); + bank_thread_hdls.push( + Builder::new() + .name(format!("solCoWorker{id:02}")) + .spawn(move || { + let _ = consume_worker.run(|_| 0); + }) + .unwrap(), + ) + } + + // Spawn the BAM scheduler thread + bank_thread_hdls.push( + Builder::new() + .name("solBamSched".to_string()) + .spawn(move || { + let scheduler = + BamScheduler::>::new( + work_senders, + finished_work_receiver, + bam_dependencies.outbound_sender.clone(), + ); + let receive_and_buffer = BamReceiveAndBuffer::new( + bam_dependencies.bam_enabled.clone(), + bam_dependencies.batch_receiver.clone(), + bam_dependencies.outbound_sender.clone(), + bank_forks.clone(), + ); + let scheduler_controller = SchedulerController::new( decision_maker.clone(), receive_and_buffer, bank_forks, scheduler, worker_metrics, - forwarder, + None::>>, blacklisted_accounts.clone(), + true, + bam_enabled, ); match scheduler_controller.run() { @@ -1007,6 +1126,8 @@ mod tests { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); drop(non_vote_sender); drop(tpu_vote_sender); @@ -1072,6 +1193,8 @@ mod tests { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); trace!("sending bank"); drop(non_vote_sender); @@ -1162,6 +1285,8 @@ mod tests { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); // fund another account so we can send 2 good transactions in a single batch. @@ -1340,6 +1465,8 @@ mod tests { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); // wait for banking_stage to eat the packets @@ -1545,6 +1672,8 @@ mod tests { HashSet::default(), BundleAccountLocker::default(), |_| 0, + None, + None, ); let keypairs = (0..100).map(|_| Keypair::new()).collect_vec(); @@ -1688,6 +1817,8 @@ mod tests { HashSet::from_iter([blacklisted_keypair.pubkey()]), BundleAccountLocker::default(), |_| 0, + None, + None, ); // bad tx diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs index 3a428920ad..cc636f8164 100644 --- a/core/src/banking_stage/consume_worker.rs +++ b/core/src/banking_stage/consume_worker.rs @@ -1,10 +1,15 @@ use { super::{ + committer::CommitTransactionDetails, consumer::{Consumer, ExecuteAndCommitTransactionsOutput, ProcessTransactionBatchOutput}, leader_slot_timing_metrics::LeaderExecuteAndCommitTimings, - scheduler_messages::{ConsumeWork, FinishedConsumeWork}, + scheduler_messages::{ + ConsumeWork, FinishedConsumeWork, FinishedConsumeWorkExtraInfo, NotCommittedReason, + TransactionResult, + }, }, crossbeam_channel::{Receiver, RecvError, SendError, Sender}, + jito_protos::proto::bam_types::TransactionCommittedResult, solana_measure::measure_us, solana_poh::leader_bank_notifier::LeaderBankNotifier, solana_runtime::bank::Bank, @@ -59,6 +64,7 @@ impl ConsumeWorker { self.metrics.clone() } + #[allow(clippy::result_large_err)] pub fn run(self, reservation_cb: impl Fn(&Bank) -> u64) -> Result<(), ConsumeWorkerError> { loop { let work = self.consume_receiver.recv()?; @@ -66,6 +72,7 @@ impl ConsumeWorker { } } + #[allow(clippy::result_large_err)] fn consume_loop( &self, work: ConsumeWork, @@ -104,6 +111,13 @@ impl ConsumeWorker { return self.retry_drain(work); } } + + if let Some(schedulable_slot) = work.schedulable_slot { + if bank.slot() != schedulable_slot { + return self.retry(work); + } + } + self.consume(&bank, work, reservation_cb)?; } @@ -111,6 +125,7 @@ impl ConsumeWorker { } /// Consume a single batch. + #[allow(clippy::result_large_err)] fn consume( &self, bank: &Arc, @@ -122,20 +137,77 @@ impl ConsumeWorker { &work.transactions, &work.max_ages, reservation_cb, + work.revert_on_error, ); self.metrics.update_for_consume(&output); self.metrics.has_data.store(true, Ordering::Relaxed); + let extra_info = if work.respond_with_extra_info { + Some(Self::generate_extra_info(&output, &work, bank)) + } else { + None + }; + self.consumed_sender.send(FinishedConsumeWork { work, retryable_indexes: output .execute_and_commit_transactions_output .retryable_transaction_indexes, + extra_info, })?; Ok(()) } + fn generate_extra_info( + output: &ProcessTransactionBatchOutput, + work: &ConsumeWork, + bank: &Arc, + ) -> FinishedConsumeWorkExtraInfo { + let Ok(commit_transactions_result) = output + .execute_and_commit_transactions_output + .commit_transactions_result + .as_ref() + else { + return FinishedConsumeWorkExtraInfo { + processed_results: vec![ + TransactionResult::NotCommitted( + NotCommittedReason::PohTimeout, + ); + work.transactions.len() + ], + }; + }; + + let errors = &output + .execute_and_commit_transactions_output + .transaction_errors; + let mut processed_results = vec![]; + for (i, commit_info) in commit_transactions_result.iter().enumerate() { + if let CommitTransactionDetails::Committed { + compute_units, + loaded_accounts_data_size, + } = commit_info + { + processed_results.push(TransactionResult::Committed(TransactionCommittedResult { + cus_consumed: *compute_units as u32, + feepayer_balance_lamports: bank.get_balance(work.transactions[i].fee_payer()), + loaded_accounts_data_size: *loaded_accounts_data_size, + })); + } else { + let not_committed_reason = errors + .get(i) + .cloned() + .flatten() + .map(NotCommittedReason::Error) + .unwrap_or(NotCommittedReason::BatchRevert); + processed_results.push(TransactionResult::NotCommitted(not_committed_reason)); + } + } + + FinishedConsumeWorkExtraInfo { processed_results } + } + /// Try to get a bank for consuming. fn get_consume_bank(&self) -> Option> { self.leader_bank_notifier @@ -149,6 +221,7 @@ impl ConsumeWorker { } /// Retry current batch and all outstanding batches. + #[allow(clippy::result_large_err)] fn retry_drain(&self, work: ConsumeWork) -> Result<(), ConsumeWorkerError> { for work in try_drain_iter(work, &self.consume_receiver) { self.retry(work)?; @@ -157,6 +230,7 @@ impl ConsumeWorker { } /// Send transactions back to scheduler as retryable. + #[allow(clippy::result_large_err)] fn retry(&self, work: ConsumeWork) -> Result<(), ConsumeWorkerError> { let retryable_indexes: Vec<_> = (0..work.transactions.len()).collect(); let num_retryable = retryable_indexes.len(); @@ -169,9 +243,22 @@ impl ConsumeWorker { .retryable_expired_bank_count .fetch_add(num_retryable, Ordering::Relaxed); self.metrics.has_data.store(true, Ordering::Relaxed); + let extra_info = if work.respond_with_extra_info { + Some(FinishedConsumeWorkExtraInfo { + processed_results: vec![ + TransactionResult::NotCommitted( + NotCommittedReason::PohTimeout, + ); + num_retryable + ], + }) + } else { + None + }; self.consumed_sender.send(FinishedConsumeWork { work, retryable_indexes, + extra_info, })?; Ok(()) } @@ -913,6 +1000,9 @@ mod tests { ids: vec![id], transactions, max_ages: vec![max_age], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }; consume_sender.send(work).unwrap(); let consumed = consumed_receiver.recv().unwrap(); @@ -962,6 +1052,9 @@ mod tests { ids: vec![id], transactions, max_ages: vec![max_age], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }; consume_sender.send(work).unwrap(); let consumed = consumed_receiver.recv().unwrap(); @@ -1013,6 +1106,9 @@ mod tests { ids: vec![id1, id2], transactions: txs, max_ages: vec![max_age, max_age], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }) .unwrap(); @@ -1074,6 +1170,9 @@ mod tests { ids: vec![id1], transactions: txs1, max_ages: vec![max_age], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }) .unwrap(); @@ -1083,6 +1182,9 @@ mod tests { ids: vec![id2], transactions: txs2, max_ages: vec![max_age], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }) .unwrap(); let consumed = consumed_receiver.recv().unwrap(); @@ -1221,6 +1323,9 @@ mod tests { alt_invalidation_slot: bank.slot() + 1, }, ], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }) .unwrap(); diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index e175673d15..a6e70a6217 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -11,10 +11,15 @@ use { unprocessed_transaction_storage::{ConsumeScannerPayload, UnprocessedTransactionStorage}, BankingStageStats, }, - crate::bundle_stage::bundle_account_locker::BundleAccountLocker, + crate::{ + bundle_stage::bundle_account_locker::BundleAccountLocker, + proxy::block_engine_stage::BlockBuilderFeeInfo, tip_manager::TipManager, + }, agave_feature_set as feature_set, + ahash::AHashSet, itertools::Itertools, solana_fee::FeeFeatures, + solana_gossip::cluster_info::ClusterInfo, solana_ledger::token_balances::collect_token_balances, solana_measure::{measure::Measure, measure_us}, solana_poh::poh_recorder::{ @@ -33,7 +38,7 @@ use { pubkey::Pubkey, saturating_add_assign, timing::timestamp, - transaction::{self, TransactionError}, + transaction::{self, TransactionError, VersionedTransaction}, }, solana_svm::{ account_loader::{validate_fee_payer, TransactionCheckResult}, @@ -43,9 +48,10 @@ use { }, solana_transaction_status::PreBalanceInfo, std::{ + cell::Cell, collections::HashSet, num::Saturating, - sync::{atomic::Ordering, Arc}, + sync::{atomic::Ordering, Arc, Mutex}, time::Instant, }, }; @@ -75,6 +81,8 @@ pub struct ExecuteAndCommitTransactionsOutput { pub(crate) error_counters: TransactionErrorMetrics, pub(crate) min_prioritization_fees: u64, pub(crate) max_prioritization_fees: u64, + + pub(crate) transaction_errors: Vec>, } #[derive(Debug, Default, PartialEq)] @@ -89,6 +97,14 @@ pub struct LeaderProcessedTransactionCounts { pub(crate) processed_with_successful_result_count: u64, } +#[derive(Clone)] +pub struct TipProcessingDependencies { + pub tip_manager: TipManager, + pub last_tip_updated_slot: Arc>, + pub block_builder_fee_info: Arc>, + pub cluster_info: Arc, +} + pub struct Consumer { committer: Committer, transaction_recorder: TransactionRecorder, @@ -96,6 +112,24 @@ pub struct Consumer { log_messages_bytes_limit: Option, blacklisted_accounts: HashSet, bundle_account_locker: BundleAccountLocker, + + tip_processing_dependencies: Option, + + reusable_seen_messages: Cell>, + seq_not_conflict_batch_reusables: Cell, +} + +#[derive(Default)] +struct SeqNotConflictBatchReusables { + aggregate_write_locks: AHashSet, + aggregate_read_locks: AHashSet, +} + +impl SeqNotConflictBatchReusables { + pub fn clear(&mut self) { + self.aggregate_write_locks.clear(); + self.aggregate_read_locks.clear(); + } } impl Consumer { @@ -114,6 +148,31 @@ impl Consumer { log_messages_bytes_limit, blacklisted_accounts, bundle_account_locker, + tip_processing_dependencies: None, + reusable_seen_messages: Cell::new(AHashSet::new()), + seq_not_conflict_batch_reusables: Cell::new(SeqNotConflictBatchReusables::default()), + } + } + + pub fn new_with_maybe_tip_processing( + committer: Committer, + transaction_recorder: TransactionRecorder, + qos_service: QosService, + log_messages_bytes_limit: Option, + blacklisted_accounts: HashSet, + bundle_account_locker: BundleAccountLocker, + tip_processing_dependencies: Option, + ) -> Self { + Self { + committer, + transaction_recorder, + qos_service, + log_messages_bytes_limit, + blacklisted_accounts, + bundle_account_locker, + tip_processing_dependencies, + reusable_seen_messages: Cell::new(AHashSet::new()), + seq_not_conflict_batch_reusables: Cell::new(SeqNotConflictBatchReusables::default()), } } @@ -143,6 +202,7 @@ impl Consumer { &mut rebuffered_packet_count, packets_to_process, reservation_cb, + false, ) }, &self.blacklisted_accounts, @@ -184,6 +244,7 @@ impl Consumer { rebuffered_packet_count: &mut usize, packets_to_process: &[Arc], reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> Option> { if payload.reached_end_of_slot { return None; @@ -197,7 +258,8 @@ impl Consumer { &payload.sanitized_transactions, banking_stage_stats, payload.slot_metrics_tracker, - reservation_cb + reservation_cb, + revert_on_error )); payload .slot_metrics_tracker @@ -246,13 +308,15 @@ impl Consumer { banking_stage_stats: &BankingStageStats, slot_metrics_tracker: &mut LeaderSlotMetricsTracker, reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> ProcessTransactionsSummary { let (mut process_transactions_summary, process_transactions_us) = measure_us!(self .process_transactions( bank, bank_creation_time, sanitized_transactions, - reservation_cb + reservation_cb, + revert_on_error, )); slot_metrics_tracker.increment_process_transactions_us(process_transactions_us); banking_stage_stats @@ -305,6 +369,7 @@ impl Consumer { bank_creation_time: &Instant, transactions: &[impl TransactionWithMeta], reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> ProcessTransactionsSummary { let mut chunk_start = 0; let mut all_retryable_tx_indexes = vec![]; @@ -319,13 +384,29 @@ impl Consumer { while chunk_start != transactions.len() { let chunk_end = std::cmp::min( transactions.len(), - chunk_start + TARGET_NUM_TRANSACTIONS_PER_BATCH, + if revert_on_error { + transactions.len() + } else { + chunk_start + TARGET_NUM_TRANSACTIONS_PER_BATCH + }, ); + let txs = &transactions[chunk_start..chunk_end]; + + // Update tip account receivers if needed + if !self.run_tip_programs_if_needed(bank, txs, &reservation_cb) { + error!("Error running tip programs for transactions: {:?}", txs); + datapoint_error!( + "process_transactions_error", + ("error", "tip_programs_error", String), + ); + } + let process_transaction_batch_output = self.process_and_record_transactions( bank, - &transactions[chunk_start..chunk_end], + txs, chunk_start, reservation_cb, + revert_on_error, ); let ProcessTransactionBatchOutput { @@ -404,17 +485,88 @@ impl Consumer { } } + fn run_tip_programs_if_needed( + &self, + bank: &Arc, + txs: &[impl TransactionWithMeta], + reservation_cb: &impl Fn(&Bank) -> u64, + ) -> bool { + let Some(tip_processing_dependencies) = &self.tip_processing_dependencies else { + return true; + }; + let TipProcessingDependencies { + tip_manager, + last_tip_updated_slot, + block_builder_fee_info, + cluster_info, + } = tip_processing_dependencies; + + // Return true if no tip accounts touched + let tip_accounts = tip_manager.get_tip_accounts(); + if !txs.iter().any(|tx| { + tx.account_keys() + .iter() + .any(|key| tip_accounts.contains(key)) + }) { + return true; + } + + let keypair = cluster_info.keypair(); + + let mut last_tip_updated_slot_guard = last_tip_updated_slot.lock().unwrap(); + if bank.slot() == *last_tip_updated_slot_guard { + return true; + } + + let initialize_tip_programs_bundle = + tip_manager.get_initialize_tip_programs_bundle(bank, &keypair); + if let Some(init_bundle) = initialize_tip_programs_bundle { + let txs = init_bundle.transactions; + let result = self.process_and_record_transactions(bank, &txs, 0, reservation_cb, true); + if result + .execute_and_commit_transactions_output + .commit_transactions_result + .is_err() + { + return false; + } + } + + let block_builder_fee_info = (*block_builder_fee_info.lock().unwrap()).clone(); + if block_builder_fee_info.block_builder == Pubkey::default() { + return false; + } + if let Ok(Some(tip_crank_bundle)) = + tip_manager.get_tip_programs_crank_bundle(bank, &keypair, &block_builder_fee_info) + { + let txs = tip_crank_bundle.transactions; + let result = self.process_and_record_transactions(bank, &txs, 0, reservation_cb, true); + if result + .execute_and_commit_transactions_output + .commit_transactions_result + .is_err() + { + return false; + } + } + + *last_tip_updated_slot_guard = bank.slot(); + true + } + pub fn process_and_record_transactions( &self, bank: &Arc, txs: &[impl TransactionWithMeta], chunk_offset: usize, reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> ProcessTransactionBatchOutput { let mut error_counters = TransactionErrorMetrics::default(); let pre_results = vec![Ok(()); txs.len()]; let check_results = bank.check_transactions(txs, &pre_results, MAX_PROCESSING_AGE, &mut error_counters); + // If checks passed, verify pre-compiles and continue processing on success. let move_precompile_verification_to_svm = bank .feature_set @@ -433,12 +585,14 @@ impl Consumer { Err(err) => Err(err), }) .collect(); + let mut output = self.process_and_record_transactions_with_pre_results( bank, txs, chunk_offset, check_results.into_iter(), reservation_cb, + revert_on_error, ); // Accumulate error counters from the initial checks into final results @@ -446,6 +600,7 @@ impl Consumer { .execute_and_commit_transactions_output .error_counters .accumulate(&error_counters); + output } @@ -455,6 +610,7 @@ impl Consumer { txs: &[impl TransactionWithMeta], max_ages: &[MaxAge], reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> ProcessTransactionBatchOutput { let move_precompile_verification_to_svm = bank .feature_set @@ -497,6 +653,7 @@ impl Consumer { 0, pre_results, reservation_cb, + revert_on_error, ) } @@ -507,7 +664,18 @@ impl Consumer { chunk_offset: usize, pre_results: impl Iterator>, reservation_cb: &impl Fn(&Bank) -> u64, + revert_on_error: bool, ) -> ProcessTransactionBatchOutput { + // Check for duplicate transactions + let mut seen_messages = self.reusable_seen_messages.take(); + seen_messages.clear(); + let pre_results = txs.iter().zip(pre_results).map(|(tx, result)| { + result?; + if !seen_messages.insert(*tx.message_hash()) { + return Err(TransactionError::AlreadyProcessed); + } + Ok(()) + }); let ( (transaction_qos_cost_results, cost_model_throttled_transactions_count), cost_model_us, @@ -517,6 +685,7 @@ impl Consumer { pre_results, reservation_cb )); + self.reusable_seen_messages.set(seen_messages); // Only lock accounts for those transactions are selected for the block; // Once accounts are locked, other threads cannot encode transactions that will modify the @@ -530,7 +699,8 @@ impl Consumer { Err(err) => Err(err.clone()), }), Some(&bundle_account_locks.read_locks()), - Some(&bundle_account_locks.write_locks()) + Some(&bundle_account_locks.write_locks()), + revert_on_error, )); drop(bundle_account_locks); @@ -538,7 +708,7 @@ impl Consumer { // WouldExceedMaxAccountCostLimit, WouldExceedMaxVoteCostLimit // and WouldExceedMaxAccountDataCostLimit let mut execute_and_commit_transactions_output = - self.execute_and_commit_transactions_locked(bank, &batch); + self.execute_and_commit_transactions_locked(bank, &batch, revert_on_error); // Once the accounts are new transactions can enter the pipeline to process them let (_, unlock_us) = measure_us!(drop(batch)); @@ -592,6 +762,7 @@ impl Consumer { &self, bank: &Arc, batch: &TransactionBatch, + revert_on_error: bool, ) -> ExecuteAndCommitTransactionsOutput { let transaction_status_sender_enabled = self.committer.transaction_status_sender_enabled(); let mut execute_and_commit_timings = LeaderExecuteAndCommitTimings::default(); @@ -658,6 +829,30 @@ impl Consumer { Ok(_) => None, }) .collect(); + if revert_on_error && batch.lock_results().iter().any(|res| res.is_err()) { + return ExecuteAndCommitTransactionsOutput { + transaction_counts: LeaderProcessedTransactionCounts { + attempted_processing_count: batch.sanitized_transactions().len() as u64, + ..Default::default() + }, + retryable_transaction_indexes, + commit_transactions_result: Ok((0..batch.sanitized_transactions().len()) + .map(|_| CommitTransactionDetails::NotCommitted) + .collect()), + execute_and_commit_timings, + error_counters, + min_prioritization_fees, + max_prioritization_fees, + transaction_errors: batch + .lock_results() + .iter() + .map(|res| match res { + Ok(_) => None, + Err(error) => Some(error.clone()), + }) + .collect(), + }; + } let (load_and_execute_transactions_output, load_execute_us) = measure_us!(bank .load_and_execute_transactions( @@ -678,6 +873,35 @@ impl Consumer { } )); execute_and_commit_timings.load_execute_us = load_execute_us; + let successful_count = load_and_execute_transactions_output + .processed_counts + .processed_with_successful_result_count as usize; + let transaction_errors = load_and_execute_transactions_output + .processing_results + .iter() + .map(|result| match result { + Ok(_) => None, + Err(error) => Some(error.clone()), + }) + .collect_vec(); + + if revert_on_error && successful_count != batch.sanitized_transactions().len() { + return ExecuteAndCommitTransactionsOutput { + transaction_counts: LeaderProcessedTransactionCounts { + attempted_processing_count: batch.sanitized_transactions().len() as u64, + ..Default::default() + }, + retryable_transaction_indexes, + commit_transactions_result: Ok((0..batch.sanitized_transactions().len()) + .map(|_| CommitTransactionDetails::NotCommitted) + .collect()), + execute_and_commit_timings, + error_counters, + min_prioritization_fees, + max_prioritization_fees, + transaction_errors, + }; + } let LoadAndExecuteTransactionsOutput { processing_results, @@ -707,9 +931,17 @@ impl Consumer { let (freeze_lock, freeze_lock_us) = measure_us!(bank.freeze_lock()); execute_and_commit_timings.freeze_lock_us = freeze_lock_us; + let mut reusables = self.seq_not_conflict_batch_reusables.take(); + let batches = Self::create_sequential_non_conflicting_batches( + &mut reusables, + processed_transactions + .into_iter() + .zip(batch.sanitized_transactions().iter()), + ); + let (record_transactions_summary, record_us) = measure_us!(self .transaction_recorder - .record_transactions(bank.slot(), vec![processed_transactions])); + .record_transactions(bank.slot(), batches)); execute_and_commit_timings.record_us = record_us; let RecordTransactionsSummary { @@ -741,6 +973,7 @@ impl Consumer { error_counters, min_prioritization_fees, max_prioritization_fees, + transaction_errors, }; } @@ -791,7 +1024,52 @@ impl Consumer { error_counters, min_prioritization_fees, max_prioritization_fees, + transaction_errors, + } + } + + fn create_sequential_non_conflicting_batches<'a>( + reusables: &mut SeqNotConflictBatchReusables, + transactions: impl Iterator, + ) -> Vec> { + let mut result = vec![]; + let mut current_batch = vec![]; + reusables.clear(); + let SeqNotConflictBatchReusables { + aggregate_write_locks, + aggregate_read_locks, + } = reusables; + + for (transaction, transaction_info) in transactions { + let account_keys = transaction_info.account_keys(); + let write_account_locks = account_keys + .iter() + .enumerate() + .filter_map(|(index, key)| transaction_info.is_writable(index).then_some(key)); + let read_account_locks = account_keys + .iter() + .enumerate() + .filter_map(|(index, key)| (!transaction_info.is_writable(index)).then_some(key)); + let has_contention = write_account_locks.clone().any(|key| { + aggregate_write_locks.contains(key) || aggregate_read_locks.contains(key) + }) || read_account_locks + .clone() + .any(|key| aggregate_write_locks.contains(key)); + if has_contention { + result.push(std::mem::take(&mut current_batch)); + aggregate_write_locks.clear(); + aggregate_read_locks.clear(); + } + current_batch.push(transaction); + aggregate_write_locks.extend(write_account_locks.cloned()); + aggregate_read_locks.extend(read_account_locks.cloned()); + } + + if !current_batch.is_empty() { + result.push(current_batch); } + + result } pub fn check_fee_payer_unlocked( @@ -876,16 +1154,21 @@ impl Consumer { mod tests { use { super::*, - crate::banking_stage::{ - immutable_deserialized_packet::DeserializedPacketError, - tests::{create_slow_genesis_config, sanitize_transactions, simulate_poh}, - unprocessed_packet_batches::{DeserializedPacket, UnprocessedPacketBatches}, - unprocessed_transaction_storage::ThreadType, + crate::{ + banking_stage::{ + immutable_deserialized_packet::DeserializedPacketError, + tests::{create_slow_genesis_config, sanitize_transactions, simulate_poh}, + unprocessed_packet_batches::{DeserializedPacket, UnprocessedPacketBatches}, + unprocessed_transaction_storage::ThreadType, + }, + tip_manager::{TipDistributionAccountConfig, TipManagerConfig}, }, agave_reserved_account_keys::ReservedAccountKeys, crossbeam_channel::{unbounded, Receiver}, + jito_tip_distribution::sdk::derive_tip_distribution_account_address, solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, solana_entry::entry::{next_entry, next_versioned_entry}, + solana_gossip::cluster_info::Node, solana_ledger::{ blockstore::{entries_to_test_shreds, Blockstore}, blockstore_processor::TransactionStatusSender, @@ -898,8 +1181,13 @@ mod tests { }, solana_perf::packet::Packet, solana_poh::poh_recorder::{PohRecorder, Record, WorkingBankEntry}, + solana_program_test::programs::spl_programs, solana_rpc::transaction_status_service::TransactionStatusService, - solana_runtime::{bank_forks::BankForks, prioritization_fee_cache::PrioritizationFeeCache}, + solana_runtime::{ + bank_forks::BankForks, genesis_utils::create_genesis_config_with_leader_ex, + installed_scheduler_pool::BankWithScheduler, + prioritization_fee_cache::PrioritizationFeeCache, + }, solana_runtime_transaction::runtime_transaction::RuntimeTransaction, solana_sdk::{ account::AccountSharedData, @@ -909,29 +1197,37 @@ mod tests { state::{AddressLookupTable, LookupTableMeta}, }, compute_budget, - fee_calculator::FeeCalculator, + fee_calculator::{ + FeeCalculator, FeeRateGovernor, DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE, + }, + genesis_config::ClusterType, hash::Hash, instruction::InstructionError, message::{ v0::{self, MessageAddressTableLookup}, Message, MessageHeader, VersionedMessage, }, + native_token::sol_to_lamports, nonce::{self, state::DurableNonce}, nonce_account::verify_nonce_account, poh_config::PohConfig, pubkey::Pubkey, + rent::Rent, signature::Keypair, signer::Signer, system_instruction, system_program, system_transaction, transaction::{Transaction, VersionedTransaction}, }, + solana_streamer::socket::SocketAddrSpace, solana_svm::account_loader::CheckedTransactionDetails, solana_timings::{ExecuteTimings, ProgramTiming}, solana_transaction_status::{TransactionStatusMeta, VersionedTransactionWithStatusMeta}, + solana_vote_program::vote_state::VoteState, std::{ borrow::Cow, num::Saturating, path::Path, + str::FromStr, sync::{ atomic::{AtomicBool, AtomicU64}, RwLock, @@ -945,12 +1241,14 @@ mod tests { fn execute_transactions_with_dummy_poh_service( bank: Arc, transactions: Vec, - ) -> ProcessTransactionsSummary { + revert_on_error: bool, + tip_processing_dependencies: Option, + ) -> (ProcessTransactionsSummary, Vec) { let transactions = sanitize_transactions(transactions); let ledger_path = get_tmp_ledger_path_auto_delete!(); let blockstore = Blockstore::open(ledger_path.path()) .expect("Expected to be able to open database ledger"); - let (poh_recorder, _entry_receiver, record_receiver) = PohRecorder::new( + let (poh_recorder, entry_receiver, record_receiver) = PohRecorder::new( bank.tick_height(), bank.last_blockhash(), bank.clone(), @@ -977,16 +1275,33 @@ mod tests { replay_vote_sender, Arc::new(PrioritizationFeeCache::new(0u64)), ); - let consumer = Consumer::new( + let consumer = Consumer::new_with_maybe_tip_processing( committer, recorder, QosService::new(1), None, HashSet::default(), BundleAccountLocker::default(), + tip_processing_dependencies, + ); + let process_transactions_summary = consumer.process_transactions( + &bank, + &Instant::now(), + &transactions, + &|_| 0, + revert_on_error, ); - let process_transactions_summary = - consumer.process_transactions(&bank, &Instant::now(), &transactions, &|_| 0); + + let recorded: Vec = entry_receiver + .try_iter() + .flat_map(|entry| { + entry + .entries_ticks + .iter() + .flat_map(|(entry, _)| entry.transactions.clone()) + .collect::>() + }) + .collect::>(); poh_recorder .read() @@ -995,7 +1310,7 @@ mod tests { .store(true, Ordering::Relaxed); let _ = poh_simulator.join(); - process_transactions_summary + (process_transactions_summary, recorded) } fn generate_new_address_lookup_table( @@ -1173,7 +1488,7 @@ mod tests { ); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, @@ -1225,7 +1540,7 @@ mod tests { )]); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, @@ -1377,7 +1692,7 @@ mod tests { ); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, commit_transactions_result, @@ -1492,7 +1807,7 @@ mod tests { ); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, @@ -1594,7 +1909,7 @@ mod tests { )]); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, @@ -1624,7 +1939,7 @@ mod tests { ]); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); let ExecuteAndCommitTransactionsOutput { transaction_counts, @@ -1748,7 +2063,7 @@ mod tests { ); let process_transactions_batch_output = - consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); poh_recorder .read() @@ -1797,15 +2112,16 @@ mod tests { // InstructionError::InsufficientFunds that is then committed. Needs to be // MAX_NUM_TRANSACTIONS_PER_BATCH at least so it doesn't conflict on account locks // with the below transaction - let mut transactions = vec![ - system_transaction::transfer( - &mint_keypair, - &Pubkey::new_unique(), - lamports + 1, - genesis_config.hash(), - ); - TARGET_NUM_TRANSACTIONS_PER_BATCH - ]; + let mut transactions = (0..TARGET_NUM_TRANSACTIONS_PER_BATCH) + .map(|_| { + system_transaction::transfer( + &mint_keypair, + &Pubkey::new_unique(), + lamports + 1, + genesis_config.hash(), + ) + }) + .collect_vec(); // Make one transaction that will succeed. transactions.push(system_transaction::transfer( @@ -1816,12 +2132,15 @@ mod tests { )); let transactions_len = transactions.len(); - let ProcessTransactionsSummary { - reached_max_poh_height, - transaction_counts, - retryable_transaction_indexes, - .. - } = execute_transactions_with_dummy_poh_service(bank, transactions); + let ( + ProcessTransactionsSummary { + reached_max_poh_height, + transaction_counts, + retryable_transaction_indexes, + .. + }, + _, + ) = execute_transactions_with_dummy_poh_service(bank, transactions, false, None); // All the transactions should have been replayed, but only 1 committed assert!(!reached_max_poh_height); @@ -1857,15 +2176,16 @@ mod tests { .set_limits(u64::MAX, u64::MAX, u64::MAX); // Make all repetitive transactions that conflict on the `mint_keypair`, so only 1 should be executed - let mut transactions = vec![ - system_transaction::transfer( - &mint_keypair, - &Pubkey::new_unique(), - 1, - genesis_config.hash() - ); - TARGET_NUM_TRANSACTIONS_PER_BATCH - ]; + let mut transactions = (0..TARGET_NUM_TRANSACTIONS_PER_BATCH) + .map(|_| { + system_transaction::transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + genesis_config.hash(), + ) + }) + .collect_vec(); // Make one more in separate batch that also conflicts, but because it's in a separate batch, it // should be executed @@ -1877,12 +2197,15 @@ mod tests { )); let transactions_len = transactions.len(); - let ProcessTransactionsSummary { - reached_max_poh_height, - transaction_counts, - retryable_transaction_indexes, - .. - } = execute_transactions_with_dummy_poh_service(bank, transactions); + let ( + ProcessTransactionsSummary { + reached_max_poh_height, + transaction_counts, + retryable_transaction_indexes, + .. + }, + _, + ) = execute_transactions_with_dummy_poh_service(bank, transactions, false, None); // All the transactions should have been replayed, but only 2 committed (first and last) assert!(!reached_max_poh_height); @@ -1960,7 +2283,7 @@ mod tests { ); let process_transactions_summary = - consumer.process_transactions(&bank, &Instant::now(), &transactions, &|_| 0); + consumer.process_transactions(&bank, &Instant::now(), &transactions, &|_| 0, false); let ProcessTransactionsSummary { reached_max_poh_height, @@ -2096,7 +2419,8 @@ mod tests { BundleAccountLocker::default(), ); - let _ = consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0); + let _ = + consumer.process_and_record_transactions(&bank, &transactions, 0, &|_| 0, false); drop(consumer); // drop/disconnect transaction_status_sender @@ -2250,8 +2574,13 @@ mod tests { BundleAccountLocker::default(), ); - let _ = - consumer.process_and_record_transactions(&bank, &[sanitized_tx.clone()], 0, &|_| 0); + let _ = consumer.process_and_record_transactions( + &bank, + &[sanitized_tx.clone()], + 0, + &|_| 0, + false, + ); drop(consumer); // drop/disconnect transaction_status_sender @@ -2739,4 +3068,375 @@ mod tests { [0, 3, 4, 5] ); } + + #[test] + fn test_process_transactions_instruction_error_revert_on_error() { + solana_logger::setup(); + let lamports = 10_000; + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_slow_genesis_config(lamports); + let (bank, _bank_forks) = Bank::new_no_wallclock_throttle_for_tests(&genesis_config); + // set cost tracker limits to MAX so it will not filter out TXs + bank.write_cost_tracker() + .unwrap() + .set_limits(u64::MAX, u64::MAX, u64::MAX); + + // Transfer more than the balance of the mint keypair, should cause a + // InstructionError::InsufficientFunds that is then committed. Needs to be + // MAX_NUM_TRANSACTIONS_PER_BATCH at least so it doesn't conflict on account locks + // with the below transaction + let mut transactions = vec![ + system_transaction::transfer( + &mint_keypair, + &Pubkey::new_unique(), + lamports + 1, + genesis_config.hash(), + ); + TARGET_NUM_TRANSACTIONS_PER_BATCH + ]; + + // Make one transaction that will succeed. + transactions.push(system_transaction::transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + genesis_config.hash(), + )); + + let transactions_len = transactions.len(); + let ( + ProcessTransactionsSummary { + reached_max_poh_height, + transaction_counts, + retryable_transaction_indexes, + .. + }, + _, + ) = execute_transactions_with_dummy_poh_service(bank, transactions, true, None); + + assert!(!reached_max_poh_height); + assert_eq!( + transaction_counts, + CommittedTransactionsCounts { + attempted_processing_count: transactions_len as u64, + // None commited; because the first transaction was an error + committed_transactions_count: 0, + committed_transactions_with_successful_result_count: 0, + processed_but_failed_commit: 0, + } + ); + assert_eq!(retryable_transaction_indexes, Vec::::new(),); + } + + pub fn create_test_recorder( + bank: &Arc, + blockstore: Arc, + poh_config: Option, + leader_schedule_cache: Option>, + ) -> ( + Arc, + Arc>, + JoinHandle<()>, + Receiver, + ) { + let leader_schedule_cache = match leader_schedule_cache { + Some(provided_cache) => provided_cache, + None => Arc::new(LeaderScheduleCache::new_from_bank(bank)), + }; + let exit = Arc::new(AtomicBool::new(false)); + let poh_config = poh_config.unwrap_or_default(); + + let (mut poh_recorder, entry_receiver, record_receiver) = PohRecorder::new( + bank.tick_height(), + bank.last_blockhash(), + bank.clone(), + Some((4, 4)), + bank.ticks_per_slot(), + blockstore, + &leader_schedule_cache, + &poh_config, + exit.clone(), + ); + poh_recorder.set_bank( + BankWithScheduler::new_without_scheduler(bank.clone()), + false, + ); + + let poh_recorder = Arc::new(RwLock::new(poh_recorder)); + let poh_simulator = simulate_poh(record_receiver, &poh_recorder); + + (exit, poh_recorder, poh_simulator, entry_receiver) + } + + struct TestFixture { + genesis_config_info: GenesisConfigInfo, + leader_keypair: Keypair, + bank: Arc, + exit: Arc, + poh_recorder: Arc>, + poh_simulator: JoinHandle<()>, + entry_receiver: Receiver, + bank_forks: Arc>, + } + + fn create_test_fixture(mint_sol: u64) -> TestFixture { + let mint_keypair = Keypair::new(); + let leader_keypair = Keypair::new(); + let voting_keypair = Keypair::new(); + + let rent = Rent::default(); + + let mut genesis_config = create_genesis_config_with_leader_ex( + sol_to_lamports(mint_sol as f64), + &mint_keypair.pubkey(), + &leader_keypair.pubkey(), + &voting_keypair.pubkey(), + &solana_sdk::pubkey::new_rand(), + rent.minimum_balance(VoteState::size_of()) + sol_to_lamports(1_000_000.0), + sol_to_lamports(1_000_000.0), + FeeRateGovernor { + // Initialize with a non-zero fee + lamports_per_signature: DEFAULT_TARGET_LAMPORTS_PER_SIGNATURE / 2, + ..FeeRateGovernor::default() + }, + rent.clone(), // most tests don't expect rent + ClusterType::Development, + spl_programs(&rent), + ); + genesis_config.ticks_per_slot *= 8; + + // workaround for https://github.com/solana-labs/solana/issues/30085 + // the test can deploy and use spl_programs in the genensis slot without waiting for the next one + let (bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + + let bank = Arc::new(Bank::new_from_parent(bank, &Pubkey::default(), 1)); + + let ledger_path = get_tmp_ledger_path_auto_delete!(); + let blockstore = Arc::new( + Blockstore::open(ledger_path.path()) + .expect("Expected to be able to open database ledger"), + ); + + let (exit, poh_recorder, poh_simulator, entry_receiver) = + create_test_recorder(&bank, blockstore, Some(PohConfig::default()), None); + + let validator_pubkey = voting_keypair.pubkey(); + TestFixture { + genesis_config_info: GenesisConfigInfo { + genesis_config, + mint_keypair, + voting_keypair, + validator_pubkey, + }, + leader_keypair, + bank, + bank_forks, + exit, + poh_recorder, + poh_simulator, + entry_receiver, + } + } + + pub fn get_executed_txns( + entry_receiver: &Receiver, + wait: Duration, + ) -> Vec { + let mut transactions = Vec::new(); + let start = std::time::Instant::now(); + while start.elapsed() < wait { + let Ok(WorkingBankEntry { + bank: _wbe_bank, + entries_ticks, + }) = entry_receiver.try_recv() + else { + continue; + }; + for (entry, _) in entries_ticks { + if !entry.transactions.is_empty() { + transactions.extend(entry.transactions); + } + } + } + transactions + } + + fn get_tip_manager(vote_account: &Pubkey) -> TipManager { + TipManager::new(TipManagerConfig { + tip_payment_program_id: Pubkey::from_str("T1pyyaTNZsKv2WcRAB8oVnk93mLJw2XzjtVYqCsaHqt") + .unwrap(), + tip_distribution_program_id: Pubkey::from_str( + "4R3gSG8BpU4t19KYj8CfnbtRpnT8gtk4dvTHxVRwc2r7", + ) + .unwrap(), + tip_distribution_account_config: TipDistributionAccountConfig { + merkle_root_upload_authority: Pubkey::new_unique(), + vote_account: *vote_account, + commission_bps: 10, + }, + }) + } + + #[test] + fn test_handle_tip_programs() { + let TestFixture { + genesis_config_info, + leader_keypair, + bank, + exit, + poh_recorder, + poh_simulator, + entry_receiver, + bank_forks: _bank_forks, + } = create_test_fixture(1); + + let (replay_vote_sender, _) = crossbeam_channel::unbounded(); + let keypair = Arc::new(leader_keypair); + let cluster_info = { + let node = Node::new_localhost_with_pubkey(&keypair.pubkey()); + ClusterInfo::new(node.info, keypair.clone(), SocketAddrSpace::Unspecified) + }; + let cluster_info = Arc::new(cluster_info); + let block_builder_pubkey = Pubkey::new_unique(); + + let tip_manager = get_tip_manager(&genesis_config_info.voting_keypair.pubkey()); + + let tip_accounts = tip_manager.get_tip_accounts(); + let tip_account = tip_accounts.iter().collect::>()[0]; + let txns = sanitize_transactions(vec![system_transaction::transfer( + &genesis_config_info.mint_keypair, + tip_account, + 1000, + genesis_config_info.genesis_config.hash(), + )]); + + let tip_processing_dependencies = TipProcessingDependencies { + tip_manager: tip_manager.clone(), + last_tip_updated_slot: Arc::new(Mutex::new(0)), + block_builder_fee_info: Arc::new(Mutex::new(BlockBuilderFeeInfo { + block_builder: block_builder_pubkey, + block_builder_commission: 5, + })), + cluster_info, + }; + + let committer = Committer::new( + None, + replay_vote_sender, + Arc::new(PrioritizationFeeCache::new(0u64)), + ); + let recorder = poh_recorder.read().unwrap().new_recorder(); + let consumer = Consumer::new_with_maybe_tip_processing( + committer, + recorder, + QosService::new(1), + None, + HashSet::default(), + BundleAccountLocker::default(), + Some(tip_processing_dependencies), + ); + + let transactions = + std::thread::spawn(move || get_executed_txns(&entry_receiver, Duration::from_secs(5))); + + let _ = consumer.process_transactions(&bank, &Instant::now(), &txns, &|_| 0, false); + + let transactions = transactions.join().unwrap(); + + assert_eq!(transactions.len(), 5); + + // expect to see initialize tip payment program, tip distribution program, + // initialize tip distribution account, change tip receiver + change block builder + assert_eq!( + transactions[0], + tip_manager + .initialize_tip_payment_program_tx(&bank, &keypair) + .to_versioned_transaction() + ); + assert_eq!( + transactions[1], + tip_manager + .initialize_tip_distribution_config_tx(&bank, &keypair) + .to_versioned_transaction() + ); + assert_eq!( + transactions[2], + tip_manager + .initialize_tip_distribution_account_tx(&bank, &keypair) + .to_versioned_transaction() + ); + // the first tip receiver + block builder are the initializer (keypair.pubkey()) as set by the + // TipPayment program during initialization + let bank_start = poh_recorder.read().unwrap().bank_start().unwrap(); + assert_eq!( + transactions[3], + tip_manager + .build_change_tip_receiver_and_block_builder_tx( + &keypair.pubkey(), + &derive_tip_distribution_account_address( + &tip_manager.tip_distribution_program_id(), + &genesis_config_info.validator_pubkey, + bank_start.working_bank.epoch() + ) + .0, + &bank_start.working_bank, + &keypair, + &keypair.pubkey(), + &block_builder_pubkey, + 5 + ) + .to_versioned_transaction() + ); + + poh_recorder + .write() + .unwrap() + .is_exited + .store(true, Ordering::Relaxed); + exit.store(true, Ordering::Relaxed); + poh_simulator.join().unwrap(); + } + + #[test] + fn test_create_sequential_non_conflicting_batches() { + let a = Pubkey::new_unique(); + let b = Pubkey::new_unique(); + let c = Pubkey::new_unique(); + let d = Pubkey::new_unique(); + let txns = vec![ + Transaction::new_unsigned(Message::new( + &[system_instruction::transfer(&a, &b, 1)], + Some(&Pubkey::new_unique()), + )), + Transaction::new_unsigned(Message::new( + &[system_instruction::transfer(&d, &d, 1)], + Some(&Pubkey::new_unique()), + )), + Transaction::new_unsigned(Message::new( + &[system_instruction::transfer(&b, &c, 1)], + Some(&Pubkey::new_unique()), + )), + ]; + let txn_infos = txns + .iter() + .map(|tx| RuntimeTransaction::from_transaction_for_tests(tx.clone())) + .collect::>(); + let txns = txns + .into_iter() + .map(VersionedTransaction::from) + .collect::>(); + let mut reusables = SeqNotConflictBatchReusables::default(); + let batches = Consumer::create_sequential_non_conflicting_batches( + &mut reusables, + txns.into_iter().zip(txn_infos.iter()), + ); + + // Expect 2 batches: one with len=2 (a,b,c), and one with len=1 (d) + assert_eq!(batches.len(), 2); + assert_eq!(batches[0].len(), 2); + assert_eq!(batches[1].len(), 1); + } } diff --git a/core/src/banking_stage/scheduler_messages.rs b/core/src/banking_stage/scheduler_messages.rs index 1c7cf31592..6ea00f08c9 100644 --- a/core/src/banking_stage/scheduler_messages.rs +++ b/core/src/banking_stage/scheduler_messages.rs @@ -1,11 +1,15 @@ use { - solana_sdk::clock::{Epoch, Slot}, + jito_protos::proto::bam_types::TransactionCommittedResult, + solana_sdk::{ + clock::{Epoch, Slot}, + transaction::TransactionError, + }, std::fmt::Display, }; /// A unique identifier for a transaction batch. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] -pub struct TransactionBatchId(u64); +pub struct TransactionBatchId(pub u64); impl TransactionBatchId { pub fn new(index: u64) -> Self { @@ -41,6 +45,9 @@ pub struct ConsumeWork { pub ids: Vec, pub transactions: Vec, pub max_ages: Vec, + pub revert_on_error: bool, + pub respond_with_extra_info: bool, + pub schedulable_slot: Option, } /// Message: [Worker -> Scheduler] @@ -48,4 +55,22 @@ pub struct ConsumeWork { pub struct FinishedConsumeWork { pub work: ConsumeWork, pub retryable_indexes: Vec, + pub extra_info: Option, +} + +pub struct FinishedConsumeWorkExtraInfo { + pub processed_results: Vec, +} + +#[derive(Clone, Debug)] +pub enum TransactionResult { + Committed(TransactionCommittedResult), + NotCommitted(NotCommittedReason), +} + +#[derive(Clone, Debug)] +pub enum NotCommittedReason { + PohTimeout, + BatchRevert, + Error(TransactionError), } diff --git a/core/src/banking_stage/transaction_scheduler/bam_receive_and_buffer.rs b/core/src/banking_stage/transaction_scheduler/bam_receive_and_buffer.rs new file mode 100644 index 0000000000..350b39f735 --- /dev/null +++ b/core/src/banking_stage/transaction_scheduler/bam_receive_and_buffer.rs @@ -0,0 +1,726 @@ +/// An implementation of the `ReceiveAndBuffer` trait that receives messages from BAM +/// and buffers from into the the `TransactionStateContainer`. Key thing to note: +/// this implementation only functions during the `Consume/Hold` phase; otherwise it will send them back +/// to BAM with a `Retryable` result. +use std::{ + cmp::min, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, RwLock, + }, + time::{Duration, Instant}, +}; +use { + super::{ + receive_and_buffer::ReceiveAndBuffer, + transaction_state_container::TransactionStateContainer, + }, + crate::banking_stage::{ + consumer::Consumer, + decision_maker::BufferedPacketsDecision, + immutable_deserialized_packet::{DeserializedPacketError, ImmutableDeserializedPacket}, + transaction_scheduler::{ + bam_utils::{convert_deserialize_error_to_proto, convert_txn_error_to_proto}, + receive_and_buffer::{calculate_max_age, calculate_priority_and_cost}, + transaction_state::SanitizedTransactionTTL, + }, + }, + crossbeam_channel::Sender, + itertools::Itertools, + jito_protos::proto::{ + bam_api::{start_scheduler_message_v0::Msg, StartSchedulerMessageV0}, + bam_types::{ + atomic_txn_batch_result, not_committed::Reason, AtomicTxnBatch, + DeserializationErrorReason, Packet, SchedulingError, + }, + }, + solana_accounts_db::account_locks::validate_account_locks, + solana_runtime::bank_forks::BankForks, + solana_runtime_transaction::{ + runtime_transaction::RuntimeTransaction, transaction_meta::StaticMeta, + }, + solana_sdk::{ + clock::MAX_PROCESSING_AGE, + packet::{PacketFlags, PACKET_DATA_SIZE}, + transaction::SanitizedTransaction, + }, + solana_svm::transaction_error_metrics::TransactionErrorMetrics, +}; + +pub struct BamReceiveAndBuffer { + bam_enabled: Arc, + bundle_receiver: crossbeam_channel::Receiver, + response_sender: Sender, + bank_forks: Arc>, +} + +impl BamReceiveAndBuffer { + pub fn new( + bam_enabled: Arc, + bundle_receiver: crossbeam_channel::Receiver, + response_sender: Sender, + bank_forks: Arc>, + ) -> Self { + Self { + bam_enabled, + bundle_receiver, + response_sender, + bank_forks, + } + } + + fn deserialize_bam_packets<'a>( + packets: impl Iterator, + ) -> Result, (usize, DeserializedPacketError)> { + let mut result = Vec::with_capacity(packets.size_hint().0); + for (index, packet) in packets.enumerate() { + let mut solana_packet = solana_sdk::packet::Packet::default(); + solana_packet.meta_mut().size = packet.data.len(); + solana_packet.meta_mut().set_discard(false); + let len_to_copy = min(packet.data.len(), PACKET_DATA_SIZE); + solana_packet.buffer_mut()[0..len_to_copy] + .copy_from_slice(&packet.data[0..len_to_copy]); + if let Some(meta) = &packet.meta { + if let Some(addr) = &meta.addr.parse().ok() { + solana_packet.meta_mut().addr = *addr; + } + solana_packet.meta_mut().port = meta.port as u16; + if let Some(flags) = &meta.flags { + if flags.simple_vote_tx { + solana_packet + .meta_mut() + .flags + .insert(PacketFlags::SIMPLE_VOTE_TX); + } + if flags.forwarded { + solana_packet + .meta_mut() + .flags + .insert(PacketFlags::FORWARDED); + } + if flags.repair { + solana_packet.meta_mut().flags.insert(PacketFlags::REPAIR); + } + } + } + result + .push(ImmutableDeserializedPacket::new(solana_packet).map_err(|err| (index, err))?); + } + + Ok(result) + } + + fn send_bundle_not_committed_result(&self, seq_id: u32, reason: Reason) { + let _ = self.response_sender.try_send(StartSchedulerMessageV0 { + msg: Some(Msg::AtomicTxnBatchResult( + jito_protos::proto::bam_types::AtomicTxnBatchResult { + seq_id, + result: Some(atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(reason), + }, + )), + }, + )), + }); + } + + fn send_no_leader_slot_txn_batch_result(&self, seq_id: u32) { + let _ = self.response_sender.try_send(StartSchedulerMessageV0 { + msg: Some(Msg::AtomicTxnBatchResult( + jito_protos::proto::bam_types::AtomicTxnBatchResult { + seq_id, + result: Some(atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(Reason::SchedulingError( + SchedulingError::OutsideLeaderSlot as i32, + )), + }, + )), + }, + )), + }); + } + + fn send_container_full_txn_batch_result(&self, seq_id: u32) { + let _ = self.response_sender.try_send(StartSchedulerMessageV0 { + msg: Some(Msg::AtomicTxnBatchResult( + jito_protos::proto::bam_types::AtomicTxnBatchResult { + seq_id, + result: Some(atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(Reason::SchedulingError( + SchedulingError::ContainerFull as i32, + )), + }, + )), + }, + )), + }); + } + + fn parse_batch( + batch: &AtomicTxnBatch, + bank_forks: &Arc>, + ) -> Result { + if batch.packets.is_empty() { + return Err(Reason::DeserializationError( + jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::Empty as i32, + }, + )); + } + + if batch.packets.len() > 5 { + return Err(Reason::DeserializationError( + jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::SanitizeError as i32, + }, + )); + } + + let Ok(revert_on_error) = batch + .packets + .iter() + .map(|p| { + p.meta + .as_ref() + .and_then(|meta| meta.flags.as_ref()) + .is_some_and(|flags| flags.revert_on_error) + }) + .all_equal_value() + else { + return Err(Reason::DeserializationError( + jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::InconsistentBundle as i32, + }, + )); + }; + + let mut parsed_packets = + Self::deserialize_bam_packets(batch.packets.iter()).map_err(|(index, err)| { + let reason = convert_deserialize_error_to_proto(&err); + Reason::DeserializationError(jito_protos::proto::bam_types::DeserializationError { + index: index as u32, + reason: reason as i32, + }) + })?; + + let (root_bank, working_bank) = { + let bank_forks = bank_forks.read().unwrap(); + let root_bank = bank_forks.root_bank(); + let working_bank = bank_forks.working_bank(); + (root_bank, working_bank) + }; + let alt_resolved_slot = root_bank.slot(); + let sanitized_epoch = root_bank.epoch(); + let transaction_account_lock_limit = working_bank.get_transaction_account_lock_limit(); + let vote_only = working_bank.vote_only_bank(); + + let mut packets = vec![]; + let mut cost: u64 = 0; + let mut transaction_ttls = vec![]; + + // Checks are taken from receive_and_buffer.rs: + // SanitizedTransactionReceiveAndBuffer::buffer_packets + for (index, parsed_packet) in parsed_packets.drain(..).enumerate() { + // Check 1 + let Some((tx, deactivation_slot)) = parsed_packet.build_sanitized_transaction( + vote_only, + root_bank.as_ref(), + root_bank.get_reserved_account_keys(), + ) else { + return Err(Reason::DeserializationError( + jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::SanitizeError as i32, + }, + )); + }; + + // Check 2 + if let Err(err) = + validate_account_locks(tx.message().account_keys(), transaction_account_lock_limit) + { + let reason = convert_txn_error_to_proto(err); + return Err(Reason::TransactionError( + jito_protos::proto::bam_types::TransactionError { + index: index as u32, + reason: reason as i32, + }, + )); + } + + // Check 3 + let fee_budget_limits = match tx + .compute_budget_instruction_details() + .sanitize_and_convert_to_compute_budget_limits(&working_bank.feature_set) + { + Ok(fee_budget_limits) => fee_budget_limits, + Err(err) => { + let reason = convert_txn_error_to_proto(err); + return Err(Reason::TransactionError( + jito_protos::proto::bam_types::TransactionError { + index: index as u32, + reason: reason as i32, + }, + )); + } + }; + + // Check 4 + let lock_results: [_; 1] = core::array::from_fn(|_| Ok(())); + let check_results = working_bank.check_transactions( + std::slice::from_ref(&tx), + &lock_results, + MAX_PROCESSING_AGE, + &mut TransactionErrorMetrics::default(), + ); + if let Some(Err(err)) = check_results.first() { + let reason = convert_txn_error_to_proto(err.clone()); + return Err(Reason::TransactionError( + jito_protos::proto::bam_types::TransactionError { + index: index as u32, + reason: reason as i32, + }, + )); + } + + // Check 5 + if let Err(err) = Consumer::check_fee_payer_unlocked( + &working_bank, + &tx, + &mut TransactionErrorMetrics::default(), + ) { + let reason = convert_txn_error_to_proto(err); + return Err(Reason::TransactionError( + jito_protos::proto::bam_types::TransactionError { + index: index as u32, + reason: reason as i32, + }, + )); + } + + let max_age = calculate_max_age(sanitized_epoch, deactivation_slot, alt_resolved_slot); + + let (_, txn_cost) = + calculate_priority_and_cost(&tx, &fee_budget_limits.into(), &working_bank); + cost = cost.saturating_add(txn_cost); + transaction_ttls.push(SanitizedTransactionTTL { + transaction: tx, + max_age, + }); + packets.push(Arc::new(parsed_packet)); + } + + let priority = seq_id_to_priority(batch.seq_id); + + Ok(ParsedBatch { + transaction_ttls, + packets, + cost, + priority, + revert_on_error, + }) + } +} + +struct ParsedBatch { + pub transaction_ttls: Vec>>, + pub packets: Vec>, + pub cost: u64, + priority: u64, + pub revert_on_error: bool, +} + +impl ReceiveAndBuffer for BamReceiveAndBuffer { + type Transaction = RuntimeTransaction; + type Container = TransactionStateContainer; + + fn receive_and_buffer_packets( + &mut self, + container: &mut Self::Container, + _: &mut super::scheduler_metrics::SchedulerTimingMetrics, + _: &mut super::scheduler_metrics::SchedulerCountMetrics, + decision: &crate::banking_stage::decision_maker::BufferedPacketsDecision, + ) -> Result { + if !self.bam_enabled.load(Ordering::Relaxed) { + std::thread::sleep(Duration::from_millis(5)); + return Ok(0); + } + + let mut result = 0; + const MAX_BUNDLES_PER_RECV: usize = 24; + match decision { + BufferedPacketsDecision::Consume(_) | BufferedPacketsDecision::Hold => { + while result < MAX_BUNDLES_PER_RECV { + let Ok(batch) = self.bundle_receiver.try_recv() else { + break; + }; + let ParsedBatch { + transaction_ttls, + packets, + cost, + priority, + revert_on_error, + } = match Self::parse_batch(&batch, &self.bank_forks) { + Ok(parsed) => parsed, + Err(reason) => { + self.send_bundle_not_committed_result(batch.seq_id, reason); + continue; + } + }; + if container + .insert_new_batch( + transaction_ttls, + packets, + priority, + cost, + revert_on_error, + batch.max_schedule_slot, + ) + .is_none() + { + self.send_container_full_txn_batch_result(batch.seq_id); + continue; + }; + + result += 1; + } + } + BufferedPacketsDecision::ForwardAndHold | BufferedPacketsDecision::Forward => { + // Send back any batches that were received while in Forward/Hold state + let deadline = Instant::now() + Duration::from_millis(100); + while let Ok(batch) = self.bundle_receiver.recv_deadline(deadline) { + self.send_no_leader_slot_txn_batch_result(batch.seq_id); + } + } + } + + Ok(result) + } +} + +pub fn seq_id_to_priority(seq_id: u32) -> u64 { + u64::MAX.saturating_sub(seq_id as u64) +} + +pub fn priority_to_seq_id(priority: u64) -> u32 { + u32::try_from(u64::MAX.saturating_sub(priority)).unwrap_or(u32::MAX) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::banking_stage::{ + tests::create_slow_genesis_config, + transaction_scheduler::{ + scheduler_metrics::{SchedulerCountMetrics, SchedulerTimingMetrics}, + transaction_state_container::StateContainer, + }, + }, + crossbeam_channel::{unbounded, Receiver}, + solana_ledger::genesis_utils::GenesisConfigInfo, + solana_pubkey::Pubkey, + solana_runtime::bank::Bank, + solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, + solana_sdk::{signature::Keypair, system_transaction::transfer}, + test_case::test_case, + }; + + #[test] + fn test_seq_id_to_priority() { + assert_eq!(seq_id_to_priority(0), u64::MAX); + assert_eq!(seq_id_to_priority(1), u64::MAX - 1); + } + + #[test] + fn test_priority_to_seq_id() { + assert_eq!(priority_to_seq_id(u64::MAX), 0); + assert_eq!(priority_to_seq_id(u64::MAX - 1), 1); + } + + fn test_bank_forks() -> (Arc>, Keypair) { + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_slow_genesis_config(u64::MAX); + + let (_bank, bank_forks) = Bank::new_no_wallclock_throttle_for_tests(&genesis_config); + (bank_forks, mint_keypair) + } + + fn setup_bam_receive_and_buffer( + receiver: crossbeam_channel::Receiver, + bank_forks: Arc>, + ) -> ( + BamReceiveAndBuffer, + TransactionStateContainer>, + crossbeam_channel::Receiver, + ) { + let (response_sender, response_receiver) = + crossbeam_channel::unbounded::(); + let receive_and_buffer = BamReceiveAndBuffer::new( + Arc::new(AtomicBool::new(true)), + receiver, + response_sender, + bank_forks, + ); + let container = TransactionStateContainer::with_capacity(100); + (receive_and_buffer, container, response_receiver) + } + + // verify container state makes sense: + // 1. Number of transactions matches expectation + // 2. All transactions IDs in priority queue exist in the map + fn verify_container( + container: &mut impl StateContainer, + expected_length: usize, + ) { + let mut actual_length: usize = 0; + while let Some(id) = container.pop() { + let Some((ids, _, _)) = container.get_batch(id.id) else { + panic!( + "transaction in queue position {} with id {} must exist.", + actual_length, id.id + ); + }; + for id in ids { + assert!( + container.get_transaction_ttl(*id).is_some(), + "Transaction ID {} not found in container", + id + ); + } + actual_length += 1; + } + + assert_eq!(actual_length, expected_length); + } + + #[test_case(setup_bam_receive_and_buffer; "testcase-bam")] + fn test_receive_and_buffer_simple_transfer( + setup_receive_and_buffer: impl FnOnce( + Receiver, + Arc>, + ) + -> (R, R::Container, Receiver), + ) { + let (sender, receiver) = unbounded(); + let (bank_forks, mint_keypair) = test_bank_forks(); + let (mut receive_and_buffer, mut container, _response_sender) = + setup_receive_and_buffer(receiver, bank_forks.clone()); + let mut timing_metrics = SchedulerTimingMetrics::default(); + let mut count_metrics = SchedulerCountMetrics::default(); + + let transaction = transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + ); + let data = bincode::serialize(&transaction).expect("serializes"); + let bundle = AtomicTxnBatch { + seq_id: 1, + packets: vec![Packet { data, meta: None }], + max_schedule_slot: 0, + }; + sender.send(bundle).unwrap(); + + let num_received = receive_and_buffer + .receive_and_buffer_packets( + &mut container, + &mut timing_metrics, + &mut count_metrics, + &BufferedPacketsDecision::Hold, + ) + .unwrap(); + + assert_eq!(num_received, 1); + verify_container(&mut container, 1); + } + + #[test] + fn test_receive_and_buffer_invalid_packet() { + let (bank_forks, _mint_keypair) = test_bank_forks(); + let (sender, receiver) = unbounded(); + let (mut receive_and_buffer, mut container, response_receiver) = + setup_bam_receive_and_buffer(receiver, bank_forks.clone()); + + // Create an invalid packet with no data + let bundle = AtomicTxnBatch { + seq_id: 1, + packets: vec![Packet { + data: vec![], + meta: None, + }], + max_schedule_slot: 0, + }; + sender.send(bundle).unwrap(); + + let result = receive_and_buffer + .receive_and_buffer_packets( + &mut container, + &mut SchedulerTimingMetrics::default(), + &mut SchedulerCountMetrics::default(), + &BufferedPacketsDecision::Hold, + ) + .unwrap(); + + assert_eq!(result, 0); + verify_container(&mut container, 0); + let response = response_receiver.recv().unwrap(); + assert!(matches!( + response.msg, + Some(Msg::AtomicTxnBatchResult(txn_batch_result)) if txn_batch_result.seq_id == 1 && + matches!(&txn_batch_result.result, Some(atomic_txn_batch_result::Result::NotCommitted(not_committed)) if + matches!(not_committed.reason, Some(Reason::DeserializationError(_)))) + )); + } + + #[test] + fn test_parse_bundle_success() { + let (bank_forks, mint_keypair) = test_bank_forks(); + let bundle = AtomicTxnBatch { + seq_id: 1, + packets: vec![Packet { + data: bincode::serialize(&transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + )) + .unwrap(), + meta: None, + }], + max_schedule_slot: 0, + }; + let result = BamReceiveAndBuffer::parse_batch(&bundle, &bank_forks); + assert!(result.is_ok()); + let parsed_bundle = result.unwrap(); + assert_eq!(parsed_bundle.packets.len(), 1); + assert_eq!(parsed_bundle.transaction_ttls.len(), 1); + } + + #[test] + fn test_parse_bundle_empty() { + let (bank_forks, _mint_keypair) = test_bank_forks(); + let batch = AtomicTxnBatch { + seq_id: 1, + packets: vec![], + max_schedule_slot: 0, + }; + let result = BamReceiveAndBuffer::parse_batch(&batch, &bank_forks); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + Reason::DeserializationError(jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::Empty as i32, + }) + ); + } + + #[test] + fn test_parse_bundle_invalid_packet() { + let (bank_forks, _mint_keypair) = test_bank_forks(); + let batch = AtomicTxnBatch { + seq_id: 1, + packets: vec![Packet { + data: vec![0; PACKET_DATA_SIZE + 1], // Invalid size + meta: None, + }], + max_schedule_slot: 0, + }; + let result = BamReceiveAndBuffer::parse_batch(&batch, &bank_forks); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + Reason::DeserializationError(jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::BincodeError as i32, + }) + ); + } + + #[test] + fn test_parse_bundle_fee_payer_doesnt_exist() { + let (bank_forks, _) = test_bank_forks(); + let fee_payer = Keypair::new(); + let batch = AtomicTxnBatch { + seq_id: 1, + packets: vec![Packet { + data: bincode::serialize(&transfer( + &fee_payer, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + )) + .unwrap(), + meta: None, + }], + max_schedule_slot: 0, + }; + let result = BamReceiveAndBuffer::parse_batch(&batch, &bank_forks); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + Reason::TransactionError(jito_protos::proto::bam_types::TransactionError { + index: 0, + reason: jito_protos::proto::bam_types::TransactionErrorReason::AccountNotFound + as i32, + }) + ); + } + + #[test] + fn test_parse_bundle_inconsistent() { + let (bank_forks, mint_keypair) = test_bank_forks(); + let bundle = AtomicTxnBatch { + seq_id: 1, + packets: vec![ + Packet { + data: bincode::serialize(&transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + )) + .unwrap(), + meta: None, + }, + Packet { + data: bincode::serialize(&transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + )) + .unwrap(), + meta: Some(jito_protos::proto::bam_types::Meta { + flags: Some(jito_protos::proto::bam_types::PacketFlags { + revert_on_error: true, + ..Default::default() + }), + ..Default::default() + }), + }, + ], + max_schedule_slot: 0, + }; + let result = BamReceiveAndBuffer::parse_batch(&bundle, &bank_forks); + assert!(result.is_err()); + assert_eq!( + result.err().unwrap(), + Reason::DeserializationError(jito_protos::proto::bam_types::DeserializationError { + index: 0, + reason: DeserializationErrorReason::InconsistentBundle as i32, + }) + ); + } +} diff --git a/core/src/banking_stage/transaction_scheduler/bam_scheduler.rs b/core/src/banking_stage/transaction_scheduler/bam_scheduler.rs new file mode 100644 index 0000000000..974e45c61c --- /dev/null +++ b/core/src/banking_stage/transaction_scheduler/bam_scheduler.rs @@ -0,0 +1,1051 @@ +/// A Scheduled implementation that pulls batches off the container, and then +/// schedules them to workers in a FIFO, account-aware manner. This is facilitated by the +/// `PrioGraph` data structure, which is a directed graph that tracks the dependencies. +use std::time::Instant; +use { + super::{ + bam_receive_and_buffer::priority_to_seq_id, + scheduler::{Scheduler, SchedulingSummary}, + scheduler_error::SchedulerError, + transaction_priority_id::TransactionPriorityId, + transaction_state::SanitizedTransactionTTL, + transaction_state_container::StateContainer, + }, + crate::banking_stage::{ + decision_maker::BufferedPacketsDecision, + scheduler_messages::{ + ConsumeWork, FinishedConsumeWork, NotCommittedReason, TransactionBatchId, + TransactionResult, + }, + transaction_scheduler::bam_utils::convert_txn_error_to_proto, + }, + ahash::HashMap, + crossbeam_channel::{Receiver, Sender}, + jito_protos::proto::{ + bam_api::{start_scheduler_message_v0::Msg, StartSchedulerMessageV0}, + bam_types::{atomic_txn_batch_result, not_committed::Reason, SchedulingError}, + }, + prio_graph::{AccessKind, GraphNode, PrioGraph}, + solana_pubkey::Pubkey, + solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, + solana_sdk::clock::Slot, + solana_svm_transaction::svm_message::SVMMessage, +}; + +type SchedulerPrioGraph = PrioGraph< + TransactionPriorityId, + Pubkey, + TransactionPriorityId, + fn(&TransactionPriorityId, &GraphNode) -> TransactionPriorityId, +>; + +#[inline(always)] +fn passthrough_priority( + id: &TransactionPriorityId, + _graph_node: &GraphNode, +) -> TransactionPriorityId { + *id +} + +const MAX_SCHEDULED_PER_WORKER: usize = 5; +const MAX_TXN_PER_BATCH: usize = 16; + +pub struct BamScheduler { + workers_scheduled_count: Vec, + consume_work_senders: Vec>>, + finished_consume_work_receiver: Receiver>, + response_sender: Sender, + + next_batch_id: u64, + inflight_batch_info: HashMap, + prio_graph: SchedulerPrioGraph, + slot: Option, + + // Reusable objects to avoid allocations + reusable_consume_work: Vec>, + reusable_priority_ids: Vec>, + reusable_batches_for_scheduling: Vec<(Vec, bool)>, +} + +// A structure to hold information about inflight batches. +// A batch can either be one 'revert_on_error' batch or multiple +// 'non-revert_on_error' batches that are scheduled together. +struct InflightBatchInfo { + pub priority_ids: Vec, + pub worker_index: usize, + pub slot: Slot, +} + +impl BamScheduler { + pub fn new( + consume_work_senders: Vec>>, + finished_consume_work_receiver: Receiver>, + response_sender: Sender, + ) -> Self { + Self { + workers_scheduled_count: vec![0; consume_work_senders.len()], + consume_work_senders, + finished_consume_work_receiver, + response_sender, + next_batch_id: 0, + inflight_batch_info: HashMap::default(), + prio_graph: PrioGraph::new(passthrough_priority), + slot: None, + reusable_consume_work: Vec::new(), + reusable_priority_ids: Vec::new(), + reusable_batches_for_scheduling: Vec::new(), + } + } + + /// Gets accessed accounts (resources) for use in `PrioGraph`. + fn get_transactions_account_access<'a>( + transactions: impl Iterator> + 'a, + ) -> impl Iterator + 'a { + transactions.flat_map(|transaction| { + let message = &transaction.transaction; + message + .account_keys() + .iter() + .enumerate() + .map(|(index, key)| { + if message.is_writable(index) { + (*key, AccessKind::Write) + } else { + (*key, AccessKind::Read) + } + }) + }) + } + + /// Insert all incoming transactions into the `PrioGraph`. + fn pull_into_prio_graph>(&mut self, container: &mut S) { + while let Some(next_batch_id) = container.pop() { + let Some((batch_ids, _, _)) = container.get_batch(next_batch_id.id) else { + error!("Batch {} not found in container", next_batch_id.id); + continue; + }; + let txns = batch_ids + .iter() + .filter_map(|txn_id| container.get_transaction_ttl(*txn_id)); + + self.prio_graph.insert_transaction( + next_batch_id, + Self::get_transactions_account_access(txns.into_iter()), + ); + } + } + + fn get_best_available_worker(&mut self) -> Option { + let mut best_worker_index = None; + let mut best_worker_count = MAX_SCHEDULED_PER_WORKER; + for (worker_index, count) in self.workers_scheduled_count.iter_mut().enumerate() { + if *count == 0 { + return Some(worker_index); + } + if best_worker_index.is_none() || *count < best_worker_count { + best_worker_index = Some(worker_index); + best_worker_count = *count; + } + } + best_worker_index + } + + fn send_to_workers( + &mut self, + container: &mut impl StateContainer, + num_scheduled: &mut usize, + ) { + let Some(slot) = self.slot else { + warn!("Slot is not set, cannot schedule transactions"); + return; + }; + + // Schedule any available transactions in prio-graph + let mut batches_for_scheduling = std::mem::take(&mut self.reusable_batches_for_scheduling); + while let Some(worker_index) = self.get_best_available_worker() { + self.get_batches_for_scheduling(&mut batches_for_scheduling, container, slot); + if batches_for_scheduling.is_empty() { + break; + } + for (priority_ids, revert_on_error) in batches_for_scheduling.drain(..) { + let len = priority_ids.len(); + let batch_id = self.get_next_schedule_id(); + let mut work = self.get_or_create_work_object(); + Self::generate_work( + &mut work, + batch_id, + &priority_ids, + revert_on_error, + container, + slot, + ); + self.send_to_worker(worker_index, priority_ids, work, slot); + *num_scheduled += len; + } + } + std::mem::swap( + &mut self.reusable_batches_for_scheduling, + &mut batches_for_scheduling, + ); + } + + /// Get batches of transactions for scheduling. + /// Build a normal txn batch up to a maximum of `MAX_TXN_PER_BATCH` transactions; + /// but if a 'revert_on_error' batch is encountered, the WIP batch is finalized + /// and the 'revert_on_error' batch is appended to the result. + fn get_batches_for_scheduling( + &mut self, + result: &mut Vec<(Vec, bool)>, + container: &mut impl StateContainer, + current_slot: Slot, + ) { + let mut current_batch_ids = self.get_or_create_priority_ids(); + while let Some(next_batch_id) = self.prio_graph.pop() { + let Some((_, revert_on_error, slot)) = container.get_batch(next_batch_id.id) else { + continue; + }; + + // These should be cleared out earlier; but if not, we remove them here + if slot != current_slot { + container.remove_by_id(next_batch_id.id); + self.prio_graph.unblock(&next_batch_id); + self.send_no_leader_slot_bundle_result(priority_to_seq_id(next_batch_id.priority)); + continue; + } + + if revert_on_error { + if !current_batch_ids.is_empty() { + result.push((std::mem::take(&mut current_batch_ids), false)); + current_batch_ids = self.get_or_create_priority_ids(); + } + result.push((vec![next_batch_id], true)); + break; + } else { + current_batch_ids.push(next_batch_id); + } + + if current_batch_ids.len() >= MAX_TXN_PER_BATCH { + break; + } + } + if !current_batch_ids.is_empty() { + result.push((current_batch_ids, false)); + } + } + + fn send_to_worker( + &mut self, + worker_index: usize, + priority_ids: Vec, + work: ConsumeWork, + slot: Slot, + ) { + let consume_work_sender = &self.consume_work_senders[worker_index]; + let batch_id = work.batch_id; + let _ = consume_work_sender.send(work); + self.inflight_batch_info.insert( + batch_id, + InflightBatchInfo { + priority_ids, + worker_index, + slot, + }, + ); + self.workers_scheduled_count[worker_index] += 1; + } + + fn get_next_schedule_id(&mut self) -> TransactionBatchId { + let result = TransactionBatchId::new(self.next_batch_id); + self.next_batch_id += 1; + result + } + + fn get_or_create_work_object(&mut self) -> ConsumeWork { + if let Some(work) = self.reusable_consume_work.pop() { + work + } else { + // These values will be overwritten by `generate_work` + ConsumeWork { + batch_id: TransactionBatchId::new(0), + ids: Vec::with_capacity(MAX_TXN_PER_BATCH), + transactions: Vec::with_capacity(MAX_TXN_PER_BATCH), + max_ages: Vec::with_capacity(MAX_TXN_PER_BATCH), + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, + } + } + } + + fn recycle_work_object(&mut self, mut work: ConsumeWork) { + // Just in case, clear the work object + work.ids.clear(); + work.transactions.clear(); + work.max_ages.clear(); + self.reusable_consume_work.push(work); + } + + fn get_or_create_priority_ids(&mut self) -> Vec { + if let Some(priority_ids) = self.reusable_priority_ids.pop() { + priority_ids + } else { + Vec::with_capacity(MAX_TXN_PER_BATCH) + } + } + + fn recycle_priority_ids(&mut self, mut priority_ids: Vec) { + priority_ids.clear(); + self.reusable_priority_ids.push(priority_ids); + } + + fn generate_work( + output: &mut ConsumeWork, + batch_id: TransactionBatchId, + priority_ids: &[TransactionPriorityId], + revert_on_error: bool, + container: &mut impl StateContainer, + slot: Slot, + ) { + output.ids.clear(); + output.ids.extend( + priority_ids + .iter() + .filter_map(|priority_id| container.get_batch(priority_id.id)) + .flat_map(|(batch_ids, _, _)| batch_ids.into_iter()) + .cloned(), + ); + + output.transactions.clear(); + output.max_ages.clear(); + for txn in output.ids.iter().filter_map(|txn_id| { + let result = container.get_mut_transaction_state(*txn_id)?; + let result = result.transition_to_pending(); + Some(result) + }) { + output.transactions.push(txn.transaction); + output.max_ages.push(txn.max_age); + } + + output.batch_id = batch_id; + output.revert_on_error = revert_on_error; + output.schedulable_slot = Some(slot); + output.respond_with_extra_info = true; + } + + fn send_no_leader_slot_bundle_result(&self, seq_id: u32) { + let _ = self.response_sender.try_send(StartSchedulerMessageV0 { + msg: Some(Msg::AtomicTxnBatchResult( + jito_protos::proto::bam_types::AtomicTxnBatchResult { + seq_id, + result: Some(atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(Reason::SchedulingError( + SchedulingError::OutsideLeaderSlot as i32, + )), + }, + )), + }, + )), + }); + } + + fn send_back_result(&self, seq_id: u32, result: atomic_txn_batch_result::Result) { + let _ = self.response_sender.try_send(StartSchedulerMessageV0 { + msg: Some(Msg::AtomicTxnBatchResult( + jito_protos::proto::bam_types::AtomicTxnBatchResult { + seq_id, + result: Some(result), + }, + )), + }); + } + + /// Generates a `bundle_result::Result` based on the processed results for 'revert_on_error' batches. + fn generate_revert_on_error_bundle_result( + processed_results: &[TransactionResult], + ) -> atomic_txn_batch_result::Result { + if processed_results + .iter() + .all(|result| matches!(result, TransactionResult::Committed(_))) + { + let transaction_results = processed_results + .iter() + .filter_map(|result| { + if let TransactionResult::Committed(processed) = result { + Some(processed.clone()) + } else { + None + } + }) + .collect(); + atomic_txn_batch_result::Result::Committed(jito_protos::proto::bam_types::Committed { + transaction_results, + }) + } else { + // Get first NotCommit Reason that is not BatchRevert + let (index, not_commit_reason) = processed_results + .iter() + .enumerate() + .find_map(|(index, result)| { + if let TransactionResult::NotCommitted(reason) = result { + if matches!(reason, &NotCommittedReason::BatchRevert) { + None + } else { + Some((index, reason.clone())) + } + } else { + None + } + }) + .unwrap_or((0, NotCommittedReason::PohTimeout)); + + atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(Self::convert_reason_to_proto(index, not_commit_reason)), + }, + ) + } + } + + /// Generates a `bundle_result::Result` based on the processed result of a single transaction. + fn generate_bundle_result(processed: &TransactionResult) -> atomic_txn_batch_result::Result { + match processed { + TransactionResult::Committed(result) => atomic_txn_batch_result::Result::Committed( + jito_protos::proto::bam_types::Committed { + transaction_results: vec![result.clone()], + }, + ), + TransactionResult::NotCommitted(reason) => { + let (index, not_commit_reason) = match reason { + NotCommittedReason::PohTimeout => (0, NotCommittedReason::PohTimeout), + NotCommittedReason::BatchRevert => (0, NotCommittedReason::BatchRevert), + NotCommittedReason::Error(err) => (0, NotCommittedReason::Error(err.clone())), + }; + atomic_txn_batch_result::Result::NotCommitted( + jito_protos::proto::bam_types::NotCommitted { + reason: Some(Self::convert_reason_to_proto(index, not_commit_reason)), + }, + ) + } + } + } + + fn convert_reason_to_proto( + index: usize, + reason: NotCommittedReason, + ) -> jito_protos::proto::bam_types::not_committed::Reason { + match reason { + NotCommittedReason::PohTimeout => { + jito_protos::proto::bam_types::not_committed::Reason::SchedulingError( + SchedulingError::PohTimeout as i32, + ) + } + // Should not happen, but just in case: + NotCommittedReason::BatchRevert => { + jito_protos::proto::bam_types::not_committed::Reason::GenericInvalid( + jito_protos::proto::bam_types::GenericInvalid {}, + ) + } + NotCommittedReason::Error(err) => { + jito_protos::proto::bam_types::not_committed::Reason::TransactionError( + jito_protos::proto::bam_types::TransactionError { + index: index as u32, + reason: convert_txn_error_to_proto(err) as i32, + }, + ) + } + } + } + + fn maybe_bank_boundary_actions( + &mut self, + decision: &BufferedPacketsDecision, + container: &mut impl StateContainer, + ) { + // Check if no bank or slot has changed + let bank_start = decision.bank_start(); + if bank_start.map(|bs| bs.working_bank.slot()) == self.slot { + return; + } + if let Some(bank_start) = bank_start { + info!( + "Bank boundary detected: slot changed from {:?} to {:?}", + self.slot, + bank_start.working_bank.slot() + ); + self.slot = Some(bank_start.working_bank.slot()); + } else { + info!("Bank boundary detected: slot changed to None"); + self.slot = None; + } + + // Drain container and send back 'retryable' + if self.slot.is_none() { + while let Some(next_batch_id) = container.pop() { + let seq_id = priority_to_seq_id(next_batch_id.priority); + self.send_no_leader_slot_bundle_result(seq_id); + container.remove_by_id(next_batch_id.id); + } + } + + // Unblock all transactions blocked by inflight batches + // and then drain the prio-graph + for (_, inflight_info) in self.inflight_batch_info.iter() { + for priority_id in &inflight_info.priority_ids { + self.prio_graph.unblock(priority_id); + } + } + while let Some((next_batch_id, _)) = self.prio_graph.pop_and_unblock() { + let seq_id = priority_to_seq_id(next_batch_id.priority); + self.send_no_leader_slot_bundle_result(seq_id); + container.remove_by_id(next_batch_id.id); + } + } +} + +impl Scheduler for BamScheduler { + fn schedule>( + &mut self, + container: &mut S, + _pre_graph_filter: impl Fn(&[&Tx], &mut [bool]), + _pre_lock_filter: impl Fn(&Tx) -> bool, + ) -> Result { + let start_time = Instant::now(); + let mut num_scheduled = 0; + + self.pull_into_prio_graph(container); + self.send_to_workers(container, &mut num_scheduled); + + Ok(SchedulingSummary { + num_scheduled, + num_unschedulable: 0, + num_filtered_out: 0, + filter_time_us: start_time.elapsed().as_micros() as u64, + }) + } + + /// Receive completed batches of transactions without blocking. + /// This also handles checking if the slot has ended and if so, it will + /// drain the container and prio-graph, sending back 'retryable' results + /// back to BAM. + fn receive_completed( + &mut self, + container: &mut impl StateContainer, + decision: &BufferedPacketsDecision, + ) -> Result<(usize, usize), SchedulerError> { + // Check if the slot/bank has changed; do what must be done + // IMPORTANT: This must be called before the receiving code below + self.maybe_bank_boundary_actions(decision, container); + + let mut num_transactions = 0; + while let Ok(result) = self.finished_consume_work_receiver.try_recv() { + num_transactions += result.work.ids.len(); + let batch_id = result.work.batch_id; + let revert_on_error = result.work.revert_on_error; + self.recycle_work_object(result.work); + + let Some(inflight_batch_info) = self.inflight_batch_info.remove(&batch_id) else { + continue; + }; + self.workers_scheduled_count[inflight_batch_info.worker_index] -= 1; + + // Should never not be 1; but just in case + let len = if revert_on_error { + 1 + } else { + inflight_batch_info.priority_ids.len() + }; + for (i, priority_id) in inflight_batch_info + .priority_ids + .iter() + .enumerate() + .take(len) + { + // If we got extra info, we can send back the result + if let Some(extra_info) = result.extra_info.as_ref() { + let bundle_result = if revert_on_error { + Self::generate_revert_on_error_bundle_result(&extra_info.processed_results) + } else { + let Some(txn_result) = extra_info.processed_results.get(i) else { + warn!( + "Processed results for batch {} are missing for index {}", + batch_id.0, i + ); + continue; + }; + Self::generate_bundle_result(txn_result) + }; + self.send_back_result(priority_to_seq_id(priority_id.priority), bundle_result); + } + + // If in the same slot, unblock the transaction + if Some(inflight_batch_info.slot) == self.slot { + self.prio_graph.unblock(priority_id); + } + + // Remove the transaction from the container + container.remove_by_id(priority_id.id); + } + self.recycle_priority_ids(inflight_batch_info.priority_ids); + } + + Ok((num_transactions, 0)) + } +} + +#[cfg(test)] +mod tests { + use { + crate::banking_stage::{ + decision_maker::BufferedPacketsDecision, + immutable_deserialized_packet::ImmutableDeserializedPacket, + scheduler_messages::{ + ConsumeWork, FinishedConsumeWork, MaxAge, NotCommittedReason, TransactionResult, + }, + tests::create_slow_genesis_config, + transaction_scheduler::{ + bam_receive_and_buffer::seq_id_to_priority, + bam_scheduler::BamScheduler, + scheduler::Scheduler, + transaction_state::SanitizedTransactionTTL, + transaction_state_container::{StateContainer, TransactionStateContainer}, + }, + }, + crossbeam_channel::unbounded, + itertools::Itertools, + jito_protos::proto::{ + bam_api::{start_scheduler_message_v0::Msg, StartSchedulerMessageV0}, + bam_types::{ + atomic_txn_batch_result::Result::{Committed, NotCommitted}, + TransactionCommittedResult, + }, + }, + solana_ledger::genesis_utils::GenesisConfigInfo, + solana_perf::packet::Packet, + solana_poh::poh_recorder::BankStart, + solana_pubkey::Pubkey, + solana_runtime::{bank::Bank, bank_forks::BankForks}, + solana_runtime_transaction::runtime_transaction::RuntimeTransaction, + solana_sdk::{ + compute_budget::ComputeBudgetInstruction, + hash::Hash, + message::Message, + signature::Keypair, + signer::Signer, + system_instruction::transfer_many, + transaction::{SanitizedTransaction, Transaction}, + }, + std::{ + borrow::Borrow, + sync::{Arc, RwLock}, + time::Instant, + }, + }; + + struct TestScheduler { + scheduler: BamScheduler>, + consume_work_receivers: + Vec>>>, + finished_consume_work_sender: crossbeam_channel::Sender< + FinishedConsumeWork>, + >, + response_receiver: crossbeam_channel::Receiver, + } + + fn create_test_scheduler(num_threads: usize) -> TestScheduler { + let (consume_work_senders, consume_work_receivers) = + (0..num_threads).map(|_| unbounded()).unzip(); + let (finished_consume_work_sender, finished_consume_work_receiver) = unbounded(); + let (response_sender, response_receiver) = unbounded(); + let scheduler = BamScheduler::new( + consume_work_senders, + finished_consume_work_receiver, + response_sender, + ); + TestScheduler { + scheduler, + consume_work_receivers, + finished_consume_work_sender, + response_receiver, + } + } + + fn prioritized_tranfers( + from_keypair: &Keypair, + to_pubkeys: impl IntoIterator>, + lamports: u64, + priority: u64, + ) -> RuntimeTransaction { + let to_pubkeys_lamports = to_pubkeys + .into_iter() + .map(|pubkey| *pubkey.borrow()) + .zip(std::iter::repeat(lamports)) + .collect_vec(); + let mut ixs = transfer_many(&from_keypair.pubkey(), &to_pubkeys_lamports); + let prioritization = ComputeBudgetInstruction::set_compute_unit_price(priority); + ixs.push(prioritization); + let message = Message::new(&ixs, Some(&from_keypair.pubkey())); + let tx = Transaction::new(&[from_keypair], message, Hash::default()); + RuntimeTransaction::from_transaction_for_tests(tx) + } + + fn create_container( + tx_infos: impl IntoIterator< + Item = ( + impl Borrow, + impl IntoIterator>, + u64, + u64, + ), + >, + ) -> TransactionStateContainer> { + let mut container = TransactionStateContainer::with_capacity(10 * 1024); + for (from_keypair, to_pubkeys, lamports, compute_unit_price) in tx_infos.into_iter() { + let transaction = prioritized_tranfers( + from_keypair.borrow(), + to_pubkeys, + lamports, + compute_unit_price, + ); + let packet = Arc::new( + ImmutableDeserializedPacket::new( + Packet::from_data(None, transaction.to_versioned_transaction()).unwrap(), + ) + .unwrap(), + ); + let transaction_ttl = SanitizedTransactionTTL { + transaction, + max_age: MaxAge::MAX, + }; + const TEST_TRANSACTION_COST: u64 = 5000; + container.insert_new_batch( + vec![transaction_ttl], + vec![packet], + compute_unit_price, + TEST_TRANSACTION_COST, + false, + 0, + ); + } + + container + } + + fn test_bank_forks() -> (Arc>, Keypair) { + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_slow_genesis_config(u64::MAX); + + let (_bank, bank_forks) = Bank::new_no_wallclock_throttle_for_tests(&genesis_config); + (bank_forks, mint_keypair) + } + + #[test] + fn test_scheduler_empty() { + let TestScheduler { + mut scheduler, + consume_work_receivers: _, + finished_consume_work_sender: _, + response_receiver: _, + } = create_test_scheduler(4); + + let mut container = TransactionStateContainer::with_capacity(100); + let result = scheduler + .schedule(&mut container, |_, _| {}, |_| true) + .unwrap(); + assert_eq!(result.num_scheduled, 0); + } + + #[test] + fn test_scheduler_basic() { + let TestScheduler { + mut scheduler, + consume_work_receivers, + finished_consume_work_sender, + response_receiver, + } = create_test_scheduler(4); + + let keypair_a = Keypair::new(); + + let first_recipient = Pubkey::new_unique(); + + let mut container = create_container(vec![ + ( + &keypair_a, + vec![Pubkey::new_unique()], + 1000, + seq_id_to_priority(1), + ), + ( + &keypair_a, + vec![first_recipient], + 1500, + seq_id_to_priority(0), + ), + ( + &keypair_a, + vec![Pubkey::new_unique()], + 1500, + seq_id_to_priority(2), + ), + ( + &Keypair::new(), + vec![Pubkey::new_unique()], + 2000, + seq_id_to_priority(3), + ), + ]); + + assert!( + scheduler.slot.is_none(), + "Scheduler slot should be None initially" + ); + + let (bank_forks, _) = test_bank_forks(); + + let decision = BufferedPacketsDecision::Consume(BankStart { + working_bank: bank_forks.read().unwrap().working_bank(), + bank_creation_time: Arc::new(Instant::now()), + }); + + // Init scheduler with bank start info + scheduler + .receive_completed(&mut container, &decision) + .unwrap(); + + assert!( + scheduler.slot.is_some(), + "Scheduler slot should be set after receiving bank start" + ); + + // Schedule the transactions + let result = scheduler + .schedule(&mut container, |_, _| {}, |_| true) + .unwrap(); + + // Only two should have been scheduled as one is blocked + assert_eq!(result.num_scheduled, 2); + + // Since both are not bundles; should be scheduled together to first worker + let work_1 = consume_work_receivers[0].try_recv().unwrap(); + assert_eq!(work_1.ids.len(), 2); + + // Check that the first transaction is from keypair_a and first recipient is the first recipient + assert_eq!( + work_1.transactions[0].message().account_keys()[0], + keypair_a.pubkey() + ); + assert_eq!( + work_1.transactions[0].message().account_keys()[1], + first_recipient + ); + + // Try scheduling; nothing should be scheduled as the remaining transaction is blocked + let result = scheduler + .schedule(&mut container, |_, _| {}, |_| true) + .unwrap(); + assert_eq!(result.num_scheduled, 0); + assert_eq!(scheduler.workers_scheduled_count[0], 1); + + // Respond with finsihed work + let finished_work = FinishedConsumeWork { + work: work_1, + retryable_indexes: vec![], + extra_info: Some( + crate::banking_stage::scheduler_messages::FinishedConsumeWorkExtraInfo { + processed_results: vec![ + TransactionResult::Committed(TransactionCommittedResult { + cus_consumed: 100, + feepayer_balance_lamports: 1000, + loaded_accounts_data_size: 10, + }), + TransactionResult::NotCommitted(NotCommittedReason::PohTimeout), + ], + }, + ), + }; + let _ = finished_consume_work_sender.send(finished_work); + + // Receive the finished work + let (num_transactions, _) = scheduler + .receive_completed(&mut container, &decision) + .unwrap(); + assert_eq!(num_transactions, 2); + assert_eq!(scheduler.workers_scheduled_count[0], 0); + + // Check the response for the first transaction (committed) + let response = response_receiver.try_recv().unwrap(); + assert!(response.msg.is_some(), "Response should contain a message"); + let msg = response.msg.unwrap(); + let Msg::AtomicTxnBatchResult(bundle_result) = msg else { + panic!("Expected AtomicTxnBatchResult message"); + }; + assert_eq!(bundle_result.seq_id, 0); + assert!( + bundle_result.result.is_some(), + "Bundle result should be present" + ); + let result = bundle_result.result.unwrap(); + match result { + Committed(committed) => { + assert_eq!(committed.transaction_results.len(), 1); + assert_eq!(committed.transaction_results[0].cus_consumed, 100); + } + NotCommitted(not_committed) => { + panic!( + "Expected Committed result, got NotCommitted: {:?}", + not_committed + ); + } + } + + // Check the response for the second transaction (not committed) + let response = response_receiver.try_recv().unwrap(); + assert!(response.msg.is_some(), "Response should contain a message"); + let msg = response.msg.unwrap(); + let Msg::AtomicTxnBatchResult(bundle_result) = msg else { + panic!("Expected AtomicTxnBatchResult message"); + }; + assert_eq!(bundle_result.seq_id, 3); + assert!( + bundle_result.result.is_some(), + "Bundle result should be present" + ); + let result = bundle_result.result.unwrap(); + match result { + Committed(_) => { + panic!("Expected NotCommitted result, got Committed"); + } + NotCommitted(not_committed) => { + assert!( + not_committed.reason.is_some(), + "NotCommitted reason should be present" + ); + let reason = not_committed.reason.unwrap(); + assert_eq!( + reason, + jito_protos::proto::bam_types::not_committed::Reason::SchedulingError( + jito_protos::proto::bam_types::SchedulingError::PohTimeout as i32 + ) + ); + } + } + + // Now try scheduling again; should schedule the remaining transaction + let result = scheduler + .schedule(&mut container, |_, _| {}, |_| true) + .unwrap(); + assert_eq!(result.num_scheduled, 1); + // Check that the remaining transaction is sent to the worker + let work_2 = consume_work_receivers[0].try_recv().unwrap(); + assert_eq!(work_2.ids.len(), 1); + assert_eq!(scheduler.workers_scheduled_count[0], 1); + + // Try scheduling; nothing should be scheduled as the remaining transaction is blocked + let result = scheduler + .schedule(&mut container, |_, _| {}, |_| true) + .unwrap(); + assert_eq!(result.num_scheduled, 0); + + // Send back the finished work for the second transaction + let finished_work = FinishedConsumeWork { + work: work_2, + retryable_indexes: vec![], + extra_info: Some( + crate::banking_stage::scheduler_messages::FinishedConsumeWorkExtraInfo { + processed_results: vec![TransactionResult::Committed( + TransactionCommittedResult { + cus_consumed: 1500, + feepayer_balance_lamports: 1500, + loaded_accounts_data_size: 20, + }, + )], + }, + ), + }; + let _ = finished_consume_work_sender.send(finished_work); + + // Receive the finished work + let (num_transactions, _) = scheduler + .receive_completed(&mut container, &decision) + .unwrap(); + assert_eq!(num_transactions, 1); + assert_eq!(scheduler.workers_scheduled_count[0], 0); + + // Check the response for the next transaction + let response = response_receiver.try_recv().unwrap(); + assert!(response.msg.is_some(), "Response should contain a message"); + let msg = response.msg.unwrap(); + let Msg::AtomicTxnBatchResult(bundle_result) = msg else { + panic!("Expected AtomicTxnBatchResult message"); + }; + assert_eq!(bundle_result.seq_id, 1); + assert!( + bundle_result.result.is_some(), + "Bundle result should be present" + ); + let result = bundle_result.result.unwrap(); + match result { + Committed(committed) => { + assert_eq!(committed.transaction_results.len(), 1); + assert_eq!(committed.transaction_results[0].cus_consumed, 1500); + } + NotCommitted(not_committed) => { + panic!( + "Expected Committed result, got NotCommitted: {:?}", + not_committed + ); + } + } + + // Receive the finished work + let (num_transactions, _) = scheduler + .receive_completed(&mut container, &BufferedPacketsDecision::Forward) + .unwrap(); + assert_eq!(num_transactions, 0); + + // Check that container + prio-graph are empty + assert!( + container.pop().is_none(), + "Container should be empty after processing all transactions" + ); + assert!( + scheduler.prio_graph.is_empty(), + "Prio-graph should be empty after processing all transactions" + ); + + // Receive the NotCommitted Result + let response = response_receiver.try_recv().unwrap(); + assert!(response.msg.is_some(), "Response should contain a message"); + let msg = response.msg.unwrap(); + let Msg::AtomicTxnBatchResult(bundle_result) = msg else { + panic!("Expected AtomicTxnBatchResult message"); + }; + assert_eq!(bundle_result.seq_id, 2); + assert!( + bundle_result.result.is_some(), + "Bundle result should be present" + ); + let result = bundle_result.result.unwrap(); + match result { + Committed(_) => { + panic!("Expected NotCommitted result, got Committed"); + } + NotCommitted(not_committed) => { + assert!( + not_committed.reason.is_some(), + "NotCommitted reason should be present" + ); + let reason = not_committed.reason.unwrap(); + assert_eq!( + reason, + jito_protos::proto::bam_types::not_committed::Reason::SchedulingError( + jito_protos::proto::bam_types::SchedulingError::OutsideLeaderSlot as i32 + ) + ); + } + } + } +} diff --git a/core/src/banking_stage/transaction_scheduler/bam_utils.rs b/core/src/banking_stage/transaction_scheduler/bam_utils.rs new file mode 100644 index 0000000000..819bb5dafa --- /dev/null +++ b/core/src/banking_stage/transaction_scheduler/bam_utils.rs @@ -0,0 +1,107 @@ +use { + crate::banking_stage::immutable_deserialized_packet::DeserializedPacketError, + jito_protos::proto::bam_types::{DeserializationErrorReason, TransactionErrorReason}, + solana_sdk::transaction::TransactionError, +}; + +pub fn convert_txn_error_to_proto(err: TransactionError) -> TransactionErrorReason { + match err { + TransactionError::AccountInUse => TransactionErrorReason::AccountInUse, + TransactionError::AccountLoadedTwice => TransactionErrorReason::AccountLoadedTwice, + TransactionError::AccountNotFound => TransactionErrorReason::AccountNotFound, + TransactionError::ProgramAccountNotFound => TransactionErrorReason::ProgramAccountNotFound, + TransactionError::InsufficientFundsForFee => { + TransactionErrorReason::InsufficientFundsForFee + } + TransactionError::InvalidAccountForFee => TransactionErrorReason::InvalidAccountForFee, + TransactionError::AlreadyProcessed => TransactionErrorReason::AlreadyProcessed, + TransactionError::BlockhashNotFound => TransactionErrorReason::BlockhashNotFound, + TransactionError::InstructionError(_, _) => TransactionErrorReason::InstructionError, + TransactionError::CallChainTooDeep => TransactionErrorReason::CallChainTooDeep, + TransactionError::MissingSignatureForFee => TransactionErrorReason::MissingSignatureForFee, + TransactionError::InvalidAccountIndex => TransactionErrorReason::InvalidAccountIndex, + TransactionError::SignatureFailure => TransactionErrorReason::SignatureFailure, + TransactionError::InvalidProgramForExecution => { + TransactionErrorReason::InvalidProgramForExecution + } + TransactionError::SanitizeFailure => TransactionErrorReason::SanitizeFailure, + TransactionError::ClusterMaintenance => TransactionErrorReason::ClusterMaintenance, + TransactionError::AccountBorrowOutstanding => { + TransactionErrorReason::AccountBorrowOutstanding + } + TransactionError::WouldExceedMaxBlockCostLimit => { + TransactionErrorReason::WouldExceedMaxBlockCostLimit + } + TransactionError::UnsupportedVersion => TransactionErrorReason::UnsupportedVersion, + TransactionError::InvalidWritableAccount => TransactionErrorReason::InvalidWritableAccount, + TransactionError::WouldExceedMaxAccountCostLimit => { + TransactionErrorReason::WouldExceedMaxAccountCostLimit + } + TransactionError::WouldExceedAccountDataBlockLimit => { + TransactionErrorReason::WouldExceedAccountDataBlockLimit + } + TransactionError::TooManyAccountLocks => TransactionErrorReason::TooManyAccountLocks, + TransactionError::AddressLookupTableNotFound => { + TransactionErrorReason::AddressLookupTableNotFound + } + TransactionError::InvalidAddressLookupTableOwner => { + TransactionErrorReason::InvalidAddressLookupTableOwner + } + TransactionError::InvalidAddressLookupTableData => { + TransactionErrorReason::InvalidAddressLookupTableData + } + TransactionError::InvalidAddressLookupTableIndex => { + TransactionErrorReason::InvalidAddressLookupTableIndex + } + TransactionError::InvalidRentPayingAccount => { + TransactionErrorReason::InvalidRentPayingAccount + } + TransactionError::WouldExceedMaxVoteCostLimit => { + TransactionErrorReason::WouldExceedMaxVoteCostLimit + } + TransactionError::WouldExceedAccountDataTotalLimit => { + TransactionErrorReason::WouldExceedAccountDataTotalLimit + } + TransactionError::DuplicateInstruction(_) => TransactionErrorReason::DuplicateInstruction, + TransactionError::InsufficientFundsForRent { .. } => { + TransactionErrorReason::InsufficientFundsForRent + } + TransactionError::MaxLoadedAccountsDataSizeExceeded => { + TransactionErrorReason::MaxLoadedAccountsDataSizeExceeded + } + TransactionError::InvalidLoadedAccountsDataSizeLimit => { + TransactionErrorReason::InvalidLoadedAccountsDataSizeLimit + } + TransactionError::ResanitizationNeeded => TransactionErrorReason::ResanitizationNeeded, + TransactionError::ProgramExecutionTemporarilyRestricted { .. } => { + TransactionErrorReason::ProgramExecutionTemporarilyRestricted + } + TransactionError::UnbalancedTransaction => TransactionErrorReason::UnbalancedTransaction, + TransactionError::ProgramCacheHitMaxLimit => { + TransactionErrorReason::ProgramCacheHitMaxLimit + } + TransactionError::CommitCancelled => TransactionErrorReason::CommitCancelled, + } +} + +pub fn convert_deserialize_error_to_proto( + err: &DeserializedPacketError, +) -> DeserializationErrorReason { + match err { + DeserializedPacketError::ShortVecError(_) => DeserializationErrorReason::BincodeError, + DeserializedPacketError::DeserializationError(_) => { + DeserializationErrorReason::BincodeError + } + DeserializedPacketError::SignatureOverflowed(_) => { + DeserializationErrorReason::SignatureOverflowed + } + DeserializedPacketError::SanitizeError(_) => DeserializationErrorReason::SanitizeError, + DeserializedPacketError::PrioritizationFailure => { + DeserializationErrorReason::PrioritizationFailure + } + DeserializedPacketError::VoteTransactionError => { + DeserializationErrorReason::VoteTransactionFailure + } + DeserializedPacketError::FailedFilter(_) => DeserializationErrorReason::FilterFailure, + } +} diff --git a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs index b20e441204..b6675e8401 100644 --- a/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/greedy_scheduler.rs @@ -13,6 +13,7 @@ use { }, crate::banking_stage::{ consumer::TARGET_NUM_TRANSACTIONS_PER_BATCH, + decision_maker::BufferedPacketsDecision, read_write_account_set::ReadWriteAccountSet, scheduler_messages::{ConsumeWork, FinishedConsumeWork, TransactionBatchId}, transaction_scheduler::thread_aware_account_locks::MAX_THREADS, @@ -216,6 +217,7 @@ impl Scheduler for GreedyScheduler { fn receive_completed( &mut self, container: &mut impl StateContainer, + _: &BufferedPacketsDecision, ) -> Result<(usize, usize), SchedulerError> { let mut total_num_transactions: usize = 0; let mut total_num_retryable: usize = 0; @@ -246,8 +248,12 @@ impl GreedyScheduler { ids, transactions, max_ages, + revert_on_error: _, + respond_with_extra_info: _, + schedulable_slot: _, }, retryable_indexes, + extra_info: _, }) => { let num_transactions = ids.len(); let num_retryable = retryable_indexes.len(); @@ -336,6 +342,9 @@ impl GreedyScheduler { ids, transactions, max_ages, + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }; self.consume_work_senders[thread_index] .send(work) diff --git a/core/src/banking_stage/transaction_scheduler/mod.rs b/core/src/banking_stage/transaction_scheduler/mod.rs index c28e3110cf..f3a9f2e278 100644 --- a/core/src/banking_stage/transaction_scheduler/mod.rs +++ b/core/src/banking_stage/transaction_scheduler/mod.rs @@ -1,3 +1,6 @@ +pub(crate) mod bam_receive_and_buffer; +pub(crate) mod bam_scheduler; +pub(crate) mod bam_utils; mod batch_id_generator; pub(crate) mod greedy_scheduler; mod in_flight_tracker; diff --git a/core/src/banking_stage/transaction_scheduler/prio_graph_scheduler.rs b/core/src/banking_stage/transaction_scheduler/prio_graph_scheduler.rs index 5d8576acff..59a7c9f500 100644 --- a/core/src/banking_stage/transaction_scheduler/prio_graph_scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/prio_graph_scheduler.rs @@ -8,6 +8,7 @@ use { }, crate::banking_stage::{ consumer::TARGET_NUM_TRANSACTIONS_PER_BATCH, + decision_maker::BufferedPacketsDecision, read_write_account_set::ReadWriteAccountSet, scheduler_messages::{ ConsumeWork, FinishedConsumeWork, MaxAge, TransactionBatchId, TransactionId, @@ -327,6 +328,7 @@ impl Scheduler for PrioGraphScheduler { fn receive_completed( &mut self, container: &mut impl StateContainer, + _: &BufferedPacketsDecision, ) -> Result<(usize, usize), SchedulerError> { let mut total_num_transactions: usize = 0; let mut total_num_retryable: usize = 0; @@ -357,8 +359,12 @@ impl PrioGraphScheduler { ids, transactions, max_ages, + revert_on_error: _, + respond_with_extra_info: _, + schedulable_slot: _, }, retryable_indexes, + extra_info: _, }) => { let num_transactions = ids.len(); let num_retryable = retryable_indexes.len(); @@ -447,6 +453,9 @@ impl PrioGraphScheduler { ids, transactions, max_ages, + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }; self.consume_work_senders[thread_index] .send(work) @@ -887,9 +896,12 @@ mod tests { .send(FinishedConsumeWork { work: thread_0_work.into_iter().next().unwrap(), retryable_indexes: vec![], + extra_info: None, }) .unwrap(); - scheduler.receive_completed(&mut container).unwrap(); + scheduler + .receive_completed(&mut container, &BufferedPacketsDecision::Hold) + .unwrap(); let scheduling_summary = scheduler .schedule(&mut container, test_pre_graph_filter, test_pre_lock_filter) .unwrap(); diff --git a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs index cec0cc78b2..081eb8dfae 100644 --- a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs +++ b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs @@ -157,7 +157,7 @@ impl SanitizedTransactionReceiveAndBuffer { } } - fn buffer_packets( + pub fn buffer_packets( &mut self, container: &mut TransactionStateContainer>, _timing_metrics: &mut SchedulerTimingMetrics, @@ -192,6 +192,7 @@ impl SanitizedTransactionReceiveAndBuffer { chunk .iter() .filter_map(|packet| { + // Check 1 packet .build_sanitized_transaction( vote_only, @@ -202,6 +203,7 @@ impl SanitizedTransactionReceiveAndBuffer { }) .inspect(|_| saturating_add_assign!(post_sanitization_count, 1)) .filter(|(_packet, tx, _deactivation_slot)| { + // Check 2 validate_account_locks( tx.message().account_keys(), transaction_account_lock_limit, @@ -209,6 +211,7 @@ impl SanitizedTransactionReceiveAndBuffer { .is_ok() }) .filter_map(|(packet, tx, deactivation_slot)| { + // Check 3 tx.compute_budget_instruction_details() .sanitize_and_convert_to_compute_budget_limits(&working_bank.feature_set) .map(|compute_budget| { @@ -227,6 +230,7 @@ impl SanitizedTransactionReceiveAndBuffer { fee_budget_limits_vec.push(fee_budget_limits); }); + // Check 4 let check_results = working_bank.check_transactions( &transactions, &lock_results[..transactions.len()], @@ -247,6 +251,7 @@ impl SanitizedTransactionReceiveAndBuffer { .zip(check_results) .filter(|(_, check_result)| check_result.is_ok()) .filter(|((((_, tx), _), _), _)| { + // Check 5 Consumer::check_fee_payer_unlocked(&working_bank, tx, &mut error_counts) .is_ok() }) @@ -643,7 +648,7 @@ impl TransactionViewReceiveAndBuffer { /// from user input. They should never be zero. /// Any difference in the prioritization is negligible for /// the current transaction costs. -fn calculate_priority_and_cost( +pub fn calculate_priority_and_cost( transaction: &impl TransactionWithMeta, fee_budget_limits: &FeeBudgetLimits, bank: &Bank, @@ -680,7 +685,7 @@ fn calculate_priority_and_cost( /// slots, the value used here is the lower-bound on the deactivation /// period, i.e. the transaction's address lookups are valid until /// AT LEAST this slot. -fn calculate_max_age( +pub fn calculate_max_age( sanitized_epoch: Epoch, deactivation_slot: Slot, current_slot: Slot, diff --git a/core/src/banking_stage/transaction_scheduler/scheduler.rs b/core/src/banking_stage/transaction_scheduler/scheduler.rs index 0aacb68630..2ad4d4d485 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler.rs @@ -1,5 +1,6 @@ use { super::{scheduler_error::SchedulerError, transaction_state_container::StateContainer}, + crate::banking_stage::decision_maker::BufferedPacketsDecision, solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, }; @@ -19,6 +20,7 @@ pub(crate) trait Scheduler { fn receive_completed( &mut self, container: &mut impl StateContainer, + decision: &BufferedPacketsDecision, ) -> Result<(usize, usize), SchedulerError>; } diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs index 11f3ae5603..8efb19ebab 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs @@ -30,7 +30,7 @@ use { solana_svm_transaction::svm_message::SVMMessage, std::{ collections::HashSet, - sync::{Arc, RwLock}, + sync::{atomic::AtomicBool, Arc, RwLock}, time::{Duration, Instant}, }, }; @@ -65,6 +65,10 @@ where forwarder: Option>, /// Blacklisted accounts blacklisted_accounts: HashSet, + /// This is the BAM controller. + bam_controller: bool, + /// Whether BAM is enabled. + bam_enabled: Arc, } impl SchedulerController @@ -81,6 +85,8 @@ where worker_metrics: Vec>, forwarder: Option>, blacklisted_accounts: HashSet, + bam_controller: bool, + bam_enabled: Arc, ) -> Self { Self { decision_maker, @@ -94,6 +100,8 @@ where worker_metrics, forwarder, blacklisted_accounts, + bam_controller, + bam_enabled, } } @@ -122,7 +130,7 @@ where self.timing_metrics .maybe_report_and_reset_slot(new_leader_slot); - self.receive_completed()?; + self.receive_completed(&decision)?; self.process_transactions(&decision)?; if self.receive_and_buffer_packets(&decision).is_err() { break; @@ -154,6 +162,10 @@ where let forwarding_enabled = self.forwarder.is_some(); match decision { BufferedPacketsDecision::Consume(bank_start) => { + if !self.scheduling_enabled() { + return Ok(()); + } + let (scheduling_summary, schedule_time_us) = measure_us!(self.scheduler.schedule( &mut self.container, |txs, results| { @@ -350,6 +362,10 @@ where /// Clears the transaction state container. /// This only clears pending transactions, and does **not** clear in-flight transactions. fn clear_container(&mut self) { + if self.bam_controller { + return; + } + let mut num_dropped_on_clear: usize = 0; while let Some(id) = self.container.pop() { self.container.remove_by_id(id.id); @@ -365,6 +381,10 @@ where /// expired, already processed, or are no longer sanitizable. /// This only clears pending transactions, and does **not** clear in-flight transactions. fn clean_queue(&mut self) { + if self.bam_controller { + return; + } + // Clean up any transactions that have already been processed, are too old, or do not have // valid nonce accounts. const MAX_TRANSACTION_CHECKS: usize = 10_000; @@ -429,9 +449,13 @@ where } /// Receives completed transactions from the workers and updates metrics. - fn receive_completed(&mut self) -> Result<(), SchedulerError> { - let ((num_transactions, num_retryable), receive_completed_time_us) = - measure_us!(self.scheduler.receive_completed(&mut self.container)?); + fn receive_completed( + &mut self, + decision: &BufferedPacketsDecision, + ) -> Result<(), SchedulerError> { + let ((num_transactions, num_retryable), receive_completed_time_us) = measure_us!(self + .scheduler + .receive_completed(&mut self.container, decision)?); self.count_metrics.update(|count_metrics| { saturating_add_assign!(count_metrics.num_finished, num_transactions); @@ -465,6 +489,10 @@ where .iter() .any(|a| blacklisted_accounts.contains(a)) } + + fn scheduling_enabled(&self) -> bool { + self.bam_controller == self.bam_enabled.load(std::sync::atomic::Ordering::Relaxed) + } } #[cfg(test)] @@ -609,6 +637,8 @@ mod tests { vec![], // no actual workers with metrics to report, this can be empty None, HashSet::default(), + true, + Arc::new(AtomicBool::new(false)), ); (test_frame, scheduler_controller) @@ -662,7 +692,7 @@ mod tests { .decision_maker .make_consume_or_forward_decision(); assert!(matches!(decision, BufferedPacketsDecision::Consume(_))); - assert!(scheduler_controller.receive_completed().is_ok()); + assert!(scheduler_controller.receive_completed(&decision).is_ok()); // Time is not a reliable way for deterministic testing. // Loop here until no more packets are received, this avoids parallel @@ -695,8 +725,12 @@ mod tests { ids: vec![], transactions: vec![], max_ages: vec![], + revert_on_error: false, + respond_with_extra_info: false, + schedulable_slot: None, }, retryable_indexes: vec![], + extra_info: None, }) .unwrap(); @@ -1030,6 +1064,7 @@ mod tests { .send(FinishedConsumeWork { work: consume_work, retryable_indexes: vec![1], + extra_info: None, }) .unwrap(); diff --git a/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs b/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs index ceca4b5a7e..4434b77a8f 100644 --- a/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs +++ b/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs @@ -8,9 +8,11 @@ use { scheduler_messages::TransactionId, }, agave_transaction_view::resolved_transaction_view::ResolvedTransactionView, - itertools::MinMaxResult, + ahash::{HashMap, HashMapExt}, + itertools::{izip, MinMaxResult}, min_max_heap::MinMaxHeap, slab::{Slab, VacantEntry}, + smallvec::SmallVec, solana_runtime_transaction::{ runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, }, @@ -46,7 +48,19 @@ use { pub(crate) struct TransactionStateContainer { capacity: usize, priority_queue: MinMaxHeap, - id_to_transaction_state: Slab>, + id_to_transaction_state: Slab>, + batch_id_to_transaction_ids: HashMap>, +} + +struct BatchInfo { + batch_id: usize, + revert_on_error: bool, + valid_for_slot: u64, +} + +enum BatchIdOrTransactionState { + Batch(BatchInfo), + TransactionState(TransactionState), } pub(crate) trait StateContainer { @@ -67,6 +81,9 @@ pub(crate) trait StateContainer { /// Panics if the transaction does not exist. fn get_transaction_ttl(&self, id: TransactionId) -> Option<&SanitizedTransactionTTL>; + /// Get the batch id and revert_on_error flag for a transaction. + fn get_batch(&self, id: TransactionId) -> Option<(&SmallVec<[TransactionId; 5]>, bool, u64)>; + /// Retries a transaction - inserts transaction back into map (but not packet). /// This transitions the transaction to `Unprocessed` state. fn retry_transaction( @@ -109,6 +126,7 @@ impl StateContainer for TransactionStateContainer StateContainer for TransactionStateContainer Option<&mut TransactionState> { - self.id_to_transaction_state.get_mut(id) + match self.id_to_transaction_state.get_mut(id) { + Some(BatchIdOrTransactionState::Batch { .. }) => None, + Some(BatchIdOrTransactionState::TransactionState(state)) => Some(state), + None => None, + } } fn get_transaction_ttl(&self, id: TransactionId) -> Option<&SanitizedTransactionTTL> { self.id_to_transaction_state .get(id) - .map(|state| state.transaction_ttl()) + .and_then(|state| match state { + BatchIdOrTransactionState::Batch { .. } => None, + BatchIdOrTransactionState::TransactionState(state) => Some(state.transaction_ttl()), + }) + } + + fn get_batch(&self, id: TransactionId) -> Option<(&SmallVec<[TransactionId; 5]>, bool, u64)> { + let Some(BatchIdOrTransactionState::Batch(batch_info)) = + self.id_to_transaction_state.get(id) + else { + return None; + }; + Some(( + self.batch_id_to_transaction_ids.get(&batch_info.batch_id)?, + batch_info.revert_on_error, + batch_info.valid_for_slot, + )) } fn push_ids_into_queue( @@ -159,7 +197,19 @@ impl StateContainer for TransactionStateContainer MinMaxResult { @@ -184,13 +234,10 @@ impl TransactionStateContainer { cost: u64, ) -> bool { let priority_id = { - let entry = self.get_vacant_map_entry(); + let entry: VacantEntry<'_, BatchIdOrTransactionState> = self.get_vacant_map_entry(); let transaction_id = entry.key(); - entry.insert(TransactionState::new( - transaction_ttl, - Some(packet), - priority, - cost, + entry.insert(BatchIdOrTransactionState::TransactionState( + TransactionState::new(transaction_ttl, Some(packet), priority, cost), )); TransactionPriorityId::new(priority, transaction_id) }; @@ -198,7 +245,47 @@ impl TransactionStateContainer { self.push_ids_into_queue(std::iter::once(priority_id)) > 0 } - fn get_vacant_map_entry(&mut self) -> VacantEntry> { + pub(crate) fn insert_new_batch( + &mut self, + transaction_ttls: Vec>, + packets: Vec>, + priority: u64, + cost: u64, + revert_on_error: bool, + valid_for_slot: u64, + ) -> Option { + let entry = self.get_vacant_map_entry(); + let batch_id = entry.key(); + entry.insert(BatchIdOrTransactionState::Batch(BatchInfo { + batch_id, + revert_on_error, + valid_for_slot, + })); + + let mut transaction_ids = SmallVec::with_capacity(transaction_ttls.len()); + + for (transaction_ttl, packet) in izip!(transaction_ttls, packets) { + let transaction_id = { + let entry = self.get_vacant_map_entry(); + let transaction_id: usize = entry.key(); + entry.insert(BatchIdOrTransactionState::TransactionState( + TransactionState::new(transaction_ttl, Some(packet), priority, cost), + )); + transaction_id + }; + transaction_ids.push(transaction_id); + } + + self.batch_id_to_transaction_ids + .insert(batch_id, transaction_ids); + + self.priority_queue + .push(TransactionPriorityId::new(priority, batch_id)); + + Some(batch_id) + } + + fn get_vacant_map_entry(&mut self) -> VacantEntry> { assert!(self.id_to_transaction_state.len() < self.id_to_transaction_state.capacity()); self.id_to_transaction_state.vacant_entry() } @@ -250,7 +337,7 @@ impl TransactionViewStateContainer { // Attempt to insert the transaction. if let Ok(state) = f(Arc::clone(bytes_entry)) { - vacant_entry.insert(state); + vacant_entry.insert(BatchIdOrTransactionState::TransactionState(state)); Some(transaction_id) } else { None @@ -305,6 +392,11 @@ impl StateContainer for TransactionViewStateContainer { self.inner.push_ids_into_queue(priority_ids) } + #[inline] + fn get_batch(&self, _: TransactionId) -> Option<(&SmallVec<[TransactionId; 5]>, bool, u64)> { + unimplemented!("get_batch not implemented for TransactionViewStateContainer"); + } + #[inline] fn remove_by_id(&mut self, id: TransactionId) { self.inner.remove_by_id(id); @@ -400,7 +492,10 @@ mod tests { container .id_to_transaction_state .iter() - .map(|ts| ts.1.priority()) + .map(|ts| match ts.1 { + BatchIdOrTransactionState::Batch(_) => panic!("unexpected batch id"), + BatchIdOrTransactionState::TransactionState(ref state) => state.priority(), + }) .next() .unwrap(), 4 @@ -501,4 +596,37 @@ mod tests { ); assert!(container.pop().is_none()); } + + #[test] + fn test_batch() { + let mut container = TransactionStateContainer::with_capacity(5); + let mut packets = Vec::with_capacity(5); + let mut transaction_ttls = Vec::with_capacity(5); + for priority in 0..5 { + let (transaction_ttl, packet, _, _) = test_transaction(priority); + packets.push(packet); + transaction_ttls.push(transaction_ttl); + } + + // Insert a batch of transactions. + let batch_id = container.insert_new_batch(transaction_ttls, packets, 10, 100, true, 0); + assert!(batch_id.is_some()); + assert_eq!(container.priority_queue.len(), 1); + assert_eq!(container.id_to_transaction_state.len(), 6); + assert_eq!(container.batch_id_to_transaction_ids.len(), 1); + + // Get the batch id and revert_on_error flag. + let batch_id = batch_id.unwrap(); + let (batch, revert_on_error, slot) = container.get_batch(batch_id).unwrap(); + assert_eq!(batch.len(), 5); + assert!(revert_on_error); + assert_eq!(slot, 0); + + // Remove a batch of transactions. + let batch_id = container.pop().unwrap(); + container.remove_by_id(batch_id.id); + assert_eq!(container.priority_queue.len(), 0); + assert_eq!(container.id_to_transaction_state.len(), 0); + assert!(container.batch_id_to_transaction_ids.is_empty()); + } } diff --git a/core/src/lib.rs b/core/src/lib.rs index 1645f3c05f..b6cca0d587 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -10,6 +10,10 @@ pub mod accounts_hash_verifier; pub mod admin_rpc_post_init; +pub mod bam_connection; +pub mod bam_dependencies; +pub mod bam_manager; +pub mod bam_payment; pub mod banking_simulation; pub mod banking_stage; pub mod banking_trace; diff --git a/core/src/proxy/block_engine_stage.rs b/core/src/proxy/block_engine_stage.rs index bc5c2bc48a..529b6c1b17 100644 --- a/core/src/proxy/block_engine_stage.rs +++ b/core/src/proxy/block_engine_stage.rs @@ -69,6 +69,7 @@ impl BlockEngineStageStats { } } +#[derive(Clone, Default)] pub struct BlockBuilderFeeInfo { pub block_builder: Pubkey, pub block_builder_commission: u64, @@ -100,6 +101,7 @@ impl BlockEngineStage { banking_packet_sender: BankingPacketSender, exit: Arc, block_builder_fee_info: &Arc>, + bam_enabled: Arc, ) -> Self { let block_builder_fee_info = block_builder_fee_info.clone(); @@ -118,6 +120,7 @@ impl BlockEngineStage { banking_packet_sender, exit, block_builder_fee_info, + bam_enabled, )); }) .unwrap(); @@ -143,6 +146,7 @@ impl BlockEngineStage { banking_packet_sender: BankingPacketSender, exit: Arc, block_builder_fee_info: Arc>, + bam_enabled: Arc, ) { const CONNECTION_TIMEOUT: Duration = Duration::from_secs(CONNECTION_TIMEOUT_S); const CONNECTION_BACKOFF: Duration = Duration::from_secs(CONNECTION_BACKOFF_S); @@ -169,6 +173,7 @@ impl BlockEngineStage { &exit, &block_builder_fee_info, &CONNECTION_TIMEOUT, + &bam_enabled, ) .await { @@ -192,6 +197,7 @@ impl BlockEngineStage { } } + #[allow(clippy::too_many_arguments)] async fn connect_auth_and_stream( local_block_engine_config: &BlockEngineConfig, global_block_engine_config: &Arc>, @@ -202,7 +208,13 @@ impl BlockEngineStage { exit: &Arc, block_builder_fee_info: &Arc>, connection_timeout: &Duration, + bam_enabled: &Arc, ) -> crate::proxy::Result<()> { + if bam_enabled.load(Ordering::Relaxed) { + tokio::time::sleep(Duration::from_millis(10)).await; + return Ok(()); + } + // Get a copy of configs here in case they have changed at runtime let keypair = cluster_info.keypair().clone(); @@ -283,6 +295,7 @@ impl BlockEngineStage { connection_timeout, keypair, cluster_info, + bam_enabled, ) .await } @@ -303,6 +316,7 @@ impl BlockEngineStage { connection_timeout: &Duration, keypair: Arc, cluster_info: &Arc, + bam_enabled: &Arc, ) -> crate::proxy::Result<()> { let subscribe_packets_stream = timeout( *connection_timeout, @@ -360,6 +374,7 @@ impl BlockEngineStage { keypair, cluster_info, connection_timeout, + bam_enabled, ) .await } @@ -384,6 +399,7 @@ impl BlockEngineStage { keypair: Arc, cluster_info: &Arc, connection_timeout: &Duration, + bam_enabled: &Arc, ) -> crate::proxy::Result<()> { const METRICS_TICK: Duration = Duration::from_secs(1); const MAINTENANCE_TICK: Duration = Duration::from_secs(10 * 60); @@ -398,6 +414,11 @@ impl BlockEngineStage { info!("connected to packet and bundle stream"); while !exit.load(Ordering::Relaxed) { + if bam_enabled.load(Ordering::Relaxed) { + info!("bam enabled, exiting block engine stage"); + return Ok(()); + } + tokio::select! { maybe_msg = packet_stream.message() => { let resp = maybe_msg?.ok_or(ProxyError::GrpcStreamDisconnected)?; diff --git a/core/src/proxy/fetch_stage_manager.rs b/core/src/proxy/fetch_stage_manager.rs index 0d26c001a7..047acbcc19 100644 --- a/core/src/proxy/fetch_stage_manager.rs +++ b/core/src/proxy/fetch_stage_manager.rs @@ -36,6 +36,7 @@ impl FetchStageManager { // Intercepted packets get piped through here. packet_tx: Sender, exit: Arc, + bam_enabled: Arc, ) -> Self { let t_hdl = Self::start( cluster_info, @@ -43,6 +44,7 @@ impl FetchStageManager { packet_intercept_rx, packet_tx, exit, + bam_enabled, ); Self { t_hdl } @@ -66,6 +68,7 @@ impl FetchStageManager { packet_intercept_rx: Receiver, packet_tx: Sender, exit: Arc, + bam_enabled: Arc, ) -> JoinHandle<()> { Builder::new().name("fetch-stage-manager".into()).spawn(move || { let my_fallback_contact_info = cluster_info.my_contact_info(); @@ -80,7 +83,16 @@ impl FetchStageManager { let metrics_tick = tick(METRICS_CADENCE); let mut packets_forwarded = 0; let mut heartbeats_received = 0; - loop { + while !exit.load(Ordering::Relaxed) { + if bam_enabled.load(Ordering::Relaxed) { + fetch_connected = false; + heartbeat_received = false; + pending_disconnect = false; + while packet_intercept_rx.try_recv().is_ok() {} + std::thread::sleep(Duration::from_millis(100)); + continue; + } + select! { recv(packet_intercept_rx) -> pkt => { match pkt { diff --git a/core/src/tpu.rs b/core/src/tpu.rs index 82e93b8fde..6024224e5a 100644 --- a/core/src/tpu.rs +++ b/core/src/tpu.rs @@ -10,7 +10,9 @@ pub use solana_sdk::net::DEFAULT_TPU_COALESCE; pub use solana_streamer::quic::DEFAULT_MAX_QUIC_CONNECTIONS_PER_PEER as MAX_QUIC_CONNECTIONS_PER_PEER; use { crate::{ - banking_stage::BankingStage, + bam_dependencies::BamDependencies, + bam_manager::BamManager, + banking_stage::{consumer::TipProcessingDependencies, BankingStage}, banking_trace::{Channels, TracerThread}, bundle_stage::{bundle_account_locker::BundleAccountLocker, BundleStage}, cluster_info_vote_listener::{ @@ -31,7 +33,7 @@ use { validator::{BlockProductionMethod, GeneratorConfig, TransactionStructure}, }, bytes::Bytes, - crossbeam_channel::{unbounded, Receiver}, + crossbeam_channel::{bounded, unbounded, Receiver}, solana_client::connection_cache::ConnectionCache, solana_gossip::cluster_info::ClusterInfo, solana_ledger::{ @@ -111,6 +113,7 @@ pub struct Tpu { block_engine_stage: BlockEngineStage, fetch_stage_manager: FetchStageManager, bundle_stage: BundleStage, + bam_manager: BamManager, } impl Tpu { @@ -159,6 +162,7 @@ impl Tpu { tip_manager_config: TipManagerConfig, shred_receiver_address: Arc>>, preallocated_bundle_cost: u64, + bam_url: Arc>>, ) -> (Self, Vec>) { let TpuSockets { transactions: transactions_sockets, @@ -280,6 +284,9 @@ impl Tpu { block_builder_commission: 0, })); + // Will be set to false by BAMManager if BAM cannot be connected to + let bam_enabled = Arc::new(AtomicBool::new(true)); + let (bundle_sender, bundle_receiver) = unbounded(); let block_engine_stage = BlockEngineStage::new( block_engine_config, @@ -289,6 +296,7 @@ impl Tpu { non_vote_sender.clone(), exit.clone(), &block_builder_fee_info, + bam_enabled.clone(), ); let (heartbeat_tx, heartbeat_rx) = unbounded(); @@ -298,6 +306,7 @@ impl Tpu { packet_intercept_receiver, packet_sender.clone(), exit.clone(), + bam_enabled.clone(), ); let relayer_stage = RelayerStage::new( @@ -338,6 +347,19 @@ impl Tpu { .saturating_mul(8) .saturating_div(10); + let (bam_batch_sender, bam_batch_receiver) = bounded(100_000); + let (bam_outbound_sender, bam_outbound_receiver) = bounded(100_000); + let bam_dependencies = BamDependencies { + bam_enabled: bam_enabled.clone(), + batch_sender: bam_batch_sender, + batch_receiver: bam_batch_receiver, + outbound_sender: bam_outbound_sender, + outbound_receiver: bam_outbound_receiver, + cluster_info: cluster_info.clone(), + block_builder_fee_info: Arc::new(Mutex::new(BlockBuilderFeeInfo::default())), + bam_node_pubkey: Arc::new(Mutex::new(Pubkey::default())), + }; + let mut blacklisted_accounts = HashSet::new(); blacklisted_accounts.insert(tip_manager.tip_payment_program_id()); let banking_stage = BankingStage::new( @@ -364,6 +386,13 @@ impl Tpu { preallocated_bundle_cost, ) }, + Some(TipProcessingDependencies { + tip_manager: tip_manager.clone(), + last_tip_updated_slot: Arc::new(Mutex::new(0)), + block_builder_fee_info: bam_dependencies.block_builder_fee_info.clone(), + cluster_info: cluster_info.clone(), + }), + Some(bam_dependencies.clone()), ); let bundle_stage = BundleStage::new( @@ -380,6 +409,13 @@ impl Tpu { prioritization_fee_cache, ); + let bam_manager = BamManager::new( + exit.clone(), + bam_url, + bam_dependencies, + poh_recorder.clone(), + ); + let (entry_receiver, tpu_entry_notifier) = if let Some(entry_notification_sender) = entry_notification_sender { let (broadcast_entry_sender, broadcast_entry_receiver) = unbounded(); @@ -425,6 +461,7 @@ impl Tpu { relayer_stage, fetch_stage_manager, bundle_stage, + bam_manager, }, vec![key_updater, forwards_key_updater, vote_streamer_key_updater], ) @@ -445,6 +482,7 @@ impl Tpu { self.relayer_stage.join(), self.block_engine_stage.join(), self.fetch_stage_manager.join(), + self.bam_manager.join(), ]; let broadcast_result = self.broadcast_stage.join(); for result in results { diff --git a/core/src/validator.rs b/core/src/validator.rs index b6ad1234b4..55ecece245 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -329,6 +329,7 @@ pub struct ValidatorConfig { pub shred_retransmit_receiver_address: Arc>>, pub tip_manager_config: TipManagerConfig, pub preallocated_bundle_cost: u64, + pub bam_url: Arc>>, } impl Default for ValidatorConfig { @@ -408,6 +409,7 @@ impl Default for ValidatorConfig { shred_retransmit_receiver_address: Arc::new(RwLock::new(None)), tip_manager_config: TipManagerConfig::default(), preallocated_bundle_cost: 0, + bam_url: Arc::new(Mutex::new(None)), } } } @@ -1642,6 +1644,7 @@ impl Validator { config.tip_manager_config.clone(), config.shred_receiver_address.clone(), config.preallocated_bundle_cost, + config.bam_url.clone(), ); datapoint_info!( diff --git a/docs/package-lock.json b/docs/package-lock.json index 13c8266148..6aebb4f7fa 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -9179,7 +9179,7 @@ "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "integrity": "sha512-Jo6dJ04CmSjuznwBAM3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "dependencies": { "yallist": "^4.0.0" }, @@ -13707,7 +13707,7 @@ "node_modules/vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "integrity": "sha512-DbamxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", "dependencies": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" @@ -20972,7 +20972,7 @@ "lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "integrity": "sha512-Jo6dJ04CmSjuznwBAM3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", "requires": { "yallist": "^4.0.0" } @@ -24168,7 +24168,7 @@ "vfile-message": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", + "integrity": "sha512-DbamxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", "requires": { "@types/unist": "^2.0.0", "unist-util-stringify-position": "^2.0.0" diff --git a/jito-protos/bam-protos b/jito-protos/bam-protos new file mode 160000 index 0000000000..fb618ad5ac --- /dev/null +++ b/jito-protos/bam-protos @@ -0,0 +1 @@ +Subproject commit fb618ad5ac3441eb45858a1179431dd75964c9bf diff --git a/jito-protos/build.rs b/jito-protos/build.rs index 30ece1620a..2e660824ea 100644 --- a/jito-protos/build.rs +++ b/jito-protos/build.rs @@ -23,6 +23,14 @@ fn main() -> Result<(), std::io::Error> { protos.push(proto); } + let proto_base_path_jds = std::path::PathBuf::from("bam-protos"); + let proto_files = ["bam_api.proto", "bam_types.proto"]; + for proto_file in &proto_files { + let proto = proto_base_path_jds.join(proto_file); + println!("cargo:rerun-if-changed={}", proto.display()); + protos.push(proto); + } + configure() .build_client(true) .build_server(false) @@ -34,5 +42,5 @@ fn main() -> Result<(), std::io::Error> { "InstructionErrorType", "#[cfg_attr(test, derive(enum_iterator::Sequence))]", ) - .compile(&protos, &[proto_base_path]) + .compile(&protos, &[proto_base_path, proto_base_path_jds]) } diff --git a/jito-protos/src/lib.rs b/jito-protos/src/lib.rs index cf630c53d2..acd163c99c 100644 --- a/jito-protos/src/lib.rs +++ b/jito-protos/src/lib.rs @@ -22,4 +22,12 @@ pub mod proto { pub mod shared { tonic::include_proto!("shared"); } + + pub mod bam_api { + tonic::include_proto!("bam_api"); + } + + pub mod bam_types { + tonic::include_proto!("bam_types"); + } } diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 5005cc214a..932f60796b 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -503,7 +503,7 @@ fn schedule_batches_for_execution( } in locked_entries { // unlock before sending to scheduler. - bank.unlock_accounts(transactions.iter().zip(lock_results.iter())); + bank.unlock_accounts(transactions.iter().zip(lock_results.iter()), false); // give ownership to scheduler. capture the first error, but continue the loop // to unlock. // scheduling is skipped if we have already detected an error in this loop @@ -809,7 +809,7 @@ fn queue_batches_with_lock_retry( // We need to unlock the transactions that succeeded to lock before the // retry. - bank.unlock_accounts(transactions.iter().zip(lock_results.iter())); + bank.unlock_accounts(transactions.iter().zip(lock_results.iter()), false); // We failed to lock, there are 2 possible reasons: // 1. A batch already in `batches` holds the lock. @@ -832,7 +832,7 @@ fn queue_batches_with_lock_retry( } Err(err) => { // We still may have succeeded to lock some accounts, unlock them. - bank.unlock_accounts(transactions.iter().zip(lock_results.iter())); + bank.unlock_accounts(transactions.iter().zip(lock_results.iter()), false); // An entry has account lock conflicts with *itself*, which should not happen // if generated by a properly functioning leader diff --git a/local-cluster/src/validator_configs.rs b/local-cluster/src/validator_configs.rs index 059bb81982..f4dcd993aa 100644 --- a/local-cluster/src/validator_configs.rs +++ b/local-cluster/src/validator_configs.rs @@ -80,6 +80,7 @@ pub fn safe_clone_config(config: &ValidatorConfig) -> ValidatorConfig { shred_retransmit_receiver_address: config.shred_retransmit_receiver_address.clone(), tip_manager_config: config.tip_manager_config.clone(), preallocated_bundle_cost: config.preallocated_bundle_cost, + bam_url: config.bam_url.clone(), } } diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index 32380771b2..73b3aaa313 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -74,7 +74,7 @@ pub struct BankStart { } impl BankStart { - fn get_working_bank_if_not_expired(&self) -> Option<&Bank> { + pub fn get_working_bank_if_not_expired(&self) -> Option<&Bank> { if self.should_working_bank_still_be_processing_txs() { Some(&self.working_bank) } else { @@ -377,6 +377,10 @@ impl PohRecorder { tick_height.saturating_sub(1) / self.ticks_per_slot } + pub fn get_current_slot(&self) -> Slot { + self.slot_for_tick_height(self.tick_height) + } + pub fn leader_after_n_slots(&self, slots: u64) -> Option { let current_slot = self.slot_for_tick_height(self.tick_height); self.leader_schedule_cache @@ -1167,6 +1171,10 @@ impl PohRecorder { let bank = Arc::new(Bank::new_for_tests(&genesis_config)); self.reset(bank, None); } + + pub fn get_blockstore(&self) -> Arc { + self.blockstore.clone() + } } fn do_create_test_recorder( diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index dfccd82440..13016213a7 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6071,6 +6071,7 @@ dependencies = [ "serde_bytes", "serde_derive", "slab", + "smallvec", "solana-accounts-db", "solana-bloom", "solana-builtins-default-costs", @@ -6117,6 +6118,7 @@ dependencies = [ "solana-vote", "solana-vote-program", "solana-wen-restart", + "spl-memo", "strum", "strum_macros", "sys-info", diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 1138114276..70eb99db79 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -3191,6 +3191,7 @@ impl Bank { transaction_results: impl Iterator>, additional_read_locks: Option<&HashSet>, additional_write_locks: Option<&HashSet>, + batched_locking: bool, ) -> TransactionBatch<'a, 'b, Tx> { // this lock_results could be: Ok, AccountInUse, WouldExceedBlockMaxLimit or WouldExceedAccountMaxLimit let tx_account_lock_limit = self.get_transaction_account_lock_limit(); @@ -3200,8 +3201,17 @@ impl Bank { tx_account_lock_limit, additional_read_locks, additional_write_locks, + batched_locking, ); - TransactionBatch::new(lock_results, self, OwnedOrBorrowed::Borrowed(transactions)) + if batched_locking { + TransactionBatch::new_batched( + lock_results, + self, + OwnedOrBorrowed::Borrowed(transactions), + ) + } else { + TransactionBatch::new(lock_results, self, OwnedOrBorrowed::Borrowed(transactions)) + } } /// Prepare a locked transaction batch from a list of sanitized transactions, and their cost @@ -3401,8 +3411,9 @@ impl Bank { pub fn unlock_accounts<'a, Tx: SVMMessage + 'a>( &self, txs_and_results: impl Iterator)> + Clone, + batched: bool, ) { - self.rc.accounts.unlock_accounts(txs_and_results) + self.rc.accounts.unlock_accounts(txs_and_results, batched); } pub fn remove_unrooted_slots(&self, slots: &[(Slot, BankId)]) { diff --git a/runtime/src/transaction_batch.rs b/runtime/src/transaction_batch.rs index 4b7dd2740e..9c005e4d49 100644 --- a/runtime/src/transaction_batch.rs +++ b/runtime/src/transaction_batch.rs @@ -25,6 +25,7 @@ pub struct TransactionBatch<'a, 'b, Tx: SVMMessage> { bank: &'a Bank, sanitized_txs: OwnedOrBorrowed<'b, Tx>, needs_unlock: bool, + batched_locking: bool, } impl<'a, 'b, Tx: SVMMessage> TransactionBatch<'a, 'b, Tx> { @@ -39,6 +40,22 @@ impl<'a, 'b, Tx: SVMMessage> TransactionBatch<'a, 'b, Tx> { bank, sanitized_txs, needs_unlock: true, + batched_locking: false, + } + } + + pub fn new_batched( + lock_results: Vec>, + bank: &'a Bank, + sanitized_txs: OwnedOrBorrowed<'b, Tx>, + ) -> Self { + assert_eq!(lock_results.len(), sanitized_txs.len()); + Self { + lock_results, + bank, + sanitized_txs, + needs_unlock: true, + batched_locking: true, } } @@ -87,7 +104,8 @@ impl<'a, 'b, Tx: SVMMessage> TransactionBatch<'a, 'b, Tx> { // Unlock the accounts for all transactions which will be updated to an // lock error below. - self.bank.unlock_accounts(txs_and_results); + self.bank + .unlock_accounts(txs_and_results, self.batched_locking); // Record all new errors by overwriting lock results. Note that it's // not valid to update from err -> ok and the assertion above enforces @@ -105,6 +123,7 @@ impl Drop for TransactionBatch<'_, '_, Tx> { self.sanitized_transactions() .iter() .zip(self.lock_results()), + self.batched_locking, ) } } diff --git a/svm/examples/Cargo.lock b/svm/examples/Cargo.lock index 451a3ae844..1bafb88627 100644 --- a/svm/examples/Cargo.lock +++ b/svm/examples/Cargo.lock @@ -5946,6 +5946,7 @@ dependencies = [ "serde_bytes", "serde_derive", "slab", + "smallvec", "solana-accounts-db", "solana-bloom", "solana-builtins-default-costs", @@ -5992,6 +5993,7 @@ dependencies = [ "solana-vote", "solana-vote-program", "solana-wen-restart", + "spl-memo", "strum", "strum_macros", "sys-info", diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index dc501bc5a7..4eca1157ae 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -65,7 +65,7 @@ use { net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, str::FromStr, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock}, time::Duration, }, tokio::time::sleep, @@ -136,6 +136,7 @@ pub struct TestValidatorGenesis { pub tpu_enable_udp: bool, pub geyser_plugin_manager: Arc>, admin_rpc_service_post_init: Arc>>, + pub bam_url: Arc>>, } impl Default for TestValidatorGenesis { @@ -169,6 +170,7 @@ impl Default for TestValidatorGenesis { geyser_plugin_manager: Arc::new(RwLock::new(GeyserPluginManager::new())), admin_rpc_service_post_init: Arc::>>::default(), + bam_url: Arc::new(Mutex::new(None)), } } } @@ -1032,6 +1034,7 @@ impl TestValidator { staked_nodes_overrides: config.staked_nodes_overrides.clone(), accounts_db_config, runtime_config, + bam_url: config.bam_url.clone(), ..ValidatorConfig::default_for_test() }; if let Some(ref tower_storage) = config.tower_storage { diff --git a/tip-distributor/Cargo.toml b/tip-distributor/Cargo.toml deleted file mode 100644 index c91a3b1b2d..0000000000 --- a/tip-distributor/Cargo.toml +++ /dev/null @@ -1,61 +0,0 @@ -[package] -name = "solana-tip-distributor" -version = { workspace = true } -edition = { workspace = true } -license = { workspace = true } -description = "Collection of binaries used to distribute MEV rewards to delegators and validators." -publish = false - -[[bin]] -name = "solana-stake-meta-generator" -path = "src/bin/stake-meta-generator.rs" - -[[bin]] -name = "solana-merkle-root-generator" -path = "src/bin/merkle-root-generator.rs" - -[[bin]] -name = "solana-merkle-root-uploader" -path = "src/bin/merkle-root-uploader.rs" - -[[bin]] -name = "solana-claim-mev-tips" -path = "src/bin/claim-mev-tips.rs" - -[dependencies] -anchor-lang = { workspace = true } -clap = { version = "4.1.11", features = ["derive", "env"] } -crossbeam-channel = { workspace = true } -env_logger = { workspace = true } -futures = { workspace = true } -gethostname = { workspace = true } -im = { workspace = true } -itertools = { workspace = true } -jito-tip-distribution = { workspace = true } -jito-tip-payment = { workspace = true } -log = { workspace = true } -num-traits = { workspace = true } -rand = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -solana-accounts-db = { workspace = true } -solana-client = { workspace = true } -solana-genesis-utils = { workspace = true } -solana-ledger = { workspace = true } -solana-measure = { workspace = true } -solana-merkle-tree = { workspace = true } -solana-metrics = { workspace = true } -solana-program = { workspace = true } -solana-program-runtime = { workspace = true } -solana-rpc-client-api = { workspace = true } -solana-runtime = { workspace = true } -solana-sdk = { workspace = true } -solana-stake-program = { workspace = true } -solana-transaction-status = { workspace = true } -solana-vote = { workspace = true } -thiserror = { workspace = true } -tokio = { workspace = true, features = ["rt-multi-thread", "macros"] } - -[dev-dependencies] -solana-runtime = { workspace = true, features = ["dev-context-only-utils"] } -solana-sdk = { workspace = true, features = ["dev-context-only-utils"] } diff --git a/tip-distributor/src/stake_meta_generator_workflow.rs b/tip-distributor/src/stake_meta_generator_workflow.rs deleted file mode 100644 index d06c48983e..0000000000 --- a/tip-distributor/src/stake_meta_generator_workflow.rs +++ /dev/null @@ -1,976 +0,0 @@ -use { - crate::{ - derive_tip_distribution_account_address, derive_tip_payment_pubkeys, Config, StakeMeta, - StakeMetaCollection, TipDistributionAccount, TipDistributionAccountWrapper, - TipDistributionMeta, - }, - anchor_lang::AccountDeserialize, - itertools::Itertools, - log::*, - solana_accounts_db::hardened_unpack::{ - open_genesis_config, OpenGenesisConfigError, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE, - }, - solana_client::client_error::ClientError, - solana_ledger::{ - bank_forks_utils::{self, BankForksUtilsError}, - blockstore::{ - default_num_compaction_threads, default_num_flush_threads, Blockstore, BlockstoreError, - }, - blockstore_options::{AccessType, BlockstoreOptions, LedgerColumnOptions}, - blockstore_processor::{BlockstoreProcessorError, ProcessOptions}, - }, - solana_program::{stake_history::StakeHistory, sysvar}, - solana_runtime::{bank::Bank, snapshot_config::SnapshotConfig, stakes::StakeAccount}, - solana_sdk::{ - account::{from_account, ReadableAccount, WritableAccount}, - clock::Slot, - pubkey::Pubkey, - }, - solana_vote::vote_account::VoteAccount, - std::{ - collections::HashMap, - fmt::{Debug, Display, Formatter}, - fs::File, - io::{BufWriter, Write}, - mem::size_of, - path::{Path, PathBuf}, - sync::{atomic::AtomicBool, Arc}, - }, - thiserror::Error, -}; - -#[derive(Error, Debug)] -pub enum StakeMetaGeneratorError { - #[error(transparent)] - AnchorError(#[from] Box), - - #[error(transparent)] - BlockstoreError(#[from] BlockstoreError), - - #[error(transparent)] - BlockstoreProcessorError(#[from] BlockstoreProcessorError), - - #[error(transparent)] - IoError(#[from] std::io::Error), - - CheckedMathError, - - #[error(transparent)] - RpcError(#[from] ClientError), - - #[error(transparent)] - SerdeJsonError(#[from] serde_json::Error), - - SnapshotSlotNotFound, - - BankForksUtilsError(#[from] BankForksUtilsError), - - GenesisConfigError(#[from] OpenGenesisConfigError), -} - -impl Display for StakeMetaGeneratorError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - Debug::fmt(&self, f) - } -} - -/// Runs the entire workflow of creating a bank from a snapshot to writing stake meta-data -/// to a JSON file. -pub fn generate_stake_meta( - ledger_path: &Path, - snapshot_slot: &Slot, - tip_distribution_program_id: &Pubkey, - out_path: &str, - tip_payment_program_id: &Pubkey, -) -> Result<(), StakeMetaGeneratorError> { - info!("Creating bank from ledger path..."); - let bank = create_bank_from_snapshot(ledger_path, snapshot_slot)?; - - info!("Generating stake_meta_collection object..."); - let stake_meta_coll = - generate_stake_meta_collection(&bank, tip_distribution_program_id, tip_payment_program_id)?; - - info!("Writing stake_meta_collection to JSON {}...", out_path); - write_to_json_file(&stake_meta_coll, out_path)?; - - Ok(()) -} - -fn create_bank_from_snapshot( - ledger_path: &Path, - snapshot_slot: &Slot, -) -> Result, StakeMetaGeneratorError> { - let genesis_config = open_genesis_config(ledger_path, MAX_GENESIS_ARCHIVE_UNPACKED_SIZE)?; - let snapshot_config = SnapshotConfig { - full_snapshot_archive_interval_slots: Slot::MAX, - incremental_snapshot_archive_interval_slots: Slot::MAX, - full_snapshot_archives_dir: PathBuf::from(ledger_path), - incremental_snapshot_archives_dir: PathBuf::from(ledger_path), - bank_snapshots_dir: PathBuf::from(ledger_path), - ..SnapshotConfig::default() - }; - let blockstore = Blockstore::open_with_options( - ledger_path, - BlockstoreOptions { - access_type: AccessType::PrimaryForMaintenance, - recovery_mode: None, - enforce_ulimit_nofile: false, - column_options: LedgerColumnOptions::default(), - num_rocksdb_compaction_threads: default_num_compaction_threads(), - num_rocksdb_flush_threads: default_num_flush_threads(), - }, - )?; - let (bank_forks, _, _) = bank_forks_utils::load_bank_forks( - &genesis_config, - &blockstore, - vec![PathBuf::from(ledger_path).join(Path::new("stake-meta.accounts"))], - Some(&snapshot_config), - &ProcessOptions::default(), - None, - None, - None, - Arc::new(AtomicBool::new(false)), - false, - )?; - - let working_bank = bank_forks.read().unwrap().working_bank(); - assert_eq!( - working_bank.slot(), - *snapshot_slot, - "expected working bank slot {}, found {}", - snapshot_slot, - working_bank.slot() - ); - - Ok(working_bank) -} - -fn write_to_json_file( - stake_meta_coll: &StakeMetaCollection, - out_path: &str, -) -> Result<(), StakeMetaGeneratorError> { - let file = File::create(out_path)?; - let mut writer = BufWriter::new(file); - let json = serde_json::to_string_pretty(&stake_meta_coll).unwrap(); - writer.write_all(json.as_bytes())?; - writer.flush()?; - - Ok(()) -} - -/// Creates a collection of [StakeMeta]'s from the given bank. -pub fn generate_stake_meta_collection( - bank: &Arc, - tip_distribution_program_id: &Pubkey, - tip_payment_program_id: &Pubkey, -) -> Result { - assert!(bank.is_frozen()); - - let epoch_vote_accounts = bank.epoch_vote_accounts(bank.epoch()).unwrap_or_else(|| { - panic!( - "No epoch_vote_accounts found for slot {} at epoch {}", - bank.slot(), - bank.epoch() - ) - }); - - let l_stakes = bank.stakes_cache.stakes(); - let delegations = l_stakes.stake_delegations(); - - let voter_pubkey_to_delegations = group_delegations_by_voter_pubkey(delegations, bank); - - // the last leader in an epoch may not crank the tip program before the epoch is over, which - // would result in MEV rewards for epoch N not being cranked until epoch N + 1. This means that - // the account balance in the snapshot could be incorrect. - // We assume that the rewards sitting in the tip program PDAs are cranked out by the time all of - // the rewards are claimed. - let tip_accounts = derive_tip_payment_pubkeys(tip_payment_program_id); - let account = bank - .get_account(&tip_accounts.config_pda) - .expect("config pda exists"); - - let config = Config::try_deserialize(&mut account.data()).expect("deserializes configuration"); - - let bb_commission_pct: u64 = config.block_builder_commission_pct; - let tip_receiver: Pubkey = config.tip_receiver; - - // includes the block builder fee - let excess_tip_balances: u64 = tip_accounts - .tip_pdas - .iter() - .map(|pubkey| { - let tip_account = bank.get_account(pubkey).expect("tip account exists"); - tip_account - .lamports() - .checked_sub(bank.get_minimum_balance_for_rent_exemption(tip_account.data().len())) - .expect("tip balance underflow") - }) - .sum(); - // matches math in tip payment program - let block_builder_tips = excess_tip_balances - .checked_mul(bb_commission_pct) - .expect("block_builder_tips overflow") - .checked_div(100) - .expect("block_builder_tips division error"); - let tip_receiver_fee = excess_tip_balances - .checked_sub(block_builder_tips) - .expect("tip_receiver_fee doesnt underflow"); - - let vote_pk_and_maybe_tdas: Vec<( - (Pubkey, &VoteAccount), - Option, - )> = epoch_vote_accounts - .iter() - .map(|(vote_pubkey, (_total_stake, vote_account))| { - let tip_distribution_pubkey = derive_tip_distribution_account_address( - tip_distribution_program_id, - vote_pubkey, - bank.epoch(), - ) - .0; - let tda = if let Some(mut account_data) = bank.get_account(&tip_distribution_pubkey) { - // TDAs may be funded with lamports and therefore exist in the bank, but would fail the deserialization step - // if the buffer is yet to be allocated thru the init call to the program. - if let Ok(tip_distribution_account) = - TipDistributionAccount::try_deserialize(&mut account_data.data()) - { - // this snapshot might have tips that weren't claimed by the time the epoch is over - // assume that it will eventually be cranked and credit the excess to this account - if tip_distribution_pubkey == tip_receiver { - account_data.set_lamports( - account_data - .lamports() - .checked_add(tip_receiver_fee) - .expect("tip overflow"), - ); - } - Some(TipDistributionAccountWrapper { - tip_distribution_account, - account_data, - tip_distribution_pubkey, - }) - } else { - None - } - } else { - None - }; - Ok(((*vote_pubkey, vote_account), tda)) - }) - .collect::>()?; - - let mut stake_metas = vec![]; - for ((vote_pubkey, vote_account), maybe_tda) in vote_pk_and_maybe_tdas { - if let Some(mut delegations) = voter_pubkey_to_delegations.get(&vote_pubkey).cloned() { - let total_delegated = delegations.iter().fold(0u64, |sum, delegation| { - sum.checked_add(delegation.lamports_delegated).unwrap() - }); - - let maybe_tip_distribution_meta = if let Some(tda) = maybe_tda { - let actual_len = tda.account_data.data().len(); - let expected_len = 8_usize.saturating_add(size_of::()); - if actual_len != expected_len { - warn!("len mismatch actual={actual_len}, expected={expected_len}"); - } - let rent_exempt_amount = - bank.get_minimum_balance_for_rent_exemption(tda.account_data.data().len()); - - Some(TipDistributionMeta::from_tda_wrapper( - tda, - rent_exempt_amount, - )?) - } else { - None - }; - - let vote_state = vote_account.vote_state(); - delegations.sort(); - stake_metas.push(StakeMeta { - maybe_tip_distribution_meta, - validator_node_pubkey: vote_state.node_pubkey, - validator_vote_account: vote_pubkey, - delegations, - total_delegated, - commission: vote_state.commission, - }); - } else { - warn!( - "voter_pubkey not found in voter_pubkey_to_delegations map [validator_vote_pubkey={}]", - vote_pubkey - ); - } - } - stake_metas.sort(); - - Ok(StakeMetaCollection { - stake_metas, - tip_distribution_program_id: *tip_distribution_program_id, - bank_hash: bank.hash().to_string(), - epoch: bank.epoch(), - slot: bank.slot(), - }) -} - -/// Given an [EpochStakes] object, return delegations grouped by voter_pubkey (validator delegated to). -fn group_delegations_by_voter_pubkey( - delegations: &im::HashMap, - bank: &Bank, -) -> HashMap> { - delegations - .into_iter() - .filter(|(_stake_pubkey, stake_account)| { - stake_account.delegation().stake( - bank.epoch(), - &from_account::( - &bank.get_account(&sysvar::stake_history::id()).unwrap(), - ) - .unwrap(), - bank.new_warmup_cooldown_rate_epoch(), - ) > 0 - }) - .into_group_map_by(|(_stake_pubkey, stake_account)| stake_account.delegation().voter_pubkey) - .into_iter() - .map(|(voter_pubkey, group)| { - ( - voter_pubkey, - group - .into_iter() - .map(|(stake_pubkey, stake_account)| crate::Delegation { - stake_account_pubkey: *stake_pubkey, - staker_pubkey: stake_account - .stake_state() - .authorized() - .map(|a| a.staker) - .unwrap_or_default(), - withdrawer_pubkey: stake_account - .stake_state() - .authorized() - .map(|a| a.withdrawer) - .unwrap_or_default(), - lamports_delegated: stake_account.delegation().stake, - }) - .collect::>(), - ) - }) - .collect() -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::derive_tip_distribution_account_address, - anchor_lang::AccountSerialize, - jito_tip_distribution::state::TipDistributionAccount, - jito_tip_payment::{ - InitBumps, TipPaymentAccount, CONFIG_ACCOUNT_SEED, TIP_ACCOUNT_SEED_0, - TIP_ACCOUNT_SEED_1, TIP_ACCOUNT_SEED_2, TIP_ACCOUNT_SEED_3, TIP_ACCOUNT_SEED_4, - TIP_ACCOUNT_SEED_5, TIP_ACCOUNT_SEED_6, TIP_ACCOUNT_SEED_7, - }, - solana_runtime::genesis_utils::{ - create_genesis_config_with_vote_accounts, GenesisConfigInfo, ValidatorVoteKeypairs, - }, - solana_sdk::{ - self, - account::{from_account, AccountSharedData}, - message::Message, - signature::{Keypair, Signer}, - stake::{ - self, - state::{Authorized, Lockup}, - }, - stake_history::StakeHistory, - sysvar, - transaction::Transaction, - }, - solana_stake_program::stake_state, - }; - - #[test] - fn test_generate_stake_meta_collection_happy_path() { - /* 1. Create a Bank seeded with some validator stake accounts */ - let validator_keypairs_0 = ValidatorVoteKeypairs::new_rand(); - let validator_keypairs_1 = ValidatorVoteKeypairs::new_rand(); - let validator_keypairs_2 = ValidatorVoteKeypairs::new_rand(); - let validator_keypairs = vec![ - &validator_keypairs_0, - &validator_keypairs_1, - &validator_keypairs_2, - ]; - const INITIAL_VALIDATOR_STAKES: u64 = 10_000; - let GenesisConfigInfo { genesis_config, .. } = create_genesis_config_with_vote_accounts( - 1_000_000_000, - &validator_keypairs, - vec![INITIAL_VALIDATOR_STAKES; 3], - ); - - let (mut bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - - /* 2. Seed the Bank with [TipDistributionAccount]'s */ - let merkle_root_upload_authority = Pubkey::new_unique(); - let tip_distribution_program_id = Pubkey::new_unique(); - let tip_payment_program_id = Pubkey::new_unique(); - - let delegator_0 = Keypair::new(); - let delegator_1 = Keypair::new(); - let delegator_2 = Keypair::new(); - let delegator_3 = Keypair::new(); - let delegator_4 = Keypair::new(); - - let delegator_0_pk = delegator_0.pubkey(); - let delegator_1_pk = delegator_1.pubkey(); - let delegator_2_pk = delegator_2.pubkey(); - let delegator_3_pk = delegator_3.pubkey(); - let delegator_4_pk = delegator_4.pubkey(); - - let d_0_data = AccountSharedData::new( - 300_000_000_000_000 * 10, - 0, - &solana_sdk::system_program::id(), - ); - let d_1_data = AccountSharedData::new( - 100_000_203_000_000 * 10, - 0, - &solana_sdk::system_program::id(), - ); - let d_2_data = AccountSharedData::new( - 100_000_235_899_000 * 10, - 0, - &solana_sdk::system_program::id(), - ); - let d_3_data = AccountSharedData::new( - 200_000_000_000_000 * 10, - 0, - &solana_sdk::system_program::id(), - ); - let d_4_data = AccountSharedData::new( - 100_000_000_777_000 * 10, - 0, - &solana_sdk::system_program::id(), - ); - - bank.store_account(&delegator_0_pk, &d_0_data); - bank.store_account(&delegator_1_pk, &d_1_data); - bank.store_account(&delegator_2_pk, &d_2_data); - bank.store_account(&delegator_3_pk, &d_3_data); - bank.store_account(&delegator_4_pk, &d_4_data); - - /* 3. Delegate some stake to the initial set of validators */ - let mut validator_0_delegations = vec![crate::Delegation { - stake_account_pubkey: validator_keypairs_0.stake_keypair.pubkey(), - staker_pubkey: validator_keypairs_0.stake_keypair.pubkey(), - withdrawer_pubkey: validator_keypairs_0.stake_keypair.pubkey(), - lamports_delegated: INITIAL_VALIDATOR_STAKES, - }]; - let stake_account = delegate_stake_helper( - &bank, - &delegator_0, - &validator_keypairs_0.vote_keypair.pubkey(), - 30_000_000_000, - ); - validator_0_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_0.pubkey(), - withdrawer_pubkey: delegator_0.pubkey(), - lamports_delegated: 30_000_000_000, - }); - let stake_account = delegate_stake_helper( - &bank, - &delegator_1, - &validator_keypairs_0.vote_keypair.pubkey(), - 3_000_000_000, - ); - validator_0_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_1.pubkey(), - withdrawer_pubkey: delegator_1.pubkey(), - lamports_delegated: 3_000_000_000, - }); - let stake_account = delegate_stake_helper( - &bank, - &delegator_2, - &validator_keypairs_0.vote_keypair.pubkey(), - 33_000_000_000, - ); - validator_0_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_2.pubkey(), - withdrawer_pubkey: delegator_2.pubkey(), - lamports_delegated: 33_000_000_000, - }); - - let mut validator_1_delegations = vec![crate::Delegation { - stake_account_pubkey: validator_keypairs_1.stake_keypair.pubkey(), - staker_pubkey: validator_keypairs_1.stake_keypair.pubkey(), - withdrawer_pubkey: validator_keypairs_1.stake_keypair.pubkey(), - lamports_delegated: INITIAL_VALIDATOR_STAKES, - }]; - let stake_account = delegate_stake_helper( - &bank, - &delegator_3, - &validator_keypairs_1.vote_keypair.pubkey(), - 4_222_364_000, - ); - validator_1_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_3.pubkey(), - withdrawer_pubkey: delegator_3.pubkey(), - lamports_delegated: 4_222_364_000, - }); - let stake_account = delegate_stake_helper( - &bank, - &delegator_4, - &validator_keypairs_1.vote_keypair.pubkey(), - 6_000_000_527, - ); - validator_1_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_4.pubkey(), - withdrawer_pubkey: delegator_4.pubkey(), - lamports_delegated: 6_000_000_527, - }); - - let mut validator_2_delegations = vec![crate::Delegation { - stake_account_pubkey: validator_keypairs_2.stake_keypair.pubkey(), - staker_pubkey: validator_keypairs_2.stake_keypair.pubkey(), - withdrawer_pubkey: validator_keypairs_2.stake_keypair.pubkey(), - lamports_delegated: INITIAL_VALIDATOR_STAKES, - }]; - let stake_account = delegate_stake_helper( - &bank, - &delegator_0, - &validator_keypairs_2.vote_keypair.pubkey(), - 1_300_123_156, - ); - validator_2_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_0.pubkey(), - withdrawer_pubkey: delegator_0.pubkey(), - lamports_delegated: 1_300_123_156, - }); - let stake_account = delegate_stake_helper( - &bank, - &delegator_4, - &validator_keypairs_2.vote_keypair.pubkey(), - 1_610_565_420, - ); - validator_2_delegations.push(crate::Delegation { - stake_account_pubkey: stake_account, - staker_pubkey: delegator_4.pubkey(), - withdrawer_pubkey: delegator_4.pubkey(), - lamports_delegated: 1_610_565_420, - }); - - /* 4. Run assertions */ - fn warmed_up(bank: &Bank, stake_pubkeys: &[Pubkey]) -> bool { - for stake_pubkey in stake_pubkeys { - let stake = - stake_state::stake_from(&bank.get_account(stake_pubkey).unwrap()).unwrap(); - - if stake.delegation.stake - != stake.stake( - bank.epoch(), - &from_account::( - &bank.get_account(&sysvar::stake_history::id()).unwrap(), - ) - .unwrap(), - bank.new_warmup_cooldown_rate_epoch(), - ) - { - return false; - } - } - - true - } - fn next_epoch(bank: &Arc) -> Arc { - bank.squash(); - - Arc::new(Bank::new_from_parent( - bank.clone(), - &Pubkey::default(), - bank.get_slots_in_epoch(bank.epoch()) + bank.slot(), - )) - } - - let mut stake_pubkeys = validator_0_delegations - .iter() - .map(|v| v.stake_account_pubkey) - .collect::>(); - stake_pubkeys.extend( - validator_1_delegations - .iter() - .map(|v| v.stake_account_pubkey), - ); - stake_pubkeys.extend( - validator_2_delegations - .iter() - .map(|v| v.stake_account_pubkey), - ); - loop { - if warmed_up(&bank, &stake_pubkeys[..]) { - break; - } - - // Cycle thru banks until we're fully warmed up - bank = next_epoch(&bank); - } - - let tip_distribution_account_0 = derive_tip_distribution_account_address( - &tip_distribution_program_id, - &validator_keypairs_0.vote_keypair.pubkey(), - bank.epoch(), - ); - let tip_distribution_account_1 = derive_tip_distribution_account_address( - &tip_distribution_program_id, - &validator_keypairs_1.vote_keypair.pubkey(), - bank.epoch(), - ); - let tip_distribution_account_2 = derive_tip_distribution_account_address( - &tip_distribution_program_id, - &validator_keypairs_2.vote_keypair.pubkey(), - bank.epoch(), - ); - - let expires_at = bank.epoch() + 3; - - let tda_0 = TipDistributionAccount { - validator_vote_account: validator_keypairs_0.vote_keypair.pubkey(), - merkle_root_upload_authority, - merkle_root: None, - epoch_created_at: bank.epoch(), - validator_commission_bps: 50, - expires_at, - bump: tip_distribution_account_0.1, - }; - let tda_1 = TipDistributionAccount { - validator_vote_account: validator_keypairs_1.vote_keypair.pubkey(), - merkle_root_upload_authority, - merkle_root: None, - epoch_created_at: bank.epoch(), - validator_commission_bps: 500, - expires_at: 0, - bump: tip_distribution_account_1.1, - }; - let tda_2 = TipDistributionAccount { - validator_vote_account: validator_keypairs_2.vote_keypair.pubkey(), - merkle_root_upload_authority, - merkle_root: None, - epoch_created_at: bank.epoch(), - validator_commission_bps: 75, - expires_at: 0, - bump: tip_distribution_account_2.1, - }; - - let tip_distro_0_tips = 1_000_000 * 10; - let tip_distro_1_tips = 69_000_420 * 10; - let tip_distro_2_tips = 789_000_111 * 10; - - let tda_0_fields = (tip_distribution_account_0.0, tda_0.validator_commission_bps); - let data_0 = - tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_0_tips, tda_0); - let tda_1_fields = (tip_distribution_account_1.0, tda_1.validator_commission_bps); - let data_1 = - tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_1_tips, tda_1); - let tda_2_fields = (tip_distribution_account_2.0, tda_2.validator_commission_bps); - let data_2 = - tda_to_account_shared_data(&tip_distribution_program_id, tip_distro_2_tips, tda_2); - - let accounts_data = create_config_account_data(&tip_payment_program_id, &bank); - for (pubkey, data) in accounts_data { - bank.store_account(&pubkey, &data); - } - - bank.store_account(&tip_distribution_account_0.0, &data_0); - bank.store_account(&tip_distribution_account_1.0, &data_1); - bank.store_account(&tip_distribution_account_2.0, &data_2); - - bank.freeze(); - let stake_meta_collection = generate_stake_meta_collection( - &bank, - &tip_distribution_program_id, - &tip_payment_program_id, - ) - .unwrap(); - assert_eq!( - stake_meta_collection.tip_distribution_program_id, - tip_distribution_program_id - ); - assert_eq!(stake_meta_collection.slot, bank.slot()); - assert_eq!(stake_meta_collection.epoch, bank.epoch()); - - let mut expected_stake_metas = HashMap::new(); - expected_stake_metas.insert( - validator_keypairs_0.vote_keypair.pubkey(), - StakeMeta { - validator_vote_account: validator_keypairs_0.vote_keypair.pubkey(), - delegations: validator_0_delegations.clone(), - total_delegated: validator_0_delegations - .iter() - .fold(0u64, |sum, delegation| { - sum.checked_add(delegation.lamports_delegated).unwrap() - }), - maybe_tip_distribution_meta: Some(TipDistributionMeta { - merkle_root_upload_authority, - tip_distribution_pubkey: tda_0_fields.0, - total_tips: tip_distro_0_tips - .checked_sub( - bank.get_minimum_balance_for_rent_exemption( - TipDistributionAccount::SIZE, - ), - ) - .unwrap(), - validator_fee_bps: tda_0_fields.1, - }), - commission: 0, - validator_node_pubkey: validator_keypairs_0.node_keypair.pubkey(), - }, - ); - expected_stake_metas.insert( - validator_keypairs_1.vote_keypair.pubkey(), - StakeMeta { - validator_vote_account: validator_keypairs_1.vote_keypair.pubkey(), - delegations: validator_1_delegations.clone(), - total_delegated: validator_1_delegations - .iter() - .fold(0u64, |sum, delegation| { - sum.checked_add(delegation.lamports_delegated).unwrap() - }), - maybe_tip_distribution_meta: Some(TipDistributionMeta { - merkle_root_upload_authority, - tip_distribution_pubkey: tda_1_fields.0, - total_tips: tip_distro_1_tips - .checked_sub( - bank.get_minimum_balance_for_rent_exemption( - TipDistributionAccount::SIZE, - ), - ) - .unwrap(), - validator_fee_bps: tda_1_fields.1, - }), - commission: 0, - validator_node_pubkey: validator_keypairs_1.node_keypair.pubkey(), - }, - ); - expected_stake_metas.insert( - validator_keypairs_2.vote_keypair.pubkey(), - StakeMeta { - validator_vote_account: validator_keypairs_2.vote_keypair.pubkey(), - delegations: validator_2_delegations.clone(), - total_delegated: validator_2_delegations - .iter() - .fold(0u64, |sum, delegation| { - sum.checked_add(delegation.lamports_delegated).unwrap() - }), - maybe_tip_distribution_meta: Some(TipDistributionMeta { - merkle_root_upload_authority, - tip_distribution_pubkey: tda_2_fields.0, - total_tips: tip_distro_2_tips - .checked_sub( - bank.get_minimum_balance_for_rent_exemption( - TipDistributionAccount::SIZE, - ), - ) - .unwrap(), - validator_fee_bps: tda_2_fields.1, - }), - commission: 0, - validator_node_pubkey: validator_keypairs_2.node_keypair.pubkey(), - }, - ); - - println!( - "validator_0 [vote_account={}, stake_account={}]", - validator_keypairs_0.vote_keypair.pubkey(), - validator_keypairs_0.stake_keypair.pubkey() - ); - println!( - "validator_1 [vote_account={}, stake_account={}]", - validator_keypairs_1.vote_keypair.pubkey(), - validator_keypairs_1.stake_keypair.pubkey() - ); - println!( - "validator_2 [vote_account={}, stake_account={}]", - validator_keypairs_2.vote_keypair.pubkey(), - validator_keypairs_2.stake_keypair.pubkey(), - ); - - assert_eq!( - expected_stake_metas.len(), - stake_meta_collection.stake_metas.len() - ); - - for actual_stake_meta in stake_meta_collection.stake_metas { - let expected_stake_meta = expected_stake_metas - .get(&actual_stake_meta.validator_vote_account) - .unwrap(); - assert_eq!( - expected_stake_meta.maybe_tip_distribution_meta, - actual_stake_meta.maybe_tip_distribution_meta - ); - assert_eq!( - expected_stake_meta.total_delegated, - actual_stake_meta.total_delegated - ); - assert_eq!(expected_stake_meta.commission, actual_stake_meta.commission); - assert_eq!( - expected_stake_meta.validator_vote_account, - actual_stake_meta.validator_vote_account - ); - - assert_eq!( - expected_stake_meta.delegations.len(), - actual_stake_meta.delegations.len() - ); - - for expected_delegation in &expected_stake_meta.delegations { - let actual_delegation = actual_stake_meta - .delegations - .iter() - .find(|d| d.stake_account_pubkey == expected_delegation.stake_account_pubkey) - .unwrap(); - - assert_eq!(expected_delegation, actual_delegation); - } - } - } - - /// Helper function that sends a delegate stake instruction to the bank. - /// Returns the created stake account pubkey. - fn delegate_stake_helper( - bank: &Bank, - from_keypair: &Keypair, - vote_account: &Pubkey, - delegation_amount: u64, - ) -> Pubkey { - let minimum_delegation = solana_stake_program::get_minimum_delegation(&bank.feature_set); - assert!( - delegation_amount >= minimum_delegation, - "{}", - format!( - "received delegation_amount {}, must be at least {}", - delegation_amount, minimum_delegation - ) - ); - if let Some(from_account) = bank.get_account(&from_keypair.pubkey()) { - assert_eq!(from_account.owner(), &solana_sdk::system_program::id()); - } else { - panic!("from_account DNE"); - } - assert!(bank.get_account(vote_account).is_some()); - - let stake_keypair = Keypair::new(); - let instructions = stake::instruction::create_account_and_delegate_stake( - &from_keypair.pubkey(), - &stake_keypair.pubkey(), - vote_account, - &Authorized::auto(&from_keypair.pubkey()), - &Lockup::default(), - delegation_amount, - ); - - let message = Message::new(&instructions[..], Some(&from_keypair.pubkey())); - let transaction = Transaction::new( - &[from_keypair, &stake_keypair], - message, - bank.last_blockhash(), - ); - - bank.process_transaction(&transaction) - .map_err(|e| { - eprintln!("Error delegating stake [error={}]", e); - e - }) - .unwrap(); - - stake_keypair.pubkey() - } - - fn tda_to_account_shared_data( - tip_distribution_program_id: &Pubkey, - lamports: u64, - tda: TipDistributionAccount, - ) -> AccountSharedData { - let mut account_data = AccountSharedData::new( - lamports, - TipDistributionAccount::SIZE, - tip_distribution_program_id, - ); - - let mut data: [u8; TipDistributionAccount::SIZE] = [0u8; TipDistributionAccount::SIZE]; - let mut cursor = std::io::Cursor::new(&mut data[..]); - tda.try_serialize(&mut cursor).unwrap(); - - account_data.set_data(data.to_vec()); - account_data - } - - fn create_config_account_data( - tip_payment_program_id: &Pubkey, - bank: &Bank, - ) -> Vec<(Pubkey, AccountSharedData)> { - let mut account_datas = vec![]; - - let config_pda = - Pubkey::find_program_address(&[CONFIG_ACCOUNT_SEED], tip_payment_program_id); - - let tip_accounts = [ - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_0], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_1], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_2], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_3], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_4], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_5], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_6], tip_payment_program_id), - Pubkey::find_program_address(&[TIP_ACCOUNT_SEED_7], tip_payment_program_id), - ]; - - let config = Config { - tip_receiver: Pubkey::new_unique(), - block_builder: Pubkey::new_unique(), - block_builder_commission_pct: 10, - bumps: InitBumps { - config: config_pda.1, - tip_payment_account_0: tip_accounts[0].1, - tip_payment_account_1: tip_accounts[1].1, - tip_payment_account_2: tip_accounts[2].1, - tip_payment_account_3: tip_accounts[3].1, - tip_payment_account_4: tip_accounts[4].1, - tip_payment_account_5: tip_accounts[5].1, - tip_payment_account_6: tip_accounts[6].1, - tip_payment_account_7: tip_accounts[7].1, - }, - }; - - let mut config_account_data = AccountSharedData::new( - bank.get_minimum_balance_for_rent_exemption(Config::SIZE), - Config::SIZE, - tip_payment_program_id, - ); - - let mut config_data: [u8; Config::SIZE] = [0u8; Config::SIZE]; - let mut config_cursor = std::io::Cursor::new(&mut config_data[..]); - config.try_serialize(&mut config_cursor).unwrap(); - config_account_data.set_data(config_data.to_vec()); - account_datas.push((config_pda.0, config_account_data)); - - account_datas.extend(tip_accounts.into_iter().map(|(pubkey, _)| { - let mut tip_account_data = AccountSharedData::new( - bank.get_minimum_balance_for_rent_exemption(TipPaymentAccount::SIZE), - TipPaymentAccount::SIZE, - tip_payment_program_id, - ); - - let mut data: [u8; TipPaymentAccount::SIZE] = [0u8; TipPaymentAccount::SIZE]; - let mut cursor = std::io::Cursor::new(&mut data[..]); - TipPaymentAccount::default() - .try_serialize(&mut cursor) - .unwrap(); - tip_account_data.set_data(data.to_vec()); - - (pubkey, tip_account_data) - })); - - account_datas - } -} diff --git a/validator/src/admin_rpc_service.rs b/validator/src/admin_rpc_service.rs index e4e4f92b44..1fa09054d6 100644 --- a/validator/src/admin_rpc_service.rs +++ b/validator/src/admin_rpc_service.rs @@ -35,11 +35,12 @@ use { net::SocketAddr, path::{Path, PathBuf}, str::FromStr, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock}, thread::{self, Builder}, time::{Duration, SystemTime}, }, tokio::runtime::Runtime, + tonic::transport::Endpoint, }; #[derive(Clone)] @@ -53,6 +54,7 @@ pub struct AdminRpcRequestMetadata { pub staked_nodes_overrides: Arc>>, pub post_init: Arc>>, pub rpc_to_plugin_manager_sender: Option>, + pub bam_url: Arc>>, } impl Metadata for AdminRpcRequestMetadata {} @@ -257,6 +259,9 @@ pub trait AdminRpc { trust_packets: bool, ) -> Result<()>; + #[rpc(meta, name = "setBamUrl")] + fn set_bam_url(&self, meta: Self::Metadata, bam_url: Option) -> Result<()>; + #[rpc(meta, name = "setRelayerConfig")] fn set_relayer_config( &self, @@ -503,6 +508,29 @@ impl AdminRpc for AdminRpcImpl { } } + fn set_bam_url(&self, meta: Self::Metadata, bam_url: Option) -> Result<()> { + let old_bam_url = meta.bam_url.lock().unwrap().clone(); + let new_bam_url = bam_url.as_ref().map(|url| url.to_string()); + debug!("set_bam_url old= {:?}, new={:?}", old_bam_url, new_bam_url); + + if let Some(new_bam_url) = &new_bam_url { + if new_bam_url.is_empty() { + return Err(jsonrpc_core::error::Error::invalid_params( + "BAM URL cannot be empty", + )); + } + + if let Err(e) = Endpoint::from_str(new_bam_url) { + return Err(jsonrpc_core::error::Error::invalid_params(format!( + "Could not create endpoint: {e}" + ))); + } + } + + *meta.bam_url.lock().unwrap() = bam_url; + Ok(()) + } + fn set_identity( &self, meta: Self::Metadata, @@ -1102,6 +1130,7 @@ mod tests { }))), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, + bam_url: Arc::new(Mutex::new(None)), }; let mut io = MetaIoHandler::default(); io.extend_with(AdminRpcImpl.to_delegate()); @@ -1521,6 +1550,7 @@ mod tests { post_init: post_init.clone(), staked_nodes_overrides: Arc::new(RwLock::new(HashMap::new())), rpc_to_plugin_manager_sender: None, + bam_url: Arc::new(Mutex::new(None)), }; let _validator = Validator::new( diff --git a/validator/src/bin/solana-test-validator.rs b/validator/src/bin/solana-test-validator.rs index 3d1d479908..dd164e630e 100644 --- a/validator/src/bin/solana-test-validator.rs +++ b/validator/src/bin/solana-test-validator.rs @@ -37,7 +37,7 @@ use { net::{IpAddr, Ipv4Addr, SocketAddr}, path::{Path, PathBuf}, process::exit, - sync::{Arc, RwLock}, + sync::{Arc, Mutex, RwLock}, time::{Duration, SystemTime, UNIX_EPOCH}, }, }; @@ -395,6 +395,10 @@ fn main() { } else { (None, None) }; + + genesis.bam_url = Arc::new(Mutex::new( + matches.value_of("bam_url").map(|url| url.into()), + )); admin_rpc_service::run( &ledger_path, admin_rpc_service::AdminRpcRequestMetadata { @@ -407,6 +411,7 @@ fn main() { post_init: admin_service_post_init, tower_storage: tower_storage.clone(), rpc_to_plugin_manager_sender, + bam_url: genesis.bam_url.clone(), }, ); let dashboard = if output == Output::Dashboard { diff --git a/validator/src/cli.rs b/validator/src/cli.rs index 818b0aa734..774f86a738 100644 --- a/validator/src/cli.rs +++ b/validator/src/cli.rs @@ -1783,6 +1783,14 @@ pub fn app<'a>(version: &'a str, default_args: &'a DefaultArgs) -> App<'a, 'a> { .multiple(true) .help("Specify the configuration file for a Runtime plugin.") ) + .arg( + Arg::with_name("bam_url") + .long("bam-url") + .help( + "URL of BAM Node; leave empty to disable BAM" + ) + .takes_value(true) + ) .args(&thread_args(&default_args.thread_args)) .args(&get_deprecated_arguments()) .after_help("The default subcommand is run") @@ -2796,6 +2804,12 @@ pub fn test_app<'a>(version: &'a str, default_args: &'a DefaultTestArgs) -> App< already exists then this parameter is silently ignored", ), ) + .arg( + Arg::with_name("bam_url") + .long("bam-url") + .help("URL of BAM Node; leave empty to disable BAM") + .takes_value(true), + ) } pub struct DefaultTestArgs { diff --git a/validator/src/main.rs b/validator/src/main.rs index 79685e3a0e..2821d21bb8 100644 --- a/validator/src/main.rs +++ b/validator/src/main.rs @@ -1013,6 +1013,7 @@ pub fn main() { )), preallocated_bundle_cost: value_of(&matches, "preallocated_bundle_cost") .expect("preallocated_bundle_cost set as default"), + bam_url: Arc::new(Mutex::new(value_of(&matches, "bam_url"))), ..ValidatorConfig::default() }; @@ -1326,6 +1327,7 @@ pub fn main() { tower_storage: validator_config.tower_storage.clone(), staked_nodes_overrides, rpc_to_plugin_manager_sender, + bam_url: validator_config.bam_url.clone(), }, );