From 7e11d604c84e703496b7988ec51922422450cb41 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Thu, 16 Jul 2026 11:33:32 -0400 Subject: [PATCH 01/30] bls-sigverify: receive certificates from blockstore (#13867) --- bls-sigverify/src/bls_sigverifier.rs | 233 +++++++++++++++++++++------ core/src/tvu.rs | 6 + ledger/src/blockstore.rs | 21 ++- 3 files changed, 210 insertions(+), 50 deletions(-) diff --git a/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index 9e5edfdc15a..209251428e1 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -16,11 +16,13 @@ use { migration::MigrationStatus, reward_certificate::AddVoteMessage, sig_verified_messages::SigVerifiedBatch, - unverified_vote_message::{DecodedWireConsensusMessage, UnverifiedVoteMessage}, + unverified_vote_message::{ + DecodedWireConsensusMessage, UnverifiedCertificate, UnverifiedVoteMessage, + }, vote::Vote, wire::{VersionedWireConsensusMessage, VotePayloadToSign}, }, - crossbeam_channel::{Receiver, RecvTimeoutError, Sender, TryRecvError}, + crossbeam_channel::{Receiver, Sender, TryRecvError, select}, log::error, rayon::{ThreadPool, ThreadPoolBuilder}, solana_bls_signatures::pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine}, @@ -49,10 +51,13 @@ use { /// This also sets an upper bound on how much storage the various structs in this module require. pub(super) const NUM_SLOTS_FOR_VERIFY: Slot = 90_000; -/// If we receive an invalid certificate or vote from a QUIC connection, we ban the sender. -/// We ban the sender for 2 days which roughly corresponds to an epoch +/// If we receive an invalid certificate or vote, we ban its attributed sender. For certificates +/// received from blockstore, that sender is the scheduled leader for the carrier slot. We ban the +/// sender for 2 days, which roughly corresponds to an epoch. pub(super) const BAN_TIMEOUT: Duration = Duration::from_hours(48); +type SigVerifierInputs = (Vec, Vec<(Slot, UnverifiedCertificate)>); + pub struct SigVerifierContext { pub migration_status: Arc, pub banlist: Arc, @@ -65,6 +70,7 @@ pub struct SigVerifierContext { pub struct SigVerifierChannels { pub packet_receiver: Receiver, + pub certificate_receiver: Receiver<(Slot, UnverifiedCertificate)>, pub channel_to_repair: VerifiedVoterSlotsSender, pub channel_to_reward: Sender, pub channel_to_pool: Sender, @@ -138,15 +144,23 @@ impl SigVerifier { fn run(mut self, exit: Arc) { while !exit.load(Ordering::Relaxed) { const SOFT_RECEIVE_CAP: usize = 5000; - let Ok(batches) = recv_batches(&self.channels.packet_receiver, SOFT_RECEIVE_CAP) else { - error!("packet_receiver disconnected: Exiting."); + let Ok((batches, certificates)) = recv_inputs( + &self.channels.packet_receiver, + &self.channels.certificate_receiver, + SOFT_RECEIVE_CAP, + ) else { + error!("sigverifier input channel disconnected: Exiting."); break; }; - if batches.is_empty() || self.migration_status.is_pre_feature_activation() { + if self.migration_status.is_pre_feature_activation() { + continue; + } + if batches.is_empty() && certificates.is_empty() { continue; } - let (verify_res, verify_time_us) = measure_us!(self.verify_and_send_batches(batches)); + let (verify_res, verify_time_us) = + measure_us!(self.verify_and_send_inputs(batches, certificates)); self.stats .verify_and_send_batch_us .add_sample(verify_time_us); @@ -159,12 +173,21 @@ impl SigVerifier { self.stats.do_report(self.sharable_banks.root().slot()); } + #[cfg(test)] fn verify_and_send_batches(&mut self, batches: Vec) -> Result<(), SigVerifyError> { + self.verify_and_send_inputs(batches, vec![]) + } + + fn verify_and_send_inputs( + &mut self, + batches: Vec, + certificates: Vec<(Slot, UnverifiedCertificate)>, + ) -> Result<(), SigVerifyError> { let root_bank = self.sharable_banks.root(); self.maybe_prune_caches(root_bank.slot()); let ((cert_groups, votes_to_verify), extract_msgs_us) = - measure_us!(self.extract_and_filter_msgs(batches, &root_bank)); + measure_us!(self.extract_and_filter_msgs(batches, certificates, &root_bank)); self.stats .extract_filter_msgs_us .add_sample(extract_msgs_us); @@ -208,9 +231,33 @@ impl SigVerifier { } } + fn add_certificate_to_group( + &mut self, + cert_groups: &mut HashMap>, + cert: UnverifiedCertificate, + sender_identity_pubkey: Pubkey, + ) { + if self.verified_certs.contains(&cert.cert_type) { + self.stats.num_verified_certs_received += 1; + return; + } + if self.generated_cert_types.has_cert(&cert.cert_type) { + self.stats.num_generated_certs_received += 1; + return; + } + cert_groups + .entry(cert.cert_type) + .or_default() + .push(CertPayload { + cert, + sender_identity_pubkey, + }); + } + fn extract_and_filter_msgs( &mut self, batches: Vec, + certificates: Vec<(Slot, UnverifiedCertificate)>, root_bank: &Bank, ) -> ( HashMap>, @@ -270,24 +317,38 @@ impl SigVerifier { self.stats.num_old_certs_received += 1; continue; } - if self.verified_certs.contains(&cert.cert_type) { - self.stats.num_verified_certs_received += 1; - continue; - } - if self.generated_cert_types.has_cert(&cert.cert_type) { - self.stats.num_generated_certs_received += 1; - continue; - } - cert_groups - .entry(cert.cert_type) - .or_default() - .push(CertPayload { - cert, - sender_identity_pubkey, - }); + self.add_certificate_to_group(&mut cert_groups, cert, sender_identity_pubkey); } } } + for (carrier_slot, certificate) in certificates { + let is_genesis = matches!(&certificate.cert_type, CertificateType::Genesis(_)); + let is_active = if is_genesis { + // Genesis certificates from blockstore are only allowed when we are in migration + self.migration_status.is_in_migration() + } else { + self.migration_status + .should_allow_block_markers(carrier_slot) + }; + if carrier_slot < root_slot + || certificate.shred_version != my_shred_version + || !is_active + { + continue; + } + if certificate.cert_type.slot() < root_slot { + self.stats.num_old_certs_received += 1; + continue; + } + let Some(sender_identity_pubkey) = self + .leader_schedule + .slot_leader_at(carrier_slot, Some(root_bank)) + .map(|leader| leader.id) + else { + continue; + }; + self.add_certificate_to_group(&mut cert_groups, certificate, sender_identity_pubkey); + } self.stats.num_pkts.add_sample(num_pkts); (cert_groups, votes) } @@ -325,38 +386,34 @@ impl SigVerifier { } } -/// Receives a `Vec` from the `receiver` while adhering to the `soft_receive_cap` limit. -/// -/// Returns `Err(())` if the channel disconnected. -fn recv_batches( - receiver: &Receiver, +/// Receives BLS packet batches and certificates recovered from blockstore. Certificate-only +/// traffic wakes the verifier immediately; packet batches retain their existing soft receive cap. +fn recv_inputs( + packet_receiver: &Receiver, + certificate_receiver: &Receiver<(Slot, UnverifiedCertificate)>, soft_receive_cap: usize, -) -> Result, ()> { - let batch = match receiver.recv_timeout(Duration::from_secs(1)) { - Ok(b) => b, - Err(e) => match e { - RecvTimeoutError::Timeout => { - return Ok(vec![]); - } - RecvTimeoutError::Disconnected => { - return Err(()); - } - }, - }; +) -> Result { let mut batches = Vec::with_capacity(soft_receive_cap); - batches.push(batch); + let mut certificates = vec![]; + select! { + recv(packet_receiver) -> batch => batches.push(batch.map_err(|_| ())?), + recv(certificate_receiver) -> certificate => { + certificates.push(certificate.map_err(|_| ())?); + }, + default(Duration::from_secs(1)) => return Ok((batches, certificates)), + } while batches.len() < soft_receive_cap { - match receiver.try_recv() { + match packet_receiver.try_recv() { Ok(b) => { batches.push(b); } - Err(e) => match e { - TryRecvError::Empty => return Ok(batches), - TryRecvError::Disconnected => return Err(()), - }, + Err(TryRecvError::Empty) => break, + Err(TryRecvError::Disconnected) => return Err(()), } } - Ok(batches) + // Certificates from blockstore are very low throughput (1 per slot), so no need for a cap here + certificates.extend(certificate_receiver.try_iter()); + Ok((batches, certificates)) } #[cfg(test)] @@ -391,6 +448,7 @@ mod tests { }, solana_signer::Signer, solana_signer_store::encode_base2, + std::sync::RwLock, }; fn new_test_banlist() -> Arc { @@ -409,6 +467,8 @@ mod tests { pool_receiver: Receiver, _metrics_receiver: ConsensusMetricsEventReceiver, generated_cert_types: Arc, + _certificate_sender: Sender<(Slot, UnverifiedCertificate)>, + _bank_forks: Arc>, } impl TestContext { @@ -450,6 +510,7 @@ mod tests { let (channel_to_repair, repair_receiver) = bounded(1024); let (channel_to_reward, reward_receiver) = bounded(1024); let (packet_sender, packet_receiver) = bounded(1024); + let (certificate_sender, certificate_receiver) = bounded(1024); let (channel_to_metrics, metrics_receiver) = bounded(1024); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); @@ -466,6 +527,7 @@ mod tests { }, SigVerifierChannels { packet_receiver, + certificate_receiver, channel_to_repair, channel_to_reward, channel_to_pool, @@ -482,6 +544,8 @@ mod tests { pool_receiver, _metrics_receiver: metrics_receiver, generated_cert_types, + _certificate_sender: certificate_sender, + _bank_forks: bank_forks, } } } @@ -522,6 +586,18 @@ mod tests { builder.build().expect("Failed to build certificate") } + fn unverified_certificate( + certificate: Certificate, + shred_version: u16, + ) -> UnverifiedCertificate { + UnverifiedCertificate { + cert_type: certificate.cert_type, + signature: certificate.signature, + bitmap: certificate.bitmap, + shred_version, + } + } + fn expect_no_receive(receiver: &Receiver) { match receiver.try_recv().unwrap_err() { TryRecvError::Empty => (), @@ -542,6 +618,65 @@ mod tests { packet } + #[test] + fn test_blockstore_certificate_requires_active_alpenglow() { + let mut ctx = TestContext::new(); + let shred_version = ctx.verifier.cluster_info.my_shred_version(); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let certificate = create_signed_certificate_message( + shred_version, + &ctx.validator_keypairs, + CertificateType::FinalizeFast(block), + &[0, 1, 2, 3, 4, 5, 6, 7], + ); + let certificate = (2, unverified_certificate(certificate, shred_version)); + + ctx.verifier + .verify_and_send_inputs(vec![], vec![certificate.clone()]) + .unwrap(); + expect_no_receive(&ctx.pool_receiver); + + ctx.verifier.migration_status.enable_alpenglow_for_tests(); + ctx.verifier + .verify_and_send_inputs(vec![], vec![certificate]) + .unwrap(); + let SigVerifiedBatch::Certificates(certs) = ctx.pool_receiver.try_recv().unwrap() else { + panic!("expected a certificate batch"); + }; + assert_eq!(certs.len(), 1); + assert_eq!(certs[0].cert_type, CertificateType::FinalizeFast(block)); + } + + #[test] + fn test_old_blockstore_certificate_is_filtered() { + let mut ctx = TestContext::new(); + let shred_version = ctx.verifier.cluster_info.my_shred_version(); + let block = Block { + slot: 1, + block_id: Hash::new_unique(), + }; + let certificate = create_signed_certificate_message( + shred_version, + &ctx.validator_keypairs, + CertificateType::FinalizeFast(block), + &[0, 1, 2, 3, 4, 5, 6, 7], + ); + let certificate = (6, unverified_certificate(certificate, shred_version)); + let root_bank = + Bank::new_from_parent(ctx.verifier.sharable_banks.root(), SlotLeader::default(), 5); + ctx.verifier.migration_status.enable_alpenglow_for_tests(); + + let (cert_groups, votes) = + ctx.verifier + .extract_and_filter_msgs(vec![], vec![certificate], &root_bank); + assert!(cert_groups.is_empty()); + assert!(votes.is_empty()); + assert_eq!(ctx.verifier.stats.num_old_certs_received.0, 1); + } + #[test] fn test_blssigverifier_send_packets() { let mut ctx = TestContext::new(); @@ -1486,6 +1621,7 @@ mod tests { )); let leader_schedule = Arc::new(LeaderScheduleCache::new_from_bank(&sharable_banks.root())); let (_packet_sender, packet_receiver) = bounded(1024); + let (_certificate_sender, certificate_receiver) = bounded(1024); let mut sig_verifier = SigVerifier::new( SigVerifierContext { migration_status: Arc::new(MigrationStatus::default()), @@ -1498,6 +1634,7 @@ mod tests { }, SigVerifierChannels { packet_receiver, + certificate_receiver, channel_to_repair: votes_for_repair_sender, channel_to_reward: reward_votes_sender, channel_to_pool: message_sender, diff --git a/core/src/tvu.rs b/core/src/tvu.rs index 9f2b18f0c7d..a5e57d0dd54 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -109,6 +109,9 @@ pub(crate) const MAX_ALPENGLOW_PACKET_NUM: usize = 10_000; /// of votes / certificate need to be refreshed. const MAX_BLS_MESSAGES_TO_SEND: usize = 1000; +/// Bounds certificates recovered from blockstore and awaiting BLS verification. +const MAX_CERTIFICATES_FROM_BLOCKSTORE: usize = 1_024; + pub struct Tvu { fetch_stage: ShredFetchStage, shred_sigverify: JoinHandle<()>, @@ -286,6 +289,8 @@ impl Tvu { let (consensus_metrics_sender, consensus_metrics_receiver) = bounded(MAX_IN_FLIGHT_CONSENSUS_EVENTS); let generated_cert_types = Arc::new(GeneratedCertTypes::default()); + let (certificate_sender, certificate_receiver) = bounded(MAX_CERTIFICATES_FROM_BLOCKSTORE); + blockstore.set_certificate_sender(certificate_sender); let bls_sigverify_threads = { let (bls_packet_sender, bls_packet_receiver) = bounded(MAX_ALPENGLOW_PACKET_NUM); @@ -338,6 +343,7 @@ impl Tvu { }, SigVerifierChannels { packet_receiver: bls_packet_receiver, + certificate_receiver, channel_to_repair: verified_voter_slots_sender, channel_to_reward: reward_votes_sender.clone(), channel_to_pool: consensus_message_sender, diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 5f8c07f7d85..8c91b3a58b5 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -26,7 +26,9 @@ use { transaction_address_lookup_table_scanner::scan_transaction, }, agave_snapshots::unpack_genesis_archive, - agave_votor_messages::migration::MigrationStatus, + agave_votor_messages::{ + migration::MigrationStatus, unverified_vote_message::UnverifiedCertificate, + }, assert_matches::{assert_matches, debug_assert_matches}, crossbeam_channel::{Receiver, Sender, TrySendError, bounded}, dashmap::DashSet, @@ -84,7 +86,7 @@ use { path::{Path, PathBuf}, rc::Rc, sync::{ - Arc, Mutex, MutexGuard, RwLock, + Arc, Mutex, MutexGuard, OnceLock, RwLock, atomic::{AtomicBool, AtomicU64, Ordering}, }, }, @@ -338,6 +340,7 @@ pub struct Blockstore { /// parent. This tiny cache avoids a blockstore lookup for each later shred /// in small insertion batches. update_parent_shred_parent_cache: Mutex, + certificate_sender: OnceLock>, pub lowest_cleanup_slot: RwLock, // A sender that feeds into the BlockstoreCleanupService request channel // to enable manual Blockstore purge requests to be issued @@ -579,6 +582,19 @@ impl Blockstore { banking_trace_path(&self.ledger_path) } + /// Wires the channel used to hand certificates recovered from blockstore to the BLS + /// sigverifier. + /// + /// The accompanying slot is the carrier slot whose entries contained the certificate, while + /// the slot in the certificate is the block being certified. The carrier slot is needed to + /// check whether block markers were active and to attribute an invalid certificate to the + /// leader that included it. + pub fn set_certificate_sender(&self, sender: Sender<(Slot, UnverifiedCertificate)>) { + self.certificate_sender + .set(sender) + .expect("certificate sender already set"); + } + /// Opens a Ledger in directory, provides "infinite" window of shreds pub fn open(ledger_path: &Path) -> Result { Self::do_open(ledger_path, BlockstoreOptions::default()) @@ -669,6 +685,7 @@ impl Blockstore { update_parent_shred_parent_cache: Mutex::new(LruCache::new( UPDATE_PARENT_SHRED_PARENT_CACHE_CAPACITY, )), + certificate_sender: OnceLock::new(), insert_shreds_lock: Mutex::<()>::default(), switch_block_lock: SwitchBlockLock(FairMutex::new(())), max_root, From e9fc69e7055a5cbcb3b76371ce13ca5e3187eae9 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Thu, 16 Jul 2026 20:21:01 +0300 Subject: [PATCH 02/30] gossip/repair: further optimize ping cache (#12911) PR #12585 made Pong validation stateful: the pings LRU can be replaced with an IndexMap. On insertion when at cap, probe random existing entries. Stale entries (unresponsive nodes) are thus reclaimed on demand without touching fresh ones. The pongs and ping_times LRUs are unchanged. --- core/src/repair/serve_repair.rs | 26 ++-- gossip/src/cluster_info.rs | 9 +- gossip/src/crds_gossip.rs | 13 +- gossip/src/crds_gossip_pull.rs | 12 +- gossip/src/crds_gossip_push.rs | 14 +- gossip/src/ping_pong.rs | 221 ++++++++++++++++++++++++++++---- gossip/tests/crds_gossip.rs | 6 +- 7 files changed, 242 insertions(+), 59 deletions(-) diff --git a/core/src/repair/serve_repair.rs b/core/src/repair/serve_repair.rs index 81447388c1f..10f1a674e6b 100644 --- a/core/src/repair/serve_repair.rs +++ b/core/src/repair/serve_repair.rs @@ -13,7 +13,7 @@ use { duplicate_repair_status::get_ancestor_hash_repair_sample_size, outstanding_requests::OutstandingRequests, repair_handler::RepairHandler, - repair_service::{OutstandingShredRepairs, REPAIR_MS, RepairInfo, RepairStats}, + repair_service::{OutstandingShredRepairs, RepairInfo, RepairStats}, request_response::RequestResponse, result::{Error, RepairVerifyError, Result}, }, @@ -65,6 +65,7 @@ use { cmp::Reverse, collections::{HashMap, HashSet}, net::{SocketAddr, UdpSocket}, + ops::Range, sync::{ Arc, RwLock, atomic::{AtomicBool, Ordering}, @@ -96,7 +97,7 @@ pub const MAX_ANCESTOR_RESPONSES: usize = const REPAIR_PING_TOKEN_SIZE: usize = HASH_BYTES; pub const REPAIR_PING_CACHE_CAPACITY: usize = 65536; pub const REPAIR_PING_CACHE_TTL: Duration = Duration::from_secs(1280); -const REPAIR_PING_CACHE_RATE_LIMIT_DELAY: Duration = Duration::from_secs(2); +const REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS: Range = 1000..2000; pub(crate) const REPAIR_RESPONSE_SERIALIZED_PING_BYTES: usize = 4 /*enum discriminator*/ + PUBKEY_BYTES + REPAIR_PING_TOKEN_SIZE + SIGNATURE_BYTES; const SIGNED_REPAIR_TIME_WINDOW: Duration = Duration::from_secs(60 * 10); // 10 min @@ -1377,12 +1378,9 @@ impl ServeRepair { ) -> JoinHandle<()> { const MAX_BYTES_PER_SECOND: u64 = 12_000_000; - // rate limit delay should be greater than the repair request iteration delay - assert!(REPAIR_PING_CACHE_RATE_LIMIT_DELAY > Duration::from_millis(REPAIR_MS)); - let mut ping_cache = PingCache::new( REPAIR_PING_CACHE_TTL, - REPAIR_PING_CACHE_RATE_LIMIT_DELAY, + REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, REPAIR_PING_CACHE_CAPACITY, ); @@ -2043,7 +2041,7 @@ mod tests { let from_addr = socketaddr!(Ipv4Addr::LOCALHOST, 1234); let mut ping_cache = PingCache::new( REPAIR_PING_CACHE_TTL, - REPAIR_PING_CACHE_RATE_LIMIT_DELAY, + REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, REPAIR_PING_CACHE_CAPACITY, ); let slot = 42; @@ -3215,11 +3213,11 @@ mod tests { assert!(!repair.verify_response(&AncestorHashesResponse::Hashes(response))); } - // A second check() within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate + // A second check() within REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT must not generate // a new ping. If it did, it would overwrite the stored token and invalidate the Pong, // making Ping fail for no reason. #[test] - fn test_repair_no_ping_overwrite_within_rate_limit_delay() { + fn test_repair_no_ping_overwrite_while_already_probing() { let mut rng = rand::rng(); let this_node = Keypair::new(); let remote_socket = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::LOCALHOST, 8001)); @@ -3227,7 +3225,7 @@ mod tests { let remote_node = (remote_keypair.pubkey(), remote_socket); let mut cache = PingCache::new( REPAIR_PING_CACHE_TTL, - REPAIR_PING_CACHE_RATE_LIMIT_DELAY, + REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, REPAIR_PING_CACHE_CAPACITY, ); let now = Instant::now(); @@ -3235,13 +3233,13 @@ mod tests { let (_, ping1) = cache.check(&mut rng, &this_node, now, remote_node); let ping1 = ping1.expect("should generate ping for unknown node"); - // Second check within REPAIR_PING_CACHE_RATE_LIMIT_DELAY must not generate - // a new ping — that would overwrite the stored hash and invalidate the in-flight pong. - let within_delay = now + REPAIR_PING_CACHE_RATE_LIMIT_DELAY - Duration::from_millis(1); + // Use the minimum possible expiry minus 1ms — guaranteed to be before any expiry. + let within_delay = + now + Duration::from_millis(REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS.start - 1); let (_, ping2) = cache.check(&mut rng, &this_node, within_delay, remote_node); assert!( ping2.is_none(), - "must not generate a second ping within REPAIR_PING_CACHE_RATE_LIMIT_DELAY" + "must not generate a second ping within REPAIR_PING_CACHE_OUTSTANDING_PING_TIMEOUT" ); // Pong for ping1 must still be valid — token was not overwritten. diff --git a/gossip/src/cluster_info.rs b/gossip/src/cluster_info.rs index 1767de3e92a..ded61d664c0 100644 --- a/gossip/src/cluster_info.rs +++ b/gossip/src/cluster_info.rs @@ -82,7 +82,7 @@ use { iter::repeat, net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener, UdpSocket}, num::NonZeroUsize, - ops::Div, + ops::{Div, Range}, path::{Path, PathBuf}, rc::Rc, result::Result, @@ -122,8 +122,9 @@ const CHANNEL_CONSUME_CAPACITY: usize = 1024; /// of `MAX_GOSSIP_TRAFFIC` (103,896). pub(crate) const GOSSIP_CHANNEL_CAPACITY: usize = 4096; // 2^12 const GOSSIP_PING_CACHE_CAPACITY: usize = 126976; -const GOSSIP_PING_CACHE_TTL: Duration = Duration::from_secs(1280); -const GOSSIP_PING_CACHE_RATE_LIMIT_DELAY: Duration = Duration::from_secs(1280 / 64); +pub(crate) const GOSSIP_PING_CACHE_TTL: Duration = Duration::from_secs(1280); +/// Per-entry Pong wait timeout is drawn uniformly from this range (in milliseconds). +pub(crate) const GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS: Range = 1000..2000; // Per-IP scan budget for incoming pull requests; validators are assumed not // to share IPs. Mirrors the ping-pong cache capacity. const GOSSIP_PULL_SCAN_BUDGET_CACHE_CAPACITY: usize = GOSSIP_PING_CACHE_CAPACITY; @@ -216,7 +217,7 @@ impl ClusterInfo { my_contact_info: RwLock::new(contact_info), ping_cache: Mutex::new(PingCache::new( GOSSIP_PING_CACHE_TTL, - GOSSIP_PING_CACHE_RATE_LIMIT_DELAY, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, GOSSIP_PING_CACHE_CAPACITY, )), pull_request_budget: KeyedRateLimiter::new( diff --git a/gossip/src/crds_gossip.rs b/gossip/src/crds_gossip.rs index fc93dd93aa2..ef8e753bd4d 100644 --- a/gossip/src/crds_gossip.rs +++ b/gossip/src/crds_gossip.rs @@ -392,7 +392,12 @@ pub(crate) fn maybe_ping_gossip_addresses( #[cfg(test)] mod test { use { - super::*, crate::contact_info::ContactInfo, solana_sha256_hasher::hash, + super::*, + crate::{ + cluster_info::{GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, GOSSIP_PING_CACHE_TTL}, + contact_info::ContactInfo, + }, + solana_sha256_hasher::hash, solana_time_utils::timestamp, }; @@ -414,9 +419,9 @@ mod test { ) .unwrap(); let ping_cache = PingCache::new( - Duration::from_secs(20 * 60), // ttl - Duration::from_secs(20 * 60) / 64, // rate_limit_delay - 128, // capacity + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + 128, // capacity (small for tests) ); let ping_cache = Mutex::new(ping_cache); crds_gossip.refresh_push_active_set( diff --git a/gossip/src/crds_gossip_pull.rs b/gossip/src/crds_gossip_pull.rs index d34389523ee..0a1b0b54493 100644 --- a/gossip/src/crds_gossip_pull.rs +++ b/gossip/src/crds_gossip_pull.rs @@ -675,7 +675,11 @@ pub(crate) fn get_max_bloom_filter_bytes(caller: &CrdsValue) -> usize { pub(crate) mod tests { use { super::*, - crate::{crds_data::CrdsData, protocol::Protocol}, + crate::{ + cluster_info::{GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, GOSSIP_PING_CACHE_TTL}, + crds_data::CrdsData, + protocol::Protocol, + }, itertools::Itertools, rand::{SeedableRng, prelude::IndexedRandom as _}, rand_chacha::ChaChaRng, @@ -739,9 +743,9 @@ pub(crate) mod tests { fn new_ping_cache() -> PingCache { PingCache::new( - Duration::from_secs(20 * 60), // ttl - Duration::from_secs(20 * 60) / 64, // rate_limit_delay - 128, // capacity + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + 128, // capacity (small for tests) ) } diff --git a/gossip/src/crds_gossip_push.rs b/gossip/src/crds_gossip_push.rs index 5f9353dd247..4ce043f9c11 100644 --- a/gossip/src/crds_gossip_push.rs +++ b/gossip/src/crds_gossip_push.rs @@ -288,15 +288,19 @@ impl CrdsGossipPush { mod tests { use { super::*, - crate::{contact_info::ContactInfo, crds_data::CrdsData}, - std::time::{Duration, Instant}, + crate::{ + cluster_info::{GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, GOSSIP_PING_CACHE_TTL}, + contact_info::ContactInfo, + crds_data::CrdsData, + }, + std::time::Instant, }; fn new_ping_cache() -> PingCache { PingCache::new( - Duration::from_secs(20 * 60), // ttl - Duration::from_secs(20 * 60) / 64, // rate_limit_delay - 128, // capacity + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + 128, // capacity (small for tests) ) } diff --git a/gossip/src/ping_pong.rs b/gossip/src/ping_pong.rs index a5bdf5d8ddd..82a679cd8ab 100644 --- a/gossip/src/ping_pong.rs +++ b/gossip/src/ping_pong.rs @@ -1,5 +1,6 @@ use { crate::cluster_info_metrics::should_report_message_signature, + indexmap::IndexMap, lazy_lru::LruCache, rand::{CryptoRng, Rng}, serde::{Deserialize, Serialize}, @@ -13,6 +14,7 @@ use { std::{ borrow::Cow, net::{IpAddr, SocketAddr}, + ops::Range, time::{Duration, Instant}, }, wincode::{SchemaRead, SchemaWrite}, @@ -58,17 +60,25 @@ pub struct Pong { pub struct PingCache { // Time-to-live of received pong messages. ttl: Duration, - // Rate limit delay to generate pings for a given address - rate_limit_delay: Duration, - // Timestamp and expected pong hash for each pinged remote node. - // Used for rate-limiting and pong validation. - pings: LruCache<(Pubkey, SocketAddr), (Instant, Hash)>, + // Timeout range (ms) waiting for a Pong. Randomized per-entry to stagger expiry. + outstanding_ping_timeout_ms: Range, + // Capacity for the pings store. + max_pings: usize, + // Expiry time and expected pong hash for each pinged remote node. + pings: IndexMap<(Pubkey, SocketAddr), (Instant, Hash)>, // Verified pong responses from remote nodes. pongs: LruCache<(Pubkey, SocketAddr), Instant>, // Timestamp of last ping message sent to a remote IP. ping_times: LruCache, } +/// max number of slots in [`PingCache::pings`] to probe when looking for a +/// reclaimable entry for a new ping. Probing only happens once the cache is +/// full. The chance of hitting at least one timed-out (evictable) slot is +/// `1 - (1 - f)^MAX_PING_PROBES`, where `f` is the fraction of entries that +/// have timed out. E.g. with `f = 0.5` that is `1 - 0.5^8` ~ 99.6%. +const MAX_PING_PROBES: usize = 8; + impl Ping { pub fn new(token: [u8; N], keypair: &Keypair) -> Self { let signature = keypair.sign_message(&token); @@ -156,15 +166,23 @@ impl Signable for Pong { } impl PingCache { - pub fn new(ttl: Duration, rate_limit_delay: Duration, cap: usize) -> Self { - // Sanity check ttl/rate_limit_delay - assert!(rate_limit_delay <= ttl / 2); + pub fn new(ttl: Duration, outstanding_ping_timeout_ms: Range, max_pings: usize) -> Self { + assert!( + outstanding_ping_timeout_ms.start < outstanding_ping_timeout_ms.end, + "outstanding_ping_timeout_ms must be non-empty" + ); + assert!( + outstanding_ping_timeout_ms.end <= (ttl / 2).as_millis() as u64, + "outstanding_ping_timeout_ms.end must be <= ttl/2" + ); + assert!(max_pings > 0, "Must cache nonzero amount of hosts"); Self { ttl, - rate_limit_delay, - pings: LruCache::new(cap), - pongs: LruCache::new(cap), - ping_times: LruCache::new(cap), + outstanding_ping_timeout_ms, + max_pings, + pings: IndexMap::with_capacity(max_pings), + pongs: LruCache::new(max_pings), + ping_times: LruCache::new(max_pings), } } @@ -173,10 +191,18 @@ impl PingCache { /// Note: Does not verify the signature. pub fn add(&mut self, pong: &Pong, socket: SocketAddr, now: Instant) -> bool { let remote_node = (pong.pubkey(), socket); - if !matches!(self.pings.peek(&remote_node), Some((_, h)) if *h == pong.hash) { + // We can not just pop an entry from self.pings based on remote_node + // contents - that value is attacker controlled and could invalidate an + // in-flight ping. + let Some((index, _, (_timeout, hash))) = self.pings.get_full(&remote_node) else { return false; }; - self.pings.pop(&remote_node); + // check only hash, a late Pong is still perfectly valid. + if *hash != pong.hash { + return false; + } + // at this point we are certain the pong is valid. + self.pings.swap_remove_index(index); self.pongs.put(remote_node, now); if let Some(sent_time) = self.ping_times.pop(&socket.ip()) && should_report_message_signature( @@ -204,12 +230,39 @@ impl PingCache { now: Instant, remote_node: (Pubkey, SocketAddr), ) -> Option> { - // Rate limit consecutive pings sent to a remote node. - if matches!(self.pings.peek(&remote_node), - Some((t, _)) if now.saturating_duration_since(*t) < self.rate_limit_delay) - { - return None; + // If the existing ping is still in-flight don't send another one. + let is_new_key = if let Some((expiry, _)) = self.pings.get(&remote_node) { + if now < *expiry { + return None; + } + false // existing entry will be updated in-place + } else { + true // no entry for this node yet + }; + + // If this is a new entry and the pings store is at capacity, + // probe random existing entries and evict the first timed-out one + // (expiry in the past, peer never responded). + // Decline if all probes are in-flight — avoids evicting challenges + // still awaiting a Pong. + if is_new_key && self.pings.len() >= self.max_pings { + let n = self.pings.len(); + let mut evicted = false; + for _ in 0..MAX_PING_PROBES { + let idx = rng.random_range(0..n); + if let Some((_, (expiry, _))) = self.pings.get_index(idx) + && now >= *expiry + { + self.pings.swap_remove_index(idx); + evicted = true; + break; + } + } + if !evicted { + return None; + } } + let token = { let mut token = [0u8; N]; const FILL: usize = std::mem::size_of::(); @@ -220,9 +273,14 @@ impl PingCache { .expect("token is known to fit FILL bytes") = entropy; token }; + // Deadline by which we expect a reply. Randomized to stagger expiries across entries. + let expiry = now + + Duration::from_millis(rng.random_range( + self.outstanding_ping_timeout_ms.start..self.outstanding_ping_timeout_ms.end, + )); // The hash we expect to see in the Pong message let ping_hash = hash_ping_token(&token); - self.pings.put(remote_node, (now, ping_hash)); + self.pings.insert(remote_node, (expiry, ping_hash)); self.ping_times.put(remote_node.1.ip(), Instant::now()); Some(Ping::new(token, keypair)) } @@ -277,6 +335,9 @@ fn hash_ping_token(token: &[u8; N]) -> Hash { mod tests { use { super::*, + crate::cluster_info::{ + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, GOSSIP_PING_CACHE_TTL, + }, std::{ collections::HashSet, iter::repeat_with, @@ -307,7 +368,8 @@ mod tests { let mut rng = rand::rng(); let ttl = Duration::from_millis(256); let delay = ttl / 64; - let mut cache = PingCache::new(ttl, delay, /*cap=*/ 1000); + let delay_ms = delay.as_millis() as u64; + let mut cache = PingCache::new(ttl, delay_ms..delay_ms + 1, /*cap=*/ 1000); let this_node = Keypair::new(); let sockets: Vec<_> = (1u8..=3) .map(|i| { @@ -465,6 +527,113 @@ mod tests { } } + #[test] + fn test_ping_cache_full_no_stale() { + // Verify that when the pings cache is at capacity and all entries are + // fresh, new pings are declined rather than evicting in-flight ones. + let mut rng = rand::rng(); + let this_node = Keypair::new(); + let cap = 3usize; + let mut cache = PingCache::<32>::new( + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + cap, + ); + let sockets: Vec = (1u8..=4) + .map(|i| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(i, i, i, i), 8000))) + .collect(); + let keypairs: Vec = (0..4).map(|_| Keypair::new()).collect(); + + // Fill cache to capacity (3 entries) + let mut pings = Vec::new(); + for i in 0..cap { + let node = (keypairs[i].pubkey(), sockets[i]); + let (check, ping) = cache.check(&mut rng, &this_node, Instant::now(), node); + assert!(!check, "No pong yet, check should be false"); + assert!(ping.is_some(), "Should issue ping for entry {i}"); + pings.push((i, ping.unwrap())); + } + assert_eq!(cache.pings.len(), cap, "Cache should be at capacity"); + + // 4th new node must be declined — cache full, no stale entries + let node4 = (keypairs[3].pubkey(), sockets[3]); + let (check, ping) = cache.check(&mut rng, &this_node, Instant::now(), node4); + assert!(!check, "No pong, check should be false"); + assert!( + ping.is_none(), + "Must decline new ping when cache is full and no stale entries" + ); + + // Complete handshake for node 0 — frees one slot + let (idx0, ping0) = &pings[0]; + let pong0 = Pong::new(ping0, &keypairs[*idx0]); + assert!( + cache.add(&pong0, sockets[0], Instant::now()), + "Valid pong should be accepted" + ); + assert_eq!( + cache.pings.len(), + cap - 1, + "One slot should have been freed" + ); + + // Now 4th node should get a ping + let (check, ping) = cache.check(&mut rng, &this_node, Instant::now(), node4); + assert!(!check, "No pong for node4 yet"); + assert!( + ping.is_some(), + "Should issue ping for node4 after slot freed" + ); + } + + #[test] + fn test_ping_cache_full_with_stale() { + // Verify that when the pings cache is at capacity and entries are timed + // out (age >= outstanding_ping_timeout, peer never responded), a new + // ping reclaims a stale slot. + let mut rng = rand::rng(); + let this_node = Keypair::new(); + let cap = 3usize; + let mut cache = PingCache::<32>::new( + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + cap, + ); + let sockets: Vec = (1u8..=4) + .map(|i| SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(i, i, i, i), 8000))) + .collect(); + let keypairs: Vec = (0..4).map(|_| Keypair::new()).collect(); + let now = Instant::now(); + + // Fill cache to capacity with nodes that won't answer pongs + for i in 0..cap { + let node = (keypairs[i].pubkey(), sockets[i]); + let (_, ping) = cache.check(&mut rng, &this_node, now, node); + assert!(ping.is_some(), "Should issue ping for entry {i}"); + } + assert_eq!(cache.pings.len(), cap, "Cache should be at capacity"); + + // Advance time so all in-flight pings are now stale + let expired = + now + Duration::from_millis(GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS.end + 1); + + // 4th node should get a ping by reclaiming a stale slot + let node4 = (keypairs[3].pubkey(), sockets[3]); + // The 8-probe eviction is normally probabilistic; but with all entries + // stale, we expect it to always succeed. + let (check, ping) = cache.check(&mut rng, &this_node, expired, node4); + assert!(!check, "No pong for node4"); + assert!( + ping.is_some(), + "Should issue ping for node4 by reclaiming a stale entry" + ); + assert_eq!( + cache.pings.len(), + cap, + "Net size unchanged: one evicted, one inserted" + ); + } + #[test] fn test_expired_pong_returns_check_false() { let mut rng = rand::rng(); @@ -472,10 +641,12 @@ mod tests { let remote_socket = SocketAddr::V4(SocketAddrV4::new(Ipv4Addr::new(10, 10, 10, 10), 8000)); let remote_node_keypair = Keypair::new(); let remote_node = (remote_node_keypair.pubkey(), remote_socket); - let ttl = Duration::from_secs(20 * 60); // 20 minutes - let delay = ttl / 64; let mut now = Instant::now(); - let mut cache = PingCache::<32>::new(ttl, delay, /*cap=*/ 1000); + let mut cache = PingCache::<32>::new( + GOSSIP_PING_CACHE_TTL, + GOSSIP_PING_CACHE_OUTSTANDING_PING_TIMEOUT_MS, + /*cap=*/ 1000, + ); // Add a pong for the remote node cache.mock_pong(remote_node.0, remote_node.1, now); @@ -486,7 +657,7 @@ mod tests { assert!(ping.is_none(), "Should not generate ping for recent pong"); // Advance time past TTL to expire the pong - now = now + ttl + Duration::from_secs(1); + now = now + GOSSIP_PING_CACHE_TTL + Duration::from_secs(1); // After expiration, check should return false but should_ping should be true (to re-verify) let (check, ping) = cache.check(&mut rng, &this_node, now, remote_node); diff --git a/gossip/tests/crds_gossip.rs b/gossip/tests/crds_gossip.rs index bc04eae01c2..36d9f83e352 100644 --- a/gossip/tests/crds_gossip.rs +++ b/gossip/tests/crds_gossip.rs @@ -661,9 +661,9 @@ fn build_gossip_thread_pool() -> ThreadPool { fn new_ping_cache() -> Mutex { let ping_cache = PingCache::new( - Duration::from_secs(20 * 60), // ttl - Duration::from_secs(20 * 60) / 64, // rate_limit_delay - 2048, // capacity + Duration::from_secs(20 * 60), + 1000..2000, + 2048, // capacity ); Mutex::new(ping_cache) } From 8d0ba3574154d8026187f3b51fb5d22383460665 Mon Sep 17 00:00:00 2001 From: Eric Semeniuc <3838856+esemeniuc@users.noreply.github.com> Date: Thu, 16 Jul 2026 12:31:52 -0500 Subject: [PATCH 03/30] Optimize status cache signature storage (#13300) add --- CHANGELOG.md | 3 ++ runtime/src/bank.rs | 35 ++++++++++++------ runtime/src/bank/tests.rs | 52 +++++++++++++++++++++++++++ runtime/src/runtime_config.rs | 3 ++ test-validator/src/lib.rs | 1 + validator/src/commands/run/execute.rs | 18 +++++++--- 6 files changed, 98 insertions(+), 14 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb175e7930..966681cede3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,9 @@ Release channels have their own copy of this changelog: * `--disable-banking-trace` is now deprecated and a no-op (banking trace is disabled by default). The flag is still accepted for backward compatibility. #### Changes +* Validators running without `--full-rpc-api` and with snapshot generation disabled no longer + store transaction signature keys in the status cache. Message hashes remain cached for duplicate + transaction detection. ### SDK #### Breaking * solana-program-test: syscall getters (e.g. `Rent::get()`, `Clock::get()`) and `solana_sysvar::get_sysvar()` now return diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 2470dcfccb0..5c395f22ffa 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -640,6 +640,7 @@ impl PartialEq for Bank { let Self { rc: _, status_cache: _, + store_transaction_signatures_in_status_cache, blockhash_queue, max_processing_age, partitioned_rewards_stake_account_stores_per_block, @@ -712,7 +713,9 @@ impl PartialEq for Bank { // Adding ".." will remove compile-time checks that if a new field // is added to the struct, this PartialEq is accordingly updated. } = self; - *blockhash_queue.read().unwrap() == *other.blockhash_queue.read().unwrap() + *store_transaction_signatures_in_status_cache + == other.store_transaction_signatures_in_status_cache + && *blockhash_queue.read().unwrap() == *other.blockhash_queue.read().unwrap() && *max_processing_age == other.max_processing_age && *partitioned_rewards_stake_account_stores_per_block == other.partitioned_rewards_stake_account_stores_per_block @@ -863,6 +866,9 @@ pub struct Bank { /// A cache of signature statuses pub status_cache: Arc>, + /// Derived from RuntimeConfig::skip_transaction_signatures_in_status_cache. + store_transaction_signatures_in_status_cache: bool, + /// FIFO queue of `recent_blockhash` items blockhash_queue: RwLock, @@ -1214,6 +1220,8 @@ impl Bank { let mut bank = Self { rc: BankRc::new(accounts), status_cache: Arc::>::default(), + store_transaction_signatures_in_status_cache: !RuntimeConfig::default() + .skip_transaction_signatures_in_status_cache, blockhash_queue: RwLock::::default(), max_processing_age: MAX_PROCESSING_AGE, partitioned_rewards_stake_account_stores_per_block, @@ -1319,6 +1327,8 @@ impl Bank { let mut bank = Self::default_with_accounts(accounts); bank.ancestors = Ancestors::from(vec![bank.slot()]); bank.compute_budget = runtime_config.compute_budget; + bank.store_transaction_signatures_in_status_cache = + !runtime_config.skip_transaction_signatures_in_status_cache; if let Some(compute_budget) = &bank.compute_budget { bank.transaction_processor .set_execution_cost(compute_budget.to_cost()); @@ -1458,6 +1468,8 @@ impl Bank { let mut new = Self { rc, status_cache, + store_transaction_signatures_in_status_cache: parent + .store_transaction_signatures_in_status_cache, slot, bank_id, epoch, @@ -2133,6 +2145,8 @@ impl Bank { let mut bank = Self { rc: bank_rc, status_cache: Arc::>::default(), + store_transaction_signatures_in_status_cache: !runtime_config + .skip_transaction_signatures_in_status_cache, blockhash_queue: RwLock::new(fields.blockhash_queue), max_processing_age: MAX_PROCESSING_AGE, partitioned_rewards_stake_account_stores_per_block, @@ -3542,15 +3556,16 @@ impl Bank { self.slot(), processed_tx.status(), ); - // Add the transaction signature to the status cache so that transaction status - // can be queried by transaction signature over RPC. In the future, this should - // only be added for API nodes because voting validators don't need to do this. - status_cache.insert( - tx.recent_blockhash(), - tx.signature(), - self.slot(), - processed_tx.status(), - ); + if self.store_transaction_signatures_in_status_cache { + // Add the transaction signature to the status cache so that transaction + // status can be queried by transaction signature over RPC. + status_cache.insert( + tx.recent_blockhash(), + tx.signature(), + self.slot(), + processed_tx.status(), + ); + } } } } diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index aad6e5dcc65..d03e2570026 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -2231,6 +2231,58 @@ fn test_tx_already_processed() { ); } +#[test] +fn test_status_cache_signature_storage_config() { + let (genesis_config, mint_keypair) = create_genesis_config(LAMPORTS_PER_SOL); + let amount = genesis_config.rent.minimum_balance(0); + + let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let tx = system_transaction::transfer( + &mint_keypair, + &Keypair::new().pubkey(), + amount, + genesis_config.hash(), + ); + assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_signature_status(&tx.signatures[0]), Some(Ok(()))); + + let mut signature_skipping_bank = Bank::new_from_genesis( + &genesis_config, + Arc::new(RuntimeConfig { + skip_transaction_signatures_in_status_cache: true, + ..RuntimeConfig::default() + }), + vec![], + None, + BankTestConfig::default().accounts_db_config, + None, + None, + Arc::default(), + None, + None, + ); + signature_skipping_bank.set_fee_structure(&FeeStructure { + lamports_per_signature: genesis_config.fee_rate_governor.lamports_per_signature, + ..FeeStructure::default() + }); + let (bank, _bank_forks) = signature_skipping_bank.wrap_with_bank_forks_for_tests(); + + let mut tx = system_transaction::transfer( + &mint_keypair, + &Keypair::new().pubkey(), + amount, + genesis_config.hash(), + ); + assert_eq!(bank.process_transaction(&tx), Ok(())); + assert_eq!(bank.get_signature_status(&tx.signatures[0]), None); + + tx.signatures[0] = Signature::default(); + assert_eq!( + bank.process_transaction(&tx), + Err(TransactionError::AlreadyProcessed) + ); +} + /// Verifies that last ids and status cache are correctly referenced from parent #[test] fn test_bank_parent_already_processed() { diff --git a/runtime/src/runtime_config.rs b/runtime/src/runtime_config.rs index 71cf0c2498f..4ec35b5f071 100644 --- a/runtime/src/runtime_config.rs +++ b/runtime/src/runtime_config.rs @@ -6,4 +6,7 @@ pub struct RuntimeConfig { pub compute_budget: Option, pub log_messages_bytes_limit: Option, pub transaction_account_lock_limit: Option, + /// When true, skip storing transaction signature keys in the status cache. + /// Message hash keys are still stored for duplicate transaction detection. + pub skip_transaction_signatures_in_status_cache: bool, } diff --git a/test-validator/src/lib.rs b/test-validator/src/lib.rs index 708903533d8..d98ce7652a8 100644 --- a/test-validator/src/lib.rs +++ b/test-validator/src/lib.rs @@ -1170,6 +1170,7 @@ impl TestValidator { }), log_messages_bytes_limit: config.log_messages_bytes_limit, transaction_account_lock_limit: config.transaction_account_lock_limit, + ..RuntimeConfig::default() }; let mut validator_config = ValidatorConfig { diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index afba484fe6e..7f1ecd2e413 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -758,6 +758,15 @@ pub fn execute( UseSnapshotArchivesAtStartup ); + let skip_transaction_signatures_in_status_cache = + !run_args.json_rpc_config.full_api && !snapshot_config.should_generate_snapshots(); + if skip_transaction_signatures_in_status_cache { + info!( + "Transaction signatures will not be stored in the status cache because full RPC and \ + snapshot generation are disabled" + ); + } + let mut validator_config = ValidatorConfig { log_config, require_tower: matches.is_present("require_tower"), @@ -774,6 +783,11 @@ pub fn execute( .map(|s| Hash::from_str(s).unwrap()), expected_shred_version, new_hard_forks: hardforks_of(matches, "hard_forks"), + runtime_config: RuntimeConfig { + log_messages_bytes_limit: value_of(matches, "log_messages_bytes_limit"), + skip_transaction_signatures_in_status_cache, + ..RuntimeConfig::default() + }, rpc_config: run_args.json_rpc_config, on_start_geyser_plugin_config_files, geyser_plugin_always_enabled: matches.is_present("geyser_plugin_always_enabled"), @@ -826,10 +840,6 @@ pub fn execute( snapshot_config, no_wait_for_vote_to_start_leader: matches.is_present("no_wait_for_vote_to_start_leader"), wait_to_vote_slot: None, - runtime_config: RuntimeConfig { - log_messages_bytes_limit: value_of(matches, "log_messages_bytes_limit"), - ..RuntimeConfig::default() - }, staked_nodes_overrides: staked_nodes_overrides.clone(), use_snapshot_archives_at_startup, ip_echo_server_threads, From c40c5f7d1d693428140154d5672bf147ecb7f64f Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Thu, 16 Jul 2026 21:47:02 +0200 Subject: [PATCH 04/30] bls-sigverifier: improves keep_vote() (#13891) --- bls-sigverify/src/bls_sigverifier.rs | 134 +++++++++++++++--------- bls-sigverify/src/bls_vote_sigverify.rs | 9 +- bls-sigverify/src/stats.rs | 13 ++- 3 files changed, 93 insertions(+), 63 deletions(-) diff --git a/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index 209251428e1..dd35a525fa4 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -19,23 +19,21 @@ use { unverified_vote_message::{ DecodedWireConsensusMessage, UnverifiedCertificate, UnverifiedVoteMessage, }, - vote::Vote, wire::{VersionedWireConsensusMessage, VotePayloadToSign}, }, crossbeam_channel::{Receiver, Sender, TryRecvError, select}, log::error, rayon::{ThreadPool, ThreadPoolBuilder}, - solana_bls_signatures::pubkey::{PopVerified, PubkeyAffine as BlsPubkeyAffine}, - solana_clock::Slot, + solana_clock::{Epoch, Slot}, solana_gossip::cluster_info::ClusterInfo, solana_ledger::leader_schedule_cache::LeaderScheduleCache, solana_measure::measure_us, solana_perf::packet::packet_config, solana_pubkey::Pubkey, - solana_runtime::{bank::Bank, bank_forks::SharableBanks}, + solana_runtime::{bank::Bank, bank_forks::SharableBanks, epoch_stakes::BLSPubkeyToRankMap}, solana_streamer::{nonblocking::simple_qos::SimpleQosBanlist, packet::PacketBatch}, std::{ - collections::{HashMap, HashSet}, + collections::{HashMap, HashSet, hash_map::Entry}, sync::{ Arc, atomic::{AtomicBool, Ordering}, @@ -91,6 +89,11 @@ pub fn spawn_service( .unwrap() } +struct ExtractedMsgs { + certs: HashMap>, + votes: HashMap>, +} + struct SigVerifier { migration_status: Arc, banlist: Arc, @@ -102,11 +105,13 @@ struct SigVerifier { verified_certs: HashSet, /// Tracks when the cache was last pruned. last_checked_root_slot: Slot, + last_checked_root_epoch: Epoch, cluster_info: Arc, leader_schedule: Arc, /// thread pool to use for all parallel tasks thread_pool: ThreadPool, generated_cert_types: Arc, + rank_map_cache: HashMap>, } impl SigVerifier { @@ -134,10 +139,12 @@ impl SigVerifier { stats: SigVerifierStats::new(root_slot), verified_certs: HashSet::new(), last_checked_root_slot: 0, + last_checked_root_epoch: 0, cluster_info, leader_schedule, thread_pool, generated_cert_types, + rank_map_cache: HashMap::new(), } } @@ -184,9 +191,9 @@ impl SigVerifier { certificates: Vec<(Slot, UnverifiedCertificate)>, ) -> Result<(), SigVerifyError> { let root_bank = self.sharable_banks.root(); - self.maybe_prune_caches(root_bank.slot()); + self.maybe_prune_caches(&root_bank); - let ((cert_groups, votes_to_verify), extract_msgs_us) = + let (extracted_msgs, extract_msgs_us) = measure_us!(self.extract_and_filter_msgs(batches, certificates, &root_bank)); self.stats .extract_filter_msgs_us @@ -195,7 +202,7 @@ impl SigVerifier { let (votes_result, certs_result) = self.thread_pool.join( || { verify_and_send_votes( - votes_to_verify, + extracted_msgs.votes, &root_bank, &self.cluster_info, &self.leader_schedule, @@ -207,7 +214,7 @@ impl SigVerifier { || { verify_and_send_certificates( &mut self.verified_certs, - cert_groups, + extracted_msgs.certs, &root_bank, &self.channels.channel_to_pool, &self.banlist, @@ -224,11 +231,19 @@ impl SigVerifier { Ok(()) } - fn maybe_prune_caches(&mut self, root_slot: Slot) { + fn maybe_prune_caches(&mut self, root_bank: &Bank) { + let root_slot = root_bank.slot(); + let root_epoch = root_bank.epoch(); if self.last_checked_root_slot < root_slot { self.last_checked_root_slot = root_slot; self.verified_certs.retain(|cert| cert.slot() >= root_slot); } + if self.last_checked_root_epoch < root_epoch { + self.last_checked_root_epoch = root_epoch; + // Keeping previous epoch as we need to look up slots older than root_slot for rewards. + self.rank_map_cache + .retain(|epoch, _| *epoch >= root_epoch.saturating_sub(1)); + } } fn add_certificate_to_group( @@ -259,10 +274,7 @@ impl SigVerifier { batches: Vec, certificates: Vec<(Slot, UnverifiedCertificate)>, root_bank: &Bank, - ) -> ( - HashMap>, - HashMap>, - ) { + ) -> ExtractedMsgs { let root_slot = root_bank.slot(); let mut cert_groups = HashMap::>::new(); let mut votes: HashMap> = HashMap::new(); @@ -295,21 +307,14 @@ impl SigVerifier { match decoded_msg { DecodedWireConsensusMessage::Vote(unverified_vote) => { - if let Some((sender_vote_account_pubkey, sender_bls_pubkey)) = - self.keep_vote(&unverified_vote.vote, &unverified_vote, root_bank) + if let Some(payload) = + self.keep_vote(unverified_vote, sender_identity_pubkey, root_bank) { let vote_payload_to_sign = VotePayloadToSign::new_from_vote( - unverified_vote.vote, - unverified_vote.shred_version, - ); - votes.entry(vote_payload_to_sign).or_default().push( - UnverifiedVotePayload { - vote_message: unverified_vote, - sender_bls_pubkey, - sender_vote_account_pubkey, - sender_identity_pubkey, - }, + payload.vote_message.vote, + payload.vote_message.shred_version, ); + votes.entry(vote_payload_to_sign).or_default().push(payload); } } DecodedWireConsensusMessage::Certificate(cert) => { @@ -350,39 +355,68 @@ impl SigVerifier { self.add_certificate_to_group(&mut cert_groups, certificate, sender_identity_pubkey); } self.stats.num_pkts.add_sample(num_pkts); - (cert_groups, votes) + ExtractedMsgs { + certs: cert_groups, + votes, + } } - /// If this vote should be verified, then returns the sender's Pubkey and BlsPubkey. + /// If this vote should be verified, then returns the [`UnverifiedVotePayload`]. fn keep_vote( &mut self, - vote: &Vote, - msg: &UnverifiedVoteMessage, + msg: UnverifiedVoteMessage, + sender_identity_pubkey: Pubkey, root_bank: &Bank, - ) -> Option<(Pubkey, PopVerified)> { + ) -> Option { let root_slot = root_bank.slot(); - let Some(rank_map) = root_bank.get_rank_map(vote.slot()) else { - self.stats.discard_vote_no_epoch_stakes += 1; + let vote_slot = msg.vote.slot(); + if vote_slot > root_slot.saturating_add(NUM_SLOTS_FOR_VERIFY) { + self.stats.vote_too_far_in_future += 1; + return None; + } + if vote_slot <= root_slot + && !rewards_wants_vote( + &self.cluster_info, + &self.leader_schedule, + root_slot, + &msg.vote, + ) + { + self.stats.num_old_votes_received += 1; return None; + } + // Genesis votes should be allowed on the TowerBFT root + if vote_slot == root_slot && !msg.vote.is_genesis_vote() { + self.stats.num_old_votes_received += 1; + return None; + } + let vote_epoch = root_bank.epoch_schedule().get_epoch(vote_slot); + let rank_map = match self.rank_map_cache.entry(vote_epoch) { + Entry::Occupied(entry) => entry.into_mut(), + Entry::Vacant(entry) => { + let Some(rank_map) = root_bank.get_rank_map(vote_slot) else { + self.stats.discard_vote_no_epoch_stakes += 1; + return None; + }; + entry.insert(rank_map.clone()) + } }; let entry = rank_map - .get_pubkey_stake_entry(msg.rank.into()) + .node_pubkey_to_stake_entry(&sender_identity_pubkey) .or_else(|| { self.stats.discard_vote_invalid_rank += 1; None })?; - let ret = Some((entry.vote_account_pubkey, entry.bls_pubkey)); - if vote.slot() > root_slot - // Genesis votes should be allowed on the TowerBFT root - || (vote.is_genesis_vote() && vote.slot() >= root_slot) - { - return ret; - } - if rewards_wants_vote(&self.cluster_info, &self.leader_schedule, root_slot, vote) { - return ret; + let rank = rank_map.get_rank_for_vote_pubkey(&entry.vote_account_pubkey)?; + if *rank != msg.rank { + return None; } - self.stats.num_old_votes_received += 1; - None + Some(UnverifiedVotePayload { + vote_message: msg, + sender_bls_pubkey: entry.bls_pubkey, + sender_vote_account_pubkey: entry.vote_account_pubkey, + sender_identity_pubkey, + }) } } @@ -669,11 +703,11 @@ mod tests { Bank::new_from_parent(ctx.verifier.sharable_banks.root(), SlotLeader::default(), 5); ctx.verifier.migration_status.enable_alpenglow_for_tests(); - let (cert_groups, votes) = + let extracted_msgs = ctx.verifier .extract_and_filter_msgs(vec![], vec![certificate], &root_bank); - assert!(cert_groups.is_empty()); - assert!(votes.is_empty()); + assert!(extracted_msgs.certs.is_empty()); + assert!(extracted_msgs.votes.is_empty()); assert_eq!(ctx.verifier.stats.num_old_certs_received.0, 1); } @@ -826,7 +860,7 @@ mod tests { )) .unwrap(); - assert_eq!(ctx.verifier.stats.discard_vote_no_epoch_stakes.0, 1); + assert_eq!(ctx.verifier.stats.vote_too_far_in_future.0, 1); // Expect no messages since the packet was malformed expect_no_receive(&ctx.pool_receiver); @@ -2072,7 +2106,7 @@ mod tests { .verify_and_send_batches(packet_batches) .unwrap(); assert_eq!(ctx.verifier.stats.cert_stats.too_far_in_future.0, 1); - assert_eq!(ctx.verifier.stats.vote_stats.too_far_in_future.0, 1); + assert_eq!(ctx.verifier.stats.vote_too_far_in_future.0, 1); } fn messages_to_batches( diff --git a/bls-sigverify/src/bls_vote_sigverify.rs b/bls-sigverify/src/bls_vote_sigverify.rs index 7669da79611..48b681192e6 100644 --- a/bls-sigverify/src/bls_vote_sigverify.rs +++ b/bls-sigverify/src/bls_vote_sigverify.rs @@ -208,12 +208,9 @@ fn verify_votes( banlist: &SimpleQosBanlist, thread_pool: &ThreadPool, ) -> Vec { - // Filter votes too far in the future. - if vote_payload_to_sign.slot() > root_bank.slot().saturating_add(NUM_SLOTS_FOR_VERIFY) { - stats.too_far_in_future += unverified_votes.len() as u64; - return vec![]; - } - + debug_assert!( + vote_payload_to_sign.slot() <= root_bank.slot().saturating_add(NUM_SLOTS_FOR_VERIFY) + ); // Fallback to individual verification let ((verified_votes, invalid_remote_pubkeys), time_us) = measure_us!(verify_individual_votes(unverified_votes, thread_pool)); diff --git a/bls-sigverify/src/stats.rs b/bls-sigverify/src/stats.rs index 8757037f540..f3c400b4bde 100644 --- a/bls-sigverify/src/stats.rs +++ b/bls-sigverify/src/stats.rs @@ -68,8 +68,10 @@ pub(super) struct SigVerifierStats { pub(super) num_verified_certs_received: Saturating, /// Number of certs received that the node has already generated. pub(super) num_generated_certs_received: Saturating, + /// Number of times a vote was too far in the future and discarded. + pub(super) vote_too_far_in_future: Saturating, /// Last time the stats were reported. - pub(super) last_report: Reporting, + last_report: Reporting, } impl SigVerifierStats { @@ -88,6 +90,7 @@ impl SigVerifierStats { num_verified_certs_received: Saturating(0), num_generated_certs_received: Saturating(0), verify_and_send_batch_us: WelfordStats::default(), + vote_too_far_in_future: Saturating(0), last_report: Reporting::new(root_slot), } } @@ -118,6 +121,7 @@ impl SigVerifierStats { discard_vote_invalid_rank, discard_vote_no_epoch_stakes, verify_and_send_batch_us, + vote_too_far_in_future, last_report: _, } = self; @@ -175,6 +179,7 @@ impl SigVerifierStats { verify_and_send_batch_us.count(), i64 ), + ("vote_too_far_in_future", vote_too_far_in_future.0, i64), ("num_pkts_max", num_pkts.maximum().unwrap_or(0), i64), ("num_pkts_mean", num_pkts.mean().unwrap_or(0), i64), ("num_pkts_count", num_pkts.count(), i64), @@ -307,8 +312,6 @@ pub(super) struct SigVerifyVoteStats { /// Number of votes [`verify_and_send_votes`] successfully verified the signature of. pub(super) sig_verified_votes: Saturating, - /// Number of times the cert was too far in the future and discarded. - pub(super) too_far_in_future: Saturating, /// Number of times we are banning a validator that was already banned. pub(super) already_banned: Saturating, /// Number of times we are banning a validator. @@ -347,7 +350,6 @@ impl SigVerifyVoteStats { let Self { votes_to_sig_verify, sig_verified_votes, - too_far_in_future, already_banned, banning_validator, metrics_sent, @@ -365,7 +367,6 @@ impl SigVerifyVoteStats { } = other; self.votes_to_sig_verify += votes_to_sig_verify; self.sig_verified_votes += sig_verified_votes; - self.too_far_in_future += too_far_in_future; self.already_banned += already_banned; self.banning_validator += banning_validator; self.metrics_sent += metrics_sent; @@ -388,7 +389,6 @@ impl SigVerifyVoteStats { let Self { votes_to_sig_verify, sig_verified_votes, - too_far_in_future, already_banned, banning_validator, metrics_sent, @@ -408,7 +408,6 @@ impl SigVerifyVoteStats { "bls_vote_sigverify_stats", ("votes_to_sig_verify", votes_to_sig_verify.0, i64), ("sig_verified_votes", sig_verified_votes.0, i64), - ("too_far_in_future", too_far_in_future.0, i64), ("already_banned", already_banned.0, i64), ("banning_validator", banning_validator.0, i64), ("metrics_sent", metrics_sent.0, i64), From fbdcd41e08def72c4e1cb7ffc489b4e0c13fba49 Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Thu, 16 Jul 2026 12:47:25 -0700 Subject: [PATCH 05/30] Exclude tombstone accounts from full snapshot archives (#13851) * Exclude tombstone accounts from full snapshot archives * Updating to use Enum - Added new testcase --- accounts-db/src/account_storage_reader.rs | 170 +++++++++++++++------- runtime/src/serde_snapshot/tests.rs | 11 +- snapshots/src/archive.rs | 26 +++- 3 files changed, 142 insertions(+), 65 deletions(-) diff --git a/accounts-db/src/account_storage_reader.rs b/accounts-db/src/account_storage_reader.rs index ff57fccab0e..d03ba03bc8b 100644 --- a/accounts-db/src/account_storage_reader.rs +++ b/accounts-db/src/account_storage_reader.rs @@ -62,15 +62,24 @@ pub fn open_storage_files<'s>( .map(move |storage| storage.accounts.open_file_for_archive(use_direct_io)) } +/// Should tombstones be included or excluded when reading from storage? +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TombstonesFilter { + /// tombstones are included when reading from storage + Include, + /// tombstones are excluded when reading from storage + Exclude, +} + /// A wrapper type around `AccountStorageEntry` that implements the `Read` trait. /// This type skips over the data in accounts contained in the obsolete accounts -/// structure. +/// structure, and optionally over tombstone accounts as well. /// /// The caller is responsible for activating the storage's file on `file_reader` /// via `set_file` (typically using a file opened with [`open_storage_files`]) /// before constructing the reader. pub struct AccountStorageReader<'r, R> { - sorted_obsolete_accounts: Vec<(Offset, usize)>, + sorted_excluded_accounts: Vec<(Offset, usize)>, reader: &'r mut R, num_alive_bytes: usize, num_total_bytes: usize, @@ -78,35 +87,49 @@ pub struct AccountStorageReader<'r, R> { impl<'a, 'r, R: FileBufRead<'a>> AccountStorageReader<'r, R> { /// Creates a new `AccountStorageReader` from an `AccountStorageEntry`. - /// The obsolete accounts structure is sorted during initialization. + /// The excluded accounts list is sorted during initialization. /// /// Expects that the caller has already attached the storage's file to /// `file_reader` via `set_file`. pub fn new( storage: &AccountStorageEntry, snapshot_slot: Option, + tombstones_filter: TombstonesFilter, file_reader: &'r mut R, ) -> io::Result { let num_total_bytes = storage.accounts.len(); - let num_alive_bytes = num_total_bytes - storage.get_obsolete_bytes(snapshot_slot); + let mut num_alive_bytes = num_total_bytes - storage.get_obsolete_bytes(snapshot_slot); - let mut sorted_obsolete_accounts: Vec<_> = storage + let mut sorted_excluded_accounts: Vec<_> = storage .obsolete_accounts_read_lock() .filter_obsolete_accounts(snapshot_slot) .collect(); // Convert the length to the size - sorted_obsolete_accounts + sorted_excluded_accounts .iter_mut() .for_each(|(_offset, len)| { *len = storage.accounts.calculate_stored_size(*len); }); - sorted_obsolete_accounts + if tombstones_filter == TombstonesFilter::Exclude { + // Tombstones are zero-lamport accounts, which store no data, so every + // tombstone record has the fixed stored size of a data-less account. + let tombstone_stored_size = storage.accounts.calculate_stored_size(0); + let tombstone_offsets = storage.tombstone_offsets_read_lock(); + num_alive_bytes -= tombstone_offsets.len() * tombstone_stored_size; + sorted_excluded_accounts.extend( + tombstone_offsets + .iter() + .map(|offset| (*offset, tombstone_stored_size)), + ); + } + + sorted_excluded_accounts .sort_unstable_by(|(a_offset, _), (b_offset, _)| b_offset.cmp(a_offset)); Ok(Self { - sorted_obsolete_accounts, + sorted_excluded_accounts, reader: file_reader, num_alive_bytes, num_total_bytes, @@ -128,23 +151,23 @@ impl<'a, R: FileBufRead<'a>> Read for AccountStorageReader<'_, R> { let buf_len = buf.len(); while total_read < buf_len { - let next_obsolete_account = self.sorted_obsolete_accounts.last(); + let next_excluded_account = self.sorted_excluded_accounts.last(); let file_offset = self.reader.get_file_offset() as usize; - if let Some(&(obsolete_start, obsolete_size)) = next_obsolete_account - && file_offset == obsolete_start + if let Some(&(excluded_start, excluded_size)) = next_excluded_account + && file_offset == excluded_start { - let skip_len = obsolete_size.min(self.num_total_bytes - obsolete_start); + let skip_len = excluded_size.min(self.num_total_bytes - excluded_start); self.reader.consume_or_skip(skip_len); - self.sorted_obsolete_accounts.pop(); + self.sorted_excluded_accounts.pop(); continue; } // Cannot read beyond the end of the buffer let bytes_left_in_buffer = buf_len.saturating_sub(total_read); - // Cannot read beyond the next obsolete account or the end of the file - let bytes_to_read_from_file = if let Some((obsolete_start, _)) = next_obsolete_account { - obsolete_start.saturating_sub(file_offset) + // Cannot read beyond the next excluded account or the end of the file + let bytes_to_read_from_file = if let Some((excluded_start, _)) = next_excluded_account { + excluded_start.saturating_sub(file_offset) } else { self.num_total_bytes.saturating_sub(file_offset) }; @@ -227,30 +250,64 @@ mod tests { buf_reader .set_file(files[0].as_ref(), storage.accounts.len() as u64) .unwrap(); - let reader = AccountStorageReader::new(&storage, None, &mut buf_reader).unwrap(); + let reader = + AccountStorageReader::new(&storage, None, TombstonesFilter::Include, &mut buf_reader) + .unwrap(); assert_eq!(reader.len(), storage.accounts.len()); } - #[test_case(0, 0)] - #[test_case(1, 0)] - #[test_case(1, 1)] - #[test_case(100, 0)] - #[test_case(100, 10)] - #[test_case(100, 100)] - fn test_account_storage_reader_with_obsolete_accounts( + #[test_case(0, 0, 0, TombstonesFilter::Include)] + #[test_case(1, 0, 0, TombstonesFilter::Include)] + #[test_case(1, 1, 0, TombstonesFilter::Include)] + #[test_case(1, 1, 0, TombstonesFilter::Exclude)] + #[test_case(1, 0, 1, TombstonesFilter::Include)] + #[test_case(100, 0, 0, TombstonesFilter::Include)] + #[test_case(100, 0, 10, TombstonesFilter::Include)] + #[test_case(100, 0, 100, TombstonesFilter::Include)] + #[test_case(100, 10, 0, TombstonesFilter::Include)] + #[test_case(100, 10, 0, TombstonesFilter::Exclude)] + #[test_case(100, 100, 0, TombstonesFilter::Include)] + #[test_case(100, 100, 0, TombstonesFilter::Exclude)] + #[test_case(100, 10, 10, TombstonesFilter::Include)] + #[test_case(100, 10, 10, TombstonesFilter::Exclude)] + fn test_account_storage_reader_with_excluded_accounts( total_accounts: usize, - number_of_accounts_to_remove: usize, + num_tombstones: usize, + num_obsolete: usize, + tombstones_filter: TombstonesFilter, ) { let (storage, _temp_dirs) = create_storage_for_storage_reader(0, AccountsFileProvider::AppendVec); let slot = 0; + // Generate a seed from entropy and log the original seed + let seed: u64 = rand::random(); + dbg!("Generated seed: {seed}"); + + // Use a seedable RNG with the generated seed for reproducibility + let mut rng = StdRng::seed_from_u64(seed); + + // Choose disjoint random index sets for the tombstone and obsolete accounts. + // Tombstones must be chosen before writing because they are written as + // zero-lamport, data-less accounts. + let chosen_indexes = (0..total_accounts) + .collect::>() + .choose_multiple(&mut rng, num_tombstones + num_obsolete) + .cloned() + .collect::>(); + let (tombstone_indexes, obsolete_indexes) = chosen_indexes.split_at(num_tombstones); + // Create a bunch of accounts and add them to the storage - let accounts: Vec<_> = - iter::repeat_with(|| AccountSharedData::new(1, 10, &Pubkey::default())) - .take(total_accounts) - .collect(); + let accounts: Vec<_> = (0..total_accounts) + .map(|index| { + if tombstone_indexes.contains(&index) { + AccountSharedData::new(0, 0, &Pubkey::default()) + } else { + AccountSharedData::new(1, 10, &Pubkey::default()) + } + }) + .collect(); let accounts_to_append: Vec<_> = accounts .into_iter() @@ -259,36 +316,28 @@ mod tests { let offsets = storage .accounts - .write_accounts(&(slot, &accounts_to_append[..])); - - // Generate a seed from entropy and log the original seed - let seed: u64 = rand::random(); - info!("Generated seed: {seed}"); - - // Use a seedable RNG with the generated seed for reproducibility - let mut rng = StdRng::seed_from_u64(seed); - - let obsolete_account_offset = offsets - .map(|offsets| { - offsets - .offsets - .choose_multiple(&mut rng, number_of_accounts_to_remove) - .cloned() - .collect::>() - }) + .write_accounts(&(slot, &accounts_to_append[..])) + .map(|stored_accounts_info| stored_accounts_info.offsets) .unwrap_or_default(); - assert_eq!(obsolete_account_offset.len(), number_of_accounts_to_remove); + let tombstone_offsets: Vec<_> = tombstone_indexes + .iter() + .map(|index| offsets[*index]) + .collect(); + let obsolete_offsets: Vec<_> = obsolete_indexes + .iter() + .map(|index| offsets[*index]) + .collect(); + + storage.batch_insert_tombstone_offsets(tombstone_offsets); // Mark the obsolete accounts in storage - let data_lens = storage - .accounts - .get_account_data_lens(&obsolete_account_offset); + let data_lens = storage.accounts.get_account_data_lens(&obsolete_offsets); storage .obsolete_accounts() .write() .unwrap() - .mark_accounts_obsolete(obsolete_account_offset.into_iter().zip(data_lens), 0); + .mark_accounts_obsolete(obsolete_offsets.iter().copied().zip(data_lens), 0); let storage = storage.reopen_as_readonly().unwrap_or(storage); @@ -305,8 +354,14 @@ mod tests { file_reader .set_file(files[0].as_ref(), storage.accounts.len() as u64) .unwrap(); - let mut reader = AccountStorageReader::new(&storage, None, &mut file_reader).unwrap(); - let current_len = storage.accounts.len() - storage.get_obsolete_bytes(None); + let mut reader = + AccountStorageReader::new(&storage, None, tombstones_filter, &mut file_reader).unwrap(); + let mut number_of_accounts_to_remove = num_obsolete; + let mut current_len = storage.accounts.len() - storage.get_obsolete_bytes(None); + if tombstones_filter == TombstonesFilter::Exclude { + number_of_accounts_to_remove += num_tombstones; + current_len -= num_tombstones * storage.accounts.calculate_stored_size(0); + } assert_eq!(reader.len(), current_len); // Create a temporary directory and a file within it @@ -430,8 +485,13 @@ mod tests { file_reader .set_file(files[0].as_ref(), storage.accounts.len() as u64) .unwrap(); - let mut reader = - AccountStorageReader::new(&storage, Some(snapshot_slot), &mut file_reader).unwrap(); + let mut reader = AccountStorageReader::new( + &storage, + Some(snapshot_slot), + TombstonesFilter::Include, + &mut file_reader, + ) + .unwrap(); let current_len = storage.accounts.len() - storage.get_obsolete_bytes(Some(snapshot_slot)); assert_eq!(reader.len(), current_len); diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 7a335eb9140..3569fffc2d3 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -19,7 +19,7 @@ mod serde_snapshot_tests { account_storage::AccountStorageMap, account_storage_entry::AccountStorageEntry, account_storage_reader::{ - AccountStorageReader, open_storage_files, storage_file_buf_reader, + AccountStorageReader, TombstonesFilter, open_storage_files, storage_file_buf_reader, }, accounts::Accounts, accounts_db::{ @@ -128,8 +128,13 @@ mod serde_snapshot_tests { let file_name = AccountsFile::file_name(storage_entry.slot(), storage_entry.id()); let output_path = output_dir.as_ref().join(file_name); buf_reader.set_file(file.as_ref(), storage_entry.accounts.len() as u64)?; - let mut reader = - AccountStorageReader::new(storage_entry, None, &mut buf_reader).unwrap(); + let mut reader = AccountStorageReader::new( + storage_entry, + None, + TombstonesFilter::Include, + &mut buf_reader, + ) + .unwrap(); let mut writer = File::create(&output_path)?; io::copy(&mut reader, &mut writer)?; diff --git a/snapshots/src/archive.rs b/snapshots/src/archive.rs index 2759bec5d13..110deb0b749 100644 --- a/snapshots/src/archive.rs +++ b/snapshots/src/archive.rs @@ -12,8 +12,8 @@ use { account_storage::AccountStoragesOrderer, account_storage_entry::AccountStorageEntry, account_storage_reader::{ - ACCOUNT_STORAGE_MAX_BUFFER_SIZE, AccountStorageReader, open_storage_files, - storage_file_buf_reader, + ACCOUNT_STORAGE_MAX_BUFFER_SIZE, AccountStorageReader, TombstonesFilter, + open_storage_files, storage_file_buf_reader, }, accounts_file::AccountsFile, }, @@ -138,6 +138,14 @@ pub fn archive_snapshot( matches!(snapshot_archive_kind, SnapshotArchiveKind::Incremental(_)); let use_direct_io = io_setup.use_direct_io && !use_page_cache; + // Full snapshots do not need to persist tombstones as their older versions are + // guaranteed to be skipped as obsolete accounts + let tombstones_filter = if matches!(snapshot_archive_kind, SnapshotArchiveKind::Full) { + TombstonesFilter::Exclude + } else { + TombstonesFilter::Include + }; + // Walk storages and their (lazily-opened) file handles in chunks, // bounding how many archive-mode fds are simultaneously open. let mut storage_file_pairs = storages_orderer @@ -176,11 +184,15 @@ pub fn archive_snapshot( .map_err(|err| { E::AccountStorageReaderError(err, storage.path().to_path_buf()) })?; - let reader = - AccountStorageReader::new(storage, Some(snapshot_slot), &mut chunk_reader) - .map_err(|err| { - E::AccountStorageReaderError(err, storage.path().to_path_buf()) - })?; + let reader = AccountStorageReader::new( + storage, + Some(snapshot_slot), + tombstones_filter, + &mut chunk_reader, + ) + .map_err(|err| { + E::AccountStorageReaderError(err, storage.path().to_path_buf()) + })?; let mut header = tar::Header::new_gnu(); header.set_path(path_in_archive).map_err(|err| { E::ArchiveAccountStorageFile(err, storage.path().to_path_buf()) From 047a53f9dc68731c80dc22186e208836cba8cd4b Mon Sep 17 00:00:00 2001 From: Brooks Date: Thu, 16 Jul 2026 18:54:02 -0400 Subject: [PATCH 06/30] accounts-db: Adds accounts file provider to AccountsDbConfig (#13896) * Adds AccountsFileProvider to AccountsDbConfig * AccountsDb::new_with_config() gets accounts file provider from accounts_db_config * Removes accounts file provider param from AccountsDb test constructors --- accounts-db/src/accounts_db.rs | 26 +++---------------- .../src/accounts_db/accounts_db_config.rs | 4 +++ accounts-db/src/accounts_db/tests.rs | 17 +++++++----- ledger-tool/src/args.rs | 2 ++ validator/src/commands/run/execute.rs | 2 ++ 5 files changed, 22 insertions(+), 29 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index c72652a710d..17873b415b9 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -1146,7 +1146,7 @@ impl AccountsDb { dirty_stores: DashMap::default(), zero_lamport_accounts_to_purge_after_full_snapshot: DashSet::default(), latest_full_snapshot_slot_advanced_since_clean: AtomicBool::default(), - accounts_file_provider: AccountsFileProvider::default(), + accounts_file_provider: accounts_db_config.accounts_file_provider, latest_full_snapshot_slot: SeqLock::new(None), last_swept_full_snapshot_slot: AtomicU64::new(0), best_ancient_slots_to_shrink: RwLock::default(), @@ -6825,33 +6825,15 @@ impl AccountsDb { AccountsDb::new_for_tests(Vec::new()) } - pub fn new_single_for_tests_with_provider_and_config( - file_provider: AccountsFileProvider, - accounts_db_config: AccountsDbConfig, - ) -> Self { - AccountsDb::new_for_tests_with_provider_and_config( - Vec::new(), - file_provider, - accounts_db_config, - ) - } - pub fn new_for_tests(paths: Vec) -> Self { - Self::new_for_tests_with_provider_and_config( - paths, - AccountsFileProvider::default(), - ACCOUNTS_DB_CONFIG_FOR_TESTING, - ) + Self::new_for_tests_with_config(paths, ACCOUNTS_DB_CONFIG_FOR_TESTING) } - fn new_for_tests_with_provider_and_config( + pub fn new_for_tests_with_config( paths: Vec, - accounts_file_provider: AccountsFileProvider, accounts_db_config: AccountsDbConfig, ) -> Self { - let mut db = AccountsDb::new_with_config(paths, accounts_db_config, None, Arc::default()); - db.accounts_file_provider = accounts_file_provider; - db + Self::new_with_config(paths, accounts_db_config, None, Arc::default()) } /// Return the number of slots marked with uncleaned pubkeys. diff --git a/accounts-db/src/accounts_db/accounts_db_config.rs b/accounts-db/src/accounts_db/accounts_db_config.rs index 32927228b69..792d6dc14d4 100644 --- a/accounts-db/src/accounts_db/accounts_db_config.rs +++ b/accounts-db/src/accounts_db/accounts_db_config.rs @@ -1,6 +1,7 @@ use { super::{AccountShrinkThreshold, DEFAULT_ACCOUNTS_SHRINK_THRESHOLD_OPTION}, crate::{ + accounts_file::AccountsFileProvider, accounts_index::{ ACCOUNTS_INDEX_CONFIG_FOR_BENCHMARKS, ACCOUNTS_INDEX_CONFIG_FOR_TESTING, AccountSecondaryIndexes, AccountsIndexConfig, ScanFilter, @@ -41,6 +42,7 @@ pub struct AccountsDbConfig { pub num_background_threads: Option, /// Number of threads for foreground operations (`thread_pool_foreground`) pub num_foreground_threads: Option, + pub accounts_file_provider: AccountsFileProvider, } pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig { @@ -61,6 +63,7 @@ pub const ACCOUNTS_DB_CONFIG_FOR_TESTING: AccountsDbConfig = AccountsDbConfig { scan_filter_for_shrinking: ScanFilter::OnlyAbnormalTest, num_background_threads: None, num_foreground_threads: None, + accounts_file_provider: AccountsFileProvider::AppendVec, }; pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig { @@ -81,4 +84,5 @@ pub const ACCOUNTS_DB_CONFIG_FOR_BENCHMARKS: AccountsDbConfig = AccountsDbConfig scan_filter_for_shrinking: ScanFilter::OnlyAbnormal, num_background_threads: None, num_foreground_threads: None, + accounts_file_provider: AccountsFileProvider::AppendVec, }; diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 5abbd7a7228..813d89d6d9c 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -149,9 +149,12 @@ macro_rules! define_accounts_db_test { fn run_test($accounts_db: AccountsDb) { $inner } - let accounts_db = AccountsDb::new_single_for_tests_with_provider_and_config( - $accounts_file_provider, - ACCOUNTS_DB_CONFIG_FOR_TESTING, + let accounts_db = AccountsDb::new_for_tests_with_config( + Vec::new(), + AccountsDbConfig { + accounts_file_provider: $accounts_file_provider, + ..ACCOUNTS_DB_CONFIG_FOR_TESTING + }, ); run_test(accounts_db); }; @@ -640,8 +643,8 @@ fn test_flush_slots_with_reclaim_old_slots() { #[test] fn test_flush_defers_write_through_until_all_cached_slots_drop() { // Build an AccountsDb whose index has IndexLimit::Threshold so write-through is enabled. - let db = AccountsDb::new_single_for_tests_with_provider_and_config( - AccountsFileProvider::AppendVec, + let db = AccountsDb::new_for_tests_with_config( + Vec::new(), AccountsDbConfig { index: Some(AccountsIndexConfig { index_limit: IndexLimit::Threshold(IndexLimitThreshold { @@ -728,8 +731,8 @@ fn test_flush_does_not_write_through_when_write_through_disabled() { /// drops its last cached slot. #[test] fn test_purge_unrooted_slot_writes_through_surviving_entry() { - let db = AccountsDb::new_single_for_tests_with_provider_and_config( - AccountsFileProvider::AppendVec, + let db = AccountsDb::new_for_tests_with_config( + Vec::new(), AccountsDbConfig { index: Some(AccountsIndexConfig { index_limit: IndexLimit::Threshold(IndexLimitThreshold { diff --git a/ledger-tool/src/args.rs b/ledger-tool/src/args.rs index 9d17cac53b6..d116b61a40d 100644 --- a/ledger-tool/src/args.rs +++ b/ledger-tool/src/args.rs @@ -5,6 +5,7 @@ use { solana_account_decoder::{UiAccountEncoding, UiDataSliceConfig}, solana_accounts_db::{ accounts_db::{AccountShrinkThreshold, AccountsDbConfig}, + accounts_file::AccountsFileProvider, accounts_index::{ AccountsIndexConfig, DEFAULT_NUM_ENTRIES_OVERHEAD, DEFAULT_NUM_ENTRIES_TO_EVICT, IndexLimit, IndexLimitThreshold, ScanFilter, @@ -384,6 +385,7 @@ pub fn get_accounts_db_config( scan_filter_for_shrinking, num_background_threads: None, num_foreground_threads: None, + accounts_file_provider: AccountsFileProvider::AppendVec, } } diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index 7f1ecd2e413..070eca1743d 100644 --- a/validator/src/commands/run/execute.rs +++ b/validator/src/commands/run/execute.rs @@ -19,6 +19,7 @@ use { rand::{rng, seq::SliceRandom}, solana_accounts_db::{ accounts_db::{AccountShrinkThreshold, AccountsDbConfig}, + accounts_file::AccountsFileProvider, accounts_index::{ AccountSecondaryIndexes, AccountsIndexConfig, DEFAULT_NUM_ENTRIES_OVERHEAD, DEFAULT_NUM_ENTRIES_TO_EVICT, IndexLimit, IndexLimitThreshold, ScanFilter, @@ -712,6 +713,7 @@ pub fn execute( scan_filter_for_shrinking, num_background_threads: Some(accounts_db_background_threads), num_foreground_threads: Some(accounts_db_foreground_threads), + accounts_file_provider: AccountsFileProvider::AppendVec, }; let on_start_geyser_plugin_config_files = if matches.is_present("geyser_plugin_config") { From e8968a7c8ee2283131517d03c46a61b17acfecab Mon Sep 17 00:00:00 2001 From: Brooks Date: Thu, 16 Jul 2026 19:03:42 -0400 Subject: [PATCH 07/30] clippy: useless_borrows_in_formatting in keygen.rs (#13898) --- keygen/src/keygen.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/keygen/src/keygen.rs b/keygen/src/keygen.rs index abfe0275f98..a2aaeb25bcd 100644 --- a/keygen/src/keygen.rs +++ b/keygen/src/keygen.rs @@ -835,10 +835,7 @@ fn do_main(matches: &ArgMatches) -> Result<(), Box> { format!("{}.json", keypair.pubkey()), ) .unwrap(); - println!( - "Wrote keypair to {}", - &format!("{}.json", keypair.pubkey()) - ); + println!("Wrote keypair to {}.json", keypair.pubkey()); } if use_mnemonic { let divider = From 7337ffa9f4f9fe0327de9dc036395b71ce6df6f9 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Fri, 17 Jul 2026 09:40:03 +0800 Subject: [PATCH 08/30] migration: agave-transaction-view to 5.0.0 (#13877) * migration: agave-transaction-view to 5.0.0 * remove transaction-view from codeowners --- .github/CODEOWNERS | 1 - Cargo.lock | 12 +- Cargo.toml | 3 +- core/src/banking_stage/consume_worker.rs | 87 +- dev-bins/Cargo.lock | 4 +- programs/sbf/Cargo.lock | 4 +- .../runtime_transaction/transaction_view.rs | 28 +- runtime/Cargo.toml | 2 +- transaction-view/Cargo.toml | 56 - transaction-view/benches/bytes.rs | 85 -- transaction-view/benches/transaction_view.rs | 243 ---- .../src/address_table_lookup_frame.rs | 314 ----- transaction-view/src/bytes.rs | 433 ------- transaction-view/src/instructions_frame.rs | 857 -------------- transaction-view/src/lib.rs | 20 - transaction-view/src/message_header_frame.rs | 112 -- .../src/resolved_transaction_view.rs | 598 ---------- transaction-view/src/result.rs | 9 - transaction-view/src/sanitize.rs | 1017 ----------------- transaction-view/src/signature_frame.rs | 112 -- .../src/static_account_keys_frame.rs | 102 -- .../src/transaction_config_frame.rs | 456 -------- transaction-view/src/transaction_data.rs | 19 - transaction-view/src/transaction_frame.rs | 993 ---------------- transaction-view/src/transaction_version.rs | 19 - transaction-view/src/transaction_view.rs | 517 --------- 26 files changed, 77 insertions(+), 6026 deletions(-) delete mode 100644 transaction-view/Cargo.toml delete mode 100644 transaction-view/benches/bytes.rs delete mode 100644 transaction-view/benches/transaction_view.rs delete mode 100644 transaction-view/src/address_table_lookup_frame.rs delete mode 100644 transaction-view/src/bytes.rs delete mode 100644 transaction-view/src/instructions_frame.rs delete mode 100644 transaction-view/src/lib.rs delete mode 100644 transaction-view/src/message_header_frame.rs delete mode 100644 transaction-view/src/resolved_transaction_view.rs delete mode 100644 transaction-view/src/result.rs delete mode 100644 transaction-view/src/sanitize.rs delete mode 100644 transaction-view/src/signature_frame.rs delete mode 100644 transaction-view/src/static_account_keys_frame.rs delete mode 100644 transaction-view/src/transaction_config_frame.rs delete mode 100644 transaction-view/src/transaction_data.rs delete mode 100644 transaction-view/src/transaction_frame.rs delete mode 100644 transaction-view/src/transaction_version.rs delete mode 100644 transaction-view/src/transaction_view.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 9b06b97eba2..e651604a918 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -20,7 +20,6 @@ /svm/ @anza-xyz/svm /tls-utils/ @anza-xyz/networking /transaction-context/ @anza-xyz/svm -/transaction-view/ @anza-xyz/tx-metadata /turbine/ @anza-xyz/networking /votor/ @anza-xyz/consensus /votor-messages/ @anza-xyz/consensus diff --git a/Cargo.lock b/Cargo.lock index 6447d0bcd8f..31547a1dd97 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -356,25 +356,19 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.3.0-alpha.1" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ac5894e2f7b6c0df6433ab25c1d366b4edb774f5faf48eda7835022fe38a1b" dependencies = [ - "agave-transaction-view", - "bincode", - "criterion", "solana-hash 4.5.0", - "solana-instruction", - "solana-keypair", "solana-message", "solana-packet 4.2.0", "solana-pubkey 4.2.0", "solana-sdk-ids", "solana-short-vec", "solana-signature", - "solana-signer", "solana-svm-transaction", - "solana-system-interface 3.2.0", "solana-transaction", - "wincode", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index fcfcffb07f3..53bab0d7ec2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -113,7 +113,6 @@ members = [ "transaction-context", "transaction-status", "transaction-status-client-types", - "transaction-view", "turbine", "udp-client", "unified-scheduler-logic", @@ -184,7 +183,7 @@ agave-reserved-account-keys = { path = "reserved-account-keys", version = "=4.3. agave-scheduler-bindings = { path = "scheduler-bindings", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } agave-scheduling-utils = { path = "scheduling-utils", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } agave-snapshots = { path = "snapshots", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } -agave-transaction-view = { path = "transaction-view", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } +agave-transaction-view = "5.0.0" agave-votor = { path = "votor", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } agave-votor-messages = { path = "votor-messages", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } agave-xdp = { path = "xdp", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } diff --git a/core/src/banking_stage/consume_worker.rs b/core/src/banking_stage/consume_worker.rs index 712552e1476..5fa8068e539 100644 --- a/core/src/banking_stage/consume_worker.rs +++ b/core/src/banking_stage/consume_worker.rs @@ -210,7 +210,6 @@ pub(crate) mod external { solana_account::ReadableAccount, solana_clock::Slot, solana_cost_model::cost_model::CostModel, - solana_message::v0::LoadedAddresses, solana_pubkey::Pubkey, solana_runtime::{ bank::Bank, @@ -219,6 +218,7 @@ pub(crate) mod external { solana_runtime_transaction::{ runtime_transaction::RuntimeTransaction, sanitize_config::sanitize_config, }, + solana_svm_transaction::svm_message::SVMMessage, solana_transaction::TransactionError, std::ptr::NonNull, }; @@ -698,42 +698,43 @@ pub(crate) mod external { "max_age_iter iterator must contain element for each sent parsed transaction", ); - // There are 3 cases here: - // 1. None - Tx format does not support ATL - // 2. Some(empty) - V0 Tx with no ATL - // 3. Some(keys) - V0 Tx with ATL - // Only in case 3 will we create a shared allocation and copy keys. - let (sharable_keys, alt_invalidation_slot) = match transaction.loaded_addresses() { - Some(loaded_addresses) if !loaded_addresses.is_empty() => { - let num_pubkeys = loaded_addresses.len(); - let pubkeys_allocation = self - .allocator - .allocate( - num_pubkeys.wrapping_mul(core::mem::size_of::()) as u32 - ) - .ok_or(ExternalConsumeWorkerError::AllocationFailure)? - .cast(); - // SAFETY: non-overlapping and appropriately sized. - unsafe { - Self::copy_loaded_addresses(loaded_addresses, pubkeys_allocation) - }; - // SAFETY: pubkeys_allocation was allocated by allocator - let offset = unsafe { self.allocator.offset(pubkeys_allocation.cast()) }; - ( - SharablePubkeys { - offset, - num_pubkeys: num_pubkeys as u32, - }, - max_age.alt_invalidation_slot, + // Address table lookups are sanitized to contain at least one account, so there + // are loaded keys exactly when account keys outnumber static account keys. + let account_keys = transaction.account_keys(); + let num_static_account_keys = transaction.static_account_keys().len(); + let (sharable_keys, alt_invalidation_slot) = if account_keys.len() + > num_static_account_keys + { + let num_pubkeys = account_keys.len().wrapping_sub(num_static_account_keys); + let pubkeys_allocation = self + .allocator + .allocate(num_pubkeys.wrapping_mul(core::mem::size_of::()) as u32) + .ok_or(ExternalConsumeWorkerError::AllocationFailure)? + .cast(); + // SAFETY: non-overlapping and appropriately sized. + unsafe { + Self::copy_loaded_addresses( + account_keys.iter().skip(num_static_account_keys), + pubkeys_allocation, ) - } - _ => ( + }; + // SAFETY: pubkeys_allocation was allocated by allocator + let offset = unsafe { self.allocator.offset(pubkeys_allocation.cast()) }; + ( + SharablePubkeys { + offset, + num_pubkeys: num_pubkeys as u32, + }, + max_age.alt_invalidation_slot, + ) + } else { + ( SharablePubkeys { offset: 0, num_pubkeys: 0, }, u64::MAX, - ), + ) }; response.resolution_slot = resolution_slot; @@ -1025,18 +1026,12 @@ pub(crate) mod external { /// # Safety /// - destination is appropriately sized /// - destination does not overlap with loaded_addresses allocation - unsafe fn copy_loaded_addresses(loaded_addresses: &LoadedAddresses, dest: NonNull) { - unsafe { - core::ptr::copy_nonoverlapping( - loaded_addresses.writable.as_ptr(), - dest.as_ptr(), - loaded_addresses.writable.len(), - ); - core::ptr::copy_nonoverlapping( - loaded_addresses.readonly.as_ptr(), - dest.add(loaded_addresses.writable.len()).as_ptr(), - loaded_addresses.readonly.len(), - ); + unsafe fn copy_loaded_addresses<'a>( + loaded_addresses: impl Iterator, + dest: NonNull, + ) { + for (index, pubkey) in loaded_addresses.enumerate() { + unsafe { dest.add(index).write(*pubkey) }; } } @@ -1124,6 +1119,7 @@ pub(crate) mod external { solana_keypair::Keypair, solana_leader_schedule::SlotLeader, solana_ledger::genesis_utils::GenesisConfigInfo, + solana_message::v0::LoadedAddresses, solana_poh::{ record_channels::{RecordReceiver, record_channels}, transaction_recorder::TransactionRecorder, @@ -1789,7 +1785,10 @@ pub(crate) mod external { let mut buffer = vec![Pubkey::default(); 7]; unsafe { ExternalWorker::copy_loaded_addresses( - &loaded_addresses, + loaded_addresses + .writable + .iter() + .chain(&loaded_addresses.readonly), NonNull::new(buffer.as_mut_ptr()).unwrap(), ) }; diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 28838b45942..840e33f3931 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -367,7 +367,9 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.3.0-alpha.1" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ac5894e2f7b6c0df6433ab25c1d366b4edb774f5faf48eda7835022fe38a1b" dependencies = [ "solana-hash 4.5.0", "solana-message", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 539ae9f5b39..7a47c1f18f1 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -272,7 +272,9 @@ dependencies = [ [[package]] name = "agave-transaction-view" -version = "4.3.0-alpha.1" +version = "5.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86ac5894e2f7b6c0df6433ab25c1d366b4edb774f5faf48eda7835022fe38a1b" dependencies = [ "solana-hash 4.5.0", "solana-message", diff --git a/runtime-transaction/src/runtime_transaction/transaction_view.rs b/runtime-transaction/src/runtime_transaction/transaction_view.rs index 124856a97cc..124fe7f10fe 100644 --- a/runtime-transaction/src/runtime_transaction/transaction_view.rs +++ b/runtime-transaction/src/runtime_transaction/transaction_view.rs @@ -158,11 +158,29 @@ impl TransactionWithMeta for RuntimeTransaction SanitizedMessage::V0(LoadedMessage { - message: Cow::Owned(message), - loaded_addresses: Cow::Owned(self.loaded_addresses().unwrap().clone()), - is_writable_account_cache, - }), + VersionedMessage::V0(message) => { + // transaction-view does not expose its loaded-address source. Reconstruct the + // legacy representation from the resolved account keys, whose layout is static, + // writable loaded, then readonly loaded. + let mut loaded_account_keys = self + .account_keys() + .iter() + .skip(self.static_account_keys().len()) + .copied(); + let loaded_addresses = LoadedAddresses { + writable: loaded_account_keys + .by_ref() + .take(usize::from(self.total_writable_lookup_accounts())) + .collect(), + readonly: loaded_account_keys.collect(), + }; + + SanitizedMessage::V0(LoadedMessage { + message: Cow::Owned(message), + loaded_addresses: Cow::Owned(loaded_addresses), + is_writable_account_cache, + }) + } VersionedMessage::V1(message) => SanitizedMessage::V1(v1::CachedMessage { message: Cow::Owned(message), is_writable_account_cache, diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index e4c9c07510c..66752c2cd61 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -199,7 +199,7 @@ wincode = { workspace = true, features = ["smallvec"] } [dev-dependencies] agave-logger = { path = "../logger", features = ["agave-unstable-api"] } -agave-transaction-view = { path = "../transaction-view", features = ["agave-unstable-api"] } +agave-transaction-view = { workspace = true } bitvec = { workspace = true } criterion = { workspace = true } libsecp256k1 = { workspace = true } diff --git a/transaction-view/Cargo.toml b/transaction-view/Cargo.toml deleted file mode 100644 index 0c278922281..00000000000 --- a/transaction-view/Cargo.toml +++ /dev/null @@ -1,56 +0,0 @@ -[package] -name = "agave-transaction-view" -description = "Agave TranactionView" -documentation = "https://docs.rs/agave-transaction-view" -version = { workspace = true } -authors = { workspace = true } -repository = { workspace = true } -homepage = { workspace = true } -license = { workspace = true } -edition = { workspace = true } - -[package.metadata.docs.rs] -targets = ["x86_64-unknown-linux-gnu"] -all-features = true -rustdoc-args = ["--cfg=docsrs"] - -[features] -agave-unstable-api = [] -dev-context-only-utils = [] - -[dependencies] -solana-hash = { workspace = true } -solana-message = { workspace = true } -solana-packet = { workspace = true } -solana-pubkey = { workspace = true } -solana-sdk-ids = { workspace = true } -solana-short-vec = { workspace = true } -solana-signature = { workspace = true } -solana-svm-transaction = { workspace = true } -solana-transaction = { workspace = true } - -[dev-dependencies] -# See order-crates-for-publishing.py for using this unusual `path = "."` -agave-transaction-view = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } -bincode = { workspace = true } -criterion = { workspace = true } -solana-instruction = { workspace = true } -solana-keypair = { workspace = true } -solana-message = { workspace = true, features = ["serde"] } -solana-pubkey = { workspace = true, features = ["rand"] } -solana-signature = { workspace = true, features = ["serde"] } -solana-signer = { workspace = true } -solana-system-interface = { workspace = true, features = ["wincode"] } -solana-transaction = { workspace = true, features = ["serde", "wincode"] } -wincode = { workspace = true } - -[[bench]] -name = "bytes" -harness = false - -[[bench]] -name = "transaction_view" -harness = false - -[lints] -workspace = true diff --git a/transaction-view/benches/bytes.rs b/transaction-view/benches/bytes.rs deleted file mode 100644 index ec82682cf5d..00000000000 --- a/transaction-view/benches/bytes.rs +++ /dev/null @@ -1,85 +0,0 @@ -use { - agave_transaction_view::bytes::{optimized_read_compressed_u16, read_compressed_u16}, - bincode::{DefaultOptions, Options, serialize_into}, - criterion::{Criterion, Throughput, criterion_group, criterion_main}, - solana_packet::PACKET_DATA_SIZE, - solana_short_vec::{ShortU16, decode_shortu16_len}, - std::hint::black_box, -}; - -fn setup() -> Vec<(u16, usize, Vec)> { - let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding - - // Create a vector of all valid u16 values serialized into 16-byte buffers. - let mut values = Vec::with_capacity(PACKET_DATA_SIZE); - for value in 0..PACKET_DATA_SIZE as u16 { - let short_u16 = ShortU16(value); - let mut buffer = vec![0u8; 16]; - let serialized_len = options - .serialized_size(&short_u16) - .expect("Failed to get serialized size"); - serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); - values.push((value, serialized_len as usize, buffer)); - } - - values -} - -fn bench_u16_parsing(c: &mut Criterion) { - let values_serialized_lengths_and_buffers = setup(); - let mut group = c.benchmark_group("compressed_u16_parsing"); - group.throughput(Throughput::Elements( - values_serialized_lengths_and_buffers.len() as u64, - )); - - // Benchmark the decode_shortu16_len function from `solana-sdk` - group.bench_function("short_u16_decode", |c| { - c.iter(|| { - decode_shortu16_len_iter(&values_serialized_lengths_and_buffers); - }) - }); - - // Benchmark `read_compressed_u16` - group.bench_function("read_compressed_u16", |c| { - c.iter(|| { - read_compressed_u16_iter(&values_serialized_lengths_and_buffers); - }) - }); - - group.bench_function("optimized_read_compressed_u16", |c| { - c.iter(|| { - optimized_read_compressed_u16_iter(&values_serialized_lengths_and_buffers); - }) - }); -} - -fn decode_shortu16_len_iter(values_serialized_lengths_and_buffers: &[(u16, usize, Vec)]) { - for (value, serialized_len, buffer) in values_serialized_lengths_and_buffers.iter() { - let (read_value, bytes_read) = decode_shortu16_len(black_box(buffer)).unwrap(); - assert_eq!(read_value, *value as usize, "Value mismatch for: {value}"); - assert_eq!(bytes_read, *serialized_len, "Offset mismatch for: {value}"); - } -} - -fn read_compressed_u16_iter(values_serialized_lengths_and_buffers: &[(u16, usize, Vec)]) { - for (value, serialized_len, buffer) in values_serialized_lengths_and_buffers.iter() { - let mut offset = 0; - let read_value = read_compressed_u16(black_box(buffer), &mut offset).unwrap(); - assert_eq!(read_value, *value, "Value mismatch for: {value}"); - assert_eq!(offset, *serialized_len, "Offset mismatch for: {value}"); - } -} - -fn optimized_read_compressed_u16_iter( - values_serialized_lengths_and_buffers: &[(u16, usize, Vec)], -) { - for (value, serialized_len, buffer) in values_serialized_lengths_and_buffers.iter() { - let mut offset = 0; - let read_value = optimized_read_compressed_u16(black_box(buffer), &mut offset).unwrap(); - assert_eq!(read_value, *value, "Value mismatch for: {value}"); - assert_eq!(offset, *serialized_len, "Offset mismatch for: {value}"); - } -} - -criterion_group!(benches, bench_u16_parsing); -criterion_main!(benches); diff --git a/transaction-view/benches/transaction_view.rs b/transaction-view/benches/transaction_view.rs deleted file mode 100644 index aaa0e5ee3df..00000000000 --- a/transaction-view/benches/transaction_view.rs +++ /dev/null @@ -1,243 +0,0 @@ -use { - agave_transaction_view::{sanitize::SanitizeConfig, transaction_view::TransactionView}, - criterion::{ - BenchmarkGroup, Criterion, Throughput, criterion_group, criterion_main, - measurement::Measurement, - }, - solana_hash::Hash, - solana_instruction::Instruction, - solana_keypair::Keypair, - solana_message::{ - Message, MessageHeader, VersionedMessage, - v0::{self, MessageAddressTableLookup}, - }, - solana_pubkey::Pubkey, - solana_signer::Signer, - solana_system_interface::instruction as system_instruction, - solana_transaction::versioned::{ - VersionedTransaction, sanitized::SanitizedVersionedTransaction, - }, - std::hint::black_box, -}; - -const NUM_TRANSACTIONS: usize = 1024; - -// Current protocol values; production callers supply these from agave. -const SANITIZE_CONFIG: SanitizeConfig = SanitizeConfig { - min_requested_heap_size: 32 * 1024, - max_requested_heap_size: 256 * 1024, - max_instructions: 64, - max_accounts_per_instruction: 255, -}; - -fn serialize_transactions(transactions: Vec) -> Vec> { - transactions - .into_iter() - .map(|transaction| wincode::serialize(&transaction).unwrap()) - .collect() -} - -fn bench_transactions_parsing( - group: &mut BenchmarkGroup, - serialized_transactions: Vec>, -) { - // Legacy Transaction Parsing - group.bench_function("VersionedTransaction", |c| { - c.iter(|| { - for bytes in serialized_transactions.iter() { - let _ = wincode::deserialize::(black_box(bytes)).unwrap(); - } - }); - }); - - // Legacy Transaction Parsing and Sanitize checks - group.bench_function("SanitizedVersionedTransaction", |c| { - c.iter(|| { - for bytes in serialized_transactions.iter() { - let tx = wincode::deserialize::(black_box(bytes)).unwrap(); - let _ = SanitizedVersionedTransaction::try_new(tx).unwrap(); - } - }); - }); - - // New Transaction Parsing - group.bench_function("TransactionView", |c| { - c.iter(|| { - for bytes in serialized_transactions.iter() { - let _ = TransactionView::try_new_unsanitized(black_box(bytes.as_ref())).unwrap(); - } - }); - }); - - // New Transaction Parsing and Sanitize checks - group.bench_function("TransactionView (Sanitized)", |c| { - c.iter(|| { - for bytes in serialized_transactions.iter() { - let _ = - TransactionView::try_new_sanitized(black_box(bytes.as_ref()), &SANITIZE_CONFIG) - .unwrap(); - } - }); - }); -} - -fn minimum_sized_transactions() -> Vec { - (0..NUM_TRANSACTIONS) - .map(|_| { - let keypair = Keypair::new(); - VersionedTransaction::try_new( - VersionedMessage::Legacy(Message::new_with_blockhash( - &[], - Some(&keypair.pubkey()), - &Hash::default(), - )), - &[&keypair], - ) - .unwrap() - }) - .collect() -} - -fn simple_transfers() -> Vec { - (0..NUM_TRANSACTIONS) - .map(|_| { - let keypair = Keypair::new(); - VersionedTransaction::try_new( - VersionedMessage::Legacy(Message::new_with_blockhash( - &[system_instruction::transfer( - &keypair.pubkey(), - &Pubkey::new_unique(), - 1, - )], - Some(&keypair.pubkey()), - &Hash::default(), - )), - &[&keypair], - ) - .unwrap() - }) - .collect() -} - -fn packed_transfers() -> Vec { - // Creating transfer instructions between same keys to maximize the number - // of transfers per transaction. We can fit up to 60 transfers. - const MAX_TRANSFERS_PER_TX: usize = 60; - - (0..NUM_TRANSACTIONS) - .map(|_| { - let keypair = Keypair::new(); - let to_pubkey = Pubkey::new_unique(); - let ixs = system_instruction::transfer_many( - &keypair.pubkey(), - &vec![(to_pubkey, 1); MAX_TRANSFERS_PER_TX], - ); - VersionedTransaction::try_new( - VersionedMessage::Legacy(Message::new(&ixs, Some(&keypair.pubkey()))), - &[&keypair], - ) - .unwrap() - }) - .collect() -} - -fn packed_noops() -> Vec { - // Creating noop instructions to maximize the number of instructions per - // transaction. We are allowed to fit up to 64 instructions per transaction. - const MAX_INSTRUCTIONS_PER_TRANSACTION: usize = 64; - - (0..NUM_TRANSACTIONS) - .map(|_| { - let keypair = Keypair::new(); - let program_id = Pubkey::new_unique(); - let ixs = (0..MAX_INSTRUCTIONS_PER_TRANSACTION) - .map(|_| Instruction::new_with_bytes(program_id, &[], vec![])); - VersionedTransaction::try_new( - VersionedMessage::Legacy(Message::new( - &ixs.collect::>(), - Some(&keypair.pubkey()), - )), - &[&keypair], - ) - .unwrap() - }) - .collect() -} - -fn packed_atls() -> Vec { - // Creating ATLs to maximize the number of ATLS per transaction. We can fit - // up to 31. - const MAX_ATLS_PER_TRANSACTION: usize = 31; - - (0..NUM_TRANSACTIONS) - .map(|_| { - let keypair = Keypair::new(); - VersionedTransaction::try_new( - VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - account_keys: vec![keypair.pubkey()], - recent_blockhash: Hash::default(), - instructions: vec![], - address_table_lookups: Vec::from_iter((0..MAX_ATLS_PER_TRANSACTION).map( - |_| MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0], - readonly_indexes: vec![], - }, - )), - }), - &[&keypair], - ) - .unwrap() - }) - .collect() -} - -fn bench_parse_min_sized_transactions(c: &mut Criterion) { - let serialized_transactions = serialize_transactions(minimum_sized_transactions()); - let mut group = c.benchmark_group("min sized transactions"); - group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); - bench_transactions_parsing(&mut group, serialized_transactions); -} - -fn bench_parse_simple_transfers(c: &mut Criterion) { - let serialized_transactions = serialize_transactions(simple_transfers()); - let mut group = c.benchmark_group("simple transfers"); - group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); - bench_transactions_parsing(&mut group, serialized_transactions); -} - -fn bench_parse_packed_transfers(c: &mut Criterion) { - let serialized_transactions = serialize_transactions(packed_transfers()); - let mut group = c.benchmark_group("packed transfers"); - group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); - bench_transactions_parsing(&mut group, serialized_transactions); -} - -fn bench_parse_packed_noops(c: &mut Criterion) { - let serialized_transactions = serialize_transactions(packed_noops()); - let mut group = c.benchmark_group("packed noops"); - group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); - bench_transactions_parsing(&mut group, serialized_transactions); -} - -fn bench_parse_packed_atls(c: &mut Criterion) { - let serialized_transactions = serialize_transactions(packed_atls()); - let mut group = c.benchmark_group("packed atls"); - group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); - bench_transactions_parsing(&mut group, serialized_transactions); -} - -criterion_group!( - benches, - bench_parse_min_sized_transactions, - bench_parse_simple_transfers, - bench_parse_packed_transfers, - bench_parse_packed_noops, - bench_parse_packed_atls -); -criterion_main!(benches); diff --git a/transaction-view/src/address_table_lookup_frame.rs b/transaction-view/src/address_table_lookup_frame.rs deleted file mode 100644 index b56f7c8c937..00000000000 --- a/transaction-view/src/address_table_lookup_frame.rs +++ /dev/null @@ -1,314 +0,0 @@ -use { - crate::{ - bytes::{ - advance_offset_for_array, advance_offset_for_type, check_remaining, - optimized_read_compressed_u16, read_byte, read_slice_data, read_type, - }, - result::{Result, TransactionViewError}, - }, - core::fmt::{Debug, Formatter}, - solana_hash::Hash, - solana_packet::PACKET_DATA_SIZE, - solana_pubkey::Pubkey, - solana_signature::Signature, - solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup, -}; - -// Each ATL has at least a Pubkey, one byte for the number of write indexes, -// and one byte for the number of read indexes. Additionally, for validity -// the ATL must have at least one write or read index giving a minimum size -// of 35 bytes. -const MIN_SIZED_ATL: usize = { - core::mem::size_of::() // account key - + 1 // writable indexes length - + 1 // readonly indexes length - + 1 // single account (either write or read) -}; - -// A valid packet with ATLs has: -// 1. At least 1 signature -// 2. 1 message prefix byte -// 3. 3 bytes for the message header -// 4. 1 static account key -// 5. 1 recent blockhash -// 6. 1 byte for the number of instructions (0) -// 7. 1 byte for the number of ATLS -const MIN_SIZED_PACKET_WITH_ATLS: usize = { - 1 // signatures count - + core::mem::size_of::() // signature - + 1 // message prefix - + 3 // message header - + 1 // static account keys count - + core::mem::size_of::() // static account key - + core::mem::size_of::() // recent blockhash - + 1 // number of instructions - + 1 // number of ATLS -}; - -/// The maximum number of ATLS that can fit in a valid packet. -const MAX_ATLS_PER_PACKET: u8 = - ((PACKET_DATA_SIZE - MIN_SIZED_PACKET_WITH_ATLS) / MIN_SIZED_ATL) as u8; - -/// Contains metadata about the address table lookups in a transaction packet. -#[derive(Debug)] -pub(crate) struct AddressTableLookupFrame { - /// The number of address table lookups in the transaction. - pub(crate) num_address_table_lookups: u8, - /// The offset to the first address table lookup in the transaction. - pub(crate) offset: u16, - /// The total number of writable lookup accounts in the transaction. - pub(crate) total_writable_lookup_accounts: u16, - /// The total number of readonly lookup accounts in the transaction. - pub(crate) total_readonly_lookup_accounts: u16, -} - -impl AddressTableLookupFrame { - /// Get the number of address table lookups (ATL) and offset to the first. - /// The offset will be updated to point to the first byte after the last - /// ATL. - /// This function will parse each ATL to ensure the data is well-formed, - /// but will not cache data related to these ATLs. - #[inline(always)] - pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { - // Maximum number of ATLs should be represented by a single byte, - // thus the MSB should not be set. - const _: () = assert!(MAX_ATLS_PER_PACKET & 0b1000_0000 == 0); - let num_address_table_lookups = read_byte(bytes, offset)?; - if num_address_table_lookups > MAX_ATLS_PER_PACKET { - return Err(TransactionViewError::ParseError); - } - - // Check that the remaining bytes are enough to hold the ATLs. - check_remaining( - bytes, - *offset, - MIN_SIZED_ATL.wrapping_mul(usize::from(num_address_table_lookups)), - )?; - - // We know the offset does not exceed packet length, and our packet - // length is less than u16::MAX, so we can safely cast to u16. - let address_table_lookups_offset = *offset as u16; - - // Check that there is no chance of overflow when calculating the total - // number of writable and readonly lookup accounts using a u32. - const _: () = - assert!(u16::MAX as usize * MAX_ATLS_PER_PACKET as usize <= u32::MAX as usize); - let mut total_writable_lookup_accounts: u32 = 0; - let mut total_readonly_lookup_accounts: u32 = 0; - - // The ATLs do not have a fixed size. So we must iterate over - // each ATL to find the total size of the ATLs in the packet, - // and check for any malformed ATLs or buffer overflows. - for _index in 0..num_address_table_lookups { - // Each ATL has 3 pieces: - // 1. Address (Pubkey) - // 2. write indexes ([u8]) - // 3. read indexes ([u8]) - - // Advance offset for address of the lookup table. - advance_offset_for_type::(bytes, offset)?; - - // Read the number of write indexes, and then update the offset. - let num_write_accounts = optimized_read_compressed_u16(bytes, offset)?; - total_writable_lookup_accounts = - total_writable_lookup_accounts.wrapping_add(u32::from(num_write_accounts)); - advance_offset_for_array::(bytes, offset, num_write_accounts)?; - - // Read the number of read indexes, and then update the offset. - let num_read_accounts = optimized_read_compressed_u16(bytes, offset)?; - total_readonly_lookup_accounts = - total_readonly_lookup_accounts.wrapping_add(u32::from(num_read_accounts)); - advance_offset_for_array::(bytes, offset, num_read_accounts)?; - } - - Ok(Self { - num_address_table_lookups, - offset: address_table_lookups_offset, - total_writable_lookup_accounts: u16::try_from(total_writable_lookup_accounts) - .map_err(|_| TransactionViewError::SanitizeError)?, - total_readonly_lookup_accounts: u16::try_from(total_readonly_lookup_accounts) - .map_err(|_| TransactionViewError::SanitizeError)?, - }) - } -} - -#[derive(Clone)] -pub struct AddressTableLookupIterator<'a> { - pub(crate) bytes: &'a [u8], - pub(crate) offset: usize, - pub(crate) num_address_table_lookups: u8, - pub(crate) index: u8, -} - -impl<'a> Iterator for AddressTableLookupIterator<'a> { - type Item = SVMMessageAddressTableLookup<'a>; - - #[inline] - fn next(&mut self) -> Option { - if self.index < self.num_address_table_lookups { - self.index = self.index.wrapping_add(1); - - // Each ATL has 3 pieces: - // 1. Address (Pubkey) - // 2. write indexes ([u8]) - // 3. read indexes ([u8]) - - // Advance offset for address of the lookup table. - const _: () = assert!(core::mem::align_of::() == 1, "Pubkey alignment"); - // SAFETY: - // - The offset is checked to be valid in the slice. - // - The alignment of Pubkey is 1. - // - `Pubkey` is a byte array, it cannot be improperly initialized. - let account_key = unsafe { read_type::(self.bytes, &mut self.offset) }.ok()?; - - // Read the number of write indexes, and then update the offset. - let num_write_accounts = - optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; - - const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - let writable_indexes = - unsafe { read_slice_data::(self.bytes, &mut self.offset, num_write_accounts) } - .ok()?; - - // Read the number of read indexes, and then update the offset. - let num_read_accounts = - optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; - - const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - let readonly_indexes = - unsafe { read_slice_data::(self.bytes, &mut self.offset, num_read_accounts) } - .ok()?; - - Some(SVMMessageAddressTableLookup { - account_key, - writable_indexes, - readonly_indexes, - }) - } else { - None - } - } -} - -impl ExactSizeIterator for AddressTableLookupIterator<'_> { - fn len(&self) -> usize { - usize::from(self.num_address_table_lookups.wrapping_sub(self.index)) - } -} - -impl Debug for AddressTableLookupIterator<'_> { - fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { - f.debug_list().entries(self.clone()).finish() - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_message::v0::MessageAddressTableLookup, solana_short_vec::ShortVec}; - - #[test] - fn test_zero_atls() { - let bytes = bincode::serialize(&ShortVec::(vec![])).unwrap(); - let mut offset = 0; - let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_address_table_lookups, 0); - assert_eq!(frame.offset, 1); - assert_eq!(offset, bytes.len()); - assert_eq!(frame.total_writable_lookup_accounts, 0); - assert_eq!(frame.total_readonly_lookup_accounts, 0); - } - - #[test] - fn test_length_too_high() { - let mut bytes = bincode::serialize(&ShortVec::(vec![])).unwrap(); - let mut offset = 0; - // modify the number of atls to be too high - bytes[0] = 5; - assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_single_atl() { - let bytes = bincode::serialize(&ShortVec::(vec![ - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![1, 2, 3], - readonly_indexes: vec![4, 5, 6], - }, - ])) - .unwrap(); - let mut offset = 0; - let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_address_table_lookups, 1); - assert_eq!(frame.offset, 1); - assert_eq!(offset, bytes.len()); - assert_eq!(frame.total_writable_lookup_accounts, 3); - assert_eq!(frame.total_readonly_lookup_accounts, 3); - } - - #[test] - fn test_multiple_atls() { - let bytes = bincode::serialize(&ShortVec::(vec![ - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![1, 2, 3], - readonly_indexes: vec![4, 5, 6], - }, - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![1, 2, 3], - readonly_indexes: vec![4, 5], - }, - ])) - .unwrap(); - let mut offset = 0; - let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_address_table_lookups, 2); - assert_eq!(frame.offset, 1); - assert_eq!(offset, bytes.len()); - assert_eq!(frame.total_writable_lookup_accounts, 6); - assert_eq!(frame.total_readonly_lookup_accounts, 5); - } - - #[test] - fn test_invalid_writable_indexes_vec() { - let mut bytes = bincode::serialize(&ShortVec(vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![1, 2, 3], - readonly_indexes: vec![4, 5, 6], - }])) - .unwrap(); - - // modify the number of accounts to be too high - bytes[33] = 127; - - let mut offset = 0; - assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_invalid_readonly_indexes_vec() { - let mut bytes = bincode::serialize(&ShortVec(vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![1, 2, 3], - readonly_indexes: vec![4, 5, 6], - }])) - .unwrap(); - - // modify the number of accounts to be too high - bytes[37] = 127; - - let mut offset = 0; - assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); - } -} diff --git a/transaction-view/src/bytes.rs b/transaction-view/src/bytes.rs deleted file mode 100644 index f3534446fd7..00000000000 --- a/transaction-view/src/bytes.rs +++ /dev/null @@ -1,433 +0,0 @@ -use crate::result::{Result, TransactionViewError}; - -/// Check that the buffer has at least `len` bytes remaining starting at -/// `offset`. Returns Err if the buffer is too short. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// * `num_bytes` - Number of bytes that must be remaining. -/// -/// Assumptions: -/// - The current offset is not greater than `bytes.len()`. -#[inline(always)] -pub fn check_remaining(bytes: &[u8], offset: usize, num_bytes: usize) -> Result<()> { - if num_bytes > bytes.len().wrapping_sub(offset) { - Err(TransactionViewError::ParseError) - } else { - Ok(()) - } -} - -/// Check that the buffer has at least 1 byte remaining starting at `offset`. -/// Returns Err if the buffer is too short. -#[inline(always)] -pub fn read_byte(bytes: &[u8], offset: &mut usize) -> Result { - // Implicitly checks that the offset is within bounds, no need - // to call `check_remaining` explicitly here. - let value = bytes - .get(*offset) - .copied() - .ok_or(TransactionViewError::ParseError); - *offset = offset.wrapping_add(1); - value -} - -/// Read a byte and advance the offset without any bounds checks. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// -/// # Safety -/// 1. `bytes` must be a valid slice of bytes. -/// 2. `offset` must be a valid offset into `bytes`. -#[inline(always)] -pub unsafe fn unchecked_read_byte(bytes: &[u8], offset: &mut usize) -> u8 { - let value = unsafe { *bytes.get_unchecked(*offset) }; - *offset = offset.wrapping_add(1); - value -} - -/// Read a compressed u16 from `bytes` starting at `offset`. -/// If the buffer is too short or the encoding is invalid, return Err. -/// `offset` is updated to point to the byte after the compressed u16. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// -/// Assumptions: -/// - The current offset is not greater than `bytes.len()`. -#[allow(dead_code)] -#[inline(always)] -pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { - let mut result = 0u16; - let mut shift = 0u16; - - for i in 0..3 { - // Implicitly checks that the offset is within bounds, no need - // to call check_remaining explicitly here. - let byte = *bytes - .get(offset.wrapping_add(i)) - .ok_or(TransactionViewError::ParseError)?; - // non-minimal encoding or overflow - if (i > 0 && byte == 0) || (i == 2 && byte > 3) { - return Err(TransactionViewError::ParseError); - } - result |= ((byte & 0x7F) as u16) << shift; - shift = shift.wrapping_add(7); - if byte & 0x80 == 0 { - *offset = offset.wrapping_add(i).wrapping_add(1); - return Ok(result); - } - } - - // if we reach here, it means that all 3 bytes were used - *offset = offset.wrapping_add(3); - Ok(result) -} - -/// Domain-specific optimization for reading a compressed u16. -/// -/// The compressed u16's are only used for array-lengths in our transaction -/// format. The transaction packet has a maximum size of 1232 bytes. -/// This means that the maximum array length within a **valid** transaction is -/// 1232. This has a minimally encoded length of 2 bytes. -/// Although the encoding scheme allows for more, any arrays with this length -/// would be too large to fit in a packet. This function optimizes for this -/// case, and reads a maximum of 2 bytes. -/// If the buffer is too short or the encoding is invalid, return Err. -/// `offset` is updated to point to the byte after the compressed u16. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -#[inline(always)] -pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { - let mut result = 0u16; - - // First byte - let byte1 = *bytes.get(*offset).ok_or(TransactionViewError::ParseError)?; - result |= (byte1 & 0x7F) as u16; - if byte1 & 0x80 == 0 { - *offset = offset.wrapping_add(1); - return Ok(result); - } - - // Second byte - let byte2 = *bytes - .get(offset.wrapping_add(1)) - .ok_or(TransactionViewError::ParseError)?; - if byte2 == 0 || byte2 & 0x80 != 0 { - return Err(TransactionViewError::ParseError); // non-minimal encoding or overflow - } - result |= ((byte2 & 0x7F) as u16) << 7; - *offset = offset.wrapping_add(2); - - Ok(result) -} - -/// Update the `offset` to point to the byte after an array of length `len` and -/// of type `T`. If the buffer is too short, return Err. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// * `num_elements` - Number of `T` elements in the array. -/// -/// Assumptions: -/// 1. The current offset is not greater than `bytes.len()`. -/// 2. The size of `T` is small enough such that a usize will not overflow if -/// given the maximum array size (u16::MAX). -#[inline(always)] -pub fn advance_offset_for_array( - bytes: &[u8], - offset: &mut usize, - num_elements: u16, -) -> Result<()> { - let array_len_bytes = usize::from(num_elements).wrapping_mul(core::mem::size_of::()); - check_remaining(bytes, *offset, array_len_bytes)?; - *offset = offset.wrapping_add(array_len_bytes); - Ok(()) -} - -/// Update the `offset` to point t the byte after the `T`. -/// If the buffer is too short, return Err. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// -/// Assumptions: -/// 1. The current offset is not greater than `bytes.len()`. -/// 2. The size of `T` is small enough such that a usize will not overflow. -#[inline(always)] -pub fn advance_offset_for_type(bytes: &[u8], offset: &mut usize) -> Result<()> { - let type_size = core::mem::size_of::(); - check_remaining(bytes, *offset, type_size)?; - *offset = offset.wrapping_add(type_size); - Ok(()) -} - -/// Return a reference to the next slice of `T` in the buffer, checking bounds -/// and advancing the offset. -/// If the buffer is too short, return Err. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// * `num_elements` - Number of `T` elements in the slice. -/// -/// # Safety -/// 1. `bytes` must be a valid slice of bytes. -/// 2. `offset` must be a valid offset into `bytes`. -/// 3. `bytes + offset` must be properly aligned for `T`. -/// 4. `T` slice must be validly initialized. -/// 5. The size of `T` is small enough such that a usize will not overflow if -/// given the maximum slice size (u16::MAX). -#[inline(always)] -pub unsafe fn read_slice_data<'a, T: Sized>( - bytes: &'a [u8], - offset: &mut usize, - num_elements: u16, -) -> Result<&'a [T]> { - let current_ptr = unsafe { bytes.as_ptr().add(*offset) }; - advance_offset_for_array::(bytes, offset, num_elements)?; - Ok(unsafe { core::slice::from_raw_parts(current_ptr as *const T, usize::from(num_elements)) }) -} - -/// Return a reference to the next slice of `T` in the buffer, -/// and advancing the offset. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// * `num_elements` - Number of `T` elements in the slice. -/// -/// # Safety -/// 1. `bytes` must be a valid slice of bytes. -/// 2. `offset` must be a valid offset into `bytes`. -/// 3. `bytes + offset` must be properly aligned for `T`. -/// 4. `T` slice must be validly initialized. -/// 5. The size of `T` is small enough such that a usize will not overflow if -/// given the maximum slice size (u16::MAX). -#[inline(always)] -pub unsafe fn unchecked_read_slice_data<'a, T: Sized>( - bytes: &'a [u8], - offset: &mut usize, - num_elements: u16, -) -> &'a [T] { - let current_ptr = unsafe { bytes.as_ptr().add(*offset) }; - let array_len_bytes = usize::from(num_elements).wrapping_mul(core::mem::size_of::()); - *offset = offset.wrapping_add(array_len_bytes); - unsafe { core::slice::from_raw_parts(current_ptr as *const T, usize::from(num_elements)) } -} - -/// Return a reference to the next `T` in the buffer, checking bounds and -/// advancing the offset. -/// If the buffer is too short, return Err. -/// -/// * `bytes` - Slice of bytes to read from. -/// * `offset` - Current offset into `bytes`. -/// -/// # Safety -/// 1. `bytes` must be a valid slice of bytes. -/// 2. `offset` must be a valid offset into `bytes`. -/// 3. `bytes + offset` must be properly aligned for `T`. -/// 4. `T` must be validly initialized. -#[inline(always)] -pub unsafe fn read_type<'a, T: Sized>(bytes: &'a [u8], offset: &mut usize) -> Result<&'a T> { - let current_ptr = unsafe { bytes.as_ptr().add(*offset) }; - advance_offset_for_type::(bytes, offset)?; - Ok(unsafe { &*(current_ptr as *const T) }) -} - -/// Copy a `T` in the buffer without checking bounds or advancing offset. -/// -/// # Safety -/// 1. `bytes` must be a valid slice of bytes. -/// 2. `offset` must be a valid offset into `bytes`. -/// 4. `T` must be validly initialized. -#[inline(always)] -pub unsafe fn unchecked_copy_value(bytes: &[u8], offset: usize) -> T { - let current_ptr = unsafe { bytes.as_ptr().add(offset) }.cast::(); - unsafe { current_ptr.read_unaligned() } -} - -#[cfg(test)] -mod tests { - use { - super::*, - bincode::{DefaultOptions, Options, serialize_into}, - solana_packet::PACKET_DATA_SIZE, - solana_short_vec::ShortU16, - }; - - #[test] - fn test_check_remaining() { - // Empty buffer checks - assert!(check_remaining(&[], 0, 0).is_ok()); - assert!(check_remaining(&[], 0, 1).is_err()); - - // Buffer with data checks - assert!(check_remaining(&[1, 2, 3], 0, 0).is_ok()); - assert!(check_remaining(&[1, 2, 3], 0, 1).is_ok()); - assert!(check_remaining(&[1, 2, 3], 0, 3).is_ok()); - assert!(check_remaining(&[1, 2, 3], 0, 4).is_err()); - - // Non-zero offset. - assert!(check_remaining(&[1, 2, 3], 1, 0).is_ok()); - assert!(check_remaining(&[1, 2, 3], 1, 1).is_ok()); - assert!(check_remaining(&[1, 2, 3], 1, 2).is_ok()); - assert!(check_remaining(&[1, 2, 3], 1, usize::MAX).is_err()); - } - - #[test] - fn test_read_byte() { - let bytes = [5, 6, 7]; - let mut offset = 0; - assert_eq!(read_byte(&bytes, &mut offset), Ok(5)); - assert_eq!(offset, 1); - assert_eq!(read_byte(&bytes, &mut offset), Ok(6)); - assert_eq!(offset, 2); - assert_eq!(read_byte(&bytes, &mut offset), Ok(7)); - assert_eq!(offset, 3); - assert!(read_byte(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_read_compressed_u16() { - let mut buffer = [0u8; 1024]; - let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding - - // Test all possible u16 values - for value in 0..=u16::MAX { - let mut offset; - let short_u16 = ShortU16(value); - - // Serialize the value into the buffer - serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); - - // Use bincode's size calculation to determine the length of the serialized data - let serialized_len = options - .serialized_size(&short_u16) - .expect("Failed to get serialized size"); - - // Reset offset - offset = 0; - - // Read the value back using unchecked_read_u16_compressed - let read_value = read_compressed_u16(&buffer, &mut offset); - - // Assert that the read value matches the original value - assert_eq!(read_value, Ok(value), "Value mismatch for: {value}"); - - // Assert that the offset matches the serialized length - assert_eq!( - offset, serialized_len as usize, - "Offset mismatch for: {value}" - ); - } - - // Test bounds. - // All 0s => 0 - assert_eq!(Ok(0), read_compressed_u16(&[0; 3], &mut 0)); - // Overflow - assert!(read_compressed_u16(&[0xFF, 0xFF, 0x04], &mut 0).is_err()); - assert_eq!( - read_compressed_u16(&[0xFF, 0xFF, 0x03], &mut 0), - Ok(u16::MAX) - ); - - // overflow errors - assert!(read_compressed_u16(&[u8::MAX; 1], &mut 0).is_err()); - assert!(read_compressed_u16(&[u8::MAX; 2], &mut 0).is_err()); - - // Minimal encoding checks - assert!(read_compressed_u16(&[0x81, 0x80, 0x00], &mut 0).is_err()); - } - - #[test] - fn test_optimized_read_compressed_u16() { - let mut buffer = [0u8; 1024]; - let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding - - // Test all possible u16 values under the packet length - for value in 0..=PACKET_DATA_SIZE as u16 { - let mut offset; - let short_u16 = ShortU16(value); - - // Serialize the value into the buffer - serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); - - // Use bincode's size calculation to determine the length of the serialized data - let serialized_len = options - .serialized_size(&short_u16) - .expect("Failed to get serialized size"); - - // Reset offset - offset = 0; - - // Read the value back using unchecked_read_u16_compressed - let read_value = optimized_read_compressed_u16(&buffer, &mut offset); - - // Assert that the read value matches the original value - assert_eq!(read_value, Ok(value), "Value mismatch for: {value}"); - - // Assert that the offset matches the serialized length - assert_eq!( - offset, serialized_len as usize, - "Offset mismatch for: {value}" - ); - } - - // Test bounds. - // All 0s => 0 - assert_eq!(Ok(0), optimized_read_compressed_u16(&[0; 3], &mut 0)); - // Overflow - assert!(optimized_read_compressed_u16(&[0xFF, 0xFF, 0x04], &mut 0).is_err()); - assert!(optimized_read_compressed_u16(&[0xFF, 0x80], &mut 0).is_err()); - - // overflow errors - assert!(optimized_read_compressed_u16(&[u8::MAX; 1], &mut 0).is_err()); - assert!(optimized_read_compressed_u16(&[u8::MAX; 2], &mut 0).is_err()); - - // Minimal encoding checks - assert!(optimized_read_compressed_u16(&[0x81, 0x00], &mut 0).is_err()); - } - - #[test] - fn test_advance_offset_for_array() { - #[repr(C)] - struct MyStruct { - _a: u8, - _b: u8, - } - const _: () = assert!(core::mem::size_of::() == 2); - - // Test with a buffer that is too short - let bytes = [0u8; 1]; - let mut offset = 0; - assert!(advance_offset_for_array::(&bytes, &mut offset, 1).is_err()); - - // Test with a buffer that is long enough - let bytes = [0u8; 4]; - let mut offset = 0; - assert!(advance_offset_for_array::(&bytes, &mut offset, 2).is_ok()); - assert_eq!(offset, 4); - } - - #[test] - fn test_advance_offset_for_type() { - #[repr(C)] - struct MyStruct { - _a: u8, - _b: u8, - } - const _: () = assert!(core::mem::size_of::() == 2); - - // Test with a buffer that is too short - let bytes = [0u8; 1]; - let mut offset = 0; - assert!(advance_offset_for_type::(&bytes, &mut offset).is_err()); - - // Test with a buffer that is long enough - let bytes = [0u8; 4]; - let mut offset = 0; - assert!(advance_offset_for_type::(&bytes, &mut offset).is_ok()); - assert_eq!(offset, 2); - } -} diff --git a/transaction-view/src/instructions_frame.rs b/transaction-view/src/instructions_frame.rs deleted file mode 100644 index 4a91437a1f1..00000000000 --- a/transaction-view/src/instructions_frame.rs +++ /dev/null @@ -1,857 +0,0 @@ -use { - crate::{ - bytes::{ - advance_offset_for_array, check_remaining, optimized_read_compressed_u16, read_byte, - unchecked_copy_value, unchecked_read_byte, unchecked_read_slice_data, - }, - result::{Result, TransactionViewError}, - }, - core::fmt::{Debug, Formatter}, - solana_svm_transaction::instruction::SVMInstruction, -}; - -/// Contains metadata about the instructions in a transaction packet. -#[derive(Debug)] -pub(crate) enum InstructionsFrame { - LegacyAndV0 { - /// The number of instructions in the transaction. - num_instructions: u16, - /// The offset to the first instruction in the transaction. - offset: u16, - frames: Vec, - }, - V1 { - num_instructions: u16, - headers_offset: u16, - payloads_offset: u16, - }, -} - -#[derive(Debug)] -pub struct LegacyAndV0InstructionFrame { - num_accounts: u16, - data_len: u16, - num_accounts_len: u8, // either 1 or 2 - data_len_len: u8, // either 1 or 2 -} - -#[allow(dead_code)] -#[repr(C)] -#[derive(Debug)] -struct V1InstructionHeader { - program_id_index: u8, - num_accounts: u8, - data_len: u16, -} - -impl InstructionsFrame { - /// Get the number of instructions and offset to the first instruction. - /// The offset will be updated to point to the first byte after the last - /// instruction. - /// This function will parse each individual instruction to ensure the - /// instruction data is well-formed, but will not cache data related to - /// these instructions. - #[inline(always)] - pub(crate) fn try_new_for_legacy_and_v0(bytes: &[u8], offset: &mut usize) -> Result { - // Read the number of instructions at the current offset. - // Each instruction needs at least 3 bytes, so do a sanity check here to - // ensure we have enough bytes to read the number of instructions. - let num_instructions = optimized_read_compressed_u16(bytes, offset)?; - check_remaining( - bytes, - *offset, - 3usize.wrapping_mul(usize::from(num_instructions)), - )?; - - // We know the offset does not exceed packet length, and our packet - // length is less than u16::MAX, so we can safely cast to u16. - let instructions_offset = *offset as u16; - - // Pre-allocate buffer for frames. - let mut frames = Vec::with_capacity(usize::from(num_instructions)); - - // The instructions do not have a fixed size. So we must iterate over - // each instruction to find the total size of the instructions, - // and check for any malformed instructions or buffer overflows. - for _index in 0..num_instructions { - // Each instruction has 3 pieces: - // 1. Program ID index (u8) - // 2. Accounts indexes ([u8]) - // 3. Data ([u8]) - - // Read the program ID index. - let _program_id_index = read_byte(bytes, offset)?; - - // Read the number of account indexes, and then update the offset - // to skip over the account indexes. - let num_accounts_offset = *offset; - let num_accounts = optimized_read_compressed_u16(bytes, offset)?; - let num_accounts_len = offset.wrapping_sub(num_accounts_offset) as u8; - advance_offset_for_array::(bytes, offset, num_accounts)?; - - // Read the length of the data, and then update the offset to skip - // over the data. - let data_len_offset = *offset; - let data_len = optimized_read_compressed_u16(bytes, offset)?; - let data_len_len = offset.wrapping_sub(data_len_offset) as u8; - advance_offset_for_array::(bytes, offset, data_len)?; - - frames.push(LegacyAndV0InstructionFrame { - num_accounts, - num_accounts_len, - data_len, - data_len_len, - }); - } - - Ok(Self::LegacyAndV0 { - num_instructions, - offset: instructions_offset, - frames, - }) - } - - #[allow(dead_code)] - #[inline(always)] - pub(crate) fn try_new_for_v1( - bytes: &[u8], - offset: &mut usize, - num_instructions: u8, - ) -> Result { - let headers_offset = *offset as u16; - let headers_len = - core::mem::size_of::().wrapping_mul(num_instructions as usize); - - check_remaining(bytes, *offset, headers_len)?; - - let mut header_offset = *offset; - *offset = offset.wrapping_add(headers_len); - - let payloads_offset = *offset as u16; - - // Tx v1 stores all instruction payloads contiguously after the header block. - // We validate headers first, accumulate the total payload size across all - // instructions, and then do a single bounds check for the whole payload region - // instead of one bounds check per instruction. - let mut total_payload_len: usize = 0; - for _ in 0..num_instructions { - // SAFETY: we have already verified bytes contains enough space for `num_instruction` headers. - let header = unsafe { Self::read_v1_header(bytes, &mut header_offset) }; - - let payload_len = usize::from(header.num_accounts) - .checked_add(usize::from(header.data_len)) - .ok_or(TransactionViewError::ParseError)?; - - total_payload_len = total_payload_len - .checked_add(payload_len) - .ok_or(TransactionViewError::ParseError)?; - } - - check_remaining(bytes, *offset, total_payload_len)?; - *offset = offset.wrapping_add(total_payload_len); - - Ok(Self::V1 { - num_instructions: u16::from(num_instructions), - headers_offset, - payloads_offset, - }) - } - - /// # Safety - /// `bytes[*offset..*offset + size_of::()]` must be valid. - #[inline(always)] - unsafe fn read_v1_header(bytes: &[u8], offset: &mut usize) -> V1InstructionHeader { - let mut header: V1InstructionHeader = unsafe { unchecked_copy_value(bytes, *offset) }; - *offset = offset.wrapping_add(core::mem::size_of::()); - header.data_len = u16::from_le(header.data_len); - header - } - - #[inline(always)] - pub(crate) fn num_instructions(&self) -> u16 { - match self { - Self::LegacyAndV0 { - num_instructions, .. - } => *num_instructions, - Self::V1 { - num_instructions, .. - } => *num_instructions, - } - } - - #[inline(always)] - pub(crate) fn iter<'a>(&'a self, bytes: &'a [u8]) -> InstructionsIterator<'a> { - match self { - Self::LegacyAndV0 { - num_instructions, - offset, - frames, - } => InstructionsIterator::LegacyAndV0 { - bytes, - offset: *offset as usize, - index: 0, - num_instructions: *num_instructions, - frames, - }, - Self::V1 { - num_instructions, - headers_offset, - payloads_offset, - } => InstructionsIterator::V1 { - bytes, - index: 0, - num_instructions: *num_instructions, - headers_offset: *headers_offset as usize, - payloads_offset: *payloads_offset as usize, - }, - } - } -} - -#[derive(Clone)] -pub enum InstructionsIterator<'a> { - LegacyAndV0 { - bytes: &'a [u8], - offset: usize, - num_instructions: u16, - index: u16, - frames: &'a [LegacyAndV0InstructionFrame], - }, - V1 { - bytes: &'a [u8], - index: u16, - num_instructions: u16, - headers_offset: usize, - payloads_offset: usize, - }, -} - -impl<'a> Iterator for InstructionsIterator<'a> { - type Item = SVMInstruction<'a>; - - #[inline] - fn next(&mut self) -> Option { - match self { - Self::LegacyAndV0 { - bytes, - offset, - index, - num_instructions, - frames, - } => { - if *index >= *num_instructions { - return None; - } - - let LegacyAndV0InstructionFrame { - num_accounts, - num_accounts_len, - data_len, - data_len_len, - } = frames[usize::from(*index)]; - - *index = index.wrapping_add(1); - - Some(unsafe { - for_legacy_and_v0( - bytes, - offset, - num_accounts, - num_accounts_len, - data_len, - data_len_len, - ) - }) - } - Self::V1 { - bytes, - index, - num_instructions, - headers_offset, - payloads_offset, - } => { - if *index >= *num_instructions { - return None; - } - - let header = unsafe { InstructionsFrame::read_v1_header(bytes, headers_offset) }; - *index = index.wrapping_add(1); - - Some(unsafe { - for_v1( - bytes, - payloads_offset, - header.program_id_index, - u16::from(header.num_accounts), - header.data_len, - ) - }) - } - } - } -} - -/// Builds SNVInstruction from legacy/v0 pre-validated frame metadata. -/// -/// # Safety -/// The caller must ensure that: -/// - `offset` points to the beginning of a serialized legacy/v0 instruction -/// in `bytes`. -/// - `num_accounts_len` and `data_len_len` are the exact encoded lengths of the -/// compact-u16 account-count and data-length fields for that instruction. -/// - `num_accounts` and `data_len` exactly match the serialized instruction at -/// `offset`. -/// - The byte ranges implied by those values are fully in bounds of `bytes`. -/// -/// These invariants are expected to have been established by the initial -/// instruction frame parsing. Violating them may cause out-of-bounds unchecked -/// reads and undefined behavior. -#[inline(always)] -unsafe fn for_legacy_and_v0<'a>( - bytes: &'a [u8], - offset: &mut usize, - num_accounts: u16, - num_accounts_len: u8, - data_len: u16, - data_len_len: u8, -) -> SVMInstruction<'a> { - // Each instruction has 3 pieces: - // 1. Program ID index (u8) - // 2. Accounts indexes ([u8]) - // 3. Data ([u8]) - - // Read the program ID index. - // SAFETY: Offset and length checks have been done in the initial parsing. - let program_id_index = unsafe { unchecked_read_byte(bytes, offset) }; - - // Move offset to accounts offset - do not re-parse u16. - *offset = offset.wrapping_add(usize::from(num_accounts_len)); - const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - // - Offset and length checks have been done in the initial parsing. - let accounts = unsafe { unchecked_read_slice_data::(bytes, offset, num_accounts) }; - - // Move offset to accounts offset - do not re-parse u16. - *offset = offset.wrapping_add(usize::from(data_len_len)); - const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - // - Offset and length checks have been done in the initial parsing. - let data = unsafe { unchecked_read_slice_data::(bytes, offset, data_len) }; - - SVMInstruction { - program_id_index, - accounts, - data, - } -} - -/// Builds SMVInstruction from v1 pre-validated frame metadata. -/// -/// # Safety -/// The caller must ensure that: -/// -/// - `payload_offset` points to the beginning of this instruction’s payload -/// (i.e. the first account index byte) within `bytes`. -/// - `num_accounts` and `data_len` exactly match the instruction header that -/// was previously parsed for this instruction. -/// - The byte range -/// `payload_offset .. payload_offset + num_accounts + data_len` -/// lies entirely within `bytes`. -/// - `bytes` has not been mutated since the initial parsing that produced -/// the instruction frames. -/// -/// These invariants are expected to have been established during the initial -/// tx-v1 instruction parsing phase, where header and payload bounds were -/// validated together. -/// -/// Violating any of these conditions may result in out-of-bounds unchecked -/// reads and thus undefined behavior. -#[inline(always)] -unsafe fn for_v1<'a>( - bytes: &'a [u8], - payloads_offset: &mut usize, - program_id_index: u8, - num_accounts: u16, - data_len: u16, -) -> SVMInstruction<'a> { - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - // - Offset and length checks have been done in the initial parsing. - let accounts = unsafe { unchecked_read_slice_data::(bytes, payloads_offset, num_accounts) }; - // SAFETY: - // - The offset is checked to be valid in the byte slice. - // - The alignment of u8 is 1. - // - The slice length is checked to be valid. - // - `u8` cannot be improperly initialized. - // - Offset and length checks have been done in the initial parsing. - let data = unsafe { unchecked_read_slice_data::(bytes, payloads_offset, data_len) }; - - SVMInstruction { - program_id_index, - accounts, - data, - } -} - -impl ExactSizeIterator for InstructionsIterator<'_> { - fn len(&self) -> usize { - match self { - Self::LegacyAndV0 { - num_instructions, - index, - .. - } => usize::from(num_instructions.wrapping_sub(*index)), - Self::V1 { - num_instructions, - index, - .. - } => usize::from(num_instructions.wrapping_sub(*index)), - } - } -} - -impl Debug for InstructionsIterator<'_> { - fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { - f.debug_list().entries(self.clone()).finish() - } -} - -#[cfg(test)] -mod tests { - use { - super::*, solana_message::compiled_instruction::CompiledInstruction, - solana_short_vec::ShortVec, - }; - - impl InstructionsFrame { - fn offset(&self) -> u16 { - match self { - Self::LegacyAndV0 { offset, .. } => *offset, - Self::V1 { headers_offset, .. } => *headers_offset, - } - } - } - - #[test] - fn test_zero_instructions() { - let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); - let mut offset = 0; - let instructions_frame = - InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - - assert_eq!(instructions_frame.num_instructions(), 0); - assert_eq!(instructions_frame.offset(), 1); - assert_eq!(offset, bytes.len()); - } - - #[test] - fn test_num_instructions_too_high() { - let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![], - data: vec![], - }])) - .unwrap(); - // modify the number of instructions to be too high - bytes[0] = 0x02; - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_single_instruction() { - let bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3], - data: vec![4, 5, 6, 7, 8, 9, 10], - }])) - .unwrap(); - let mut offset = 0; - let instructions_frame = - InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - assert_eq!(instructions_frame.num_instructions(), 1); - assert_eq!(instructions_frame.offset(), 1); - assert_eq!(offset, bytes.len()); - } - - #[test] - fn test_multiple_instructions() { - let bytes = bincode::serialize(&ShortVec(vec![ - CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3], - data: vec![4, 5, 6, 7, 8, 9, 10], - }, - CompiledInstruction { - program_id_index: 1, - accounts: vec![4, 5, 6], - data: vec![7, 8, 9, 10, 11, 12, 13], - }, - ])) - .unwrap(); - let mut offset = 0; - let instructions_frame = - InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - assert_eq!(instructions_frame.num_instructions(), 2); - assert_eq!(instructions_frame.offset(), 1); - assert_eq!(offset, bytes.len()); - } - - #[test] - fn test_invalid_instruction_accounts_vec() { - let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3], - data: vec![4, 5, 6, 7, 8, 9, 10], - }])) - .unwrap(); - - // modify the number of accounts to be too high - bytes[2] = 127; - - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_invalid_instruction_data_vec() { - let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3], - data: vec![4, 5, 6, 7, 8, 9, 10], - }])) - .unwrap(); - - // modify the number of data bytes to be too high - bytes[6] = 127; - - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_txv1_instructions_iterator() { - let message = solana_message::v1::Message { - instructions: vec![ - CompiledInstruction { - program_id_index: 0, - accounts: vec![1, 2, 3], - data: vec![4, 5, 6, 7, 8, 9, 10], - }, - CompiledInstruction { - program_id_index: 10, - accounts: vec![11, 12], - data: vec![13, 14, 15, 16, 17, 18, 19, 20], - }, - ], - ..solana_message::v1::Message::default() - }; - - let serialized = solana_message::v1::Message::serialize(&message); - - let mut offset = 42; // 1-byte V1_PREFIX + 41-byte header (no config, 0 addresses), per spec - let instructions_frame = - InstructionsFrame::try_new_for_v1(&serialized, &mut offset, 2).unwrap(); - - let mut iter = instructions_frame.iter(&serialized); - assert_eq!( - iter.next(), - Some(SVMInstruction { - program_id_index: 0, - accounts: &[1, 2, 3], - data: &[4, 5, 6, 7, 8, 9, 10] - }) - ); - assert_eq!( - iter.next(), - Some(SVMInstruction { - program_id_index: 10, - accounts: &[11, 12], - data: &[13, 14, 15, 16, 17, 18, 19, 20] - }) - ); - assert_eq!(iter.next(), None); - } - - fn short_u16_1(x: u8) -> Vec { - vec![x] - } - - // short_vec / compact-u16 encoding for 128..=16383 style values - fn short_u16_2(x: u16) -> Vec { - assert!(x >= 128); - vec![((x & 0x7f) as u8) | 0x80, (x >> 7) as u8] - } - - #[test] - fn test_try_new_legacy_single_instruction() { - // num_instructions = 1 - // instruction: - // program_id_index = 7 - // num_accounts = 2 - // accounts = [3, 4] - // data_len = 3 - // data = [9, 8, 7] - let bytes = vec![ - 1, // num_instructions - 7, // program_id_index - 2, // num_accounts - 3, 4, // account indexes - 3, // data_len - 9, 8, 7, // data - ]; - - let mut offset = 0; - let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - - assert_eq!(offset, bytes.len()); - - match frame { - InstructionsFrame::LegacyAndV0 { - num_instructions, - offset, - frames, - } => { - assert_eq!(num_instructions, 1); - assert_eq!(offset, 1); - assert_eq!(frames.len(), 1); - let ix = &frames[0]; - assert_eq!(ix.num_accounts, 2); - assert_eq!(ix.data_len, 3); - assert_eq!(ix.num_accounts_len, 1); - assert_eq!(ix.data_len_len, 1); - } - _ => panic!("expected legacy/v0 repr"), - } - } - - #[test] - fn test_try_new_legacy_two_byte_lengths() { - let num_accounts = 128u16; - let data_len = 130u16; - - let mut bytes = Vec::new(); - bytes.push(1); // num_instructions - bytes.push(42); // program_id_index - bytes.extend_from_slice(&short_u16_2(num_accounts)); - bytes.extend(std::iter::repeat_n(5u8, num_accounts as usize)); - bytes.extend_from_slice(&short_u16_2(data_len)); - bytes.extend(std::iter::repeat_n(9u8, data_len as usize)); - - let mut offset = 0; - let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - - assert_eq!(offset, bytes.len()); - - match frame { - InstructionsFrame::LegacyAndV0 { - num_instructions, - offset, - frames, - } => { - assert_eq!(num_instructions, 1); - assert_eq!(offset, 1); - assert_eq!(frames.len(), 1); - let ix = &frames[0]; - assert_eq!(ix.num_accounts, num_accounts); - assert_eq!(ix.data_len, data_len); - - assert_eq!(ix.num_accounts_len, 2); - assert_eq!(ix.data_len_len, 2); - } - _ => panic!("expected legacy/v0 repr"), - } - } - - #[test] - fn test_try_new_for_v1_single_instruction() { - // one v1 instruction - // header: - // program_id_index = 9 - // num_accounts = 2 - // data_len = 3 - // payload: - // accounts = [10, 11] - // data = [1, 2, 3] - let bytes = vec![ - 9, // program_id_index - 2, // num_accounts - 3, 0, // data_len (u16 LE) - 10, 11, // payload accounts - 1, 2, 3, // payload data - ]; - - let mut offset = 0; - let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).unwrap(); - - assert_eq!(offset, bytes.len()); - - match frame { - InstructionsFrame::V1 { - num_instructions, - headers_offset, - payloads_offset, - } => { - assert_eq!(num_instructions, 1); - assert_eq!(headers_offset, 0); - assert_eq!(payloads_offset, 4); - let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 0) }; - assert_eq!(hdr.program_id_index, 9); - assert_eq!(hdr.num_accounts, 2); - assert_eq!(hdr.data_len, 3); - } - _ => panic!("expected v1 repr"), - } - } - - #[test] - fn test_try_new_for_v1_two_instructions() { - // headers: - // ix0: pid=1, accounts=2, data_len=1 - // ix1: pid=7, accounts=1, data_len=2 - // - // payloads: - // ix0: [20, 21] [99] - // ix1: [42] [5, 6] - let bytes = vec![ - // header 0 - 1, 2, 1, 0, // header 1 - 7, 1, 2, 0, // payload 0 - 20, 21, 99, // payload 1 - 42, 5, 6, - ]; - - let mut offset = 0; - let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 2).unwrap(); - - assert_eq!(offset, bytes.len()); - match frame { - InstructionsFrame::V1 { - num_instructions, - headers_offset, - payloads_offset, - } => { - assert_eq!(num_instructions, 2); - assert_eq!(headers_offset, 0); - assert_eq!(payloads_offset, 8); - let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 0) }; - assert_eq!(hdr.program_id_index, 1); - assert_eq!(hdr.num_accounts, 2); - assert_eq!(hdr.data_len, 1); - let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 4) }; - assert_eq!(hdr.program_id_index, 7); - assert_eq!(hdr.num_accounts, 1); - assert_eq!(hdr.data_len, 2); - } - _ => panic!("expected v1 repr"), - } - } - - #[test] - fn test_try_new_for_v1_truncated_header_fails() { - // num_instructions = 1, but only 3 header bytes instead of 4 - let bytes = vec![ - 9, // program_id_index - 2, // num_accounts - 3, // incomplete data_len - ]; - - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).is_err()); - } - - #[test] - fn test_try_new_for_v1_truncated_payload_fails() { - // header says payload len = 2 + 3 = 5, but only 4 bytes provided - let bytes = vec![ - 9, 2, 3, 0, // header - 10, 11, 1, 2, // truncated payload - ]; - - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).is_err()); - } - - #[test] - fn test_try_new_legacy_truncated_payload_fails() { - // data_len says 3, only 2 bytes provided - let bytes = vec![ - 1, // num_instructions - 7, // program_id_index - 1, // num_accounts - 9, // account idx - 3, // data_len - 1, 2, // truncated data - ]; - - let mut offset = 0; - assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_try_new_for_v1_zero_instructions() { - let bytes = vec![]; - let mut offset = 0; - - let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 0).unwrap(); - assert_eq!(offset, 0); - match frame { - InstructionsFrame::V1 { - num_instructions, - headers_offset, - payloads_offset, - } => { - assert_eq!(num_instructions, 0); - assert_eq!(headers_offset, 0); - assert_eq!(payloads_offset, 0); - } - _ => panic!("expected v1 repr"), - } - } - - #[test] - fn data_len_max_header_fails_parse() { - // header: pid=1, accounts=1, data_len=65535 - let bytes = vec![1, 1, 0xff, 0xff]; - let mut offset = 0; - assert_eq!( - InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1) - .err() - .unwrap(), - TransactionViewError::ParseError - ); - } - - #[test] - fn test_try_new_legacy_zero_instructions() { - let bytes = short_u16_1(0); - let mut offset = 0; - - let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); - assert_eq!(offset, 1); - - match frame { - InstructionsFrame::LegacyAndV0 { - num_instructions, - offset, - frames, - } => { - assert_eq!(num_instructions, 0); - assert_eq!(offset, 1); - assert!(frames.is_empty()); - } - _ => panic!("expected legacy/v0 repr"), - } - } -} diff --git a/transaction-view/src/lib.rs b/transaction-view/src/lib.rs deleted file mode 100644 index 83ce9626487..00000000000 --- a/transaction-view/src/lib.rs +++ /dev/null @@ -1,20 +0,0 @@ -#![cfg(feature = "agave-unstable-api")] -// Parsing helpers only need to be public for benchmarks. -#[cfg(feature = "dev-context-only-utils")] -pub mod bytes; -#[cfg(not(feature = "dev-context-only-utils"))] -mod bytes; - -mod address_table_lookup_frame; -mod instructions_frame; -mod message_header_frame; -pub mod resolved_transaction_view; -pub mod result; -pub mod sanitize; -mod signature_frame; -mod static_account_keys_frame; -mod transaction_config_frame; -pub mod transaction_data; -mod transaction_frame; -pub mod transaction_version; -pub mod transaction_view; diff --git a/transaction-view/src/message_header_frame.rs b/transaction-view/src/message_header_frame.rs deleted file mode 100644 index 1fca4e407eb..00000000000 --- a/transaction-view/src/message_header_frame.rs +++ /dev/null @@ -1,112 +0,0 @@ -use { - crate::{ - bytes::read_byte, - result::{Result, TransactionViewError}, - transaction_version::TransactionVersion, - }, - solana_message::MESSAGE_VERSION_PREFIX, -}; - -/// Metadata for accessing message header fields in a transaction view. -#[derive(Debug)] -pub(crate) struct MessageHeaderFrame { - /// The offset to the first byte of the message in the transaction packet. - pub(crate) offset: u16, - /// The version of the transaction. - pub(crate) version: TransactionVersion, - /// The number of signatures required for this message to be considered - /// valid. - pub(crate) num_required_signatures: u8, - /// The last `num_readonly_signed_accounts` of the signed keys are - /// read-only. - pub(crate) num_readonly_signed_accounts: u8, - /// The last `num_readonly_unsigned_accounts` of the unsigned keys are - /// read-only accounts. - pub(crate) num_readonly_unsigned_accounts: u8, -} - -impl MessageHeaderFrame { - #[inline(always)] - pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { - // Get the message offset. - // We know the offset does not exceed packet length, and our packet - // length is less than u16::MAX, so we can safely cast to u16. - let message_offset = *offset as u16; - - // Read the message prefix byte if present. This byte is present in V0 - // transactions but not in legacy transactions. - // The message header begins immediately after the message prefix byte - // if present. - let message_prefix = read_byte(bytes, offset)?; - let (version, num_required_signatures) = if message_prefix & MESSAGE_VERSION_PREFIX != 0 { - let version = message_prefix & !MESSAGE_VERSION_PREFIX; - match version { - 0 => (TransactionVersion::V0, read_byte(bytes, offset)?), - _ => return Err(TransactionViewError::ParseError), - } - } else { - // Legacy transaction. The `message_prefix` that was just read is - // actually the number of required signatures. - (TransactionVersion::Legacy, message_prefix) - }; - - let num_readonly_signed_accounts = read_byte(bytes, offset)?; - let num_readonly_unsigned_accounts = read_byte(bytes, offset)?; - - Ok(Self { - offset: message_offset, - version, - num_required_signatures, - num_readonly_signed_accounts, - num_readonly_unsigned_accounts, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_invalid_version() { - let bytes = [0b1000_0001]; - let mut offset = 0; - assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_legacy_transaction_missing_header_byte() { - let bytes = [5, 0]; - let mut offset = 0; - assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_legacy_transaction_valid() { - let bytes = [5, 1, 2]; - let mut offset = 0; - let header = MessageHeaderFrame::try_new(&bytes, &mut offset).unwrap(); - assert!(matches!(header.version, TransactionVersion::Legacy)); - assert_eq!(header.num_required_signatures, 5); - assert_eq!(header.num_readonly_signed_accounts, 1); - assert_eq!(header.num_readonly_unsigned_accounts, 2); - } - - #[test] - fn test_v0_transaction_missing_header_byte() { - let bytes = [MESSAGE_VERSION_PREFIX, 5, 1]; - let mut offset = 0; - assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_v0_transaction_valid() { - let bytes = [MESSAGE_VERSION_PREFIX, 5, 1, 2]; - let mut offset = 0; - let header = MessageHeaderFrame::try_new(&bytes, &mut offset).unwrap(); - assert!(matches!(header.version, TransactionVersion::V0)); - assert_eq!(header.num_required_signatures, 5); - assert_eq!(header.num_readonly_signed_accounts, 1); - assert_eq!(header.num_readonly_unsigned_accounts, 2); - } -} diff --git a/transaction-view/src/resolved_transaction_view.rs b/transaction-view/src/resolved_transaction_view.rs deleted file mode 100644 index 7e34570567a..00000000000 --- a/transaction-view/src/resolved_transaction_view.rs +++ /dev/null @@ -1,598 +0,0 @@ -use { - crate::{ - result::{Result, TransactionViewError}, - transaction_data::TransactionData, - transaction_version::TransactionVersion, - transaction_view::TransactionView, - }, - core::{ - fmt::{Debug, Formatter}, - ops::Deref, - }, - solana_hash::Hash, - solana_message::{AccountKeys, v0::LoadedAddresses}, - solana_pubkey::Pubkey, - solana_sdk_ids::bpf_loader_upgradeable, - solana_signature::Signature, - solana_svm_transaction::{ - instruction::SVMInstruction, - message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::{SVMMessage, SVMStaticMessage}, - svm_transaction::SVMTransaction, - }, - std::collections::HashSet, -}; - -/// A parsed and sanitized transaction view that has had all address lookups -/// resolved. -pub struct ResolvedTransactionView { - /// The parsed and sanitized transaction view. - view: TransactionView, - /// The resolved address lookups. - resolved_addresses: Option, - /// A cache for whether an address is writable. - // Sanitized transactions are guaranteed to have a maximum of 256 keys, - // because account indexing is done with a u8. - writable_cache: [bool; 256], -} - -impl Deref for ResolvedTransactionView { - type Target = TransactionView; - - fn deref(&self) -> &Self::Target { - &self.view - } -} - -impl ResolvedTransactionView { - /// Given a parsed and sanitized transaction view, and a set of resolved - /// addresses, create a resolved transaction view. - pub fn try_new( - view: TransactionView, - resolved_addresses: Option, - reserved_account_keys: &HashSet, - ) -> Result { - let resolved_addresses_ref = resolved_addresses.as_ref(); - - // verify that the number of readable and writable match up. - // This is a basic sanity check to make sure we're not passing a totally - // invalid set of resolved addresses. - // Additionally if it is a v0 transaction it *must* have resolved - // addresses, even if they are empty. - if matches!(view.version(), TransactionVersion::V0) && resolved_addresses_ref.is_none() { - return Err(TransactionViewError::AddressLookupMismatch); - } - if let Some(loaded_addresses) = resolved_addresses_ref { - if loaded_addresses.writable.len() != usize::from(view.total_writable_lookup_accounts()) - || loaded_addresses.readonly.len() - != usize::from(view.total_readonly_lookup_accounts()) - { - return Err(TransactionViewError::AddressLookupMismatch); - } - } else if view.total_writable_lookup_accounts() != 0 - || view.total_readonly_lookup_accounts() != 0 - { - return Err(TransactionViewError::AddressLookupMismatch); - } - - let writable_cache = - Self::cache_is_writable(&view, resolved_addresses_ref, reserved_account_keys); - Ok(Self { - view, - resolved_addresses, - writable_cache, - }) - } - - /// Helper function to check if an address is writable, - /// and cache the result. - /// This is done so we avoid recomputing the expensive checks each time we call - /// `is_writable` - since there is more to it than just checking index. - fn cache_is_writable( - view: &TransactionView, - resolved_addresses: Option<&LoadedAddresses>, - reserved_account_keys: &HashSet, - ) -> [bool; 256] { - // Build account keys so that we can iterate over and check if - // an address is writable. - let account_keys = AccountKeys::new(view.static_account_keys(), resolved_addresses); - - let mut is_writable_cache = [false; 256]; - let num_static_account_keys = usize::from(view.num_static_account_keys()); - let num_writable_lookup_accounts = usize::from(view.total_writable_lookup_accounts()); - let num_signed_accounts = usize::from(view.num_required_signatures()); - let num_writable_unsigned_static_accounts = - usize::from(view.num_writable_unsigned_static_accounts()); - let num_writable_signed_static_accounts = - usize::from(view.num_writable_signed_static_accounts()); - - for (index, key) in account_keys.iter().enumerate() { - let is_requested_write = { - // If the account is a resolved address, check if it is writable. - if index >= num_static_account_keys { - let loaded_address_index = index.wrapping_sub(num_static_account_keys); - loaded_address_index < num_writable_lookup_accounts - } else if index >= num_signed_accounts { - let unsigned_account_index = index.wrapping_sub(num_signed_accounts); - unsigned_account_index < num_writable_unsigned_static_accounts - } else { - index < num_writable_signed_static_accounts - } - }; - - // If the key is reserved it cannot be writable. - is_writable_cache[index] = is_requested_write && !reserved_account_keys.contains(key); - } - - // If a program account is locked, it cannot be writable unless the - // upgradable loader is present. - // However, checking for the upgradable loader is somewhat expensive, so - // we only do it if we find a writable program id. - let mut is_upgradable_loader_present = None; - for ix in view.instructions_iter() { - let program_id_index = usize::from(ix.program_id_index); - if is_writable_cache[program_id_index] - && !*is_upgradable_loader_present.get_or_insert_with(|| { - for key in account_keys.iter() { - if key == &bpf_loader_upgradeable::ID { - return true; - } - } - false - }) - { - is_writable_cache[program_id_index] = false; - } - } - - is_writable_cache - } - - pub fn loaded_addresses(&self) -> Option<&LoadedAddresses> { - self.resolved_addresses.as_ref() - } - - pub fn into_view(self) -> TransactionView { - self.view - } -} - -impl SVMStaticMessage for ResolvedTransactionView { - fn version(&self) -> solana_transaction::versioned::TransactionVersion { - self.view.version().into() - } - - fn num_transaction_signatures(&self) -> u64 { - u64::from(self.view.num_required_signatures()) - } - - fn num_write_locks(&self) -> u64 { - self.view.num_requested_write_locks() - } - - fn recent_blockhash(&self) -> &Hash { - self.view.recent_blockhash() - } - - fn num_instructions(&self) -> usize { - usize::from(self.view.num_instructions()) - } - - fn instructions_iter(&self) -> impl Iterator> { - self.view.instructions_iter() - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator< - Item = ( - &solana_pubkey::Pubkey, - solana_svm_transaction::instruction::SVMInstruction<'_>, - ), - > + Clone { - self.view.program_instructions_iter() - } - - fn static_account_keys(&self) -> &[Pubkey] { - self.view.static_account_keys() - } - - fn fee_payer(&self) -> &Pubkey { - &self.view.static_account_keys()[0] - } - - fn num_lookup_tables(&self) -> usize { - usize::from(self.view.num_address_table_lookups()) - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - self.view.address_table_lookup_iter() - } -} - -impl SVMMessage for ResolvedTransactionView { - fn account_keys(&self) -> AccountKeys<'_> { - AccountKeys::new( - self.view.static_account_keys(), - self.resolved_addresses.as_ref(), - ) - } - - fn is_writable(&self, index: usize) -> bool { - self.writable_cache.get(index).copied().unwrap_or(false) - } - - fn is_signer(&self, index: usize) -> bool { - index < usize::from(self.view.num_required_signatures()) - } - - fn is_invoked(&self, key_index: usize) -> bool { - let Ok(index) = u8::try_from(key_index) else { - return false; - }; - self.view - .instructions_iter() - .any(|ix| ix.program_id_index == index) - } -} - -impl SVMTransaction for ResolvedTransactionView { - fn signature(&self) -> &Signature { - &self.view.signatures()[0] - } - - fn signatures(&self) -> &[Signature] { - self.view.signatures() - } -} - -impl Debug for ResolvedTransactionView { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - f.debug_struct("ResolvedTransactionView") - .field("view", &self.view) - .finish() - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::{sanitize::SanitizeConfig, transaction_view::SanitizedTransactionView}, - solana_message::{ - MessageHeader, VersionedMessage, - compiled_instruction::CompiledInstruction, - v0::{self, MessageAddressTableLookup}, - }, - solana_sdk_ids::{system_program, sysvar}, - solana_signature::Signature, - solana_transaction::versioned::VersionedTransaction, - }; - - // Current protocol values; production callers supply these from agave. - fn test_config() -> SanitizeConfig { - SanitizeConfig { - min_requested_heap_size: 32 * 1024, - max_requested_heap_size: 256 * 1024, - max_instructions: 64, - max_accounts_per_instruction: 255, - } - } - - #[test] - fn test_expected_loaded_addresses() { - // Expected addresses passed in, but `None` was passed. - let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; - let transaction = VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - instructions: vec![], - account_keys: static_keys, - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0], - readonly_indexes: vec![1], - }], - recent_blockhash: Hash::default(), - }), - }; - let bytes = wincode::serialize(&transaction).unwrap(); - let view = - SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); - let result = ResolvedTransactionView::try_new(view, None, &HashSet::default()); - assert!(matches!( - result, - Err(TransactionViewError::AddressLookupMismatch) - )); - } - - #[test] - fn test_unexpected_loaded_addresses() { - // Expected no addresses passed in, but `Some` was passed. - let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; - let loaded_addresses = LoadedAddresses { - writable: vec![Pubkey::new_unique()], - readonly: vec![], - }; - let transaction = VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - instructions: vec![], - account_keys: static_keys, - address_table_lookups: vec![], - recent_blockhash: Hash::default(), - }), - }; - let bytes = wincode::serialize(&transaction).unwrap(); - let view = - SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); - let result = - ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); - assert!(matches!( - result, - Err(TransactionViewError::AddressLookupMismatch) - )); - } - - #[test] - fn test_mismatched_loaded_address_lengths() { - // Loaded addresses only has 1 writable address, no readonly. - // The message ATL has 1 writable and 1 readonly. - let static_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; - let loaded_addresses = LoadedAddresses { - writable: vec![Pubkey::new_unique()], - readonly: vec![], - }; - let transaction = VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - instructions: vec![], - account_keys: static_keys, - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0], - readonly_indexes: vec![1], - }], - recent_blockhash: Hash::default(), - }), - }; - let bytes = wincode::serialize(&transaction).unwrap(); - let view = - SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()).unwrap(); - let result = - ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); - assert!(matches!( - result, - Err(TransactionViewError::AddressLookupMismatch) - )); - } - - #[test] - fn test_is_writable() { - let reserved_account_keys = HashSet::from_iter([sysvar::clock::id(), system_program::id()]); - // Create a versioned transaction. - let create_transaction_with_keys = - |static_keys: Vec, loaded_addresses: &LoadedAddresses| VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - account_keys: static_keys[..2].to_vec(), - recent_blockhash: Hash::default(), - instructions: vec![], - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (0..loaded_addresses.writable.len()) - .map(|x| (static_keys.len() + x) as u8) - .collect(), - readonly_indexes: (0..loaded_addresses.readonly.len()) - .map(|x| { - (static_keys.len() + loaded_addresses.writable.len() + x) as u8 - }) - .collect(), - }], - }), - }; - - let key0 = Pubkey::new_unique(); - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - { - let static_keys = vec![sysvar::clock::id(), key0]; - let loaded_addresses = LoadedAddresses { - writable: vec![key1], - readonly: vec![key2], - }; - let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses), - &reserved_account_keys, - ) - .unwrap(); - - // demote reserved static key to readonly - let expected = vec![false, false, true, false]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - - { - let static_keys = vec![system_program::id(), key0]; - let loaded_addresses = LoadedAddresses { - writable: vec![key1], - readonly: vec![key2], - }; - let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses), - &reserved_account_keys, - ) - .unwrap(); - - // demote reserved static key to readonly - let expected = vec![false, false, true, false]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - - { - let static_keys = vec![key0, key1]; - let loaded_addresses = LoadedAddresses { - writable: vec![system_program::id()], - readonly: vec![key2], - }; - let transaction = create_transaction_with_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses), - &reserved_account_keys, - ) - .unwrap(); - - // demote loaded key to readonly - let expected = vec![true, false, false, false]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - } - - #[test] - fn test_demote_writable_program() { - let reserved_account_keys = HashSet::default(); - let key0 = Pubkey::new_unique(); - let key1 = Pubkey::new_unique(); - let key2 = Pubkey::new_unique(); - let key3 = Pubkey::new_unique(); - let key4 = Pubkey::new_unique(); - let loaded_addresses = LoadedAddresses { - writable: vec![key3, key4], - readonly: vec![], - }; - let create_transaction_with_static_keys = - |static_keys: Vec, loaded_addresses: &LoadedAddresses| VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V0(v0::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - instructions: vec![CompiledInstruction { - program_id_index: 1, - accounts: vec![0], - data: vec![], - }], - account_keys: static_keys, - address_table_lookups: vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (0..loaded_addresses.writable.len()) - .map(|x| x as u8) - .collect(), - readonly_indexes: (0..loaded_addresses.readonly.len()) - .map(|x| (loaded_addresses.writable.len() + x) as u8) - .collect(), - }], - recent_blockhash: Hash::default(), - }), - }; - - // Demote writable program - static - { - let static_keys = vec![key0, key1, key2]; - let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses.clone()), - &reserved_account_keys, - ) - .unwrap(); - - let expected = vec![true, false, true, true, true]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - - // Do not demote writable program - static address: upgradable loader - { - let static_keys = vec![key0, key1, bpf_loader_upgradeable::ID]; - let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses.clone()), - &reserved_account_keys, - ) - .unwrap(); - - let expected = vec![true, true, true, true, true]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - - // Do not demote writable program - loaded address: upgradable loader - { - let static_keys = vec![key0, key1, key2]; - let loaded_addresses = LoadedAddresses { - writable: vec![key3], - readonly: vec![bpf_loader_upgradeable::ID], - }; - let transaction = create_transaction_with_static_keys(static_keys, &loaded_addresses); - let bytes = wincode::serialize(&transaction).unwrap(); - let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), &test_config()) - .unwrap(); - - let resolved_view = ResolvedTransactionView::try_new( - view, - Some(loaded_addresses.clone()), - &reserved_account_keys, - ) - .unwrap(); - - let expected = vec![true, true, true, true, false]; - for (index, expected) in expected.into_iter().enumerate() { - assert_eq!(resolved_view.is_writable(index), expected); - } - } - } -} diff --git a/transaction-view/src/result.rs b/transaction-view/src/result.rs deleted file mode 100644 index 028a7f1134b..00000000000 --- a/transaction-view/src/result.rs +++ /dev/null @@ -1,9 +0,0 @@ -#[derive(Debug, PartialEq, Eq)] -#[repr(u8)] // repr(u8) is used to ensure that the enum is represented as a single byte in memory. -pub enum TransactionViewError { - ParseError, - SanitizeError, - AddressLookupMismatch, -} - -pub type Result = core::result::Result; diff --git a/transaction-view/src/sanitize.rs b/transaction-view/src/sanitize.rs deleted file mode 100644 index b0d67a416d8..00000000000 --- a/transaction-view/src/sanitize.rs +++ /dev/null @@ -1,1017 +0,0 @@ -use crate::{ - result::{Result, TransactionViewError}, - signature_frame::MAX_SIGNATURES_PER_PACKET, - transaction_data::TransactionData, - transaction_version::TransactionVersion, - transaction_view::UnsanitizedTransactionView, -}; - -/// Protocol limits enforced during sanitization. -/// -/// These values are consensus parameters owned by the caller; this crate -/// intentionally does not define defaults for them. -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub struct SanitizeConfig { - /// Inclusive lower bound for a V1 requested heap size, in bytes. - pub min_requested_heap_size: u32, - /// Inclusive upper bound for a V1 requested heap size, in bytes. - pub max_requested_heap_size: u32, - /// SIMD-160: maximum number of top-level instructions. - pub max_instructions: usize, - /// SIMD-406: maximum number of accounts per instruction. - pub max_accounts_per_instruction: usize, -} - -pub(crate) fn sanitize( - view: &UnsanitizedTransactionView, - config: &SanitizeConfig, -) -> Result<()> { - sanitize_transaction_size(view)?; - sanitize_message_header(view)?; - sanitize_config(view, config)?; - sanitize_signatures(view)?; - sanitize_account_access(view)?; - sanitize_instructions(view, config)?; - sanitize_address_table_lookups(view) -} - -/// Transaction constraints: -/// * size <= 4096 bytes -fn sanitize_transaction_size( - view: &UnsanitizedTransactionView, -) -> Result<()> { - let max_transaction_size = match view.version() { - TransactionVersion::Legacy | TransactionVersion::V0 => solana_packet::PACKET_DATA_SIZE, - TransactionVersion::V1 => solana_message::v1::MAX_TRANSACTION_SIZE, - }; - - if view.data().len() > max_transaction_size { - return Err(TransactionViewError::SanitizeError); - } - Ok(()) -} - -/// message header constraints: -/// * num_required_signatures >= 1 -/// * num_readonly_signed_accounts < num_required_signatures (fee payer must be writable) -/// * num_readonly_unsigned_accounts <= (num_addresses - num_required_signatures) -fn sanitize_message_header(view: &UnsanitizedTransactionView) -> Result<()> { - if view.num_required_signatures() < 1 { - return Err(TransactionViewError::SanitizeError); - } - - if view.num_readonly_signed_static_accounts() >= view.num_required_signatures() { - return Err(TransactionViewError::SanitizeError); - } - - // Check there is no overlap of signing area and readonly non-signing area. - // We have already checked that `num_required_signatures` is less than or equal to `num_static_account_keys`, - // so it is safe to use wrapping arithmetic. - if view.num_readonly_unsigned_static_accounts() - > view - .num_static_account_keys() - .wrapping_sub(view.num_required_signatures()) - { - return Err(TransactionViewError::SanitizeError); - } - - Ok(()) -} - -/// Config Constraints: -/// * heap_size must be multiples of 1024, if specified -fn sanitize_config( - view: &UnsanitizedTransactionView, - config: &SanitizeConfig, -) -> Result<()> { - if let Some(requested_heap_bytes) = view - .transaction_config() - .and_then(|config| config.requested_heap_size()) - && (!(config.min_requested_heap_size..=config.max_requested_heap_size) - .contains(&requested_heap_bytes) - || !requested_heap_bytes.is_multiple_of(1024)) - { - return Err(TransactionViewError::SanitizeError); - } - - Ok(()) -} - -/// Sigantures Constraint: -/// * Number of signatures must equal: num_required_signatures -/// * Max signatures <= 12 -fn sanitize_signatures(view: &UnsanitizedTransactionView) -> Result<()> { - // Check the required number of signatures matches the number of signatures. - if view.num_signatures() != view.num_required_signatures() { - return Err(TransactionViewError::SanitizeError); - } - - if view.num_signatures() > MAX_SIGNATURES_PER_PACKET { - return Err(TransactionViewError::SanitizeError); - } - - // Each signature is associated with a unique static public key. - // Check that there are at least as many static account keys as signatures. - if view.num_static_account_keys() < view.num_signatures() { - return Err(TransactionViewError::SanitizeError); - } - - Ok(()) -} - -/// Accounts (aka Addresses) Constraints: -/// * for v1: 1 <= NumAddresses <= 64 -/// * legacy/v0 uses current limits of: num_accounts <= 256 (u8 bound) -/// * No duplicate addresses -fn sanitize_account_access(view: &UnsanitizedTransactionView) -> Result<()> { - let addresses_limit = match view.version() { - TransactionVersion::Legacy | TransactionVersion::V0 => 256, - TransactionVersion::V1 => 64, - }; - - if total_number_of_accounts(view) > addresses_limit { - return Err(TransactionViewError::SanitizeError); - } - - // No duplicated accounts - // Note: This check is performed downstream in `validate_account_locks()`. - // It is skipped here to avoid redundant work on the hot path. - - Ok(()) -} - -/// Instructions Constraints -/// * NumInstructions <= 64 -/// * Per instruction: -/// * 0 < program_id_index < MaxProgramIdIndex -/// * all account indices < MaxAccountIndex -fn sanitize_instructions( - view: &UnsanitizedTransactionView, - config: &SanitizeConfig, -) -> Result<()> { - // SIMD-160: transaction can not have more than 64 top level instructions - if usize::from(view.num_instructions()) > config.max_instructions { - return Err(TransactionViewError::SanitizeError); - } - - // already verified there is at least one static account. - let max_program_id_index = view.num_static_account_keys().wrapping_sub(1); - // verified that there are no more than 256 accounts in `sanitize_account_access` - let max_account_index = total_number_of_accounts(view).wrapping_sub(1) as u8; - - for instruction in view.instructions_iter() { - // Check that program indexes are static account keys. - if instruction.program_id_index > max_program_id_index { - return Err(TransactionViewError::SanitizeError); - } - - // Check that the program index is not the fee-payer. - if instruction.program_id_index == 0 { - return Err(TransactionViewError::SanitizeError); - } - - // Check that all account indexes are valid. - for account_index in instruction.accounts.iter().copied() { - if account_index > max_account_index { - return Err(TransactionViewError::SanitizeError); - } - } - - if instruction.accounts.len() > config.max_accounts_per_instruction { - return Err(TransactionViewError::SanitizeError); - } - } - - Ok(()) -} - -fn sanitize_address_table_lookups( - view: &UnsanitizedTransactionView, -) -> Result<()> { - for address_table_lookup in view.address_table_lookup_iter() { - // Check that there is at least one account lookup. - if address_table_lookup.writable_indexes.is_empty() - && address_table_lookup.readonly_indexes.is_empty() - { - return Err(TransactionViewError::SanitizeError); - } - } - - Ok(()) -} - -fn total_number_of_accounts(view: &UnsanitizedTransactionView) -> u16 { - u16::from(view.num_static_account_keys()) - .saturating_add(view.total_writable_lookup_accounts()) - .saturating_add(view.total_readonly_lookup_accounts()) -} - -#[cfg(test)] -mod tests { - use { - super::*, - crate::transaction_view::TransactionView, - solana_hash::Hash, - solana_message::{ - Message, MessageHeader, VersionedMessage, - compiled_instruction::CompiledInstruction, - v0::{self, MessageAddressTableLookup}, - v1::{self, TransactionConfig}, - }, - solana_pubkey::Pubkey, - solana_signature::Signature, - solana_system_interface::instruction as system_instruction, - solana_transaction::versioned::VersionedTransaction, - }; - - // Current protocol values; production callers supply these from agave. - fn test_config() -> SanitizeConfig { - SanitizeConfig { - min_requested_heap_size: 32 * 1024, - max_requested_heap_size: 256 * 1024, - max_instructions: 64, - max_accounts_per_instruction: 255, - } - } - - fn create_legacy_transaction( - num_signatures: u8, - header: MessageHeader, - account_keys: Vec, - instructions: Vec, - ) -> VersionedTransaction { - VersionedTransaction { - signatures: vec![Signature::default(); num_signatures as usize], - message: VersionedMessage::Legacy(Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - }), - } - } - - fn create_v0_transaction( - num_signatures: u8, - header: MessageHeader, - account_keys: Vec, - instructions: Vec, - address_table_lookups: Vec, - ) -> VersionedTransaction { - VersionedTransaction { - signatures: vec![Signature::default(); num_signatures as usize], - message: VersionedMessage::V0(v0::Message { - header, - account_keys, - recent_blockhash: Hash::default(), - instructions, - address_table_lookups, - }), - } - } - - fn create_v1_transaction( - num_signatures: u8, - header: MessageHeader, - account_keys: Vec, - instructions: Vec, - config: TransactionConfig, - ) -> VersionedTransaction { - VersionedTransaction { - signatures: vec![Signature::default(); num_signatures as usize], - message: VersionedMessage::V1(v1::Message { - header, - account_keys, - lifetime_specifier: Hash::default(), - instructions, - config, - }), - } - } - - fn multiple_transfers() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::Legacy(Message::new( - &[ - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - ], - Some(&payer), - )), - } - } - - #[test] - fn test_sanitize_multiple_transfers() { - let transaction = multiple_transfers(); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(view.sanitize(&test_config()).is_ok()); - } - - #[test] - fn test_sanitize_transaction_size_too_large() { - let account_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; - let transaction = create_legacy_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - account_keys, - vec![CompiledInstruction { - program_id_index: 1, - accounts: vec![0], - data: vec![0; 5000], - }], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_transaction_size(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - #[test] - fn test_sanitize_signatures() { - // Too few signatures. - { - let transaction = create_legacy_transaction( - 1, - MessageHeader { - num_required_signatures: 2, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..3).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Too many signatures. - { - let transaction = create_legacy_transaction( - 2, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..3).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Not enough static accounts. - { - let transaction = create_legacy_transaction( - 2, - MessageHeader { - num_required_signatures: 2, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..1).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // More than 12 signatures. - { - let transaction = create_legacy_transaction( - 13, - MessageHeader { - num_required_signatures: 13, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..13).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()); - // SignatureFrame validates number of signatures, it throw ParseError if - // it is less than 12 - assert!(matches!(view, Err(TransactionViewError::ParseError))); - } - - { - let transaction = create_v1_transaction( - 13, - MessageHeader { - num_required_signatures: 13, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..13).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Not enough static accounts. - { - let transaction = create_legacy_transaction( - 2, - MessageHeader { - num_required_signatures: 2, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..1).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Not enough static accounts - with look up accounts - { - let transaction = create_v0_transaction( - 2, - MessageHeader { - num_required_signatures: 2, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..1).map(|_| Pubkey::new_unique()).collect(), - vec![], - vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0, 1, 2, 3, 4, 5], - readonly_indexes: vec![6, 7, 8], - }], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_signatures(&view), - Err(TransactionViewError::SanitizeError) - ); - } - } - - #[test] - fn test_sanitize_account_access() { - // num_required_signatures must be >= 1. - { - let transaction = create_legacy_transaction( - 0, - MessageHeader { - num_required_signatures: 0, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - vec![Pubkey::new_unique()], - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()); - // SignatureFrame validates number of signatures, it throw ParseError if - // it is less than 1 - assert!(matches!(view, Err(TransactionViewError::ParseError))); - } - { - let transaction = create_v1_transaction( - 0, - MessageHeader { - num_required_signatures: 0, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - vec![Pubkey::new_unique()], - vec![], - TransactionConfig::empty(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_message_header(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Overlap of signing and readonly non-signing accounts. - { - let transaction = create_legacy_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 2, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_message_header(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Not enough writable accounts. - { - let transaction = create_legacy_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 1, - num_readonly_unsigned_accounts: 0, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_message_header(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // Too many accounts in legacy/v0 - { - let transaction = create_v0_transaction( - 2, - MessageHeader { - num_required_signatures: 2, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - (0..1).map(|_| Pubkey::new_unique()).collect(), - vec![], - vec![ - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (0..100).collect(), - readonly_indexes: (100..200).collect(), - }, - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: (100..200).collect(), - readonly_indexes: (0..100).collect(), - }, - ], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_account_access(&view), - Err(TransactionViewError::SanitizeError) - ); - } - - // V1: too many static accounts. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 63, - }, - (0..65).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_account_access(&view), - Err(TransactionViewError::SanitizeError) - ); - } - } - - #[test] - fn test_sanitize_instructions() { - let num_signatures = 1; - let header = MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }; - let account_keys = vec![ - Pubkey::new_unique(), - Pubkey::new_unique(), - Pubkey::new_unique(), - ]; - let valid_instructions = vec![ - CompiledInstruction { - program_id_index: 1, - accounts: vec![0, 1], - data: vec![1, 2, 3], - }, - CompiledInstruction { - program_id_index: 2, - accounts: vec![1, 0], - data: vec![3, 2, 1, 4], - }, - ]; - let atls = vec![MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0, 1], - readonly_indexes: vec![2], - }]; - - // Verify that the unmodified transaction(s) are valid/sanitized. - { - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - valid_instructions.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_instructions(&view, &test_config()).is_ok()); - - let transaction = create_v0_transaction( - num_signatures, - header, - account_keys.clone(), - valid_instructions.clone(), - atls.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_instructions(&view, &test_config()).is_ok()); - } - - for instruction_index in 0..valid_instructions.len() { - // Invalid program index. - { - let mut instructions = valid_instructions.clone(); - instructions[instruction_index].program_id_index = account_keys.len() as u8; - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - instructions, - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Invalid program index with lookups. - { - let mut instructions = valid_instructions.clone(); - instructions[instruction_index].program_id_index = account_keys.len() as u8; - let transaction = create_v0_transaction( - num_signatures, - header, - account_keys.clone(), - instructions, - atls.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Program index is fee-payer. - { - let mut instructions = valid_instructions.clone(); - instructions[instruction_index].program_id_index = 0; - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - instructions, - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Invalid account index. - { - let mut instructions = valid_instructions.clone(); - instructions[instruction_index] - .accounts - .push(account_keys.len() as u8); - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - instructions, - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Invalid account index with v0. - { - let num_lookup_accounts = - atls[0].writable_indexes.len() + atls[0].readonly_indexes.len(); - let total_accounts = (account_keys.len() + num_lookup_accounts) as u8; - let mut instructions = valid_instructions.clone(); - instructions[instruction_index] - .accounts - .push(total_accounts); - let transaction = create_v0_transaction( - num_signatures, - header, - account_keys.clone(), - instructions, - atls.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - } - - // SIMD-0160, too many instructions are invalid - { - let too_many_instructions: Vec<_> = valid_instructions - .iter() - .cycle() - .take(65) - .cloned() - .collect(); - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - too_many_instructions.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - - let transaction = create_v0_transaction( - num_signatures, - header, - account_keys.clone(), - too_many_instructions.clone(), - atls.clone(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // SIMD-406: Limit instruction accounts to 255 - { - let mut accounts: Vec = vec![0; 254]; - accounts.push(1); - accounts.push(2); - let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts); - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - vec![instr], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_instructions(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // SIMD-406: Limit instruction accounts to 255 - { - let mut accounts: Vec = vec![0; 254]; - accounts.push(1); - let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts); - let transaction = create_legacy_transaction( - num_signatures, - header, - account_keys.clone(), - vec![instr], - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - // Exactly 255 accounts must pass sanitization. - assert!(sanitize_instructions(&view, &test_config()).is_ok()); - } - } - - #[test] - fn test_sanitize_address_table_lookups() { - fn create_transaction(empty_index: usize) -> VersionedTransaction { - let payer = Pubkey::new_unique(); - let mut address_table_lookups = vec![ - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0, 1], - readonly_indexes: vec![], - }, - MessageAddressTableLookup { - account_key: Pubkey::new_unique(), - writable_indexes: vec![0, 1], - readonly_indexes: vec![], - }, - ]; - address_table_lookups[empty_index].writable_indexes.clear(); - create_v0_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - vec![payer], - vec![], - address_table_lookups, - ) - } - - for empty_index in 0..2 { - let transaction = create_transaction(empty_index); - assert_eq!( - transaction.message.address_table_lookups().unwrap().len(), - 2 - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_address_table_lookups(&view), - Err(TransactionViewError::SanitizeError) - ); - } - } - - #[test] - fn test_sanitize_config() { - // Valid min heap size. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty().with_heap_size(test_config().min_requested_heap_size), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view, &test_config()).is_ok()); - } - - // Valid max heap size. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty().with_heap_size(test_config().max_requested_heap_size), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view, &test_config()).is_ok()); - } - - // Heap size below min. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty() - .with_heap_size(test_config().min_requested_heap_size - 1), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_config(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Heap size above max. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty() - .with_heap_size(test_config().max_requested_heap_size + 1), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_config(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Heap size not multiple of 1024. - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty() - .with_heap_size(test_config().min_requested_heap_size + 1), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert_eq!( - sanitize_config(&view, &test_config()), - Err(TransactionViewError::SanitizeError) - ); - } - - // Config is not set, default is OK - { - let transaction = create_v1_transaction( - 1, - MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - (0..2).map(|_| Pubkey::new_unique()).collect(), - vec![], - TransactionConfig::empty(), - ); - let data = wincode::serialize(&transaction).unwrap(); - let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); - assert!(sanitize_config(&view, &test_config()).is_ok()); - } - } -} diff --git a/transaction-view/src/signature_frame.rs b/transaction-view/src/signature_frame.rs deleted file mode 100644 index 091d32d1b30..00000000000 --- a/transaction-view/src/signature_frame.rs +++ /dev/null @@ -1,112 +0,0 @@ -use { - crate::{ - bytes::{advance_offset_for_array, read_byte}, - result::{Result, TransactionViewError}, - }, - solana_packet::PACKET_DATA_SIZE, - solana_pubkey::Pubkey, - solana_signature::Signature, -}; - -// The packet has a maximum length of 1232 bytes. -// Each signature must be paired with a unique static pubkey, so each -// signature really requires 96 bytes. This means the maximum number of -// signatures in a **valid** transaction packet is 12. -// In our u16 encoding scheme, 12 would be encoded as a single byte. -// Rather than using the u16 decoding, we can simply read the byte and -// verify that the MSB is not set. -pub(crate) const MAX_SIGNATURES_PER_PACKET: u8 = - (PACKET_DATA_SIZE / (core::mem::size_of::() + core::mem::size_of::())) as u8; - -/// Metadata for accessing transaction-level signatures in a transaction view. -#[derive(Debug)] -pub(crate) struct SignatureFrame { - /// The number of signatures in the transaction. - pub(crate) num_signatures: u8, - /// Offset to the first signature in the transaction packet. - pub(crate) offset: u16, -} - -impl SignatureFrame { - /// Get the number of signatures and the offset to the first signature in - /// the transaction packet, starting at the given `offset`. - #[inline(always)] - pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { - // Maximum number of signatures should be represented by a single byte, - // thus the MSB should not be set. - const _: () = assert!(MAX_SIGNATURES_PER_PACKET & 0b1000_0000 == 0); - - let num_signatures = read_byte(bytes, offset)?; - if num_signatures == 0 || num_signatures > MAX_SIGNATURES_PER_PACKET { - return Err(TransactionViewError::ParseError); - } - - let signature_offset = *offset as u16; - advance_offset_for_array::(bytes, offset, u16::from(num_signatures))?; - - Ok(Self { - num_signatures, - offset: signature_offset, - }) - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_short_vec::ShortVec}; - - #[test] - fn test_zero_signatures() { - let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); - let mut offset = 0; - assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_one_signature() { - let bytes = bincode::serialize(&ShortVec(vec![Signature::default()])).unwrap(); - let mut offset = 0; - let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_signatures, 1); - assert_eq!(frame.offset, 1); - assert_eq!(offset, 1 + core::mem::size_of::()); - } - - #[test] - fn test_max_signatures() { - let signatures = vec![Signature::default(); usize::from(MAX_SIGNATURES_PER_PACKET)]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_signatures, 12); - assert_eq!(frame.offset, 1); - assert_eq!(offset, 1 + 12 * core::mem::size_of::()); - } - - #[test] - fn test_non_zero_offset() { - let mut bytes = bincode::serialize(&ShortVec(vec![Signature::default()])).unwrap(); - bytes.insert(0, 0); // Insert a byte at the beginning of the packet. - let mut offset = 1; // Start at the second byte. - let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_signatures, 1); - assert_eq!(frame.offset, 2); - assert_eq!(offset, 2 + core::mem::size_of::()); - } - - #[test] - fn test_too_many_signatures() { - let signatures = vec![Signature::default(); usize::from(MAX_SIGNATURES_PER_PACKET) + 1]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_u16_max_signatures() { - let signatures = vec![Signature::default(); u16::MAX as usize]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); - } -} diff --git a/transaction-view/src/static_account_keys_frame.rs b/transaction-view/src/static_account_keys_frame.rs deleted file mode 100644 index cabe542bfd9..00000000000 --- a/transaction-view/src/static_account_keys_frame.rs +++ /dev/null @@ -1,102 +0,0 @@ -use { - crate::{ - bytes::{advance_offset_for_array, read_byte}, - result::{Result, TransactionViewError}, - }, - solana_packet::PACKET_DATA_SIZE, - solana_pubkey::Pubkey, -}; - -// A legacy/v0 packet has a maximum length of 1232 bytes. -// This means the maximum number of 32 byte keys is 38. -// 38 as an min-sized encoded u16 is 1 byte. -// We can simply read this byte, if it's >38 we can return None. -const LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET: u8 = - (PACKET_DATA_SIZE / core::mem::size_of::()) as u8; - -/// Contains metadata about the static account keys in a transaction packet. -#[derive(Debug, Default)] -pub(crate) struct StaticAccountKeysFrame { - /// The number of static accounts in the transaction. - pub(crate) num_static_accounts: u8, - /// The offset to the first static account in the transaction. - pub(crate) offset: u16, -} - -impl StaticAccountKeysFrame { - #[inline(always)] - pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { - // Max size must not have the MSB set so that it is size 1. - const _: () = assert!(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET & 0b1000_0000 == 0); - - let num_static_accounts = read_byte(bytes, offset)?; - if num_static_accounts == 0 - || num_static_accounts > LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET - { - return Err(TransactionViewError::ParseError); - } - - // We also know that the offset must be less than 3 here, since the - // compressed u16 can only use up to 3 bytes, so there is no need to - // check if the offset is greater than u16::MAX. - let static_accounts_offset = *offset as u16; - // Update offset for array of static accounts. - advance_offset_for_array::(bytes, offset, u16::from(num_static_accounts))?; - - Ok(Self { - num_static_accounts, - offset: static_accounts_offset, - }) - } -} - -#[cfg(test)] -mod tests { - use {super::*, solana_short_vec::ShortVec}; - - #[test] - fn test_zero_accounts() { - let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); - let mut offset = 0; - assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_one_account() { - let bytes = bincode::serialize(&ShortVec(vec![Pubkey::default()])).unwrap(); - let mut offset = 0; - let frame = StaticAccountKeysFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_static_accounts, 1); - assert_eq!(frame.offset, 1); - assert_eq!(offset, 1 + core::mem::size_of::()); - } - - #[test] - fn test_max_accounts() { - let signatures = - vec![Pubkey::default(); usize::from(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET)]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - let frame = StaticAccountKeysFrame::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(frame.num_static_accounts, 38); - assert_eq!(frame.offset, 1); - assert_eq!(offset, 1 + 38 * core::mem::size_of::()); - } - - #[test] - fn test_too_many_accounts() { - let signatures = - vec![Pubkey::default(); usize::from(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET) + 1]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); - } - - #[test] - fn test_u16_max_accounts() { - let signatures = vec![Pubkey::default(); u16::MAX as usize]; - let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); - let mut offset = 0; - assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); - } -} diff --git a/transaction-view/src/transaction_config_frame.rs b/transaction-view/src/transaction_config_frame.rs deleted file mode 100644 index 448978cf7c4..00000000000 --- a/transaction-view/src/transaction_config_frame.rs +++ /dev/null @@ -1,456 +0,0 @@ -use crate::{ - bytes::{advance_offset_for_array, unchecked_copy_value}, - result::{Result, TransactionViewError}, -}; - -/// Metadata for accessing the tx-v1 transaction config section. -/// -/// This frame is a permanent part of `TransactionFrame`, but it is only -/// applicable to tx-v1. For legacy and v0 transactions, use -/// `TransactionConfigFrame::not_applicable()`. -/// -/// Layout, per SIMD-0385: -/// TransactionConfigMask (u32 LE) -/// ... -/// ConfigValues [[u8; 4]] // len = popcount(mask) -/// -/// Notes: -/// - `mask_offset == 0` is reserved to mean "not applicable" (legacy/v0). -/// - Parsed tx-v1 config frames should always have `mask_offset != 0`. -#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] -pub(crate) struct TransactionConfigFrame { - /// Offset of the 4-byte TransactionConfigMask. - /// - /// `0` means "not applicable" (legacy/v0). - pub(crate) mask_offset: u16, - - /// Decoded TransactionConfigMask. - pub(crate) mask: u32, - - /// Offset of the first ConfigValues word. - /// - /// `0` means "not applicable" (legacy/v0) - pub(crate) values_offset: u16, - - /// Number of 4-byte words in ConfigValues. - pub(crate) num_values: u8, -} - -#[allow(dead_code)] -impl TransactionConfigFrame { - pub(crate) const MASK_SIZE: usize = core::mem::size_of::(); - pub(crate) const CONFIG_VALUE_SIZE: usize = core::mem::size_of::(); - - /// Sentinel for legacy / v0 transactions. - #[inline(always)] - pub(crate) const fn not_applicable() -> Self { - Self { - mask_offset: 0, - mask: 0, - values_offset: 0, - num_values: 0, - } - } - - /// Returns true if this frame represents a tx-v1 transaction config. - #[inline(always)] - pub(crate) const fn is_present(&self) -> bool { - self.mask_offset != 0 - } - - /// Config Mask has been successfully parsed before advancing to `ConfigValues` - /// region; Now can try to create TransactionConfigFrame by parsing values. - #[inline(always)] - pub(crate) fn try_new( - bytes: &[u8], - mask_offset: usize, - mask: u32, - offset: &mut usize, - ) -> Result { - assert!(mask_offset > 0, "txv1 mask offset must be greater than 0"); - - Self::sanitize_mask(mask)?; - let num_values = mask.count_ones() as u8; - let mask_offset = - u16::try_from(mask_offset).map_err(|_| TransactionViewError::SanitizeError)?; - let values_offset = - u16::try_from(*offset).map_err(|_| TransactionViewError::SanitizeError)?; - - // advance offset - advance_offset_for_array::(bytes, offset, num_values as u16)?; - - Ok(Self { - mask_offset, - mask, - values_offset, - num_values, - }) - } - - /// Validate mask semantics. - /// - /// Check unknown / reserved bits are not used; And - /// Bits 0 and 1 together encode one logical 8-byte priority-fee field, - /// so they must either both be set or both be clear. - #[inline(always)] - fn sanitize_mask(mask: u32) -> Result<()> { - const ALLOWED_TRANSACTION_CONFIG_MASK: u32 = 0b1_1111; - - // Reject unknown / reserved bits - if mask & !ALLOWED_TRANSACTION_CONFIG_MASK != 0 { - return Err(TransactionViewError::SanitizeError); - } - - // priority fee uses first 2 bits - let bit0 = Self::has_bit(mask, 0); - let bit1 = Self::has_bit(mask, 1); - if bit0 ^ bit1 { - return Err(TransactionViewError::SanitizeError); - } - - Ok(()) - } - - #[inline(always)] - fn has_bit(mask: u32, bit: u8) -> bool { - bit < 32 && ((mask >> bit) & 1) != 0 - } - - /// Return the packed word index for a given set bit. Eg: counts - /// bits set below `bit`. - /// - /// Example: - /// mask = 0b0001_1100 - /// bit 2 -> 0 - /// bit 3 -> 1 - /// bit 4 -> 2 - #[inline(always)] - pub(crate) fn word_index_for_bit(&self, bit: u8) -> Option { - if !self.is_present() || !Self::has_bit(self.mask, bit) { - return None; - } - - let mask_before_bit = (1u32 << bit).wrapping_sub(1); - Some((self.mask & mask_before_bit).count_ones() as u8) - } - - #[inline(always)] - fn word_offset(&self, bit: u8) -> Option { - let word_index = usize::from(self.word_index_for_bit(bit)?); - Some( - usize::from(self.values_offset) - .wrapping_add(word_index.wrapping_mul(Self::CONFIG_VALUE_SIZE)), - ) - } -} - -#[derive(Debug, Clone, Copy)] -pub struct TransactionConfigView<'a> { - pub(crate) transaction_config_frame: &'a TransactionConfigFrame, - pub(crate) bytes: &'a [u8], -} - -impl<'a> TransactionConfigView<'a> { - #[inline(always)] - pub fn priority_fee_lamports(&self) -> Option { - // bit 0 and 1 have been sanitized to be in same state, - self.transaction_config_frame.word_offset(0).map(|offset| { - // SAFETY: - // - The offsets are checked to be valid in the byte slice. - // - u64 is valid for any bytes - u64::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) - }) - } - - #[inline(always)] - pub fn compute_unit_limit(&self) -> Option { - self.transaction_config_frame.word_offset(2).map(|offset| { - // SAFETY: - // - The offsets are checked to be valid in the byte slice. - // - u32 is valid for any bytes - u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) - }) - } - - #[inline(always)] - pub fn loaded_accounts_data_size_limit(&self) -> Option { - self.transaction_config_frame.word_offset(3).map(|offset| { - // SAFETY: - // - The offsets are checked to be valid in the byte slice. - // - u32 is valid for any bytes - u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) - }) - } - - #[inline(always)] - pub fn requested_heap_size(&self) -> Option { - self.transaction_config_frame.word_offset(4).map(|offset| { - // SAFETY: - // - The offsets are checked to be valid in the byte slice. - // - u32 is valid for any bytes - u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) - }) - } - - #[inline(always)] - pub fn mask(&self) -> u32 { - self.transaction_config_frame.mask - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn u32le(x: u32) -> [u8; 4] { - x.to_le_bytes() - } - - fn u64_words_le(x: u64) -> ([u8; 4], [u8; 4]) { - let bytes = x.to_le_bytes(); - ( - [bytes[0], bytes[1], bytes[2], bytes[3]], - [bytes[4], bytes[5], bytes[6], bytes[7]], - ) - } - - #[test] - fn test_not_applicable_defaults() { - let frame = TransactionConfigFrame::not_applicable(); - - assert!(!frame.is_present()); - assert_eq!(TransactionConfigFrame::sanitize_mask(frame.mask), Ok(())); - } - - #[test] - fn test_try_new_zero_mask_is_present() { - let mask = 0u32; - let bytes = mask.to_le_bytes(); - let mut buf = vec![0u8; 5]; - let mask_offset = 5; - buf.extend_from_slice(&bytes); - - let mut offset = buf.len(); - let frame = TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset).unwrap(); - - assert!(frame.is_present()); - assert_eq!(frame.mask_offset, 5); - assert_eq!(frame.mask, 0); - assert_eq!(frame.num_values, 0); - assert_eq!(offset, 9); - } - - #[test] - fn test_try_new_invalid_priority_fee_half_set_low_bit() { - let mask = 0b00001u32; - let bytes = mask.to_le_bytes(); - let mut buf = vec![0u8; 5]; - let mask_offset = 5; - buf.extend_from_slice(&bytes); - let mut offset = 5; - - assert_eq!( - TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset), - Err(TransactionViewError::SanitizeError) - ); - } - - #[test] - fn test_try_new_invalid_priority_fee_half_set_high_bit() { - let mask = 0b00010u32; - let bytes = mask.to_le_bytes(); - let mut buf = vec![0u8; 5]; - let mask_offset = 5; - buf.extend_from_slice(&bytes); - let mut offset = 5; - - assert_eq!( - TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset), - Err(TransactionViewError::SanitizeError) - ); - } - - #[test] - fn test_try_new_invalid_config_values() { - // bits 0,1,2 => 3 words => 12 bytes needed - let mask = 0b00111u32; - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[7u8; 3]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - - let mut offset = bytes.len(); - assert_eq!( - TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset), - Err(TransactionViewError::ParseError) - ); - } - - #[test] - fn test_read_defaults_when_bits_unset() { - let mask = 0u32; - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[1u8; 2]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - - let mut offset = bytes.len(); - let frame = - TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset).unwrap(); - let view = TransactionConfigView { - transaction_config_frame: &frame, - bytes: &bytes, - }; - - assert!(view.priority_fee_lamports().is_none()); - assert!(view.compute_unit_limit().is_none()); - assert!(view.loaded_accounts_data_size_limit().is_none()); - assert!(view.requested_heap_size().is_none()); - } - - #[test] - fn test_unknown_bits_rejected() { - // Single unknown bit (bit 5) - assert_eq!( - TransactionConfigFrame::sanitize_mask(0b10_0000), - Err(TransactionViewError::SanitizeError) - ); - // Multiple unknown bits - assert_eq!( - TransactionConfigFrame::sanitize_mask(0b1111_1111), - Err(TransactionViewError::SanitizeError) - ); - // High bits set - assert_eq!( - TransactionConfigFrame::sanitize_mask(1 << 31), - Err(TransactionViewError::SanitizeError) - ); - // Unknown bits mixed with valid bits - assert_eq!( - TransactionConfigFrame::sanitize_mask(0b1_1111 | (1 << 16)), - Err(TransactionViewError::SanitizeError) - ); - } - - #[test] - fn test_priority_fee_only() { - let mask = 0b00011u32; - let fee = 123_456_789u64; - let (lo, hi) = u64_words_le(fee); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[9u8; 4]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - let values_offset = bytes.len(); - bytes.extend_from_slice(&lo); - bytes.extend_from_slice(&hi); - - let mut offset = values_offset; - let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) - .inspect(|_| assert_eq!(offset, bytes.len())) - .unwrap(); - assert!(frame.is_present()); - assert_eq!(frame.num_values, 2); - - let view = TransactionConfigView { - transaction_config_frame: &frame, - bytes: &bytes, - }; - assert_eq!(view.priority_fee_lamports().unwrap(), fee); - assert!(view.compute_unit_limit().is_none()); - assert!(view.loaded_accounts_data_size_limit().is_none()); - assert!(view.requested_heap_size().is_none()); - } - - #[test] - fn test_all_initial_fields_present() { - // bits 0,1,2,3,4 => priority fee + cu + loaded data size + heap size - let mask = 0b1_1111u32; - let fee = 99u64; - let cu = 1_400_000u32; - let loaded = 64_000u32; - let heap = 64 * 1024u32; - - let (fee_lo, fee_hi) = u64_words_le(fee); - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[9u8; 7]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - let values_offset = bytes.len(); - bytes.extend_from_slice(&fee_lo); - bytes.extend_from_slice(&fee_hi); - bytes.extend_from_slice(&u32le(cu)); - bytes.extend_from_slice(&u32le(loaded)); - bytes.extend_from_slice(&u32le(heap)); - - let mut offset = values_offset; - let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) - .inspect(|_| assert_eq!(offset, bytes.len())) - .unwrap(); - assert!(frame.is_present()); - assert_eq!(frame.num_values, 5); - - let view = TransactionConfigView { - transaction_config_frame: &frame, - bytes: &bytes, - }; - assert_eq!(view.priority_fee_lamports().unwrap(), fee); - assert_eq!(view.compute_unit_limit().unwrap(), cu); - assert_eq!(view.loaded_accounts_data_size_limit().unwrap(), loaded); - assert_eq!(view.requested_heap_size().unwrap(), heap); - } - - #[test] - fn test_sparse_bits_word_indexing() { - // bits 2 and 4 only - let mask = 0b10100u32; - let cu = 777u32; - let heap = 48 * 1024u32; - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[1u8; 3]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - let values_offset = bytes.len(); - bytes.extend_from_slice(&u32le(cu)); // bit 2 -> word 0 - bytes.extend_from_slice(&u32le(heap)); // bit 4 -> word 1 - - let mut offset = values_offset; - let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) - .inspect(|_| assert_eq!(offset, bytes.len())) - .unwrap(); - assert_eq!(frame.word_index_for_bit(2), Some(0)); - assert_eq!(frame.word_index_for_bit(4), Some(1)); - assert_eq!(frame.word_index_for_bit(3), None); - - let view = TransactionConfigView { - transaction_config_frame: &frame, - bytes: &bytes, - }; - assert!(view.priority_fee_lamports().is_none()); - assert_eq!(view.compute_unit_limit().unwrap(), cu); - assert!(view.loaded_accounts_data_size_limit().is_none()); - assert_eq!(view.requested_heap_size().unwrap(), heap); - } - - #[test] - fn test_truncated_priority_fee_values() { - let mask = 0b00011u32; - - let mut bytes = Vec::new(); - bytes.extend_from_slice(&[5u8; 2]); - let mask_offset = bytes.len(); - bytes.extend_from_slice(&mask.to_le_bytes()); - let values_offset = bytes.len(); - bytes.extend_from_slice(&[1, 2, 3, 4]); // only one word present - - let mut offset = values_offset; - assert_eq!( - TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset), - Err(TransactionViewError::ParseError) - ); - } -} diff --git a/transaction-view/src/transaction_data.rs b/transaction-view/src/transaction_data.rs deleted file mode 100644 index 323c085660f..00000000000 --- a/transaction-view/src/transaction_data.rs +++ /dev/null @@ -1,19 +0,0 @@ -/// Trait for accessing transaction data from an abstract byte container. -pub trait TransactionData { - /// Returns a reference to the serialized transaction data. - fn data(&self) -> &[u8]; -} - -impl TransactionData for &[u8] { - #[inline] - fn data(&self) -> &[u8] { - self - } -} - -impl TransactionData for std::sync::Arc> { - #[inline] - fn data(&self) -> &[u8] { - self.as_ref() - } -} diff --git a/transaction-view/src/transaction_frame.rs b/transaction-view/src/transaction_frame.rs deleted file mode 100644 index 89a0beae3dc..00000000000 --- a/transaction-view/src/transaction_frame.rs +++ /dev/null @@ -1,993 +0,0 @@ -use { - crate::{ - address_table_lookup_frame::{AddressTableLookupFrame, AddressTableLookupIterator}, - bytes::{ - advance_offset_for_array, advance_offset_for_type, check_remaining, - unchecked_copy_value, unchecked_read_byte, - }, - instructions_frame::{InstructionsFrame, InstructionsIterator}, - message_header_frame::MessageHeaderFrame, - result::{Result, TransactionViewError}, - signature_frame::SignatureFrame, - static_account_keys_frame::StaticAccountKeysFrame, - transaction_config_frame::TransactionConfigFrame, - transaction_version::TransactionVersion, - }, - solana_hash::Hash, - solana_pubkey::Pubkey, - solana_signature::Signature, -}; - -#[derive(Debug)] -pub(crate) struct TransactionFrame { - /// Signature framing data. - signature: SignatureFrame, - /// Message header framing data. - message_header: MessageHeaderFrame, - /// Static account keys framing data. - static_account_keys: StaticAccountKeysFrame, - /// Recent blockhash offset. - recent_blockhash_offset: u16, - /// Instructions framing data. - instructions: InstructionsFrame, - /// Address table lookup framing data. - address_table_lookup: AddressTableLookupFrame, - /// Transaction config framing data - transaction_config_frame: TransactionConfigFrame, - /// The data length in bytes - data_len: u16, -} - -impl TransactionFrame { - /// Parse a serialized transaction and verify basic structure. - /// The `bytes` parameter must have no trailing data. - pub(crate) fn try_new(bytes: &[u8]) -> Result { - if Self::is_legacy_or_v0(bytes)? { - Self::try_new_as_legacy_or_v0(bytes) - } else { - Self::try_new_as_v1(bytes) - } - } - - #[inline(always)] - fn checked_offset(offset: usize) -> Result { - u16::try_from(offset).map_err(|_| TransactionViewError::ParseError) - } - - fn try_new_as_legacy_or_v0(bytes: &[u8]) -> Result { - let mut offset = 0; - let signature = SignatureFrame::try_new(bytes, &mut offset)?; - let message_header = MessageHeaderFrame::try_new(bytes, &mut offset)?; - let static_account_keys = StaticAccountKeysFrame::try_new(bytes, &mut offset)?; - - // The recent blockhash is the first account key after the static - // account keys. The recent blockhash is always present in a valid - // transaction and has a fixed size of 32 bytes. - let recent_blockhash_offset = Self::checked_offset(offset)?; - advance_offset_for_type::(bytes, &mut offset)?; - - let instructions = InstructionsFrame::try_new_for_legacy_and_v0(bytes, &mut offset)?; - let address_table_lookup = match message_header.version { - TransactionVersion::Legacy => AddressTableLookupFrame { - num_address_table_lookups: 0, - offset: 0, - total_writable_lookup_accounts: 0, - total_readonly_lookup_accounts: 0, - }, - TransactionVersion::V0 => AddressTableLookupFrame::try_new(bytes, &mut offset)?, - TransactionVersion::V1 => unreachable!("unexpected variant"), - }; - - // Verify that the entire transaction was parsed. - if offset != bytes.len() { - return Err(TransactionViewError::ParseError); - } - - Ok(Self { - signature, - message_header, - static_account_keys, - recent_blockhash_offset, - instructions, - address_table_lookup, - transaction_config_frame: TransactionConfigFrame::not_applicable(), - data_len: Self::checked_offset(offset)?, - }) - } - - fn try_new_as_v1(bytes: &[u8]) -> Result { - let mut offset: usize = 0; - - // Fixed-size txv1 prefix up through NumAddresses: - // VersionByte (u8) - // LegacyHeader (u8, u8, u8) - // TransactionConfigMask (u32) - // LifetimeSpecifier ([u8; 32]) - // NumInstructions (u8) - // NumAddresses (u8) - const FIXED_V1_PREFIX_LEN: usize = 1 + 3 + 4 + core::mem::size_of::() + 1 + 1; - - check_remaining(bytes, offset, FIXED_V1_PREFIX_LEN)?; - - // SAFETY: have checked bytes have enough space for preifx all the way up to - // NumAddresses. - - // message offset would be the first byte of txv1 packet, which is version byte - let message_offset = offset as u16; - // Version Byte - let version = unsafe { unchecked_read_byte(bytes, &mut offset) }; - let version = match version & !solana_message::MESSAGE_VERSION_PREFIX { - 1 => TransactionVersion::V1, - _ => return Err(TransactionViewError::ParseError), - }; - // Legacy Header - let num_required_signatures = unsafe { unchecked_read_byte(bytes, &mut offset) }; - let num_readonly_signed_accounts = unsafe { unchecked_read_byte(bytes, &mut offset) }; - let num_readonly_unsigned_accounts = unsafe { unchecked_read_byte(bytes, &mut offset) }; - // Transaction Config Bit Mask - let transaction_config_mask_offset = offset; - let transaction_config_mask: u32 = unsafe { unchecked_copy_value(bytes, offset) }; - offset = offset.wrapping_add(core::mem::size_of::()); - // Lifetime specifier - let recent_blockhash_offset = Self::checked_offset(offset)?; - offset = offset.wrapping_add(core::mem::size_of::()); - // Num instructions and addresses - let num_instructions = unsafe { unchecked_read_byte(bytes, &mut offset) }; - let num_addresses = unsafe { unchecked_read_byte(bytes, &mut offset) }; - - // addresses - let addresses_offset = Self::checked_offset(offset)?; - advance_offset_for_array::(bytes, &mut offset, u16::from(num_addresses))?; - // config value slots: one 4-byte slot per set bit in mask - let transaction_config_frame = TransactionConfigFrame::try_new( - bytes, - transaction_config_mask_offset, - transaction_config_mask, - &mut offset, - )?; - // instruction headers and payloads - let instructions = InstructionsFrame::try_new_for_v1(bytes, &mut offset, num_instructions)?; - // signatures - let signatures_offset = Self::checked_offset(offset)?; - advance_offset_for_array::( - bytes, - &mut offset, - u16::from(num_required_signatures), - )?; - // Verify that the entire transaction was parsed. - if offset != bytes.len() { - return Err(TransactionViewError::ParseError); - } - - let frame = Self { - signature: SignatureFrame { - num_signatures: num_required_signatures, - offset: signatures_offset, - }, - message_header: MessageHeaderFrame { - offset: message_offset, - version, - num_required_signatures, - num_readonly_signed_accounts, - num_readonly_unsigned_accounts, - }, - static_account_keys: StaticAccountKeysFrame { - num_static_accounts: num_addresses, // always static accounts in txv1 - offset: addresses_offset, - }, - recent_blockhash_offset, - instructions, - // Don't have ATL in txv1 - address_table_lookup: AddressTableLookupFrame { - num_address_table_lookups: 0, - offset: 0, - total_writable_lookup_accounts: 0, - total_readonly_lookup_accounts: 0, - }, - transaction_config_frame, - data_len: Self::checked_offset(offset)?, - }; - - Ok(frame) - } - - fn is_legacy_or_v0(bytes: &[u8]) -> Result { - let first_byte = *bytes.first().ok_or(TransactionViewError::ParseError)?; - - // In wire format: - // - Legacy/v0 transactions start with signatures (compact-u16 count). - // Packet size limits keep the signature count well below 128, so the - // first byte never has MSB set. - // - v1 transactions start with a version byte with MSB = 1. - Ok((first_byte & solana_message::MESSAGE_VERSION_PREFIX) == 0) - } - - /// Return the number of signatures in the transaction. - #[inline] - pub(crate) fn num_signatures(&self) -> u8 { - self.signature.num_signatures - } - - /// Return the version of the transaction. - #[inline] - pub(crate) fn version(&self) -> TransactionVersion { - self.message_header.version - } - - /// Return the number of required signatures in the transaction. - #[inline] - pub(crate) fn num_required_signatures(&self) -> u8 { - self.message_header.num_required_signatures - } - - /// Return the number of readonly signed static accounts in the transaction. - #[inline] - pub(crate) fn num_readonly_signed_static_accounts(&self) -> u8 { - self.message_header.num_readonly_signed_accounts - } - - /// Return the number of readonly unsigned static accounts in the transaction. - #[inline] - pub(crate) fn num_readonly_unsigned_static_accounts(&self) -> u8 { - self.message_header.num_readonly_unsigned_accounts - } - - /// Return the number of static account keys in the transaction. - #[inline] - pub(crate) fn num_static_account_keys(&self) -> u8 { - self.static_account_keys.num_static_accounts - } - - /// Return the number of instructions in the transaction. - #[inline] - pub(crate) fn num_instructions(&self) -> u16 { - self.instructions.num_instructions() - } - - /// Return the number of address table lookups in the transaction. - #[inline] - pub(crate) fn num_address_table_lookups(&self) -> u8 { - self.address_table_lookup.num_address_table_lookups - } - - /// Return the number of writable lookup accounts in the transaction. - #[inline] - pub(crate) fn total_writable_lookup_accounts(&self) -> u16 { - self.address_table_lookup.total_writable_lookup_accounts - } - - /// Return the number of readonly lookup accounts in the transaction. - #[inline] - pub(crate) fn total_readonly_lookup_accounts(&self) -> u16 { - self.address_table_lookup.total_readonly_lookup_accounts - } - - /// Return the range to the message as [begin, end] - #[inline] - pub(crate) fn message_range(&self) -> (u16, u16) { - let end = match self.version() { - TransactionVersion::V1 => self.signature.offset, - _ => self.data_len, - }; - (self.message_header.offset, end) - } - - /// Return transaction_config_frame - #[inline] - pub(crate) fn transaction_config_frame(&self) -> &TransactionConfigFrame { - &self.transaction_config_frame - } -} - -// Separate implementation for `unsafe` accessor methods. -impl TransactionFrame { - /// Return the slice of signatures in the transaction. - /// # Safety - /// - This function must be called with the same `bytes` slice that was - /// used to create the `TransactionFrame` instance. - #[inline] - pub(crate) unsafe fn signatures<'a>(&self, bytes: &'a [u8]) -> &'a [Signature] { - // Verify at compile time there are no alignment constraints. - const _: () = assert!( - core::mem::align_of::() == 1, - "Signature alignment" - ); - // The length of the slice is not greater than isize::MAX. - const _: () = - assert!(u8::MAX as usize * core::mem::size_of::() <= isize::MAX as usize); - - // SAFETY: - // - If this `TransactionFrame` was created from `bytes`: - // - the pointer is valid for the range and is properly aligned. - // - `num_signatures` has been verified against the bounds if - // `TransactionFrame` was created successfully. - // - `Signature` are just byte arrays; there is no possibility the - // `Signature` are not initialized properly. - // - The lifetime of the returned slice is the same as the input - // `bytes`. This means it will not be mutated or deallocated while - // holding the slice. - // - The length does not overflow `isize`. - unsafe { - core::slice::from_raw_parts( - bytes.as_ptr().add(usize::from(self.signature.offset)) as *const Signature, - usize::from(self.signature.num_signatures), - ) - } - } - - /// Return the slice of static account keys in the transaction. - /// - /// # Safety - /// - This function must be called with the same `bytes` slice that was - /// used to create the `TransactionFrame` instance. - #[inline] - pub(crate) unsafe fn static_account_keys<'a>(&self, bytes: &'a [u8]) -> &'a [Pubkey] { - // Verify at compile time there are no alignment constraints. - const _: () = assert!(core::mem::align_of::() == 1, "Pubkey alignment"); - // The length of the slice is not greater than isize::MAX. - const _: () = - assert!(u8::MAX as usize * core::mem::size_of::() <= isize::MAX as usize); - - // SAFETY: - // - If this `TransactionFrame` was created from `bytes`: - // - the pointer is valid for the range and is properly aligned. - // - `num_static_accounts` has been verified against the bounds if - // `TransactionFrame` was created successfully. - // - `Pubkey` are just byte arrays; there is no possibility the - // `Pubkey` are not initialized properly. - // - The lifetime of the returned slice is the same as the input - // `bytes`. This means it will not be mutated or deallocated while - // holding the slice. - // - The length does not overflow `isize`. - unsafe { - core::slice::from_raw_parts( - bytes - .as_ptr() - .add(usize::from(self.static_account_keys.offset)) - as *const Pubkey, - usize::from(self.static_account_keys.num_static_accounts), - ) - } - } - - /// Return the recent blockhash in the transaction. - /// # Safety - /// - This function must be called with the same `bytes` slice that was - /// used to create the `TransactionFrame` instance. - #[inline] - pub(crate) unsafe fn recent_blockhash<'a>(&self, bytes: &'a [u8]) -> &'a Hash { - // Verify at compile time there are no alignment constraints. - const _: () = assert!(core::mem::align_of::() == 1, "Hash alignment"); - - // SAFETY: - // - The pointer is correctly aligned (no alignment constraints). - // - `Hash` is just a byte array; there is no possibility the `Hash` - // is not initialized properly. - // - Aliasing rules are respected because the lifetime of the returned - // reference is the same as the input/source `bytes`. - unsafe { - &*(bytes - .as_ptr() - .add(usize::from(self.recent_blockhash_offset)) as *const Hash) - } - } - - /// Return an iterator over the instructions in the transaction. - /// # Safety - /// - This function must be called with the same `bytes` slice that was - /// used to create the `TransactionFrame` instance. - #[inline] - pub(crate) unsafe fn instructions_iter<'a>( - &'a self, - bytes: &'a [u8], - ) -> InstructionsIterator<'a> { - self.instructions.iter(bytes) - } - - /// Return an iterator over the address table lookups in the transaction. - /// # Safety - /// - This function must be called with the same `bytes` slice that was - /// used to create the `TransactionFrame` instance. - #[inline] - pub(crate) unsafe fn address_table_lookup_iter<'a>( - &self, - bytes: &'a [u8], - ) -> AddressTableLookupIterator<'a> { - AddressTableLookupIterator { - bytes, - offset: usize::from(self.address_table_lookup.offset), - num_address_table_lookups: self.address_table_lookup.num_address_table_lookups, - index: 0, - } - } -} - -#[cfg(test)] -impl TransactionFrame { - pub(crate) fn message_offset(&self) -> u16 { - self.message_header.offset - } - - pub(crate) fn signatures_offset(&self) -> u16 { - self.signature.offset - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_message::{ - AddressLookupTableAccount, Message, MessageHeader, VersionedMessage, - compiled_instruction::CompiledInstruction, v0, v1, - }, - solana_pubkey::Pubkey, - solana_signature::Signature, - solana_system_interface::instruction::{self as system_instruction, SystemInstruction}, - solana_transaction::versioned::VersionedTransaction, - }; - - fn verify_transaction_view_frame(tx: &VersionedTransaction) { - let bytes = wincode::serialize(tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - assert_eq!(frame.signature.num_signatures, tx.signatures.len() as u8); - assert_eq!(frame.signature.offset as usize, 1); - - assert_eq!( - frame.message_header.num_required_signatures, - tx.message.header().num_required_signatures - ); - assert_eq!( - frame.message_header.num_readonly_signed_accounts, - tx.message.header().num_readonly_signed_accounts - ); - assert_eq!( - frame.message_header.num_readonly_unsigned_accounts, - tx.message.header().num_readonly_unsigned_accounts - ); - - assert_eq!( - frame.static_account_keys.num_static_accounts, - tx.message.static_account_keys().len() as u8 - ); - assert_eq!( - frame.instructions.num_instructions(), - tx.message.instructions().len() as u16 - ); - assert_eq!( - frame.address_table_lookup.num_address_table_lookups, - tx.message - .address_table_lookups() - .map(|x| x.len() as u8) - .unwrap_or(0) - ); - } - - fn minimally_sized_transaction() -> VersionedTransaction { - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::Legacy(Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - account_keys: vec![Pubkey::default()], - recent_blockhash: Hash::default(), - instructions: vec![], - }), - } - } - - fn simple_transfer() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::Legacy(Message::new( - &[system_instruction::transfer( - &payer, - &Pubkey::new_unique(), - 1, - )], - Some(&payer), - )), - } - } - - fn simple_transfer_v0() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::V0( - v0::Message::try_compile( - &payer, - &[system_instruction::transfer( - &payer, - &Pubkey::new_unique(), - 1, - )], - &[], - Hash::default(), - ) - .unwrap(), - ), - } - } - - fn multiple_transfers() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::Legacy(Message::new( - &[ - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - ], - Some(&payer), - )), - } - } - - fn v0_with_single_lookup() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - let to = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::V0( - v0::Message::try_compile( - &payer, - &[system_instruction::transfer(&payer, &to, 1)], - &[AddressLookupTableAccount { - key: Pubkey::new_unique(), - addresses: vec![to], - }], - Hash::default(), - ) - .unwrap(), - ), - } - } - - fn v0_with_multiple_lookups() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - let to1 = Pubkey::new_unique(); - let to2 = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::V0( - v0::Message::try_compile( - &payer, - &[ - system_instruction::transfer(&payer, &to1, 1), - system_instruction::transfer(&payer, &to2, 1), - ], - &[ - AddressLookupTableAccount { - key: Pubkey::new_unique(), - addresses: vec![to1], - }, - AddressLookupTableAccount { - key: Pubkey::new_unique(), - addresses: vec![to2], - }, - ], - Hash::default(), - ) - .unwrap(), - ), - } - } - - fn simple_v1_transaction() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - let program = Pubkey::new_unique(); - let other = Pubkey::new_unique(); - - VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V1(v1::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 1, - }, - config: v1::TransactionConfig { - priority_fee: Some(123), - compute_unit_limit: Some(456), - loaded_accounts_data_size_limit: Some(789), - heap_size: Some(1024), - }, - lifetime_specifier: Hash::default(), - account_keys: vec![payer, other, program], - instructions: vec![ - CompiledInstruction { - program_id_index: 2, - accounts: vec![0, 1], - data: vec![10, 11, 12], - }, - CompiledInstruction { - program_id_index: 2, - accounts: vec![], - data: vec![99], - }, - ], - }), - } - } - - #[test] - fn test_minimal_sized_transaction() { - verify_transaction_view_frame(&minimally_sized_transaction()); - } - - #[test] - fn test_simple_transfer() { - verify_transaction_view_frame(&simple_transfer()); - } - - #[test] - fn test_simple_transfer_v0() { - verify_transaction_view_frame(&simple_transfer_v0()); - } - - #[test] - fn test_v0_with_lookup() { - verify_transaction_view_frame(&v0_with_single_lookup()); - } - - #[test] - fn test_trailing_byte() { - let tx = simple_transfer(); - let mut bytes = wincode::serialize(&tx).unwrap(); - bytes.push(0); - assert!(TransactionFrame::try_new(&bytes).is_err()); - } - - #[test] - fn test_insufficient_bytes() { - let tx = simple_transfer(); - let bytes = wincode::serialize(&tx).unwrap(); - assert!(TransactionFrame::try_new(&bytes[..bytes.len().wrapping_sub(1)]).is_err()); - } - - #[test] - fn test_signature_overflow() { - let tx = simple_transfer(); - let mut bytes = wincode::serialize(&tx).unwrap(); - // Set the number of signatures to u16::MAX - bytes[0] = 0xff; - bytes[1] = 0xff; - bytes[2] = 0xff; - assert!(TransactionFrame::try_new(&bytes).is_err()); - } - - #[test] - fn test_account_key_overflow() { - let tx = simple_transfer(); - let mut bytes = wincode::serialize(&tx).unwrap(); - // Set the number of accounts to u16::MAX - let offset = 1 + core::mem::size_of::() + 3; - bytes[offset] = 0xff; - bytes[offset + 1] = 0xff; - bytes[offset + 2] = 0xff; - assert!(TransactionFrame::try_new(&bytes).is_err()); - } - - #[test] - fn test_instructions_overflow() { - let tx = simple_transfer(); - let mut bytes = wincode::serialize(&tx).unwrap(); - // Set the number of instructions to u16::MAX - let offset = 1 - + core::mem::size_of::() - + 3 - + 1 - + 3 * core::mem::size_of::() - + core::mem::size_of::(); - bytes[offset] = 0xff; - bytes[offset + 1] = 0xff; - bytes[offset + 2] = 0xff; - assert!(TransactionFrame::try_new(&bytes).is_err()); - } - - #[test] - fn test_alt_overflow() { - let tx = simple_transfer_v0(); - let ix_bytes = tx.message.instructions()[0].data.len(); - let mut bytes = wincode::serialize(&tx).unwrap(); - // Set the number of instructions to u16::MAX - let offset = 1 // byte for num signatures - + core::mem::size_of::() // signature - + 1 // version byte - + 3 // message header - + 1 // byte for num account keys - + 3 * core::mem::size_of::() // account keys - + core::mem::size_of::() // recent blockhash - + 1 // byte for num instructions - + 1 // program index - + 1 // byte for num accounts - + 2 // bytes for account index - + 1 // byte for data length - + ix_bytes; - bytes[offset] = 0x01; - assert!(TransactionFrame::try_new(&bytes).is_err()); - } - - #[test] - fn test_basic_accessors() { - let tx = simple_transfer(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - assert_eq!(frame.num_signatures(), 1); - assert!(matches!(frame.version(), TransactionVersion::Legacy)); - assert_eq!(frame.num_required_signatures(), 1); - assert_eq!(frame.num_readonly_signed_static_accounts(), 0); - assert_eq!(frame.num_readonly_unsigned_static_accounts(), 1); - assert_eq!(frame.num_static_account_keys(), 3); - assert_eq!(frame.num_instructions(), 1); - assert_eq!(frame.num_address_table_lookups(), 0); - - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let signatures = frame.signatures(&bytes); - assert_eq!(signatures, &tx.signatures); - - let static_account_keys = frame.static_account_keys(&bytes); - assert_eq!(static_account_keys, tx.message.static_account_keys()); - - let recent_blockhash = frame.recent_blockhash(&bytes); - assert_eq!(recent_blockhash, tx.message.recent_blockhash()); - } - } - - #[test] - fn test_instructions_iter_empty() { - let tx = minimally_sized_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.instructions_iter(&bytes); - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_instructions_iter_single() { - let tx = simple_transfer(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.instructions_iter(&bytes); - let ix = iter.next().unwrap(); - assert_eq!(ix.program_id_index, 2); - assert_eq!(ix.accounts, &[0, 1]); - assert_eq!( - ix.data, - &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() - ); - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_instructions_iter_multiple() { - let tx = multiple_transfers(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.instructions_iter(&bytes); - let ix = iter.next().unwrap(); - assert_eq!(ix.program_id_index, 3); - assert_eq!(ix.accounts, &[0, 1]); - assert_eq!( - ix.data, - &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() - ); - let ix = iter.next().unwrap(); - assert_eq!(ix.program_id_index, 3); - assert_eq!(ix.accounts, &[0, 2]); - assert_eq!( - ix.data, - &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() - ); - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_address_table_lookup_iter_empty() { - let tx = simple_transfer(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.address_table_lookup_iter(&bytes); - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_address_table_lookup_iter_single() { - let tx = v0_with_single_lookup(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - let atls_actual = tx.message.address_table_lookups().unwrap(); - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.address_table_lookup_iter(&bytes); - let lookup = iter.next().unwrap(); - assert_eq!(lookup.account_key, &atls_actual[0].account_key); - assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); - assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_address_table_lookup_iter_multiple() { - let tx = v0_with_multiple_lookups(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - let atls_actual = tx.message.address_table_lookups().unwrap(); - // SAFETY: `bytes` is the same slice used to create `frame`. - unsafe { - let mut iter = frame.address_table_lookup_iter(&bytes); - - let lookup = iter.next().unwrap(); - assert_eq!(lookup.account_key, &atls_actual[0].account_key); - assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); - assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); - - let lookup = iter.next().unwrap(); - assert_eq!(lookup.account_key, &atls_actual[1].account_key); - assert_eq!(lookup.writable_indexes, atls_actual[1].writable_indexes); - assert_eq!(lookup.readonly_indexes, atls_actual[1].readonly_indexes); - - assert!(iter.next().is_none()); - } - } - - #[test] - fn test_v1_transaction_frame_parses() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - assert!(matches!(frame.version(), TransactionVersion::V1)); - assert_eq!(frame.num_signatures(), 1); - assert_eq!(frame.num_required_signatures(), 1); - assert_eq!(frame.num_readonly_signed_static_accounts(), 0); - assert_eq!(frame.num_readonly_unsigned_static_accounts(), 1); - assert_eq!(frame.num_static_account_keys(), 3); - assert_eq!(frame.num_instructions(), 2); - - // txv1 should not have ALTs - assert_eq!(frame.num_address_table_lookups(), 0); - assert_eq!(frame.total_writable_lookup_accounts(), 0); - assert_eq!(frame.total_readonly_lookup_accounts(), 0); - - // new v1-only frame metadata - assert!(frame.signatures_offset() > frame.message_offset()); - } - - #[test] - fn test_v1_is_not_legacy_or_v0() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - - assert!(!TransactionFrame::is_legacy_or_v0(&bytes).unwrap()); - } - - #[test] - fn test_legacy_is_legacy_or_v0() { - let payer = Pubkey::new_unique(); - let tx = VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::Legacy(solana_message::Message::new(&[], Some(&payer))), - }; - let bytes = wincode::serialize(&tx).unwrap(); - - assert!(TransactionFrame::is_legacy_or_v0(&bytes).unwrap()); - } - - #[test] - fn test_is_legacy_or_v0_empty_bytes() { - assert!(matches!( - TransactionFrame::is_legacy_or_v0(&[]), - Err(TransactionViewError::ParseError), - )); - } - - #[test] - fn test_v1_rejects_unknown_version() { - let tx = simple_v1_transaction(); - let mut bytes = wincode::serialize(&tx).unwrap(); - - // First byte is version-tagged for versioned messages. - // Flip underlying version to an unsupported value. - bytes[0] = solana_message::MESSAGE_VERSION_PREFIX | 2; - - assert!(matches!( - TransactionFrame::try_new(&bytes), - Err(TransactionViewError::ParseError), - )); - } - - #[test] - fn test_v1_rejects_trailing_byte() { - let tx = simple_v1_transaction(); - let mut bytes = wincode::serialize(&tx).unwrap(); - bytes.push(0); - - assert!(matches!( - TransactionFrame::try_new(&bytes), - Err(TransactionViewError::ParseError), - )); - } - - #[test] - fn test_rejects_bytes_with_unrepresentable_frame_offsets() { - let mut bytes = Vec::new(); - bytes.push(v1::V1_PREFIX); - bytes.extend_from_slice(&[1, 0, 0]); - bytes.extend_from_slice(&0u32.to_le_bytes()); - bytes.extend_from_slice(&[0; 32]); - bytes.push(1); - bytes.push(1); - bytes.extend_from_slice(&[1; 32]); - bytes.extend_from_slice(&[0, 0]); - bytes.extend_from_slice(&u16::MAX.to_le_bytes()); - bytes.extend(std::iter::repeat_n(0, u16::MAX as usize)); - bytes.extend_from_slice(&[0; 64]); - - assert!(bytes.len() > u16::MAX as usize); - assert!(matches!( - TransactionFrame::try_new(&bytes), - Err(TransactionViewError::ParseError), - )); - } - - #[test] - fn test_v1_rejects_truncated_bytes() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - - assert!(matches!( - TransactionFrame::try_new(&bytes[..bytes.len() - 1]), - Err(TransactionViewError::ParseError), - )); - } - - #[test] - fn test_v1_instruction_iteration() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - let frame = TransactionFrame::try_new(&bytes).unwrap(); - - let mut iter = unsafe { frame.instructions_iter(&bytes) }; - - let ix0 = iter.next().unwrap(); - assert_eq!(ix0.program_id_index, 2); - assert_eq!(ix0.accounts, &[0, 1]); - assert_eq!(ix0.data, &[10, 11, 12]); - - let ix1 = iter.next().unwrap(); - assert_eq!(ix1.program_id_index, 2); - assert_eq!(ix1.accounts, &[] as &[u8]); - assert_eq!(ix1.data, &[99]); - - assert!(iter.next().is_none()); - } -} diff --git a/transaction-view/src/transaction_version.rs b/transaction-view/src/transaction_version.rs deleted file mode 100644 index e2c2978eee7..00000000000 --- a/transaction-view/src/transaction_version.rs +++ /dev/null @@ -1,19 +0,0 @@ -/// A byte that represents the version of the transaction. -#[derive(Copy, Clone, Debug, Default)] -#[repr(u8)] -pub enum TransactionVersion { - #[default] - Legacy = u8::MAX, - V0 = 0, - V1 = 1, -} - -impl From for solana_transaction::versioned::TransactionVersion { - fn from(version: TransactionVersion) -> Self { - match version { - TransactionVersion::Legacy => Self::LEGACY, - TransactionVersion::V0 => Self::Number(0), - TransactionVersion::V1 => Self::Number(1), - } - } -} diff --git a/transaction-view/src/transaction_view.rs b/transaction-view/src/transaction_view.rs deleted file mode 100644 index 503743b9069..00000000000 --- a/transaction-view/src/transaction_view.rs +++ /dev/null @@ -1,517 +0,0 @@ -use { - crate::{ - address_table_lookup_frame::AddressTableLookupIterator, - instructions_frame::InstructionsIterator, - result::Result, - sanitize::{SanitizeConfig, sanitize}, - transaction_config_frame::TransactionConfigView, - transaction_data::TransactionData, - transaction_frame::TransactionFrame, - transaction_version::TransactionVersion, - }, - core::fmt::{Debug, Formatter}, - solana_hash::Hash, - solana_pubkey::Pubkey, - solana_signature::Signature, - solana_svm_transaction::{ - instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, - svm_message::SVMStaticMessage, - }, -}; - -// alias for convenience -pub type UnsanitizedTransactionView = TransactionView; -pub type SanitizedTransactionView = TransactionView; - -/// A view into a serialized transaction. -/// -/// This struct provides access to the transaction data without -/// deserializing it. This is done by parsing and caching metadata -/// about the layout of the serialized transaction. -/// The owned `data` is abstracted through the `TransactionData` trait, -/// so that different containers for the serialized transaction can be used. -pub struct TransactionView { - data: D, - frame: TransactionFrame, -} - -impl TransactionView { - /// Creates a new `TransactionView` without running sanitization checks. - pub fn try_new_unsanitized(data: D) -> Result { - let frame = TransactionFrame::try_new(data.data())?; - Ok(Self { data, frame }) - } - - /// Sanitizes the transaction view, returning a sanitized view on success. - pub fn sanitize(self, config: &SanitizeConfig) -> Result> { - sanitize(&self, config)?; - Ok(SanitizedTransactionView { - data: self.data, - frame: self.frame, - }) - } -} - -impl TransactionView { - /// Creates a new `TransactionView`, running sanitization checks. - pub fn try_new_sanitized(data: D, config: &SanitizeConfig) -> Result { - let unsanitized_view = TransactionView::try_new_unsanitized(data)?; - unsanitized_view.sanitize(config) - } -} - -impl TransactionView { - /// Return the number of signatures in the transaction. - #[inline] - pub fn num_signatures(&self) -> u8 { - self.frame.num_signatures() - } - - /// Return the version of the transaction. - #[inline] - pub fn version(&self) -> TransactionVersion { - self.frame.version() - } - - /// Return the number of required signatures in the transaction. - #[inline] - pub fn num_required_signatures(&self) -> u8 { - self.frame.num_required_signatures() - } - - /// Return the number of readonly signed static accounts in the transaction. - #[inline] - pub fn num_readonly_signed_static_accounts(&self) -> u8 { - self.frame.num_readonly_signed_static_accounts() - } - - /// Return the number of readonly unsigned static accounts in the transaction. - #[inline] - pub fn num_readonly_unsigned_static_accounts(&self) -> u8 { - self.frame.num_readonly_unsigned_static_accounts() - } - - /// Return the number of static account keys in the transaction. - #[inline] - pub fn num_static_account_keys(&self) -> u8 { - self.frame.num_static_account_keys() - } - - /// Return the number of instructions in the transaction. - #[inline] - pub fn num_instructions(&self) -> u16 { - self.frame.num_instructions() - } - - /// Return the number of address table lookups in the transaction. - #[inline] - pub fn num_address_table_lookups(&self) -> u8 { - self.frame.num_address_table_lookups() - } - - /// Return the number of writable lookup accounts in the transaction. - #[inline] - pub fn total_writable_lookup_accounts(&self) -> u16 { - self.frame.total_writable_lookup_accounts() - } - - /// Return the number of readonly lookup accounts in the transaction. - #[inline] - pub fn total_readonly_lookup_accounts(&self) -> u16 { - self.frame.total_readonly_lookup_accounts() - } - - /// Return the slice of signatures in the transaction. - #[inline] - pub fn signatures(&self) -> &[Signature] { - let data = self.data(); - // SAFETY: `frame` was created from `data`. - unsafe { self.frame.signatures(data) } - } - - /// Return the slice of static account keys in the transaction. - #[inline] - pub fn static_account_keys(&self) -> &[Pubkey] { - let data = self.data(); - // SAFETY: `frame` was created from `data`. - unsafe { self.frame.static_account_keys(data) } - } - - /// Return the recent blockhash in the transaction. - #[inline] - pub fn recent_blockhash(&self) -> &Hash { - let data = self.data(); - // SAFETY: `frame` was created from `data`. - unsafe { self.frame.recent_blockhash(data) } - } - - /// Return an iterator over the instructions in the transaction. - #[inline] - pub fn instructions_iter(&self) -> InstructionsIterator<'_> { - let data = self.data(); - // SAFETY: `frame` was created from `data`. - unsafe { self.frame.instructions_iter(data) } - } - - /// Return an iterator over the address table lookups in the transaction. - #[inline] - pub fn address_table_lookup_iter(&self) -> AddressTableLookupIterator<'_> { - let data = self.data(); - // SAFETY: `frame` was created from `data`. - unsafe { self.frame.address_table_lookup_iter(data) } - } - - /// Return Some(TransactionConfigView) for V1, None for legacy/V0 - #[inline] - pub fn transaction_config(&self) -> Option> { - let transaction_config_frame = self.frame.transaction_config_frame(); - transaction_config_frame - .is_present() - .then_some(TransactionConfigView { - transaction_config_frame, - bytes: self.data(), - }) - } - - /// Return the full serialized transaction data. - #[inline] - pub fn data(&self) -> &[u8] { - self.data.data() - } - - /// Return the serialized **message** data. - /// This does not include the signatures. - #[inline] - pub fn message_data(&self) -> &[u8] { - let (start, end) = self.frame.message_range(); - &self.data()[usize::from(start)..usize::from(end)] - } - - #[inline] - pub fn inner_data(&self) -> &D { - &self.data - } - - #[inline] - pub fn into_inner_data(self) -> D { - self.data - } -} - -// Implementation that relies on sanitization checks having been run. -impl TransactionView { - /// Return an iterator over the instructions paired with their program ids. - pub fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - self.instructions_iter().map(|ix| { - let program_id_index = usize::from(ix.program_id_index); - let program_id = &self.static_account_keys()[program_id_index]; - (program_id, ix) - }) - } - - /// Return the number of unsigned static account keys. - #[inline] - pub(crate) fn num_static_unsigned_static_accounts(&self) -> u8 { - self.num_static_account_keys() - .wrapping_sub(self.num_required_signatures()) - } - - /// Return the number of writable unsigned static accounts. - #[inline] - pub(crate) fn num_writable_unsigned_static_accounts(&self) -> u8 { - self.num_static_unsigned_static_accounts() - .wrapping_sub(self.num_readonly_unsigned_static_accounts()) - } - - /// Return the number of writable unsigned static accounts. - #[inline] - pub(crate) fn num_writable_signed_static_accounts(&self) -> u8 { - self.num_required_signatures() - .wrapping_sub(self.num_readonly_signed_static_accounts()) - } - - /// Return the total number of accounts in the transactions. - #[inline] - pub fn total_num_accounts(&self) -> u16 { - u16::from(self.num_static_account_keys()) - .wrapping_add(self.total_writable_lookup_accounts()) - .wrapping_add(self.total_readonly_lookup_accounts()) - } - - /// Return the number of requested writable keys. - #[inline] - pub fn num_requested_write_locks(&self) -> u64 { - u64::from( - u16::from( - (self.num_static_account_keys()) - .wrapping_sub(self.num_readonly_signed_static_accounts()) - .wrapping_sub(self.num_readonly_unsigned_static_accounts()), - ) - .wrapping_add(self.total_writable_lookup_accounts()), - ) - } -} - -// Manual implementation of `Debug` - avoids bound on `D`. -// Prints nicely formatted struct-ish fields even for the iterator fields. -impl Debug for TransactionView { - fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { - f.debug_struct("TransactionView") - .field("frame", &self.frame) - .field("signatures", &self.signatures()) - .field("static_account_keys", &self.static_account_keys()) - .field("recent_blockhash", &self.recent_blockhash()) - .field("instructions", &self.instructions_iter()) - .field("address_table_lookups", &self.address_table_lookup_iter()) - .finish() - } -} - -impl SVMStaticMessage for TransactionView { - fn version(&self) -> solana_transaction::versioned::TransactionVersion { - self.version().into() - } - - fn num_transaction_signatures(&self) -> u64 { - self.num_required_signatures() as u64 - } - - fn num_write_locks(&self) -> u64 { - self.num_requested_write_locks() - } - - fn recent_blockhash(&self) -> &Hash { - self.recent_blockhash() - } - - fn num_instructions(&self) -> usize { - self.num_instructions() as usize - } - - fn instructions_iter(&self) -> impl Iterator> { - self.instructions_iter() - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - self.program_instructions_iter() - } - - fn static_account_keys(&self) -> &[Pubkey] { - self.static_account_keys() - } - - fn fee_payer(&self) -> &Pubkey { - &self.static_account_keys()[0] - } - - fn num_lookup_tables(&self) -> usize { - self.num_address_table_lookups() as usize - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - self.address_table_lookup_iter() - } -} - -impl SVMStaticMessage for &TransactionView { - fn version(&self) -> solana_transaction::versioned::TransactionVersion { - as SVMStaticMessage>::version(self) - } - - fn num_transaction_signatures(&self) -> u64 { - as SVMStaticMessage>::num_transaction_signatures(self) - } - - fn num_write_locks(&self) -> u64 { - as SVMStaticMessage>::num_write_locks(self) - } - - fn recent_blockhash(&self) -> &Hash { - as SVMStaticMessage>::recent_blockhash(self) - } - - fn num_instructions(&self) -> usize { - as SVMStaticMessage>::num_instructions(self) - } - - fn instructions_iter(&self) -> impl Iterator> { - as SVMStaticMessage>::instructions_iter(self) - } - - fn program_instructions_iter( - &self, - ) -> impl Iterator)> + Clone { - as SVMStaticMessage>::program_instructions_iter(self) - } - - fn static_account_keys(&self) -> &[Pubkey] { - as SVMStaticMessage>::static_account_keys(self) - } - - fn fee_payer(&self) -> &Pubkey { - as SVMStaticMessage>::fee_payer(self) - } - - fn num_lookup_tables(&self) -> usize { - as SVMStaticMessage>::num_lookup_tables(self) - } - - fn message_address_table_lookups( - &self, - ) -> impl Iterator> { - as SVMStaticMessage>::message_address_table_lookups(self) - } -} - -#[cfg(test)] -mod tests { - use { - super::*, - solana_message::{ - Message, MessageHeader, VersionedMessage, compiled_instruction::CompiledInstruction, v1, - }, - solana_pubkey::Pubkey, - solana_signature::Signature, - solana_system_interface::instruction as system_instruction, - solana_transaction::versioned::VersionedTransaction, - }; - - fn verify_transaction_view_frame(tx: &VersionedTransaction) { - let bytes = wincode::serialize(tx).unwrap(); - let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); - - assert_eq!(view.num_signatures(), tx.signatures.len() as u8); - - assert_eq!( - view.num_required_signatures(), - tx.message.header().num_required_signatures - ); - assert_eq!( - view.num_readonly_signed_static_accounts(), - tx.message.header().num_readonly_signed_accounts - ); - assert_eq!( - view.num_readonly_unsigned_static_accounts(), - tx.message.header().num_readonly_unsigned_accounts - ); - - assert_eq!( - view.num_static_account_keys(), - tx.message.static_account_keys().len() as u8 - ); - assert_eq!( - view.num_instructions(), - tx.message.instructions().len() as u16 - ); - assert_eq!( - view.num_address_table_lookups(), - tx.message - .address_table_lookups() - .map(|x| x.len() as u8) - .unwrap_or(0) - ); - - assert!(view.transaction_config().is_none()); - } - - fn multiple_transfers() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - VersionedTransaction { - signatures: vec![Signature::default()], // 1 signature to be valid. - message: VersionedMessage::Legacy(Message::new( - &[ - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), - ], - Some(&payer), - )), - } - } - - #[test] - fn test_multiple_transfers() { - verify_transaction_view_frame(&multiple_transfers()); - } - - fn simple_v1_transaction() -> VersionedTransaction { - let payer = Pubkey::new_unique(); - let program = Pubkey::new_unique(); - - VersionedTransaction { - signatures: vec![Signature::default()], - message: VersionedMessage::V1(v1::Message { - header: MessageHeader { - num_required_signatures: 1, - num_readonly_signed_accounts: 0, - num_readonly_unsigned_accounts: 0, - }, - config: v1::TransactionConfig { - priority_fee: Some(111), - compute_unit_limit: Some(222), - loaded_accounts_data_size_limit: Some(333), - heap_size: Some(1024), - }, - lifetime_specifier: Hash::default(), - account_keys: vec![payer, program], - instructions: vec![CompiledInstruction { - program_id_index: 1, - accounts: vec![0], - data: vec![1, 2, 3, 4], - }], - }), - } - } - - #[test] - fn test_v1_transaction_config_present() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); - - assert!(matches!(view.version(), TransactionVersion::V1)); - - let config = view.transaction_config().expect("v1 should have config"); - assert_eq!(config.priority_fee_lamports().unwrap(), 111); - assert_eq!(config.compute_unit_limit().unwrap(), 222); - assert_eq!(config.loaded_accounts_data_size_limit().unwrap(), 333); - assert_eq!(config.requested_heap_size().unwrap(), 1024); - } - - #[test] - fn test_v1_message_data_excludes_signatures() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); - - let message_data = view.message_data(); - - // For v1, message_data should stop before the signatures region. - assert!(message_data.len() < bytes.len()); - - let full_message = &bytes - [usize::from(view.frame.message_offset())..usize::from(view.frame.signatures_offset())]; - assert_eq!(message_data, full_message); - } - - #[test] - fn test_v1_signatures_accessible() { - let tx = simple_v1_transaction(); - let bytes = wincode::serialize(&tx).unwrap(); - let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); - - assert_eq!(view.signatures().len(), 1); - assert_eq!(view.static_account_keys().len(), 2); - - let instructions: Vec<_> = view.instructions_iter().collect(); - assert_eq!(instructions.len(), 1); - assert_eq!(instructions[0].program_id_index, 1); - assert_eq!(instructions[0].accounts, &[0]); - assert_eq!(instructions[0].data, &[1, 2, 3, 4]); - } -} From ad143fecabb9d67e98856710dd9cdd5cc6a5ad6b Mon Sep 17 00:00:00 2001 From: Michal Rostecki Date: Fri, 17 Jul 2026 08:24:10 +0200 Subject: [PATCH 09/30] refactor(runtime): Deduplicate three `From` implementations with a macro (#13892) Three `From` implementations (`Stakes` -> `Delegation`/`Stake` and `Stakes` -> `Delegation`) shared identical destructuring and struct construction logic, differing only in the extraction closure. Consolidate them into an `impl_stake_format_conversion!` macro that takes the source type, target type, and closure as parameters. The main motivation behind this change is reducing the diff of https://github.com/anza-xyz/agave/pull/13781. --- runtime/src/stakes.rs | 88 ++++++++++++++++--------------------------- 1 file changed, 33 insertions(+), 55 deletions(-) diff --git a/runtime/src/stakes.rs b/runtime/src/stakes.rs index d5bac4dae41..f4081c9beac 100644 --- a/runtime/src/stakes.rs +++ b/runtime/src/stakes.rs @@ -699,68 +699,46 @@ impl Stakes { } } -/// This conversion is very memory intensive so should only be used in -/// development contexts. +/// Macro to generate `From> for Stakes` impls. #[cfg(feature = "dev-context-only-utils")] -impl From> for Stakes { - fn from(stakes: Stakes) -> Self { - let stake_delegations = stakes - .stake_delegations - .into_iter() - .map(|(pubkey, stake_account)| (pubkey, *stake_account.delegation())) - .collect(); - Self { - vote_accounts: stakes.vote_accounts, - stake_delegations, - delegated_stakes: DelegatedStakes::default(), - unused: stakes.unused, - epoch: stakes.epoch, - stake_history: stakes.stake_history, +macro_rules! impl_stake_format_conversion { + ($from:ty, $to:ty, |$binding:ident| $expr:expr) => { + /// This conversion is memory intensive so should only be used in development contexts. + impl From> for Stakes<$to> { + fn from(stakes: Stakes<$from>) -> Self { + let Stakes { + vote_accounts, + stake_delegations, + delegated_stakes: _, + unused, + epoch, + stake_history, + } = stakes; + let stake_delegations = stake_delegations + .into_iter() + .map(|(pubkey, $binding)| (pubkey, $expr)) + .collect(); + Self { + vote_accounts, + stake_delegations, + delegated_stakes: DelegatedStakes::default(), + unused, + epoch, + stake_history, + } + } } - } + }; } -/// This conversion is very memory intensive so should only be used in -/// development contexts. #[cfg(feature = "dev-context-only-utils")] -impl From> for Stakes { - fn from(stakes: Stakes) -> Self { - let stake_delegations = stakes - .stake_delegations - .into_iter() - .map(|(pubkey, stake_account)| (pubkey, *stake_account.stake())) - .collect(); - Self { - vote_accounts: stakes.vote_accounts, - stake_delegations, - delegated_stakes: DelegatedStakes::default(), - unused: stakes.unused, - epoch: stakes.epoch, - stake_history: stakes.stake_history, - } - } -} +impl_stake_format_conversion!(StakeAccount, Delegation, |sa| *sa.delegation()); -/// This conversion is memory intensive so should only be used in development -/// contexts. #[cfg(feature = "dev-context-only-utils")] -impl From> for Stakes { - fn from(stakes: Stakes) -> Self { - let stake_delegations = stakes - .stake_delegations - .into_iter() - .map(|(pubkey, stake)| (pubkey, stake.delegation)) - .collect(); - Self { - vote_accounts: stakes.vote_accounts, - stake_delegations, - delegated_stakes: DelegatedStakes::default(), - unused: stakes.unused, - epoch: stakes.epoch, - stake_history: stakes.stake_history, - } - } -} +impl_stake_format_conversion!(StakeAccount, Stake, |sa| *sa.stake()); + +#[cfg(feature = "dev-context-only-utils")] +impl_stake_format_conversion!(Stake, Delegation, |stake| stake.delegation); fn merge_delegated_stakes( mut stakes: HashMap, From f30a2496f56ed381846102f0d646c9a54ac9124b Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Fri, 17 Jul 2026 11:46:29 +0300 Subject: [PATCH 10/30] solana-client: add send_and_confirm_transactions_in_parallel_v3 (#13799) Allows switching to tpu-client-next Allows sending VersionedTransaction API is not misleading the user --- Cargo.lock | 7 + client-test/Cargo.toml | 6 + ...nd_and_confirm_transactions_in_parallel.rs | 181 ++++++++- client/Cargo.toml | 1 + ...nd_and_confirm_transactions_in_parallel.rs | 372 +++++++++++++++--- dev-bins/Cargo.lock | 1 + programs/sbf/Cargo.lock | 1 + tpu-client-next/src/workers_cache.rs | 5 - 8 files changed, 502 insertions(+), 72 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 31547a1dd97..e1cbd36db32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7887,6 +7887,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", @@ -7898,7 +7899,9 @@ name = "solana-client-test" version = "4.3.0-alpha.1" dependencies = [ "agave-logger", + "async-trait", "futures-util", + "log", "serde_json", "solana-client", "solana-clock", @@ -7908,6 +7911,7 @@ dependencies = [ "solana-message", "solana-native-token", "solana-net-utils", + "solana-packet 4.2.0", "solana-pubkey 4.2.0", "solana-pubsub-client", "solana-rpc", @@ -7918,8 +7922,11 @@ dependencies = [ "solana-system-interface 3.2.0", "solana-system-transaction", "solana-test-validator", + "solana-tpu-client-next", "solana-transaction-status", "solana-version", + "spl-memo-interface", + "test-case", "tokio", "tungstenite", ] diff --git a/client-test/Cargo.toml b/client-test/Cargo.toml index 7a6f00e602a..ff9abb2ed3e 100644 --- a/client-test/Cargo.toml +++ b/client-test/Cargo.toml @@ -37,15 +37,21 @@ solana-system-interface = { workspace = true } solana-system-transaction = { workspace = true } solana-transaction-status = { workspace = true } solana-version = { workspace = true } +test-case = { workspace = true } tokio = { workspace = true, features = ["full"] } tungstenite = { workspace = true, features = ["rustls-tls-webpki-roots"] } [dev-dependencies] agave-logger = { path = "../logger", features = ["agave-unstable-api"] } +async-trait = { workspace = true } +log = { workspace = true } solana-net-utils = { path = "../net-utils", features = ["agave-unstable-api"] } +solana-packet = { workspace = true } solana-rpc = { path = "../rpc", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-test-validator = { path = "../test-validator", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-tpu-client-next = { workspace = true, features = ["dev-context-only-utils"] } +spl-memo-interface = { workspace = true } [lints] workspace = true diff --git a/client-test/tests/send_and_confirm_transactions_in_parallel.rs b/client-test/tests/send_and_confirm_transactions_in_parallel.rs index daecc39ed97..394874c67d5 100644 --- a/client-test/tests/send_and_confirm_transactions_in_parallel.rs +++ b/client-test/tests/send_and_confirm_transactions_in_parallel.rs @@ -1,22 +1,43 @@ use { + async_trait::async_trait, solana_client::{ nonblocking::tpu_client::TpuClient, rpc_config::RpcSendTransactionConfig, send_and_confirm_transactions_in_parallel::{ - SendAndConfirmConfigV2, send_and_confirm_transactions_in_parallel_blocking_v2, + SendAndConfirmConfigV2, SendAndConfirmConfigV3, SendTransport, + send_and_confirm_transactions_in_parallel_blocking_v2, + send_and_confirm_transactions_in_parallel_v3, }, }, solana_commitment_config::CommitmentConfig, solana_keypair::Keypair, - solana_message::Message, + solana_message::{Message, VersionedMessage}, solana_native_token::LAMPORTS_PER_SOL, - solana_net_utils::SocketAddrSpace, + solana_net_utils::{SocketAddrSpace, sockets}, + solana_packet::PACKET_DATA_SIZE, solana_pubkey::Pubkey, - solana_rpc_client::rpc_client::RpcClient, + solana_rpc_client::{ + nonblocking::rpc_client::RpcClient as NonblockingRpcClient, rpc_client::RpcClient, + }, solana_signer::Signer, solana_system_interface::instruction as system_instruction, solana_test_validator::TestValidator, - std::sync::Arc, + solana_tpu_client_next::{ + client_builder::ClientBuilder, + connection_workers_scheduler::{ConnectionWorkersSchedulerError, WorkersBroadcaster}, + leader_updater::create_pinned_leader_updater, + transaction_batch::TransactionBatch, + workers_cache::WorkersCache, + }, + spl_memo_interface::{instruction::build_memo, v3 as memo_program}, + std::{ + net::SocketAddr, + num::NonZeroUsize, + sync::Arc, + time::{Duration, Instant}, + }, + test_case::test_case, + tokio::runtime::Runtime, }; const NUM_TRANSACTIONS: usize = 1000; @@ -167,3 +188,153 @@ fn test_send_and_confirm_transactions_in_parallel_with_tpu_client() { original_alice_balance - sum - total_fees ); } + +/// Builds a memo message with a `memo_data_len`-byte payload. `tag` is embedded +/// in the memo so that every message has a distinct signature (and is therefore +/// not deduped by the TPU). `memo_data_len` must be at least 8 bytes. +fn tagged_memo_message(payer: &Pubkey, memo_data_len: usize, tag: usize) -> Message { + let mut memo = vec![b'a'; memo_data_len]; + // Zero-padded decimal keeps the memo valid UTF-8 while making it unique. + let tag = format!("{tag:016}"); + memo[..tag.len()].copy_from_slice(tag.as_bytes()); + Message::new(&[build_memo(&memo_program::id(), &memo, &[])], Some(payer)) +} + +/// [`BackpressuredBroadcaster`] sends transactions to all the workers, awaiting +/// free capacity on each worker's channel instead of dropping the batch when +/// the channel is full (as the default `NonblockingBroadcaster` does). +/// +/// Note: leaders in the fanout are served sequentially, so a particularly slow +/// leader delays delivery to the remaining leaders in the same batch. +struct BackpressuredBroadcaster; + +#[async_trait] +impl WorkersBroadcaster for BackpressuredBroadcaster { + async fn send_to_workers( + &self, + workers: &mut WorkersCache, + leaders: &[SocketAddr], + transaction_batch: TransactionBatch, + ) -> Result<(), ConnectionWorkersSchedulerError> { + for leader in leaders { + // Unlike `try_send_transactions_to_address`, this awaits until the + // worker channel has room, so a full channel never surfaces as an + // error and the batch is not dropped. + let send_res = workers + .send_transactions_to_address(leader, transaction_batch.clone()) + .await; + if let Err(err) = send_res { + log::debug!("Failed to send transactions to {leader:?}, worker send error: {err}."); + // `send_transactions_to_address` already evicts the worker on + // `ReceiverDropped`; the remaining errors (`WorkerNotFound`, + // `ShutdownError`) are transient/expected and non-fatal here. + } + } + Ok(()) + } +} + +/// Submits a large burst of maximum-size transactions through the tpu-client-next +/// transport of `send_and_confirm_transactions_in_parallel_v3` and asserts every +/// one lands. +#[allow(clippy::arithmetic_side_effects)] +#[test_case(true; "Use staked connection")] +#[test_case(false; "Use unstaked connection")] +fn test_send_and_confirm_transactions_in_parallel_v3(staked: bool) { + /// Total wire bytes to push through the transport. + const TARGET_TOTAL_BYTES: usize = 10 * 1024 * 1024; + /// Each transaction is packet-sized, so this many of them ~= the target. + const NUM_MAX_SIZE_MESSAGES: usize = TARGET_TOTAL_BYTES.div_ceil(PACKET_DATA_SIZE); + /// Serialized size of the signatures for a legacy + /// transaction with one signer (which is what we send here) + const SIGNED_TX_SIGNATURES_LEN: usize = 1 + 64; + agave_logger::setup(); + + let signer = Keypair::new(); + let test_validator = + TestValidator::start_with_config(signer.pubkey(), None, SocketAddrSpace::Unspecified); + let signer = if staked { + test_validator.cluster_info().keypair().insecure_clone() + } else { + signer + }; + let fee_payer = signer.pubkey(); + + let rpc_client = Arc::new(NonblockingRpcClient::new_with_commitment( + test_validator.rpc_url(), + CommitmentConfig::confirmed(), + )); + let runtime = Runtime::new().unwrap(); + + // Figure out padding for memo so its transaction fills a packet + let empty_memo = Message::new( + &[build_memo(&memo_program::id(), &[], &[])], + Some(&fee_payer), + ); + let empty_memo_len = SIGNED_TX_SIGNATURES_LEN + empty_memo.serialize().len(); + let memo_padding_len = PACKET_DATA_SIZE - empty_memo_len - 1; + + let messages: Vec = (0..NUM_MAX_SIZE_MESSAGES) + .map(|tag| VersionedMessage::Legacy(tagged_memo_message(&fee_payer, memo_padding_len, tag))) + .collect(); + let mut total_wire_bytes = 0usize; + for message in &messages { + let tx_len = SIGNED_TX_SIGNATURES_LEN + message.serialize().len(); + assert!( + (PACKET_DATA_SIZE - 2..=PACKET_DATA_SIZE).contains(&tx_len), + "each transaction must be packet-sized, got {tx_len} bytes" + ); + total_wire_bytes += tx_len; + } + + let bind_socket = sockets::bind_to_localhost_unique().unwrap(); + let leader_updater = create_pinned_leader_updater(*test_validator.tpu_quic()); + + let (transaction_sender, client) = runtime.block_on(async { + ClientBuilder::new(leader_updater) + .bind_socket(bind_socket) + .identity(&signer) + // Apply backpressure instead of dropping batches on a + // full worker channel. + .broadcaster(BackpressuredBroadcaster) + .build() + .expect("Failed to build TPU client") + }); + //std::thread::sleep(Duration::from_secs(5)); + log::info!( + "sending {NUM_MAX_SIZE_MESSAGES} transactions ({:.2} MB wire) over tpu-client-next", + total_wire_bytes as f64 / (1024.0 * 1024.0) + ); + let started = Instant::now(); + let result = runtime.block_on(send_and_confirm_transactions_in_parallel_v3( + rpc_client.clone(), + // TPU-only: never fall back to RPC for sending. + SendTransport::Tpu(transaction_sender), + messages, + &[&signer], + SendAndConfirmConfigV3 { + with_spinner: true, + // Large bursts may outlive a single blockhash; allow re-signing. + max_sign_attempts: NonZeroUsize::new(16).unwrap(), + // Recheck landing / resend at most once a second. + check_interval: Duration::from_secs(1), + // Pace at ~333 TPS: a single write-locked account saturates at + // around 250 TPS, we could send more and just retransmit but why?. + send_interval: Duration::from_millis(3), + }, + )); + let elapsed = started.elapsed(); + runtime.block_on(async { + let _ = client.shutdown().await; + }); + + let transaction_errors = result.expect("all transactions must land within the resign window"); + assert_eq!(transaction_errors.len(), NUM_MAX_SIZE_MESSAGES); + log::info!( + "landed {NUM_MAX_SIZE_MESSAGES} transactions ({:.2} MB) in {:.2}s ({:.2} MB/s, {:.0} tx/s)", + total_wire_bytes as f64 / TARGET_TOTAL_BYTES as f64, + elapsed.as_secs_f64(), + total_wire_bytes as f64 / TARGET_TOTAL_BYTES as f64 / elapsed.as_secs_f64(), + NUM_MAX_SIZE_MESSAGES as f64 / elapsed.as_secs_f64(), + ); +} diff --git a/client/Cargo.toml b/client/Cargo.toml index 0ac76cc726d..6fa323dc381 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -44,6 +44,7 @@ solana-streamer = { workspace = true } solana-time-utils = { workspace = true } solana-tls-utils = { workspace = true } solana-tpu-client = { workspace = true, features = ["default"] } +solana-tpu-client-next = { workspace = true } solana-transaction = { workspace = true } solana-transaction-error = { workspace = true } solana-udp-client = { workspace = true } diff --git a/client/src/send_and_confirm_transactions_in_parallel.rs b/client/src/send_and_confirm_transactions_in_parallel.rs index b0517dc6838..4a99c06d0a6 100644 --- a/client/src/send_and_confirm_transactions_in_parallel.rs +++ b/client/src/send_and_confirm_transactions_in_parallel.rs @@ -7,7 +7,7 @@ use { dashmap::DashMap, futures_util::future::join_all, solana_hash::Hash, - solana_message::Message, + solana_message::{Message, VersionedMessage}, solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool}, solana_rpc_client::spinner::{self, SendTransactionProgress}, solana_rpc_client_api::{ @@ -19,16 +19,23 @@ use { solana_signature::Signature, solana_signer::{SignerError, signers::Signers}, solana_tpu_client::tpu_client::{Result, TpuSenderError}, - solana_transaction::Transaction, + solana_tpu_client_next::client_builder::TransactionSender, + solana_transaction::versioned::VersionedTransaction, solana_transaction_error::TransactionError, std::{ + future::Future, + num::NonZeroUsize, sync::{ Arc, atomic::{AtomicU64, AtomicUsize, Ordering}, }, time::Duration, }, - tokio::{sync::RwLock, task::JoinHandle}, + tokio::{ + sync::{Notify, RwLock}, + task::JoinHandle, + time::sleep, + }, }; const BLOCKHASH_REFRESH_RATE: Duration = Duration::from_secs(5); @@ -40,10 +47,45 @@ const SEND_TIMEOUT_INTERVAL: Duration = Duration::from_secs(5); type QuicTpuClient = TpuClient; +/// Abstracts sending a single serialized transaction to the current leader's +/// TPU, so the send & confirm engine can drive either the legacy +/// connection-cache [`QuicTpuClient`] or the [`TransactionSender`] from +/// `tpu-client-next`. +trait WireTransactionSender: Send + Sync { + /// Returns `true` if the transaction was handed off to the transport. + fn send(&self, wire_transaction: Vec) -> impl Future + Send; +} + +impl WireTransactionSender for QuicTpuClient { + fn send(&self, wire_transaction: Vec) -> impl Future + Send { + let fut = self.send_wire_transaction(wire_transaction); + async move { + tokio::time::timeout(SEND_TIMEOUT_INTERVAL, fut) + .await + .unwrap_or(false) + } + } +} + +impl WireTransactionSender for TransactionSender { + // tpu-client-next is fire-and-forget: a successful local enqueue here means nothing. + // The future returned here will only resolve to false if tpu-client-next + // instance is dropped. + fn send(&self, wire_transaction: Vec) -> impl Future + Send { + let sender = self.clone(); + async move { + sender + .send_transactions_in_batch(vec![wire_transaction]) + .await + .is_ok() + } + } +} + #[derive(Clone, Debug)] struct TransactionData { last_valid_block_height: u64, - message: Message, + message: VersionedMessage, index: usize, serialized_transaction: Vec, } @@ -62,6 +104,70 @@ pub struct SendAndConfirmConfigV2 { pub rpc_send_transaction_config: RpcSendTransactionConfig, } +pub enum SendTransport { + /// Send directly to the leader through tpu-client-next. + Tpu(TransactionSender), + /// Send through RPC. + Rpc(RpcSendTransactionConfig), +} + +#[derive(Clone, Debug, Copy)] +pub struct SendAndConfirmConfigV3 { + /// Shows a spinner with progress in the console while sending. + pub with_spinner: bool, + /// Maximum number of signing passes to make before giving up. A new + /// signing pass is made whenever blockhash expires. + pub max_sign_attempts: NonZeroUsize, + /// How long to wait between checking whether transactions have landed (and + /// resending those that have not). Should be at least a slot. + pub check_interval: Duration, + /// Delay between consecutive transactions within a send pass. Transaction + /// `n` is delayed by `n * send_interval`, capping the per-pass send rate at + /// `1 / send_interval`. + pub send_interval: Duration, +} +const DEFAULT_MAX_SIGN_ATTEMPTS: NonZeroUsize = NonZeroUsize::new(1).unwrap(); +const DEFAULT_CHECK_INTERVAL: Duration = Duration::from_secs(1); + +impl Default for SendAndConfirmConfigV3 { + fn default() -> Self { + Self { + with_spinner: false, + max_sign_attempts: DEFAULT_MAX_SIGN_ATTEMPTS, + check_interval: DEFAULT_CHECK_INTERVAL, + send_interval: SEND_INTERVAL, + } + } +} + +/// Internal configuration shared by the v2 and v3 entry points. +struct ParallelSendConfig { + with_spinner: bool, + max_sign_attempts: NonZeroUsize, + rpc_send_transaction_config: Option, + // only the legacy v2 API sets this. + dedupe_signers: bool, + send_interval: Duration, + check_interval: Duration, +} + +impl From for ParallelSendConfig { + fn from(config: SendAndConfirmConfigV2) -> Self { + Self { + with_spinner: config.with_spinner, + max_sign_attempts: config + .resign_txs_count + .and_then(NonZeroUsize::new) + .unwrap_or(DEFAULT_MAX_SIGN_ATTEMPTS), + // v2 always sends over RPC when the TPU transport is absent or fails. + rpc_send_transaction_config: Some(config.rpc_send_transaction_config), + dedupe_signers: true, + send_interval: SEND_INTERVAL, + check_interval: DEFAULT_CHECK_INTERVAL, + } + } +} + /// Sends and confirms transactions concurrently in a sync context pub fn send_and_confirm_transactions_in_parallel_blocking_v2( rpc_client: Arc, @@ -70,12 +176,12 @@ pub fn send_and_confirm_transactions_in_parallel_blocking_v2 Result>> { - let fut = send_and_confirm_transactions_in_parallel_v2( + let fut = send_and_confirm_transactions_in_parallel_impl( rpc_client.get_inner_client().clone(), tpu_client, - messages, + messages.iter().cloned().map(VersionedMessage::Legacy), signers, - config, + config.into(), ); tokio::task::block_in_place(|| rpc_client.runtime().block_on(fut)) } @@ -111,12 +217,16 @@ fn create_transaction_confirmation_task( unconfirmed_transaction_map: Arc>, errors_map: Arc>, num_confirmed_transactions: Arc, + check_interval: Duration, + confirmation_signal: Arc, ) -> JoinHandle<()> { tokio::spawn(async move { // check transactions that are not expired or have just expired between two checks let mut last_block_height = current_block_height.load(Ordering::Relaxed); loop { + // we sleep before first check to give sender a chance + tokio::time::sleep(check_interval).await; if !unconfirmed_transaction_map.is_empty() { let current_block_height = current_block_height.load(Ordering::Relaxed); let transactions_to_verify: Vec = unconfirmed_transaction_map @@ -160,7 +270,8 @@ fn create_transaction_confirmation_task( last_block_height = current_block_height; } - tokio::time::sleep(Duration::from_secs(1)).await; + // Wake the resend loop with a fresh view of what has landed. + confirmation_signal.notify_one(); } }) } @@ -173,6 +284,16 @@ struct SendingContext { num_confirmed_transactions: Arc, total_transactions: usize, current_block_height: Arc, + /// Delay between consecutive transactions within a send pass. Transaction + /// `n` is delayed by `n * send_interval`, so this caps the per-pass send + /// rate at `1 / send_interval`. + send_interval: Duration, + /// Liveness fallback for the resend loop when the confirmation task is not + /// signaling (e.g. RPC stalled). The signal below is the primary driver. + check_interval: Duration, + /// Signaled by the confirmation task after each poll, so the resend loop + /// only resends once it has a fresh view of what has landed. + confirmation_signal: Arc, } fn progress_from_context_and_block_height( context: &SendingContext, @@ -190,35 +311,38 @@ fn progress_from_context_and_block_height( } } -async fn send_transaction_with_rpc_fallback( +async fn send_transaction_with_rpc_fallback( rpc_client: &RpcClient, - tpu_client: &Option, - transaction: Transaction, + tpu_client: &Option, + transaction: VersionedTransaction, serialized_transaction: Vec, context: &SendingContext, index: usize, - rpc_send_transaction_config: RpcSendTransactionConfig, + rpc_send_transaction_config: Option, ) -> Result<()> { - let send_over_rpc = if let Some(tpu_client) = tpu_client { - !tokio::time::timeout( - SEND_TIMEOUT_INTERVAL, - tpu_client.send_wire_transaction(serialized_transaction), + // Prefer to send directly to the leader when possible. + let handed_off_over_tpu = match tpu_client { + Some(tpu_client) => tpu_client.send(serialized_transaction).await, + None => false, + }; + if handed_off_over_tpu { + return Ok(()); + } + // The hand-off failed or there is no TPU transport. + // The expect can only panic in v3 version of the API where RPC send config is mutually + // exclusive with TPU send config. + let rpc_send_transaction_config = rpc_send_transaction_config + .expect("TPU client must outlive the transaction sending process."); + + if let Err(e) = rpc_client + .send_transaction_with_config( + &transaction, + RpcSendTransactionConfig { + preflight_commitment: Some(rpc_client.commitment().commitment), + ..rpc_send_transaction_config + }, ) .await - .unwrap_or(false) - } else { - true - }; - if send_over_rpc - && let Err(e) = rpc_client - .send_transaction_with_config( - &transaction, - RpcSendTransactionConfig { - preflight_commitment: Some(rpc_client.commitment().commitment), - ..rpc_send_transaction_config - }, - ) - .await { match e.kind() { ErrorKind::Io(_) | ErrorKind::Reqwest(_) => { @@ -259,27 +383,69 @@ async fn send_transaction_with_rpc_fallback( Ok(()) } -async fn sign_all_messages_and_send( +/// Signs `message` with `signers`. +/// +/// When `dedupe_signers` is set, a signer that appears more than once (e.g. the +/// same key used as both fee payer and authority) is tolerated by matching each +/// required signer against the `signers` entry with that pubkey. The +/// legacy [`solana_transaction::Transaction::try_sign`] deduplicated signers +/// this way, but [`VersionedTransaction::try_new`] rejects the redundant entry +/// as `TooManySigners`. This flag preserves the old behavior for the deprecated +/// v2 API; v3 callers get the strict `try_new` semantics. +fn sign_versioned_message( + message: VersionedMessage, + signers: &T, + dedupe_signers: bool, +) -> std::result::Result { + // Once send_and_confirm_transactions_in_parallel_v2 is retired, this fn should be retired, + // and the segment below inlined into callers. + if !dedupe_signers { + return VersionedTransaction::try_new(message, signers); + } + let signer_keys = signers.try_pubkeys()?; + let unordered_signatures = signers.try_sign_message(&message.serialize())?; + let account_keys = message.static_account_keys(); + let num_required_signatures = message.header().num_required_signatures as usize; + let signatures = account_keys + .get(..num_required_signatures) + .ok_or_else(|| SignerError::InvalidInput("invalid message".to_string()))? + .iter() + .map(|required_key| { + signer_keys + .iter() + .position(|key| key == required_key) + .and_then(|i| unordered_signatures.get(i).copied()) + .ok_or(SignerError::NotEnoughSigners) + }) + .collect::, _>>()?; + Ok(VersionedTransaction { + signatures, + message, + }) +} + +async fn sign_all_messages_and_send( progress_bar: &Option, rpc_client: &RpcClient, - tpu_client: &Option, - messages_with_index: Vec<(usize, Message)>, + tpu_client: &Option, + messages_with_index: Vec<(usize, VersionedMessage)>, signers: &T, + dedupe_signers: bool, context: &SendingContext, - rpc_send_transaction_config: RpcSendTransactionConfig, + rpc_send_transaction_config: Option, ) -> Result<()> { let current_transaction_count = messages_with_index.len(); let mut futures = vec![]; // send all the transaction messages - for (counter, (index, message)) in messages_with_index.iter().enumerate() { - let mut transaction = Transaction::new_unsigned(message.clone()); + for (counter, (index, message)) in messages_with_index.into_iter().enumerate() { futures.push(async move { - tokio::time::sleep(SEND_INTERVAL.saturating_mul(counter as u32)).await; + sleep(context.send_interval.saturating_mul(counter as u32)).await; let blockhashdata = *context.blockhash_data_rw.read().await; + let mut message = message; + message.set_recent_blockhash(blockhashdata.blockhash); // we have already checked if all transactions are signable. - transaction - .try_sign(signers, blockhashdata.blockhash) + let transaction = sign_versioned_message(message.clone(), signers, dedupe_signers) .expect("Transaction should be signable"); let serialized_transaction = serialize(&transaction).expect("Transaction should serialize"); @@ -289,10 +455,10 @@ async fn sign_all_messages_and_send( context.unconfirmed_transaction_map.insert( signature, TransactionData { - index: *index, + index, serialized_transaction: serialized_transaction.clone(), last_valid_block_height: blockhashdata.last_valid_block_height, - message: message.clone(), + message, }, ); if let Some(progress_bar) = progress_bar { @@ -315,7 +481,7 @@ async fn sign_all_messages_and_send( transaction, serialized_transaction, context, - *index, + index, rpc_send_transaction_config, ) .await @@ -329,9 +495,11 @@ async fn sign_all_messages_and_send( Ok(()) } -async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction_over_tpu( +async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction_over_tpu< + S: WireTransactionSender, +>( progress_bar: &Option, - tpu_client: &Option, + tpu_client: &Option, context: &SendingContext, ) { let unconfirmed_transaction_map = context.unconfirmed_transaction_map.clone(); @@ -358,6 +526,14 @@ async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction while !unconfirmed_transaction_map.is_empty() && current_block_height.load(Ordering::Relaxed) <= max_valid_block_height { + // Resend only after the confirmation task reports a fresh view of + // what has landed. The timeout is a liveness fallback in case the + // confirmation task stalls, so the loop can still re-check its exit + // condition (e.g. blockhash expiry). + tokio::select! { + () = context.confirmation_signal.notified() => {} + () = tokio::time::sleep(context.check_interval.saturating_mul(2)) => {} + } let block_height = current_block_height.load(Ordering::Relaxed); if let Some(tpu_client) = tpu_client { @@ -376,8 +552,6 @@ async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction context, ) .await; - } else { - tokio::time::sleep(Duration::from_millis(100)).await; } if let Some(max_valid_block_height_in_remaining_transaction) = unconfirmed_transaction_map @@ -391,9 +565,9 @@ async fn confirm_transactions_till_block_height_and_resend_unexpired_transaction } } -async fn send_staggered_transactions( +async fn send_staggered_transactions( progress_bar: &Option, - tpu_client: &QuicTpuClient, + tpu_client: &S, wire_transactions: Vec>, last_valid_block_height: u64, context: &SendingContext, @@ -403,7 +577,7 @@ async fn send_staggered_transactions( .into_iter() .enumerate() .map(|(counter, transaction)| async move { - tokio::time::sleep(SEND_INTERVAL.saturating_mul(counter as u32)).await; + tokio::time::sleep(context.send_interval.saturating_mul(counter as u32)).await; if let Some(progress_bar) = progress_bar { let progress = progress_from_context_and_block_height(context, last_valid_block_height); @@ -416,17 +590,14 @@ async fn send_staggered_transactions( ), ); } - tokio::time::timeout( - SEND_TIMEOUT_INTERVAL, - tpu_client.send_wire_transaction(transaction), - ) - .await + tpu_client.send(transaction).await }) .collect::>(); join_all(futures).await; } -/// Sends and confirms transactions concurrently +/// Sends and confirms transactions concurrently using the legacy +/// connection-cache [`QuicTpuClient`] as the TPU transport. /// /// The sending and confirmation of transactions is done in parallel tasks /// The method signs transactions just before sending so that blockhash does not @@ -438,6 +609,76 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( signers: &T, config: SendAndConfirmConfigV2, ) -> Result>> { + send_and_confirm_transactions_in_parallel_impl( + rpc_client, + tpu_client, + messages.iter().cloned().map(VersionedMessage::Legacy), + signers, + config.into(), + ) + .await +} + +/// Sends and confirms transactions. +/// +/// `rpc_client` is used to target the leader and confirm landing. +/// `transport` selects how transactions are sent. +/// `signers` should provide unique private keys to sign transactions. +/// +/// Transactions are signed with a fresh blockhash if necessary to guarantee +/// eventual landing. +/// +/// With TPU transport, set the RPC client commitment to Confirmed to minimize +/// retransmit latency. +pub async fn send_and_confirm_transactions_in_parallel_v3( + rpc_client: Arc, + transport: SendTransport, + messages: M, + signers: &T, + config: SendAndConfirmConfigV3, +) -> Result>> +where + T: Signers + ?Sized, + M: IntoIterator, +{ + let (tpu_transaction_sender, rpc_send_transaction_config) = match transport { + SendTransport::Tpu(sender) => (Some(sender), None), + SendTransport::Rpc(rpc_config) => (None, Some(rpc_config)), + }; + send_and_confirm_transactions_in_parallel_impl( + rpc_client, + tpu_transaction_sender, + messages, + signers, + ParallelSendConfig { + with_spinner: config.with_spinner, + max_sign_attempts: config.max_sign_attempts, + rpc_send_transaction_config, + dedupe_signers: false, + // Pace sends client-side: the transport backpressures to its own + // capacity, but the leader still drops transactions it cannot ingest + // fast enough, so full-rate sending just fuels resends. + send_interval: config.send_interval, + check_interval: config.check_interval, + }, + ) + .await +} + +async fn send_and_confirm_transactions_in_parallel_impl( + rpc_client: Arc, + tpu_transaction_sender: Option, + messages: M, + signers: &T, + config: ParallelSendConfig, +) -> Result>> +where + T: Signers + ?Sized, + S: WireTransactionSender, + M: IntoIterator, +{ + let messages: Vec = messages.into_iter().collect(); + // get current blockhash and corresponding last valid block height let (blockhash, last_valid_block_height) = rpc_client .get_latest_blockhash_with_commitment(rpc_client.commitment()) @@ -451,8 +692,9 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( messages .iter() .map(|x| { - let mut transaction = Transaction::new_unsigned(x.clone()); - transaction.try_sign(signers, blockhash) + let mut message = x.clone(); + message.set_recent_blockhash(blockhash); + sign_versioned_message(message, signers, config.dedupe_signers).map(|_| ()) }) .collect::, SignerError>>()?; @@ -476,6 +718,7 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( let unconfirmed_transasction_map = Arc::new(DashMap::::new()); let error_map = Arc::new(DashMap::new()); let num_confirmed_transactions = Arc::new(AtomicUsize::new(0)); + let confirmation_signal = Arc::new(Notify::new()); // tasks which confirms the transactions that were sent let transaction_confirming_task = create_transaction_confirmation_task( rpc_client.clone(), @@ -483,12 +726,13 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( unconfirmed_transasction_map.clone(), error_map.clone(), num_confirmed_transactions.clone(), + config.check_interval, + confirmation_signal.clone(), ); // transaction sender task let total_transactions = messages.len(); let mut initial = true; - let signing_count = config.resign_txs_count.unwrap_or(1); let context = SendingContext { unconfirmed_transaction_map: unconfirmed_transasction_map.clone(), blockhash_data_rw: blockhash_data_rw.clone(), @@ -496,11 +740,14 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( current_block_height: current_block_height.clone(), error_map: error_map.clone(), total_transactions, + send_interval: config.send_interval, + check_interval: config.check_interval, + confirmation_signal, }; - for expired_blockhash_retries in (0..signing_count).rev() { + for expired_blockhash_retries in (0..config.max_sign_attempts.get()).rev() { // only send messages which have not been confirmed - let messages_with_index: Vec<(usize, Message)> = if initial { + let messages_with_index: Vec<(usize, VersionedMessage)> = if initial { initial = false; messages.iter().cloned().enumerate().collect() } else { @@ -521,16 +768,17 @@ pub async fn send_and_confirm_transactions_in_parallel_v2( sign_all_messages_and_send( &progress_bar, &rpc_client, - &tpu_client, + &tpu_transaction_sender, messages_with_index, signers, + config.dedupe_signers, &context, config.rpc_send_transaction_config, ) .await?; confirm_transactions_till_block_height_and_resend_unexpired_transaction_over_tpu( &progress_bar, - &tpu_client, + &tpu_transaction_sender, &context, ) .await; diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 840e33f3931..fe1911e7306 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -6274,6 +6274,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 7a47c1f18f1..5dc0700954d 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6425,6 +6425,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", diff --git a/tpu-client-next/src/workers_cache.rs b/tpu-client-next/src/workers_cache.rs index 9d71580b30e..1d3dadacaa9 100644 --- a/tpu-client-next/src/workers_cache.rs +++ b/tpu-client-next/src/workers_cache.rs @@ -258,11 +258,6 @@ impl WorkersCache { /// If the worker for the peer is disconnected or fails, it /// is removed from the cache. If no worker exists for the peer, /// it returns [`WorkersCacheError::WorkerNotFound`]. - #[allow( - dead_code, - reason = "This method will be used in the upcoming changes to implement optional \ - backpressure on the sender." - )] pub async fn send_transactions_to_address( &mut self, peer: &SocketAddr, From 8e41fe356f7ba983e51d2af160aed8a414bcbc71 Mon Sep 17 00:00:00 2001 From: Alex Pyattaev Date: Fri, 17 Jul 2026 13:01:49 +0300 Subject: [PATCH 11/30] solana-client: start deprecations (#13912) start deprecations in solana-client --- client/src/connection_cache.rs | 1 + client/src/nonblocking/tpu_client.rs | 33 ++++++++++++++-------- client/src/tpu_client.rs | 36 ++++++++++++++---------- tpu-client/src/nonblocking/tpu_client.rs | 4 +++ tpu-client/src/tpu_client.rs | 5 ++++ 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/client/src/connection_cache.rs b/client/src/connection_cache.rs index 75f5200104d..0146b320470 100644 --- a/client/src/connection_cache.rs +++ b/client/src/connection_cache.rs @@ -163,6 +163,7 @@ impl ConnectionCache { } } + #[deprecated(since = "4.3.0", note = "ConnectionCache is deprecated")] pub fn get_nonblocking_connection(&self, addr: &SocketAddr) -> NonblockingClientConnection { match self { Self::Quic(cache) => { diff --git a/client/src/nonblocking/tpu_client.rs b/client/src/nonblocking/tpu_client.rs index 45eaf91507c..3b04d6b9640 100644 --- a/client/src/nonblocking/tpu_client.rs +++ b/client/src/nonblocking/tpu_client.rs @@ -1,6 +1,5 @@ pub use solana_tpu_client::nonblocking::tpu_client::{LeaderTpuService, TpuSenderError}; use { - crate::{connection_cache::ConnectionCache, tpu_client::TpuClientConfig}, solana_connection_cache::connection_cache::{ ConnectionCache as BackendConnectionCache, ConnectionManager, ConnectionPool, NewConnectionConfig, @@ -9,7 +8,10 @@ use { solana_quic_client::{QuicConfig, QuicConnectionManager, QuicPool}, solana_rpc_client::nonblocking::rpc_client::RpcClient, solana_signer::signers::Signers, - solana_tpu_client::nonblocking::tpu_client::{Result, TpuClient as BackendTpuClient}, + solana_tpu_client::{ + nonblocking::tpu_client::{Result, TpuClient as BackendTpuClient}, + tpu_client::TpuClientConfig, + }, solana_transaction::{Transaction, versioned::VersionedTransaction}, solana_transaction_error::{TransactionError, TransportResult}, std::sync::Arc, @@ -86,15 +88,19 @@ impl TpuClient { websocket_url: &str, config: TpuClientConfig, ) -> Result { - let connection_cache = match ConnectionCache::new(name) { - ConnectionCache::Quic(cache) => cache, - ConnectionCache::Udp(_) => { - return Err(TpuSenderError::Custom(String::from( - "Invalid default connection cache", - ))); - } - }; - Self::new_with_connection_cache(rpc_client, websocket_url, config, connection_cache).await + let connection_manager = QuicConnectionManager::new_with_connection_config( + QuicConfig::new().expect("QUIC client config must be constructible"), + ); + Ok(Self { + tpu_client: BackendTpuClient::new( + name, + rpc_client, + websocket_url, + config, + connection_manager, + ) + .await?, + }) } } @@ -122,11 +128,16 @@ where }) } + #[deprecated( + since = "4.3.0", + note = "prefer send_and_confirm_transactions_in_parallel_v3" + )] pub async fn send_and_confirm_messages_with_spinner( &self, messages: &[Message], signers: &T, ) -> Result>> { + #[allow(deprecated)] self.tpu_client .send_and_confirm_messages_with_spinner(messages, signers) .await diff --git a/client/src/tpu_client.rs b/client/src/tpu_client.rs index d8ba28b6758..02efecdfc6b 100644 --- a/client/src/tpu_client.rs +++ b/client/src/tpu_client.rs @@ -1,5 +1,8 @@ +pub use solana_tpu_client::{ + nonblocking::tpu_client::TpuSenderError, + tpu_client::{DEFAULT_FANOUT_SLOTS, MAX_FANOUT_SLOTS, TpuClientConfig}, +}; use { - crate::connection_cache::ConnectionCache, solana_connection_cache::connection_cache::{ ConnectionCache as BackendConnectionCache, ConnectionManager, ConnectionPool, NewConnectionConfig, @@ -14,10 +17,6 @@ use { solana_udp_client::{UdpConfig, UdpConnectionManager, UdpPool}, std::sync::Arc, }; -pub use { - crate::nonblocking::tpu_client::TpuSenderError, - solana_tpu_client::tpu_client::{DEFAULT_FANOUT_SLOTS, MAX_FANOUT_SLOTS, TpuClientConfig}, -}; pub enum TpuClientWrapper { Quic(BackendTpuClient), @@ -83,15 +82,18 @@ impl TpuClient { websocket_url: &str, config: TpuClientConfig, ) -> Result { - let connection_cache = match ConnectionCache::new("connection_cache_tpu_client") { - ConnectionCache::Quic(cache) => cache, - ConnectionCache::Udp(_) => { - return Err(TpuSenderError::Custom(String::from( - "Invalid default connection cache", - ))); - } - }; - Self::new_with_connection_cache(rpc_client, websocket_url, config, connection_cache) + let connection_manager = QuicConnectionManager::new_with_connection_config( + QuicConfig::new().expect("QUIC client config must be constructible"), + ); + Ok(Self { + tpu_client: BackendTpuClient::new( + "connection_cache_tpu_client", + rpc_client, + websocket_url, + config, + connection_manager, + )?, + }) } } @@ -117,12 +119,16 @@ where )?, }) } - + #[deprecated( + since = "4.3.0", + note = "prefer send_and_confirm_transactions_in_parallel_v3" + )] pub fn send_and_confirm_messages_with_spinner( &self, messages: &[Message], signers: &T, ) -> Result>> { + #[allow(deprecated)] self.tpu_client .send_and_confirm_messages_with_spinner(messages, signers) } diff --git a/tpu-client/src/nonblocking/tpu_client.rs b/tpu-client/src/nonblocking/tpu_client.rs index 53dc64ba3be..f5d4873ac9f 100644 --- a/tpu-client/src/nonblocking/tpu_client.rs +++ b/tpu-client/src/nonblocking/tpu_client.rs @@ -542,6 +542,10 @@ where }) } + #[deprecated( + since = "4.3.0", + note = "prefer solana_client::send_and_confirm_transactions_in_parallel_v3" + )] #[cfg(feature = "spinner")] pub async fn send_and_confirm_messages_with_spinner( &self, diff --git a/tpu-client/src/tpu_client.rs b/tpu-client/src/tpu_client.rs index 7ba7670379b..e0d3df6cb0a 100644 --- a/tpu-client/src/tpu_client.rs +++ b/tpu-client/src/tpu_client.rs @@ -215,6 +215,10 @@ where }) } + #[deprecated( + since = "4.3.0", + note = "prefer send_and_confirm_transactions_in_parallel_v3" + )] #[cfg(feature = "spinner")] pub fn send_and_confirm_messages_with_spinner( &self, @@ -222,6 +226,7 @@ where signers: &T, ) -> Result>> { self.invoke( + #[allow(deprecated)] self.tpu_client .send_and_confirm_messages_with_spinner(messages, signers), ) From 4e8075854a36b3c43b6a3d87ba274b81ad9e4cc7 Mon Sep 17 00:00:00 2001 From: Gabe Rodriguez Date: Fri, 17 Jul 2026 14:49:03 +0200 Subject: [PATCH 12/30] Drop SysvarSerialize pt 2: replace loader-v3 account helpers (#13882) replace loader-v3 account helpers --- cli/src/program.rs | 39 +++++++++------- cli/tests/program.rs | 46 +++++++++++++------ dev-bins/Cargo.lock | 1 + ledger-tool/Cargo.toml | 1 + ledger-tool/src/program.rs | 6 +-- programs/bpf-loader-tests/tests/common.rs | 5 +- .../bank/builtins/core_bpf_migration/mod.rs | 11 ++--- runtime/src/bank/tests.rs | 45 ++++++++++-------- runtime/src/snapshot_minimizer.rs | 4 +- svm/Cargo.toml | 5 +- svm/src/account_loader.rs | 16 +++---- svm/src/program_loader.rs | 10 ++-- 12 files changed, 109 insertions(+), 80 deletions(-) diff --git a/cli/src/program.rs b/cli/src/program.rs index 2cc0ca94a9d..200078b9ec1 100644 --- a/cli/src/program.rs +++ b/cli/src/program.rs @@ -15,7 +15,6 @@ use { bip39::{Language, Mnemonic}, clap::{App, AppSettings, Arg, ArgMatches, SubCommand}, log::*, - solana_account::state_traits::StateMut, solana_account_decoder::{UiAccount, UiAccountEncoding, UiDataSliceConfig}, solana_clap_utils::{ self, @@ -1323,7 +1322,7 @@ async fn process_program_deploy( true } else if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(&account.data) { if let Some(account) = rpc_client .get_account_with_commitment(&programdata_address, config.commitment) @@ -1333,7 +1332,7 @@ async fn process_program_deploy( if let Ok(UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address: program_authority_pubkey, - }) = account.state() + }) = bincode::deserialize(&account.data) { if program_authority_pubkey.is_none() { return Err( @@ -1539,7 +1538,9 @@ async fn fetch_buffer_program_data( .into()); } - if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = account.state() { + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = + bincode::deserialize(&account.data) + { if authority_address.is_none() { return Err(format!("Buffer {buffer_pubkey} is immutable").into()); } @@ -1923,7 +1924,9 @@ async fn get_buffers( "It should be impossible at this point for the account data not to be decodable. \ Ensure that the account was fetched using a binary encoding.", ); - if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = account.state() { + if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = + bincode::deserialize(&account.data) + { buffers.push(CliUpgradeableBuffer { address: address.to_string(), authority: authority_address @@ -1979,7 +1982,7 @@ async fn get_programs( if let Ok(UpgradeableLoaderState::ProgramData { slot, upgrade_authority_address, - }) = programdata_account.state() + }) = bincode::deserialize(&programdata_account.data) { let mut bytes = vec![2, 0, 0, 0]; bytes.extend_from_slice(programdata_address.as_ref()); @@ -2065,7 +2068,7 @@ async fn process_show( } else if account.owner == bpf_loader_upgradeable::id() { if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(&account.data) { if let Some(programdata_account) = rpc_client .get_account_with_commitment(&programdata_address, config.commitment) @@ -2075,7 +2078,7 @@ async fn process_show( if let Ok(UpgradeableLoaderState::ProgramData { upgrade_authority_address, slot, - }) = programdata_account.state() + }) = bincode::deserialize(&programdata_account.data) { Ok(config .output_format @@ -2100,7 +2103,7 @@ async fn process_show( Err(format!("Program {account_pubkey} has been closed").into()) } } else if let Ok(UpgradeableLoaderState::Buffer { authority_address }) = - account.state() + bincode::deserialize(&account.data) { Ok(config .output_format @@ -2160,7 +2163,7 @@ async fn process_dump( } else if account.owner == bpf_loader_upgradeable::id() { if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(&account.data) { if let Some(programdata_account) = rpc_client .get_account_with_commitment(&programdata_address, config.commitment) @@ -2168,7 +2171,7 @@ async fn process_dump( .value { if let Ok(UpgradeableLoaderState::ProgramData { .. }) = - programdata_account.state() + bincode::deserialize(&programdata_account.data) { let offset = UpgradeableLoaderState::size_of_programdata_metadata(); let program_data = &programdata_account.data[offset..]; @@ -2181,7 +2184,9 @@ async fn process_dump( } else { Err(format!("Program {account_pubkey} has been closed").into()) } - } else if let Ok(UpgradeableLoaderState::Buffer { .. }) = account.state() { + } else if let Ok(UpgradeableLoaderState::Buffer { .. }) = + bincode::deserialize(&account.data) + { let offset = UpgradeableLoaderState::size_of_buffer_metadata(); let program_data = &account.data[offset..]; let mut f = File::create(output_location)?; @@ -2269,7 +2274,7 @@ async fn process_close( .await? .value { - match account.state() { + match bincode::deserialize(&account.data) { Ok(UpgradeableLoaderState::Buffer { authority_address }) => { if authority_address != Some(authority_signer.pubkey()) { return Err(format!( @@ -2315,7 +2320,7 @@ async fn process_close( if let Ok(UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address: authority_pubkey, - }) = account.state() + }) = bincode::deserialize(&account.data) { if authority_pubkey != Some(authority_signer.pubkey()) { Err(format!( @@ -2423,7 +2428,7 @@ async fn process_extend_program( return Err(format!("Account {program_pubkey} is not an upgradeable program").into()); } - let programdata_pubkey = match program_account.state() { + let programdata_pubkey = match bincode::deserialize(&program_account.data) { Ok(UpgradeableLoaderState::Program { programdata_address: programdata_pubkey, }) => Ok(programdata_pubkey), @@ -2441,7 +2446,7 @@ async fn process_extend_program( None => Err(format!("Program {program_pubkey} is closed")), }?; - let upgrade_authority_address = match programdata_account.state() { + let upgrade_authority_address = match bincode::deserialize(&programdata_account.data) { Ok(UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address, @@ -2952,7 +2957,7 @@ async fn extend_program_data_if_needed( return Ok(()); }; - let upgrade_authority_address = match program_data_account.state() { + let upgrade_authority_address = match bincode::deserialize(&program_data_account.data) { Ok(UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address, diff --git a/cli/tests/program.rs b/cli/tests/program.rs index 3117f075569..6a49496f1c6 100644 --- a/cli/tests/program.rs +++ b/cli/tests/program.rs @@ -4,7 +4,7 @@ use { agave_feature_set::{enable_alt_bn128_syscall, loader_v3_minimum_extend_program_size}, assert_matches::assert_matches, serde_json::Value, - solana_account::{ReadableAccount, state_traits::StateMut}, + solana_account::ReadableAccount, solana_borsh::v1::try_from_slice_unchecked, solana_cli::{ cli::{CliCommand, CliConfig, process_command}, @@ -1204,7 +1204,7 @@ async fn test_cli_program_deploy_with_authority() { if let UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address, - } = programdata_account.state().unwrap() + } = bincode::deserialize(&programdata_account.data).unwrap() { assert_eq!(upgrade_authority_address, None); } else { @@ -1407,7 +1407,7 @@ async fn test_cli_program_upgrade_auto_extend(skip_preflight: bool) { if let UpgradeableLoaderState::ProgramData { slot: _, upgrade_authority_address, - } = programdata_account.state().unwrap() + } = bincode::deserialize(&programdata_account.data).unwrap() { assert_eq!(upgrade_authority_address, None); } else { @@ -1919,7 +1919,9 @@ async fn test_cli_program_write_buffer() { let buffer_account = rpc_client.get_account(&new_buffer_pubkey).await.unwrap(); assert_eq!(buffer_account.lamports, minimum_balance_for_buffer_default); assert_eq!(buffer_account.owner, bpf_loader_upgradeable::id()); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(keypair.pubkey())); } else { panic!("not a buffer account"); @@ -1964,7 +1966,9 @@ async fn test_cli_program_write_buffer() { .unwrap(); assert_eq!(buffer_account.lamports, minimum_balance_for_buffer); assert_eq!(buffer_account.owner, bpf_loader_upgradeable::id()); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(keypair.pubkey())); } else { panic!("not a buffer account"); @@ -2034,7 +2038,9 @@ async fn test_cli_program_write_buffer() { .unwrap(); assert_eq!(buffer_account.lamports, minimum_balance_for_buffer_default); assert_eq!(buffer_account.owner, bpf_loader_upgradeable::id()); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(authority_keypair.pubkey())); } else { panic!("not a buffer account"); @@ -2074,7 +2080,9 @@ async fn test_cli_program_write_buffer() { let buffer_account = rpc_client.get_account(&buffer_pubkey).await.unwrap(); assert_eq!(buffer_account.lamports, minimum_balance_for_buffer_default); assert_eq!(buffer_account.owner, bpf_loader_upgradeable::id()); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(authority_keypair.pubkey())); } else { panic!("not a buffer account"); @@ -2420,7 +2428,9 @@ async fn test_cli_program_set_buffer_authority() { .get_account(&buffer_keypair.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(keypair.pubkey())); } else { panic!("not a buffer account"); @@ -2452,7 +2462,9 @@ async fn test_cli_program_set_buffer_authority() { .get_account(&buffer_keypair.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(new_buffer_authority.pubkey())); } else { panic!("not a buffer account"); @@ -2513,7 +2525,9 @@ async fn test_cli_program_set_buffer_authority() { .get_account(&buffer_keypair.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(buffer_keypair.pubkey())); } else { panic!("not a buffer account"); @@ -2608,7 +2622,9 @@ async fn test_cli_program_mismatch_buffer_authority() { .get_account(&buffer_keypair.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(buffer_authority.pubkey())); } else { panic!("not a buffer account"); @@ -3195,7 +3211,9 @@ async fn create_buffer_with_offline_authority<'a>( .get_account(&buffer_signer.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(online_signer.pubkey())); } else { panic!("not a buffer account"); @@ -3214,7 +3232,9 @@ async fn create_buffer_with_offline_authority<'a>( .get_account(&buffer_signer.pubkey()) .await .unwrap(); - if let UpgradeableLoaderState::Buffer { authority_address } = buffer_account.state().unwrap() { + if let UpgradeableLoaderState::Buffer { authority_address } = + bincode::deserialize(&buffer_account.data).unwrap() + { assert_eq!(authority_address, Some(offline_signer.pubkey())); } else { panic!("not a buffer account"); diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index fe1911e7306..c2b86fc3f6c 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -175,6 +175,7 @@ dependencies = [ "agave-snapshots", "agave-votor", "assert_cmd", + "bincode", "chrono", "clap", "crossbeam-channel", diff --git a/ledger-tool/Cargo.toml b/ledger-tool/Cargo.toml index 8c8162efb8a..55e6fc36290 100644 --- a/ledger-tool/Cargo.toml +++ b/ledger-tool/Cargo.toml @@ -25,6 +25,7 @@ agave-logger = { workspace = true } agave-reserved-account-keys = { workspace = true } agave-snapshots = { workspace = true } agave-votor = { workspace = true } +bincode = { workspace = true } chrono = { workspace = true, features = ["default"] } clap = { workspace = true } crossbeam-channel = { workspace = true } diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs index ab353e0d1c4..0b38bd6175b 100644 --- a/ledger-tool/src/program.rs +++ b/ledger-tool/src/program.rs @@ -4,9 +4,7 @@ use { log::*, serde::{Deserialize, Serialize}, serde_json::Result, - solana_account::{ - AccountSharedData, create_account_shared_data_for_test, state_traits::StateMut, - }, + solana_account::{AccountSharedData, create_account_shared_data_for_test}, solana_cli_output::{OutputFormat, QuietDisplay, VerboseDisplay}, solana_clock::Slot, solana_ledger::blockstore_options::AccessType, @@ -415,7 +413,7 @@ pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { if bpf_loader_upgradeable::check_id(&owner) && let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(account.data()) { debug!("Program data address {programdata_address}"); if bank diff --git a/programs/bpf-loader-tests/tests/common.rs b/programs/bpf-loader-tests/tests/common.rs index 503580ed7da..cc991e5a328 100644 --- a/programs/bpf-loader-tests/tests/common.rs +++ b/programs/bpf-loader-tests/tests/common.rs @@ -2,7 +2,7 @@ use { agave_feature_set::loader_v3_minimum_extend_program_size, - solana_account::{AccountSharedData, state_traits::StateMut}, + solana_account::{AccountSharedData, WritableAccount}, solana_instruction::{Instruction, error::InstructionError}, solana_keypair::Keypair, solana_loader_v3_interface::state::UpgradeableLoaderState, @@ -83,8 +83,7 @@ pub async fn add_upgradeable_loader_account( account_data_len, &id(), ); - account - .set_state(account_state) + bincode::serialize_into(account.data_as_mut_slice(), account_state) .expect("state failed to serialize into account data"); account_callback(&mut account); context.set_account(account_address, &account); diff --git a/runtime/src/bank/builtins/core_bpf_migration/mod.rs b/runtime/src/bank/builtins/core_bpf_migration/mod.rs index 405980be78a..bf3c9b677e8 100644 --- a/runtime/src/bank/builtins/core_bpf_migration/mod.rs +++ b/runtime/src/bank/builtins/core_bpf_migration/mod.rs @@ -520,9 +520,7 @@ pub(crate) mod tests { agave_feature_set::FeatureSet, agave_snapshots::snapshot_config::SnapshotConfig, assert_matches::assert_matches, - solana_account::{ - AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, - }, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, solana_accounts_db::accounts_db::ACCOUNTS_DB_CONFIG_FOR_TESTING, solana_builtins::{ BUILTINS, @@ -718,7 +716,8 @@ pub(crate) mod tests { // Program account has the correct state, with a pointer to its program // data address. - let program_account_state: UpgradeableLoaderState = program_account.state().unwrap(); + let program_account_state: UpgradeableLoaderState = + bincode::deserialize(program_account.data()).unwrap(); assert_eq!( program_account_state, UpgradeableLoaderState::Program { @@ -1085,7 +1084,7 @@ pub(crate) mod tests { let program_data_address = get_program_data_address(&builtin_id); let program_data_account = bank.get_account(&program_data_address).unwrap(); let program_data_account_state: UpgradeableLoaderState = - program_data_account.state().unwrap(); + bincode::deserialize(program_data_account.data()).unwrap(); assert_eq!( program_data_account_state, UpgradeableLoaderState::ProgramData { @@ -1259,7 +1258,7 @@ pub(crate) mod tests { let program_data_address = get_program_data_address(&program_address); let program_data_account = bank.get_account(&program_data_address).unwrap(); let program_data_account_state: UpgradeableLoaderState = - program_data_account.state().unwrap(); + bincode::deserialize(program_data_account.data()).unwrap(); assert_eq!( program_data_account_state, UpgradeableLoaderState::ProgramData { diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index d03e2570026..fb1af73d93d 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -5914,12 +5914,14 @@ fn test_bank_load_program() { programdata_data_offset + elf.len(), &bpf_loader_upgradeable::id(), ); - programdata_account - .set_state(&UpgradeableLoaderState::ProgramData { + bincode::serialize_into( + programdata_account.data_as_mut_slice(), + &UpgradeableLoaderState::ProgramData { slot: 42, upgrade_authority_address: None, - }) - .unwrap(); + }, + ) + .unwrap(); programdata_account.data_as_mut_slice()[programdata_data_offset..].copy_from_slice(&elf); programdata_account.set_rent_epoch(1); bank.store_account_and_update_capitalization(&program_key, &program_account); @@ -6025,11 +6027,13 @@ fn test_bpf_loader_upgradeable_deploy_with_max_len() { UpgradeableLoaderState::size_of_buffer(elf.len()), &bpf_loader_upgradeable::id(), ); - account - .set_state(&UpgradeableLoaderState::Buffer { + bincode::serialize_into( + account.data_as_mut_slice(), + &UpgradeableLoaderState::Buffer { authority_address: Some(upgrade_authority_keypair.pubkey()), - }) - .unwrap(); + }, + ) + .unwrap(); account .data_as_mut_slice() .get_mut(UpgradeableLoaderState::size_of_buffer_metadata()..) @@ -6153,7 +6157,7 @@ fn test_bpf_loader_upgradeable_deploy_with_max_len() { post_program_account.data().len(), UpgradeableLoaderState::size_of_program() ); - let state: UpgradeableLoaderState = post_program_account.state().unwrap(); + let state: UpgradeableLoaderState = bincode::deserialize(post_program_account.data()).unwrap(); assert_eq!( state, UpgradeableLoaderState::Program { @@ -6166,7 +6170,8 @@ fn test_bpf_loader_upgradeable_deploy_with_max_len() { post_programdata_account.owner(), &bpf_loader_upgradeable::id() ); - let state: UpgradeableLoaderState = post_programdata_account.state().unwrap(); + let state: UpgradeableLoaderState = + bincode::deserialize(post_programdata_account.data()).unwrap(); assert_eq!( state, UpgradeableLoaderState::ProgramData { @@ -11569,11 +11574,13 @@ fn test_deploy_last_epoch_slot() { UpgradeableLoaderState::size_of_buffer(program_len), &bpf_loader_upgradeable::id(), ); - account - .set_state(&UpgradeableLoaderState::Buffer { + bincode::serialize_into( + account.data_as_mut_slice(), + &UpgradeableLoaderState::Buffer { authority_address: Some(upgrade_authority_keypair.pubkey()), - }) - .unwrap(); + }, + ) + .unwrap(); account .data_as_mut_slice() .get_mut(UpgradeableLoaderState::size_of_buffer_metadata()..) @@ -12024,11 +12031,13 @@ fn test_bpf_loader_upgradeable_deploy_with_more_than_255_accounts() { UpgradeableLoaderState::size_of_buffer(elf.len()), &bpf_loader_upgradeable::id(), ); - account - .set_state(&UpgradeableLoaderState::Buffer { + bincode::serialize_into( + account.data_as_mut_slice(), + &UpgradeableLoaderState::Buffer { authority_address: Some(upgrade_authority_keypair.pubkey()), - }) - .unwrap(); + }, + ) + .unwrap(); account .data_as_mut_slice() .get_mut(UpgradeableLoaderState::size_of_buffer_metadata()..) diff --git a/runtime/src/snapshot_minimizer.rs b/runtime/src/snapshot_minimizer.rs index 888783bc055..4f94bbb3b03 100644 --- a/runtime/src/snapshot_minimizer.rs +++ b/runtime/src/snapshot_minimizer.rs @@ -10,7 +10,7 @@ use { iter::{IntoParallelIterator, IntoParallelRefIterator, ParallelIterator}, prelude::ParallelSlice, }, - solana_account::{ReadableAccount, state_traits::StateMut}, + solana_account::ReadableAccount, solana_accounts_db::{ account_storage_entry::AccountStorageEntry, accounts_db::{AccountsDb, GetUniqueAccountsResult, UpdateIndexThreadSelection}, @@ -176,7 +176,7 @@ impl<'a> SnapshotMinimizer<'a> { .filter_map(|account| { if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(account.data()) { Some(programdata_address) } else { diff --git a/svm/Cargo.toml b/svm/Cargo.toml index 64622537d10..6b7a0fb4418 100644 --- a/svm/Cargo.toml +++ b/svm/Cargo.toml @@ -24,7 +24,6 @@ agave-unstable-api = [] conformance = [ "dep:agave-feature-set", "dep:agave-precompiles", - "dep:bincode", "dep:prost", "dep:protosol", "dep:solana-poseidon", @@ -36,7 +35,6 @@ conformance = [ "solana-pubkey/default", ] dev-context-only-utils = [ - "dep:bincode", "dep:qualifier_attr", "dep:solana-bpf-loader-program", "dep:solana-compute-budget", @@ -68,7 +66,7 @@ svm-internal = ["dep:qualifier_attr"] agave-feature-set = { workspace = true, optional = true } agave-precompiles = { workspace = true, optional = true } ahash = { workspace = true } -bincode = { workspace = true, optional = true } +bincode = { workspace = true } log = { workspace = true } percentage = { workspace = true } prost = { version = "0.11", optional = true } @@ -125,7 +123,6 @@ xxhash-rust = { workspace = true, optional = true, features = ["xxh64"] } [dev-dependencies] assert_matches = { workspace = true } -bincode = { workspace = true } env_logger = { workspace = true } libsecp256k1 = { workspace = true } openssl = { workspace = true } diff --git a/svm/src/account_loader.rs b/svm/src/account_loader.rs index 4e5867889d3..e97deca4f0e 100644 --- a/svm/src/account_loader.rs +++ b/svm/src/account_loader.rs @@ -8,9 +8,7 @@ use { transaction_error_metrics::TransactionErrorMetrics, }, ahash::{AHashMap, AHashSet}, - solana_account::{ - Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, - }, + solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, solana_clock::Slot, solana_fee_structure::FeeDetails, solana_instruction::{BorrowedAccountMeta, BorrowedInstruction}, @@ -564,7 +562,7 @@ fn load_transaction_accounts( if bpf_loader_upgradeable::check_id(account.owner()) && let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = account.state() + }) = bincode::deserialize(account.data()) { // ...its programdata was not already counted and will not later be counted... if !account_keys.iter().any(|key| programdata_address == *key) @@ -2663,11 +2661,13 @@ mod tests { } if has_programdata || rng.random() { - account - .set_state(&UpgradeableLoaderState::Program { + bincode::serialize_into( + account.data_as_mut_slice(), + &UpgradeableLoaderState::Program { programdata_address, - }) - .unwrap(); + }, + ) + .unwrap(); } } diff --git a/svm/src/program_loader.rs b/svm/src/program_loader.rs index f2bd553bc68..d44b8120b1c 100644 --- a/svm/src/program_loader.rs +++ b/svm/src/program_loader.rs @@ -1,7 +1,7 @@ #[cfg(feature = "metrics")] use solana_program_runtime::program_metrics::LoadProgramMetrics; use { - solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, + solana_account::{AccountSharedData, ReadableAccount}, solana_clock::Slot, solana_instruction::error::InstructionError, solana_loader_v3_interface::state::UpgradeableLoaderState, @@ -53,7 +53,7 @@ pub(crate) fn load_program_accounts( } else if bpf_loader_upgradeable::check_id(program_account.owner()) { if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = program_account.state() + }) = bincode::deserialize(program_account.data()) { if let Some((programdata_account, _slot)) = callbacks.get_account_shared_data(&programdata_address) @@ -62,7 +62,7 @@ pub(crate) fn load_program_accounts( if let Ok(UpgradeableLoaderState::ProgramData { slot, upgrade_authority_address: _, - }) = programdata_account.state() + }) = bincode::deserialize(programdata_account.data()) { ProgramAccountLoadResult::ProgramOfLoaderV3( program_account, @@ -220,7 +220,7 @@ pub(crate) fn get_program_deployment_slot( ProgramCacheEntryOwner::LoaderV3 => { if let Ok(UpgradeableLoaderState::Program { programdata_address, - }) = program.state() + }) = bincode::deserialize(program.data()) { let (programdata, _slot) = callbacks .get_account_shared_data(&programdata_address) @@ -228,7 +228,7 @@ pub(crate) fn get_program_deployment_slot( if let Ok(UpgradeableLoaderState::ProgramData { slot, upgrade_authority_address: _, - }) = programdata.state() + }) = bincode::deserialize(programdata.data()) { return Ok(slot); } From 59ea2d368974bea36389765572a286192b306273 Mon Sep 17 00:00:00 2001 From: Mircea Colonescu Date: Fri, 17 Jul 2026 09:38:03 -0400 Subject: [PATCH 13/30] feat(ci): honor channel pins in xtask channel-info (#13843) Port the pin overrides from ci/channel-info.sh into the xtask resolver so xtask can fully replace the bash script. --- ci/xtask/src/commands/channel_info.rs | 9 +- ci/xtask/src/commands/channel_info/fetch.rs | 31 +++- ci/xtask/src/commands/channel_info/resolve.rs | 144 ++++++++++++++++-- 3 files changed, 169 insertions(+), 15 deletions(-) diff --git a/ci/xtask/src/commands/channel_info.rs b/ci/xtask/src/commands/channel_info.rs index 3d3d85bbad1..20f9e949bde 100644 --- a/ci/xtask/src/commands/channel_info.rs +++ b/ci/xtask/src/commands/channel_info.rs @@ -35,9 +35,16 @@ pub async fn run(args: CommandArgs) -> Result<()> { .await?; let versions: BTreeMap = heads.into_iter().zip(fetched).collect(); + let pins = fetch::channel_pins(&client).await?; let branch = pick_env("CI_BASE_BRANCH").or_else(|| pick_env("CI_BRANCH")); let channel = pick_env("CHANNEL"); - let info = derive_channels(&versions, &tags, branch.as_deref(), channel.as_deref())?; + let info = derive_channels( + &versions, + &tags, + branch.as_deref(), + channel.as_deref(), + &pins, + )?; if args.json { print_channel_info_json(&info)?; diff --git a/ci/xtask/src/commands/channel_info/fetch.rs b/ci/xtask/src/commands/channel_info/fetch.rs index 3db482c2cb8..3e0e74a81d7 100644 --- a/ci/xtask/src/commands/channel_info/fetch.rs +++ b/ci/xtask/src/commands/channel_info/fetch.rs @@ -1,9 +1,12 @@ use { - super::resolve::BranchVersion, + super::resolve::{BranchVersion, ChannelPins}, anyhow::{Result, anyhow, bail}, semver::Version, serde::Deserialize, - std::process::Command, + std::{ + process::Command, + time::{SystemTime, UNIX_EPOCH}, + }, }; const REMOTE: &str = "https://github.com/anza-xyz/agave.git"; @@ -69,6 +72,30 @@ struct PackageSection { version: Version, } +/// Fetch pin overrides from `master` (single source of truth for all +/// branches). Cache-busted so a pin change takes effect at once; transport +/// errors hard-fail rather than silently falling back to auto-resolution. +pub async fn channel_pins(client: &reqwest::Client) -> Result { + let ts = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_secs()) + .unwrap_or(0); + let url = format!("{RAW_BASE}/master/ci/channel-overrides"); + let resp = client + .get(&url) + .query(&[("ts", ts)]) + .send() + .await + .map_err(|e| anyhow!("GET {url}: {e}"))? + .error_for_status() + .map_err(|e| anyhow!("GET {url}: {e}"))?; + let raw = resp + .text() + .await + .map_err(|e| anyhow!("read body for {url}: {e}"))?; + ChannelPins::parse(&raw) +} + pub async fn workspace_version(client: &reqwest::Client, bv: BranchVersion) -> Result { let url = format!("{RAW_BASE}/{bv}/Cargo.toml"); let resp = client diff --git a/ci/xtask/src/commands/channel_info/resolve.rs b/ci/xtask/src/commands/channel_info/resolve.rs index 84c9b5a0bd7..a04507bfcff 100644 --- a/ci/xtask/src/commands/channel_info/resolve.rs +++ b/ci/xtask/src/commands/channel_info/resolve.rs @@ -58,6 +58,44 @@ pub fn stage_of(v: &Version) -> Result { } } +/// Manual channel pins parsed from `ci/channel-overrides` on master. A set pin +/// forces the corresponding channel to a specific `vX.Y` line, overriding the +/// version-derived value; `None` leaves auto-resolution in place. +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] +pub struct ChannelPins { + pub beta: Option, + pub stable: Option, +} + +impl ChannelPins { + pub fn parse(contents: &str) -> Result { + Ok(Self { + beta: extract_pin(contents, "PINNED_BETA_CHANNEL")?, + stable: extract_pin(contents, "PINNED_STABLE_CHANNEL")?, + }) + } +} + +fn extract_pin(contents: &str, key: &str) -> Result> { + for line in contents.lines() { + let Some((k, v)) = line.split_once('=') else { + continue; + }; + if k.trim() != key { + continue; + } + let v = v.trim(); + if v.is_empty() { + return Ok(None); + } + return v + .parse::() + .map(Some) + .map_err(|e| anyhow!("invalid {key} pin `{v}`: {e}")); + } + Ok(None) +} + #[derive(Debug, Serialize)] #[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct ChannelInfo { @@ -75,6 +113,7 @@ pub fn derive_channels( tags: &[Version], branch: Option<&str>, channel: Option<&str>, + pins: &ChannelPins, ) -> Result { for (bv, v) in versions { if v.major != bv.major || v.minor != bv.minor { @@ -97,6 +136,9 @@ pub fn derive_channels( None => (None, None), }; + let beta = pins.beta.or(beta); + let stable = pins.stable.or(stable); + let beta = beta.ok_or_else(|| anyhow!("no BETA-eligible vX.Y head"))?; if let Some(s) = stable @@ -185,7 +227,7 @@ mod tests { fn promotes_top_when_top_is_beta() { let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); - let info = derive_channels(&vs, &[], None, None).unwrap(); + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); assert_eq!(info.beta_channel, "v4.1"); assert_eq!(info.stable_channel, "v4.0"); @@ -195,7 +237,7 @@ mod tests { fn promotes_top_when_top_is_ga() { let vs = versions(&[(bv(4, 0), "4.0.5"), (bv(4, 1), "4.1.0")]); - let info = derive_channels(&vs, &[], None, None).unwrap(); + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); assert_eq!(info.beta_channel, "v4.1"); assert_eq!(info.stable_channel, "v4.0"); @@ -209,7 +251,7 @@ mod tests { (bv(4, 1), "4.1.0-alpha.0"), ]); - let info = derive_channels(&vs, &[], None, None).unwrap(); + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); assert_eq!(info.beta_channel, "v4.0"); assert_eq!(info.stable_channel, "v3.1"); @@ -219,7 +261,7 @@ mod tests { fn rc_top_is_promoted() { let vs = versions(&[(bv(4, 0), "4.0.10"), (bv(4, 1), "4.1.0-rc.2")]); - let info = derive_channels(&vs, &[], None, None).unwrap(); + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); assert_eq!(info.beta_channel, "v4.1"); assert_eq!(info.stable_channel, "v4.0"); @@ -229,7 +271,7 @@ mod tests { fn rejects_mismatched_workspace_version() { let vs = versions(&[(bv(4, 0), "5.0.0")]); - let err = derive_channels(&vs, &[], None, None).unwrap_err(); + let err = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap_err(); assert!(err.to_string().contains("does not match branch")); } @@ -238,7 +280,7 @@ mod tests { fn rejects_unknown_prerelease_label() { let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-dev.0")]); - let err = derive_channels(&vs, &[], None, None).unwrap_err(); + let err = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap_err(); assert!(err.to_string().contains("unknown prerelease label")); } @@ -247,14 +289,15 @@ mod tests { fn rejects_when_only_alpha_top_and_nothing_below() { let vs = versions(&[(bv(4, 1), "4.1.0-alpha.0")]); - let err = derive_channels(&vs, &[], None, None).unwrap_err(); + let err = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap_err(); assert!(err.to_string().contains("no BETA-eligible")); } #[test] fn rejects_empty() { - let err = derive_channels(&BTreeMap::new(), &[], None, None).unwrap_err(); + let err = derive_channels(&BTreeMap::new(), &[], None, None, &ChannelPins::default()) + .unwrap_err(); assert!(err.to_string().contains("no BETA-eligible")); } @@ -273,7 +316,7 @@ mod tests { v("2.9.9"), ]; - let info = derive_channels(&vs, &tags, None, None).unwrap(); + let info = derive_channels(&vs, &tags, None, None, &ChannelPins::default()).unwrap(); assert_eq!(info.beta_channel_latest_tag, "v3.1.0-beta.1"); assert_eq!(info.stable_channel_latest_tag, "v3.0.6"); @@ -283,7 +326,7 @@ mod tests { fn channel_from_branch_match() { let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); - let info = derive_channels(&vs, &[], Some("v4.1"), None).unwrap(); + let info = derive_channels(&vs, &[], Some("v4.1"), None, &ChannelPins::default()).unwrap(); assert_eq!(info.channel, "beta"); } @@ -292,7 +335,14 @@ mod tests { fn channel_env_var_wins() { let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); - let info = derive_channels(&vs, &[], Some("master"), Some("stable")).unwrap(); + let info = derive_channels( + &vs, + &[], + Some("master"), + Some("stable"), + &ChannelPins::default(), + ) + .unwrap(); assert_eq!(info.channel, "stable"); } @@ -300,7 +350,7 @@ mod tests { #[test] fn json_uses_screaming_snake_case_keys() { let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); - let info = derive_channels(&vs, &[], None, None).unwrap(); + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); let json: serde_json::Value = serde_json::from_str(&serde_json::to_string(&info).unwrap()).unwrap(); @@ -314,6 +364,76 @@ mod tests { assert_eq!(json["CHANNEL_LATEST_TAG"], ""); } + #[test] + fn pin_overrides_auto_derived_channels() { + let vs = versions(&[ + (bv(4, 0), "4.0.0"), + (bv(4, 1), "4.1.0"), + (bv(4, 2), "4.2.0-beta.0"), + ]); + let pins = ChannelPins { + beta: Some(bv(4, 1)), + stable: Some(bv(4, 0)), + }; + + let info = derive_channels(&vs, &[], None, None, &pins).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn empty_pins_leave_auto_resolution_intact() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + + let info = derive_channels(&vs, &[], None, None, &ChannelPins::default()).unwrap(); + + assert_eq!(info.beta_channel, "v4.1"); + assert_eq!(info.stable_channel, "v4.0"); + } + + #[test] + fn pinned_channel_picks_its_own_latest_tag() { + let vs = versions(&[(bv(4, 0), "4.0.0"), (bv(4, 1), "4.1.0-beta.0")]); + let tags = vec![v("4.0.3"), v("4.0.1")]; + let pins = ChannelPins { + beta: None, + stable: Some(bv(4, 0)), + }; + + let info = derive_channels(&vs, &tags, None, None, &pins).unwrap(); + + assert_eq!(info.stable_channel, "v4.0"); + assert_eq!(info.stable_channel_latest_tag, "v4.0.3"); + } + + #[test] + fn parse_reads_set_and_empty_pins() { + let contents = "\ +# comment line +PINNED_BETA_CHANNEL=v4.1 +PINNED_STABLE_CHANNEL= +"; + let pins = ChannelPins::parse(contents).unwrap(); + + assert_eq!(pins.beta, Some(bv(4, 1))); + assert_eq!(pins.stable, None); + } + + #[test] + fn parse_absent_keys_are_none() { + let pins = ChannelPins::parse("# nothing here\n").unwrap(); + + assert_eq!(pins, ChannelPins::default()); + } + + #[test] + fn parse_rejects_malformed_pin() { + let err = ChannelPins::parse("PINNED_BETA_CHANNEL=4.1\n").unwrap_err(); + + assert!(err.to_string().contains("invalid PINNED_BETA_CHANNEL pin")); + } + #[test] fn stage_of_classifies_known_labels() { assert_eq!(stage_of(&v("4.0.0")).unwrap(), Stage::Ga); From 9505e429643bf56314f3ea8e2bbd693888d4f774 Mon Sep 17 00:00:00 2001 From: Akhilesh Singhania Date: Fri, 17 Jul 2026 16:53:34 +0200 Subject: [PATCH 14/30] bls-sigverify: reduces NUM_SLOTS_FOR_VERIFY (#13917) --- bls-sigverify/src/bls_sigverifier.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index dd35a525fa4..783c14e244f 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -47,7 +47,10 @@ use { /// invalid and discarded. /// /// This also sets an upper bound on how much storage the various structs in this module require. -pub(super) const NUM_SLOTS_FOR_VERIFY: Slot = 90_000; +/// +/// At 200ms slot times, 30K slots is 100mins. We do not expect a node to catch up if it has +/// fallen so far behind. +pub(super) const NUM_SLOTS_FOR_VERIFY: Slot = 30_000; /// If we receive an invalid certificate or vote, we ban its attributed sender. For certificates /// received from blockstore, that sender is the scheduled leader for the carrier slot. We ban the From f06f41ca25768e7d40b2952fa1a44b6022a978ac Mon Sep 17 00:00:00 2001 From: Kamil Skalski Date: Fri, 17 Jul 2026 16:55:44 +0200 Subject: [PATCH 15/30] test(snapshot): exercise the wincode path in capped-file data-file tests (#13922) The snapshot data-file helpers (serialize_snapshot_data_file / deserialize_snapshot_data_files) are only ever driven with wincode serializers in production, but their unit tests still round-tripped a u32 through bincode's serialize_into / deserialize_from. Switch those tests to the wincode serialize_into / deserialize_wincode_from helpers so they cover the path that is actually used. The extra-data test wrote two u32s with two serialize_into calls; the wincode writer finalizes on finish, so write both in one call via a (u32, u32) tuple to keep the trailing-bytes scenario. --- runtime/src/snapshot_utils.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/runtime/src/snapshot_utils.rs b/runtime/src/snapshot_utils.rs index 1fbdabe7eb2..f96c6ff320f 100644 --- a/runtime/src/snapshot_utils.rs +++ b/runtime/src/snapshot_utils.rs @@ -1873,6 +1873,7 @@ pub fn create_tmp_accounts_dir_for_tests() -> (TempDir, PathBuf) { mod tests { use { super::*, + crate::serde_snapshot::{deserialize_wincode_from, serialize_into}, agave_snapshots::{ paths::{ full_snapshot_archives_iter, get_highest_full_snapshot_archive_slot, @@ -1884,7 +1885,6 @@ mod tests { }, }, assert_matches::assert_matches, - bincode::{deserialize_from, serialize_into}, solana_accounts_db::accounts_file::{AccountsFile, AccountsFileProvider}, solana_hash::Hash, std::{convert::TryFrom, mem::size_of}, @@ -1951,8 +1951,8 @@ mod tests { &snapshot_root_paths, expected_consumed_size, |stream| { - Ok(deserialize_from::<_, u32>( - &mut stream.full_snapshot_stream, + Ok(deserialize_wincode_from::<_, u32>( + &mut *stream.full_snapshot_stream, )?) }, ) @@ -1986,8 +1986,8 @@ mod tests { &snapshot_root_paths, expected_consumed_size - 1, |stream| { - Ok(deserialize_from::<_, u32>( - &mut stream.full_snapshot_stream, + Ok(deserialize_wincode_from::<_, u32>( + &mut *stream.full_snapshot_stream, )?) }, ); @@ -2005,8 +2005,9 @@ mod tests { expected_consumed_size * 2, &IoSetupState::default(), |stream| { - serialize_into(&mut *stream, &expected_data)?; - serialize_into(&mut *stream, &expected_data)?; + // Write two u32s (in one call, since the wincode writer finalizes on finish) so + // the file has trailing bytes left over after a single-u32 deserialize. + serialize_into(&mut *stream, &(expected_data, expected_data))?; Ok(()) }, ) @@ -2021,8 +2022,8 @@ mod tests { &snapshot_root_paths, expected_consumed_size * 2, |stream| { - Ok(deserialize_from::<_, u32>( - &mut stream.full_snapshot_stream, + Ok(deserialize_wincode_from::<_, u32>( + &mut *stream.full_snapshot_stream, )?) }, ); From 31e6639df36716a15033e0b71e48a89d910a56f7 Mon Sep 17 00:00:00 2001 From: Ashwin Sekar Date: Fri, 17 Jul 2026 11:17:11 -0400 Subject: [PATCH 16/30] PER: account for burns in total inflation reward calculation for ag (#13903) --- .../partitioned_epoch_rewards/calculation.rs | 103 +++++++++++++++++- runtime/src/bank_forks.rs | 2 + .../epoch_inflation_account_state.rs | 6 + 3 files changed, 110 insertions(+), 1 deletion(-) diff --git a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs index ad237102b08..463ed4b0c6a 100644 --- a/runtime/src/bank/partitioned_epoch_rewards/calculation.rs +++ b/runtime/src/bank/partitioned_epoch_rewards/calculation.rs @@ -13,6 +13,7 @@ use { RewardCalcTracer, RewardCalculationEvent, RewardsMetrics, fee_distribution::ExternalCollectorType, null_tracer, }, + block_component_processor::vote_reward::epoch_inflation_account_state::EpochInflationAccountState, inflation_rewards::{ delegation_may_need_adjustment, points::{ @@ -417,7 +418,18 @@ impl Bank { ) -> PartitionedRewardsCalculation { let capitalization = self.capitalization(); let epoch_inflation_rewards = - self.calculate_epoch_inflation_rewards(capitalization, rewarded_epoch); + if AlpenglowEpochType::is_alpenglow_or_migration_epoch(self, rewarded_epoch) { + EpochInflationAccountState::new_from_bank(self) + .and_then(|state| state.inflation_rewards_for_epoch(rewarded_epoch)) + .unwrap_or_else(|| { + panic!( + "Missing epoch inflation state for non-Tower reward epoch \ + {rewarded_epoch}" + ) + }) + } else { + self.calculate_epoch_inflation_rewards(capitalization, rewarded_epoch) + }; // `distribution_epoch_vote_accounts` is the post-VAT-filter snapshot // produced upstream of this call (or unfiltered when VAT is off), // so its length is the right value for the `epoch_rewards` metric. @@ -2584,6 +2596,95 @@ mod tests { ); } + #[test] + fn test_alpenglow_partitioned_rewards_use_epoch_start_budget_after_burn() { + let validator_keypairs = vec![genesis_utils::ValidatorVoteKeypairs::new_rand()]; + let GenesisConfigInfo { + mut genesis_config, .. + } = genesis_utils::create_genesis_config_with_alpenglow_vote_accounts( + 1_000_000_000 * LAMPORTS_PER_SOL, + &validator_keypairs, + vec![100 * LAMPORTS_PER_SOL], + ); + genesis_config.epoch_schedule = EpochSchedule::new(SLOTS_PER_EPOCH); + let features_to_deactivate = crate::slot_params::slot_time_feature_ids().to_vec(); + deactivate_features(&mut genesis_config, &features_to_deactivate); + + let (bank, bank_forks) = + Bank::new_for_tests(&genesis_config).wrap_with_bank_forks_for_tests(); + let bank = Bank::new_from_parent_with_bank_forks( + bank_forks.as_ref(), + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH, + ); + assert_eq!(bank.epoch(), 1); + + let recorded_budget = EpochInflationAccountState::new_from_bank(&bank) + .and_then(|state| state.inflation_rewards_for_epoch(bank.epoch())) + .expect("epoch-start inflation budget must be persisted"); + // Alpenglow rewards are rounded down once per slot, so this is the largest + // payout that can actually have been recorded during the epoch. + let recorded_payout = recorded_budget / SLOTS_PER_EPOCH * SLOTS_PER_EPOCH; + + let vote_pubkey = validator_keypairs[0].vote_keypair.pubkey(); + let mut vote_account = bank.get_account(&vote_pubkey).unwrap(); + let VoteStateVersions::V4(mut vote_state) = vote_account + .deserialize_data::() + .unwrap() + else { + panic!("unexpected vote state version"); + }; + let last_credits = vote_state + .epoch_credits + .last() + .map(|(_epoch, final_credits, _initial_credits)| *final_credits) + .unwrap_or_default(); + vote_state + .epoch_credits + .push((bank.epoch(), last_credits + recorded_payout, last_credits)); + vote_account + .serialize_data(&VoteStateVersions::V4(vote_state)) + .unwrap(); + bank.store_account(&vote_pubkey, &vote_account); + + // Freezing burns the VAT transferred to the incinerator at the epoch + // boundary, reducing capitalization after the reward budget was fixed. + bank.freeze(); + let recalculated_ceiling = + bank.calculate_epoch_inflation_rewards(bank.capitalization(), bank.epoch()); + assert!( + recorded_payout > recalculated_ceiling, + "the test must reproduce a payout above the post-burn ceiling: \ + recorded_payout={recorded_payout}, recalculated_ceiling={recalculated_ceiling}" + ); + + let bank = Bank::new_from_parent( + bank, + SlotLeader::default(), + SLOTS_PER_EPOCH.saturating_mul(2), + ); + assert_eq!(bank.epoch(), 2); + + let epoch_rewards = bank.get_epoch_rewards_sysvar(); + let EpochRewardStatus::Active(EpochRewardPhase::Calculation(calculation_status)) = + &bank.epoch_reward_status + else { + panic!("{:?} not active calculation", bank.epoch_reward_status); + }; + let stake_rewards = calculation_status + .all_stake_rewards + .enumerated_rewards_iter() + .map(|(_index, reward)| reward.inflation.stake_reward) + .sum::(); + assert_eq!(epoch_rewards.total_rewards, recorded_budget); + assert_eq!( + epoch_rewards.distributed_rewards + stake_rewards, + recorded_payout, + "every recorded reward lamport must still be paid" + ); + } + #[test] fn test_alpenglow_reward_epoch_delegated_stakes_account_is_bounded() { let num_validators = crate::bank::MAX_ALPENGLOW_VOTE_ACCOUNTS + 1; diff --git a/runtime/src/bank_forks.rs b/runtime/src/bank_forks.rs index 2da03dede42..e5e9c601992 100644 --- a/runtime/src/bank_forks.rs +++ b/runtime/src/bank_forks.rs @@ -768,6 +768,7 @@ mod tests { super::*, crate::{ bank::test_utils::update_vote_account_timestamp, + block_component_processor::vote_reward::epoch_inflation_account_state::EpochInflationAccountState, genesis_utils::{ GenesisConfigInfo, create_genesis_config, create_genesis_config_with_leader, }, @@ -1000,6 +1001,7 @@ mod tests { genesis_config .accounts .insert(*GENESIS_CERTIFICATE_ACCOUNT, cert_account); + EpochInflationAccountState::insert_into_genesis_config(&mut genesis_config); } let mut feature_set = FeatureSet::default(); diff --git a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs index 0a4fab1e4ee..8389282747c 100644 --- a/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs +++ b/runtime/src/block_component_processor/vote_reward/epoch_inflation_account_state.rs @@ -98,6 +98,12 @@ impl EpochInflationAccountState { .and_then(|acct| wincode::deserialize(acct.data()).ok()) } + /// Returns the epoch-start inflation rewards recorded for `epoch`. + pub(crate) fn inflation_rewards_for_epoch(self, epoch: Epoch) -> Option { + self.get_epoch_state(epoch) + .map(|state| state.max_possible_validator_reward) + } + /// Serializes and updates [`Self`] into the accounts in the [`Bank`]. fn set_state(&self, bank: &Bank) { let data = wincode::serialize(&self).unwrap(); From 406ef5a245c65f5d10fdbdd86d47730816673cb0 Mon Sep 17 00:00:00 2001 From: kirill lykov Date: Fri, 17 Jul 2026 17:56:56 +0200 Subject: [PATCH 17/30] Update rpc/std to use new api of tpu-client-next (#13900) Update rpc/sts to use new api of tpu-client-next --- banks-server/src/banks_server.rs | 15 +- rpc/src/rpc.rs | 15 +- rpc/src/rpc_service.rs | 31 ++- .../src/send_transaction_service.rs | 81 +++--- send-transaction-service/src/test_utils.rs | 15 +- .../src/transaction_client.rs | 232 +++++++----------- 6 files changed, 176 insertions(+), 213 deletions(-) diff --git a/banks-server/src/banks_server.rs b/banks-server/src/banks_server.rs index 22960597413..5702f1d61fb 100644 --- a/banks-server/src/banks_server.rs +++ b/banks-server/src/banks_server.rs @@ -23,7 +23,9 @@ use { solana_send_transaction_service::{ send_transaction_service::{Config, SendTransactionService, TransactionInfo}, tpu_info::NullTpuInfo, - transaction_client::TpuClientNextClient, + transaction_client::{ + TpuSender, create_client as create_tpu_client, create_leader_updater, + }, }, solana_signature::Signature, solana_transaction::{ @@ -439,7 +441,7 @@ fn create_client( maybe_runtime: Option, my_tpu_address: SocketAddr, exit: Arc, -) -> TpuClientNextClient { +) -> TpuSender { let runtime_handle = maybe_runtime.unwrap_or_else(|| { Handle::try_current().expect("runtime handle not provided, and not inside Tokio runtime") }); @@ -465,16 +467,17 @@ fn create_client( }); let leader_forward_count = 0; - TpuClientNextClient::new::( + let leader_updater = create_leader_updater::(None, my_tpu_address, None); + let (tpu_sender, _client) = create_tpu_client( runtime_handle, - my_tpu_address, - None, - None, + leader_updater, leader_forward_count, None, bind_socket, cancel, ) + .expect("Should be able to create TPU client."); + tpu_sender } pub async fn start_tcp_server( diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index e6927478af2..c2813424c37 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -479,12 +479,13 @@ impl JsonRpcRequestProcessor { .. } = config; let runtime = service_runtime(rpc_threads, rpc_blocking_threads, rpc_niceness_adj); - let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); + let (tpu_sender, _client) = + create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( bank_forks.clone(), transaction_receiver, - client, + tpu_sender, SendTransactionServiceConfig { retry_rate_ms: 1_000, leader_forward_count: 1, @@ -6978,11 +6979,12 @@ pub mod tests { runtime.clone(), ); - let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); + let (tpu_sender, _client) = + create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); SendTransactionService::new( bank_forks.clone(), receiver, - client, + tpu_sender, SendTransactionServiceConfig { retry_rate_ms: 1_000, leader_forward_count: 1, @@ -7270,7 +7272,8 @@ pub mod tests { .. } = config; let runtime = service_runtime(rpc_threads, rpc_blocking_threads, rpc_niceness_adj); - let client = create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); + let (tpu_sender, _client) = + create_client_for_tests(runtime.handle().clone(), my_tpu_address, None, 1); let (request_processor, receiver) = JsonRpcRequestProcessor::new( config, None, @@ -7294,7 +7297,7 @@ pub mod tests { SendTransactionService::new( bank_forks, receiver, - client, + tpu_sender, SendTransactionServiceConfig { retry_rate_ms: 1_000, leader_forward_count: 1, diff --git a/rpc/src/rpc_service.rs b/rpc/src/rpc_service.rs index 491e3fa4805..d447dbf075c 100644 --- a/rpc/src/rpc_service.rs +++ b/rpc/src/rpc_service.rs @@ -41,7 +41,7 @@ use { }, solana_send_transaction_service::{ send_transaction_service::{self, SendTransactionService}, - transaction_client::{TpuClientNextClient, TransactionClient}, + transaction_client::{TpuClient, TpuSender, create_client, create_leader_updater}, }, solana_storage_bigtable::CredentialType, solana_tls_utils::NotifyKeyUpdate, @@ -530,16 +530,19 @@ impl JsonRpcService { "Invalid {:?} socket address for TPU", Protocol::QUIC ))?; - let client = TpuClientNextClient::new( - client_runtime, + let leader_updater = create_leader_updater( + leader_info, my_tpu_address, config.send_transaction_service_config.tpu_peers.clone(), - leader_info, + ); + let (tpu_sender, client) = create_client( + client_runtime, + leader_updater, config.send_transaction_service_config.leader_forward_count, Some(identity_keypair), tpu_client_socket, cancel, - ); + )?; let json_rpc_service = Self::new( config.rpc_addr, @@ -558,6 +561,7 @@ impl JsonRpcService { config.send_transaction_service_config, config.max_slots, config.leader_schedule_cache, + tpu_sender, client, config.max_complete_transaction_status_slot, config.prioritization_fee_cache, @@ -567,14 +571,7 @@ impl JsonRpcService { } #[allow(clippy::too_many_arguments)] - fn new< - Client: TransactionClient - + NotifyKeyUpdate - + Clone - + std::marker::Send - + std::marker::Sync - + 'static, - >( + fn new( rpc_addr: SocketAddr, config: JsonRpcConfig, snapshot_config: Option, @@ -591,7 +588,8 @@ impl JsonRpcService { send_transaction_service_config: send_transaction_service::Config, max_slots: Arc, leader_schedule_cache: Arc, - client: Client, + tpu_sender: TpuSender, + client: TpuClient, max_complete_transaction_status_slot: Arc, prioritization_fee_cache: Option>, runtime: Arc, @@ -691,7 +689,7 @@ impl JsonRpcService { let _send_transaction_service = Arc::new(SendTransactionService::new( bank_forks.clone(), receiver, - client.clone(), + tpu_sender, send_transaction_service_config, exit, )); @@ -887,7 +885,7 @@ mod tests { ..send_transaction_service::Config::default() }; - let client = create_client_for_tests( + let (tpu_sender, client) = create_client_for_tests( runtime.handle().clone(), tpu_address, send_transaction_service_config.tpu_peers.clone(), @@ -910,6 +908,7 @@ mod tests { send_transaction_service_config, Arc::new(MaxSlots::default()), Arc::new(LeaderScheduleCache::default()), + tpu_sender, client, Arc::new(AtomicU64::default()), Some(Arc::new(PrioritizationFeeCache::default())), diff --git a/send-transaction-service/src/send_transaction_service.rs b/send-transaction-service/src/send_transaction_service.rs index 2333c566fb4..2a25ebc3c71 100644 --- a/send-transaction-service/src/send_transaction_service.rs +++ b/send-transaction-service/src/send_transaction_service.rs @@ -3,7 +3,7 @@ use { send_transaction_service_stats::{ SendTransactionServiceStats, SendTransactionServiceStatsReport, }, - transaction_client::TransactionClient, + transaction_client::TpuSender, }, crossbeam_channel::{Receiver, RecvTimeoutError}, itertools::Itertools, @@ -157,10 +157,10 @@ impl Default for Config { pub const MAX_RETRY_SLEEP_MS: u64 = 1000; impl SendTransactionService { - pub fn new( + pub fn new( bank_forks: Arc>, receiver: Receiver, - client: Client, + tpu_sender: TpuSender, config: Config, exit: Arc, ) -> Self { @@ -170,7 +170,7 @@ impl SendTransactionService { let receive_txn_thread = Self::receive_txn_thread( receiver, - client.clone(), + tpu_sender.clone(), retry_transactions.clone(), config.clone(), stats_report.clone(), @@ -179,7 +179,7 @@ impl SendTransactionService { let retry_thread = Self::retry_thread( bank_forks, - client, + tpu_sender, retry_transactions, config, stats_report, @@ -193,9 +193,9 @@ impl SendTransactionService { } /// Thread responsible for receiving transactions from RPC clients. - fn receive_txn_thread( + fn receive_txn_thread( receiver: Receiver, - client: Client, + tpu_sender: TpuSender, retry_transactions: Arc>>, Config { batch_send_rate_ms, @@ -260,8 +260,8 @@ impl SendTransactionService { let wire_transactions = transactions .values() .map(|transaction_info| transaction_info.wire_transaction.clone()) - .collect::>>(); - client.send_transactions_in_batch(wire_transactions, stats); + .collect(); + tpu_sender.send_transactions_in_batch(wire_transactions, stats); let last_sent_time = Instant::now(); { // take a lock of retry_transactions and move the batch to the retry set. @@ -307,9 +307,9 @@ impl SendTransactionService { } /// Thread responsible for retrying transactions - fn retry_thread( + fn retry_thread( bank_forks: Arc>, - client: Client, + tpu_sender: TpuSender, retry_transactions: Arc>>, config: Config, stats_report: Arc, @@ -344,7 +344,7 @@ impl SendTransactionService { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, stats, ); @@ -367,11 +367,11 @@ impl SendTransactionService { } /// Retry transactions sent before. - fn process_transactions( + fn process_transactions( working_bank: &Bank, root_bank: &Bank, transactions: &mut HashMap, - client: &Client, + tpu_sender: &TpuSender, &Config { retry_rate_ms, service_max_retries, @@ -509,7 +509,7 @@ impl SendTransactionService { let iter = wire_transactions.chunks(batch_size); for chunk in &iter { let chunk = chunk.collect(); - client.send_transactions_in_batch(chunk, stats); + tpu_sender.send_transactions_in_batch(chunk, stats); } } @@ -556,13 +556,13 @@ mod test { let bank_forks = BankForks::new_rw_arc(bank); let (sender, receiver) = bounded(1024); - let client = + let (tpu_sender, client) = create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let send_transaction_service = SendTransactionService::new( bank_forks, receiver, - client.clone(), + tpu_sender.clone(), Config { retry_rate_ms: 1000, ..Config::default() @@ -572,7 +572,7 @@ mod test { drop(sender); send_transaction_service.join().unwrap(); - client.cancel(); + client.shutdown().await.unwrap(); } #[tokio::test(flavor = "multi_thread")] @@ -594,12 +594,12 @@ mod test { }; let exit = Arc::new(AtomicBool::new(false)); - let client = + let (tpu_sender, client) = create_client_for_tests(Handle::current(), "127.0.0.1:0".parse().unwrap(), None, 1); let _send_transaction_service = SendTransactionService::new( bank_forks, receiver, - client.clone(), + tpu_sender.clone(), Config { retry_rate_ms: 1000, ..Config::default() @@ -609,9 +609,12 @@ mod test { sender.send(dummy_tx_info()).unwrap(); + let runtime_handle = Handle::current(); thread::spawn(move || { exit.store(true, Ordering::Relaxed); - client.cancel(); + runtime_handle.spawn(async move { + let _ = client.shutdown().await; + }); }); let mut option = Ok(()); @@ -703,7 +706,7 @@ mod test { ), ); - let client = create_client_for_tests( + let (tpu_sender, client) = create_client_for_tests( Handle::current(), "127.0.0.1:0".parse().unwrap(), config.tpu_peers.clone(), @@ -713,7 +716,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -744,7 +747,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -775,7 +778,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -806,7 +809,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -839,7 +842,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -884,7 +887,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -897,7 +900,7 @@ mod test { ..ProcessTransactionsResult::default() } ); - client.cancel(); + client.shutdown().await.unwrap(); } #[tokio::test(flavor = "multi_thread")] @@ -989,7 +992,7 @@ mod test { ), ); let stats = SendTransactionServiceStats::default(); - let client = create_client_for_tests( + let (tpu_sender, client) = create_client_for_tests( Handle::current(), "127.0.0.1:0".parse().unwrap(), config.tpu_peers.clone(), @@ -999,7 +1002,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1029,7 +1032,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1061,7 +1064,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1091,7 +1094,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1122,7 +1125,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1153,7 +1156,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1186,7 +1189,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1214,7 +1217,7 @@ mod test { &working_bank, &root_bank, &mut transactions, - &client, + &tpu_sender, &config, &stats, ); @@ -1226,6 +1229,6 @@ mod test { ..ProcessTransactionsResult::default() } ); - client.cancel(); + client.shutdown().await.unwrap(); } } diff --git a/send-transaction-service/src/test_utils.rs b/send-transaction-service/src/test_utils.rs index 37a9e607ed6..eb8c82028d4 100644 --- a/send-transaction-service/src/test_utils.rs +++ b/send-transaction-service/src/test_utils.rs @@ -2,7 +2,10 @@ //! with the client type. use { - crate::{tpu_info::NullTpuInfo, transaction_client::TpuClientNextClient}, + crate::{ + tpu_info::NullTpuInfo, + transaction_client::{TpuClient, TpuSender, create_client, create_leader_updater}, + }, solana_net_utils::sockets::{bind_to, localhost_port_range_for_tests}, std::net::{IpAddr, Ipv4Addr, SocketAddr}, tokio::runtime::Handle, @@ -14,18 +17,18 @@ pub fn create_client_for_tests( my_tpu_address: SocketAddr, tpu_peers: Option>, leader_forward_count: u64, -) -> TpuClientNextClient { +) -> (TpuSender, TpuClient) { let port_range = localhost_port_range_for_tests(); let bind_socket = bind_to(IpAddr::V4(Ipv4Addr::LOCALHOST), port_range.0) .expect("Should be able to open UdpSocket for tests."); - TpuClientNextClient::new::( + let leader_updater = create_leader_updater::(None, my_tpu_address, tpu_peers); + create_client( runtime_handle, - my_tpu_address, - tpu_peers, - None, + leader_updater, leader_forward_count, None, bind_socket, CancellationToken::new(), ) + .expect("Should be able to create TPU client for tests.") } diff --git a/send-transaction-service/src/transaction_client.rs b/send-transaction-service/src/transaction_client.rs index f6b24dd9bd1..6dc45995abb 100644 --- a/send-transaction-service/src/transaction_client.rs +++ b/send-transaction-service/src/transaction_client.rs @@ -6,12 +6,7 @@ use { solana_measure::measure::Measure, solana_tls_utils::NotifyKeyUpdate, solana_tpu_client_next::{ - ConnectionWorkersScheduler, - connection_workers_scheduler::{ - BindTarget, ConnectionWorkersSchedulerConfig, Fanout, StakeIdentity, - }, - leader_updater::LeaderUpdater, - transaction_batch::TransactionBatch, + Client, ClientBuilder, ClientError, TransactionSender, leader_updater::LeaderUpdater, }, std::{ net::{SocketAddr, UdpSocket}, @@ -19,13 +14,7 @@ use { sync::atomic::Ordering, time::{Duration, Instant}, }, - tokio::{ - runtime::Handle, - sync::{ - mpsc::{self}, - watch, - }, - }, + tokio::runtime::Handle, tokio_util::sync::CancellationToken, }; @@ -37,16 +26,43 @@ const MAX_CONNECTIONS: NonZeroUsize = NonZeroUsize::new(1024).unwrap(); pub trait TpuInfoWithSendStatic: TpuInfo + std::marker::Send + 'static {} impl TpuInfoWithSendStatic for T where T: TpuInfo + std::marker::Send + 'static {} -pub trait TransactionClient { - fn send_transactions_in_batch( +/// The leader info refresh rate. +pub const LEADER_INFO_REFRESH_RATE_MS: u64 = 1000; + +const METRICS_REPORTING_INTERVAL: Duration = Duration::from_secs(3); + +/// A synchronous adapter that schedules transaction batches on a Tokio runtime. +#[derive(Clone)] +pub struct TpuSender { + runtime_handle: Handle, + sender: TransactionSender, +} + +impl TpuSender { + pub fn send_transactions_in_batch( &self, wire_transactions: Vec>, stats: &SendTransactionServiceStats, - ); -} + ) { + let mut measure = Measure::start("send-us"); + self.runtime_handle.spawn({ + let sender = self.sender.clone(); + async move { + if sender + .send_transactions_in_batch(wire_transactions) + .await + .is_err() + { + warn!("Failed to send transactions to channel: it is closed."); + } + } + }); -/// The leader info refresh rate. -pub const LEADER_INFO_REFRESH_RATE_MS: u64 = 1000; + measure.stop(); + stats.send_us.fetch_add(measure.as_us(), Ordering::Relaxed); + stats.send_attempt_count.fetch_add(1, Ordering::Relaxed); + } +} /// A struct responsible for holding up-to-date leader information /// used for sending transactions. @@ -95,143 +111,79 @@ where } } -/// `TpuClientNextClient` provides an interface for managing the -/// [`ConnectionWorkersScheduler`]. -/// -/// It allows: -/// * Create and initializes the scheduler with runtime configurations, -/// * Send transactions to the connection scheduler, -/// * Update the validator identity keypair and propagate the changes to the -/// scheduler. Most of the complexity of this structure arises from this -/// functionality. -#[derive(Clone)] -pub struct TpuClientNextClient { - runtime_handle: Handle, - sender: mpsc::Sender, - update_certificate_sender: watch::Sender>, - #[cfg(any(test, feature = "dev-context-only-utils"))] - cancel: CancellationToken, -} +pub struct TpuClient(Client); -const METRICS_REPORTING_INTERVAL: Duration = Duration::from_secs(3); -impl TpuClientNextClient { - pub fn new( - runtime_handle: Handle, - my_tpu_address: SocketAddr, - tpu_peers: Option>, - leader_info: Option, - leader_forward_count: u64, - identity: Option<&Keypair>, - bind_socket: UdpSocket, - cancel: CancellationToken, - ) -> Self - where - T: TpuInfoWithSendStatic + Clone, - { - // The channel size represents 8s worth of transactions at a rate of - // 1000 tps, assuming batch size is 64. - let (sender, receiver) = mpsc::channel(128); - - let (update_certificate_sender, update_certificate_receiver) = watch::channel(None); - - let leader_info_provider = CurrentLeaderInfo::new(leader_info); - let leader_updater: SendTransactionServiceLeaderUpdater = - SendTransactionServiceLeaderUpdater { - leader_info_provider, - my_tpu_address, - tpu_peers, - }; - let config = Self::create_config(bind_socket, identity, leader_forward_count as usize); - - let scheduler = ConnectionWorkersScheduler::new( - Box::new(leader_updater), - receiver, - update_certificate_receiver, - cancel.clone(), - ); - // leaking handle to this task, as it will run until the cancel signal is received - runtime_handle.spawn(scheduler.get_stats().report_to_influxdb( - "send-transaction-service-TPU-client", - METRICS_REPORTING_INTERVAL, - cancel.clone(), - )); - let _handle = runtime_handle.spawn(scheduler.run(config)); - Self { - runtime_handle, - sender, - update_certificate_sender, - #[cfg(any(test, feature = "dev-context-only-utils"))] - cancel, - } - } - - fn create_config( - bind_socket: UdpSocket, - stake_identity: Option<&Keypair>, - leader_forward_count: usize, - ) -> ConnectionWorkersSchedulerConfig { - ConnectionWorkersSchedulerConfig { - bind: BindTarget::Socket(bind_socket), - stake_identity: stake_identity.map(StakeIdentity::new), - num_connections: MAX_CONNECTIONS, - skip_check_transaction_age: true, - // experimentally found parameter values - worker_channel_size: 64, - max_reconnect_attempts: 4, - // We open connection to one more leader in advance, which time-wise means ~1.6s - leaders_fanout: Fanout { - connect: leader_forward_count + 1, - send: leader_forward_count, - }, - override_initial_congestion_window: None, - } - } - - #[cfg(any(test, feature = "dev-context-only-utils"))] - pub fn cancel(&self) { - self.cancel.cancel(); +impl TpuClient { + pub async fn shutdown(self) -> Result<(), ClientError> { + self.0.shutdown().await } } -impl NotifyKeyUpdate for TpuClientNextClient { - fn update_key(&self, identity: &Keypair) -> Result<(), Box> { - let stake_identity = StakeIdentity::new(identity); - self.update_certificate_sender - .send(Some(stake_identity)) - .map_err(|e| Box::new(e) as Box) +impl NotifyKeyUpdate for TpuClient { + fn update_key(&self, identity: &Keypair) -> Result<(), Box> { + self.0 + .update_identity(identity) + .map_err(|e| Box::new(e) as Box) } } -impl TransactionClient for TpuClientNextClient { - fn send_transactions_in_batch( - &self, - wire_transactions: Vec>, - stats: &SendTransactionServiceStats, - ) { - let mut measure = Measure::start("send-us"); - self.runtime_handle.spawn({ - let sender = self.sender.clone(); - async move { - let res = sender.send(TransactionBatch::new(wire_transactions)).await; - if res.is_err() { - warn!("Failed to send transaction to channel: it is closed."); - } - } +pub fn create_client( + runtime_handle: Handle, + leader_updater: Box, + leader_forward_count: u64, + identity: Option<&Keypair>, + bind_socket: UdpSocket, + cancel: CancellationToken, +) -> Result<(TpuSender, TpuClient), String> { + let sender_runtime_handle = runtime_handle.clone(); + let client_builder = ClientBuilder::new(leader_updater) + .runtime_handle(runtime_handle) + .bind_socket(bind_socket) + .leader_send_fanout(leader_forward_count as usize) + .identity(identity) + .max_cache_size(MAX_CONNECTIONS) + .cancel_token(cancel) + .worker_channel_size(64) + .sender_channel_size(128) + .max_reconnect_attempts(4) + .metric_reporter(|stats, cancel| async move { + stats + .report_to_influxdb( + "send-transaction-service-TPU-client", + METRICS_REPORTING_INTERVAL, + cancel, + ) + .await; }); - measure.stop(); - stats.send_us.fetch_add(measure.as_us(), Ordering::Relaxed); - stats.send_attempt_count.fetch_add(1, Ordering::Relaxed); - } + let (sender, client) = client_builder.build().map_err(|e| e.to_string())?; + Ok(( + TpuSender { + runtime_handle: sender_runtime_handle, + sender, + }, + TpuClient(client), + )) } -#[derive(Clone)] -pub struct SendTransactionServiceLeaderUpdater { +struct SendTransactionServiceLeaderUpdater { leader_info_provider: CurrentLeaderInfo, my_tpu_address: SocketAddr, tpu_peers: Option>, } +pub fn create_leader_updater( + leader_info: Option, + my_tpu_address: SocketAddr, + tpu_peers: Option>, +) -> Box { + Box::new(SendTransactionServiceLeaderUpdater { + leader_info_provider: CurrentLeaderInfo::new(leader_info), + my_tpu_address, + tpu_peers, + }) +} + #[async_trait] impl LeaderUpdater for SendTransactionServiceLeaderUpdater where From e0ab53f73757c915759fd44380d7369868cca5e8 Mon Sep 17 00:00:00 2001 From: hana <81144685+2501babe@users.noreply.github.com> Date: Fri, 17 Jul 2026 09:30:23 -0700 Subject: [PATCH 18/30] core: filter duplicate nonce account transactions (#13689) * core: filter duplicate nonce account transactions * refactor `handle_packet_batch_message()` to fully handle each transaction without batching * add `nonces_in_use` to `TransactionStateContainer` which tracks nonce account use by received transactions * immediately after parsing a nonce transaction, drop it if lower priority than a conflicting one, or if a conflicting one is in-flight * if an incoming higher-priority nonce transaction fully verifies, drop the conflicting queued one * various tests that `nonces_in_use` stays in sync with `id_to_transaction_state` * final cleanup * cut EXTRA_CAPACITY to 1 since we removed ingress batching * simplify error helpers * push individually in test --- .../receive_and_buffer.rs | 864 ++++++++++++++---- .../scheduler_controller.rs | 109 +++ .../scheduler_metrics.rs | 8 + .../transaction_state.rs | 16 +- .../transaction_state_container.rs | 106 ++- runtime/src/bank/check_transactions.rs | 153 +++- runtime/src/bank/tests.rs | 1 + 7 files changed, 1033 insertions(+), 224 deletions(-) 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 3782a25e72d..b4cbc320b2d 100644 --- a/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs +++ b/core/src/banking_stage/transaction_scheduler/receive_and_buffer.rs @@ -3,8 +3,7 @@ use { transaction_priority_id::TransactionPriorityId, transaction_state::TransactionState, transaction_state_container::{ - EXTRA_CAPACITY, SharedBytes, StateContainer, TransactionViewState, - TransactionViewStateContainer, + SharedBytes, StateContainer, TransactionViewState, TransactionViewStateContainer, }, }, crate::{ @@ -19,7 +18,6 @@ use { transaction_data::TransactionData, transaction_version::TransactionVersion, transaction_view::SanitizedTransactionView, }, - arrayvec::ArrayVec, core::time::Duration, crossbeam_channel::{RecvTimeoutError, TryRecvError}, solana_accounts_db::account_locks::validate_account_locks, @@ -46,6 +44,7 @@ use { pub(crate) struct DisconnectedError; /// Stats/metrics returned by `receive_and_buffer_packets`. +#[derive(Debug, Default)] pub(crate) struct ReceivingStats { pub num_received: usize, /// Count of packets that passed sigverify but were dropped @@ -61,14 +60,45 @@ pub(crate) struct ReceivingStats { pub num_dropped_on_fee_payer: usize, pub num_dropped_on_filter_key: usize, pub num_dropped_on_capacity: usize, + pub num_dropped_on_nonce_dedup: usize, pub num_buffered: usize, + pub num_evicted_on_nonce_dedup: usize, pub receive_time_us: u64, pub buffer_time_us: u64, } impl ReceivingStats { + fn add_packet_handling_error(&mut self, err: &PacketHandlingError) { + match err { + PacketHandlingError::Sanitization | PacketHandlingError::ALTResolution => { + self.num_dropped_on_parsing_and_sanitization += 1; + } + PacketHandlingError::LockValidation => { + self.num_dropped_on_lock_validation += 1; + } + PacketHandlingError::ComputeBudget => { + self.num_dropped_on_compute_budget += 1; + } + PacketHandlingError::FilterKey => { + self.num_dropped_on_filter_key += 1; + } + } + } + + fn add_transaction_error(&mut self, err: &TransactionError) { + match err { + TransactionError::BlockhashNotFound => { + self.num_dropped_on_age += 1; + } + TransactionError::AlreadyProcessed => { + self.num_dropped_on_already_processed += 1; + } + _ => {} + } + } + fn accumulate(&mut self, other: ReceivingStats) { self.num_received += other.num_received; self.num_dropped_without_parsing += other.num_dropped_without_parsing; @@ -81,7 +111,9 @@ impl ReceivingStats { self.num_dropped_on_fee_payer += other.num_dropped_on_fee_payer; self.num_dropped_on_filter_key += other.num_dropped_on_filter_key; self.num_dropped_on_capacity += other.num_dropped_on_capacity; + self.num_dropped_on_nonce_dedup += other.num_dropped_on_nonce_dedup; self.num_buffered += other.num_buffered; + self.num_evicted_on_nonce_dedup += other.num_evicted_on_nonce_dedup; self.receive_time_us += other.receive_time_us; self.buffer_time_us += other.buffer_time_us; @@ -128,21 +160,7 @@ impl ReceiveAndBuffer for TransactionViewReceiveAndBuffer { let start = Instant::now(); let mut received_message = false; - let mut stats = ReceivingStats { - num_received: 0, - num_dropped_without_parsing: 0, - num_dropped_on_parsing_and_sanitization: 0, - num_dropped_on_lock_validation: 0, - num_dropped_on_compute_budget: 0, - num_dropped_on_age: 0, - num_dropped_on_already_processed: 0, - num_dropped_on_fee_payer: 0, - num_dropped_on_filter_key: 0, - num_dropped_on_capacity: 0, - num_buffered: 0, - receive_time_us: 0, - buffer_time_us: 0, - }; + let mut stats = ReceivingStats::default(); // If not leader/unknown, do a blocking-receive initially. This lets // the thread sleep until a message is received, or until the timeout. @@ -207,21 +225,7 @@ impl ReceiveAndBuffer for TransactionViewReceiveAndBuffer { } } - Ok(ReceivingStats { - num_received: stats.num_received, - num_dropped_without_parsing: stats.num_dropped_without_parsing, - num_dropped_on_parsing_and_sanitization: stats.num_dropped_on_parsing_and_sanitization, - num_dropped_on_lock_validation: stats.num_dropped_on_lock_validation, - num_dropped_on_compute_budget: stats.num_dropped_on_compute_budget, - num_dropped_on_age: stats.num_dropped_on_age, - num_dropped_on_already_processed: stats.num_dropped_on_already_processed, - num_dropped_on_fee_payer: stats.num_dropped_on_fee_payer, - num_dropped_on_filter_key: stats.num_dropped_on_filter_key, - num_dropped_on_capacity: stats.num_dropped_on_capacity, - num_buffered: stats.num_buffered, - receive_time_us: stats.receive_time_us, - buffer_time_us: stats.buffer_time_us, - }) + Ok(stats) } } @@ -250,98 +254,17 @@ impl TransactionViewReceiveAndBuffer { let sanitize_config = sanitize_config(); let transaction_account_lock_limit = working_bank.get_transaction_account_lock_limit(); - // Create temporary batches of transactions to be age-checked. - let mut transaction_priority_ids = ArrayVec::<_, EXTRA_CAPACITY>::new(); - let lock_results: [_; EXTRA_CAPACITY] = core::array::from_fn(|_| Ok(())); let mut error_counters = TransactionErrorMetrics::default(); - let mut num_dropped_on_age = 0; - let mut num_dropped_on_already_processed = 0; - let mut num_dropped_on_fee_payer = 0; - let mut num_dropped_on_filter_key = 0; - let mut num_dropped_on_capacity = 0; - let mut num_buffered = 0; - - let mut check_and_push_to_queue = |container: &mut TransactionViewStateContainer, - transaction_priority_ids: &mut ArrayVec< - TransactionPriorityId, - EXTRA_CAPACITY, - >| { - // Temporary scope so that transaction references are immediately - // dropped and transactions not passing - let mut check_results = { - let mut transactions = ArrayVec::<_, EXTRA_CAPACITY>::new(); - transactions.extend(transaction_priority_ids.iter().map(|priority_id| { - container - .get_transaction(priority_id.id) - .expect("transaction must exist") - })); - working_bank.check_transactions_without_status_cache::>( - &transactions, - &lock_results[..transactions.len()], - working_bank.max_processing_age(), - true, - &mut error_counters, - ) - }; - - // Remove errored transactions - for (result, priority_id) in check_results - .iter_mut() - .zip(transaction_priority_ids.iter()) - { - if let Err(err) = result { - match err { - TransactionError::BlockhashNotFound => { - num_dropped_on_age += 1; - } - TransactionError::AlreadyProcessed => { - num_dropped_on_already_processed += 1; - } - _ => {} - } - container.remove_by_id(priority_id.id); - continue; - } - let transaction = container - .get_transaction(priority_id.id) - .expect("transaction must exist"); - if let Err(err) = Consumer::check_fee_payer_unlocked( - working_bank, - transaction, - &mut error_counters, - ) { - *result = Err(err); - num_dropped_on_fee_payer += 1; - container.remove_by_id(priority_id.id); - continue; - } - - num_buffered += 1; - } - // Push non-errored transaction into queue. - num_dropped_on_capacity += container.push_ids_into_queue( - check_results - .into_iter() - .zip(transaction_priority_ids.drain(..)) - .filter(|(r, _)| r.is_ok()) - .map(|(_, id)| id), - ); - }; - - let mut num_received = 0; - let mut num_dropped_without_parsing = 0; - let mut num_dropped_on_parsing_and_sanitization = 0; - let mut num_dropped_on_lock_validation = 0; - let mut num_dropped_on_compute_budget = 0; + let mut receiving_stats = ReceivingStats::default(); for packet in packet_batch_message.iter() { let Some(packet_data) = packet.data(..) else { continue; }; - num_received += 1; + receiving_stats.num_received += 1; if !should_parse { - num_dropped_without_parsing += 1; + receiving_stats.num_dropped_without_parsing += 1; continue; } @@ -356,59 +279,107 @@ impl TransactionViewReceiveAndBuffer { &sanitize_config, &self.filter_keys, ) { + // Parent giving us state means successful parse, ALTs resolved, no obvious static issues. Ok(state) => Ok(state), - Err( - PacketHandlingError::Sanitization | PacketHandlingError::ALTResolution, - ) => { - num_dropped_on_parsing_and_sanitization += 1; - Err(()) - } - Err(PacketHandlingError::LockValidation) => { - num_dropped_on_lock_validation += 1; - Err(()) - } - Err(PacketHandlingError::ComputeBudget) => { - num_dropped_on_compute_budget += 1; - Err(()) - } - Err(PacketHandlingError::FilterKey) => { - num_dropped_on_filter_key += 1; + + // Parsing or some other static checks failed. + Err(ref err) => { + receiving_stats.add_packet_handling_error(err); Err(()) } } }) { - let priority = container + let (priority, raw_nonce_address) = container .get_mut_transaction_state(transaction_id) - .expect("transaction must exist") - .priority(); - transaction_priority_ids.push(TransactionPriorityId::new(priority, transaction_id)); + .map(|state| { + ( + state.priority(), + state.transaction().get_durable_nonce().cloned(), + ) + }) + .expect("transaction must exist"); + let priority_id = TransactionPriorityId::new(priority, transaction_id); + + // When we first receive a transaction, we drop it if a) it looks nonce-like, AND + // b) there is a higher-priority nonce transaction using the same nonce in the queue + // or any in-flight nonce transaction using the same nonce. This means we discard + // blockhash transactions structured like nonce transactions; this is acceptable because + // they would fail after the earlier nonce transaction is processed, and it allows us to + // prefilter without loading from accounts-db. + let drop_incoming_nonce_tx = raw_nonce_address + .and_then(|address| container.get_nonce_transaction_priority_id(&address)) + .is_some_and(|existing| { + existing.priority >= priority || !container.is_queued(existing) + }); + + if drop_incoming_nonce_tx { + receiving_stats.num_dropped_on_nonce_dedup += 1; + container.remove_by_id(transaction_id); + continue; + } - // If at capacity, run checks and remove invalid transactions. - if transaction_priority_ids.len() == EXTRA_CAPACITY { - check_and_push_to_queue(container, &mut transaction_priority_ids); + let transaction = container + .get_transaction(transaction_id) + .expect("transaction must exist"); + + // Check blockhash transaction age is ok, or nonce transaction has a valid nonce. + // Only a fully validated nonce address can be used for priority queue eviction. + let validated_nonce_address = match working_bank + .check_transaction_without_status_cache( + transaction, + working_bank.max_processing_age(), + &mut error_counters, + ) { + // Valid nonce transaction + Ok(Some(nonce_address)) => Some(nonce_address), + + // Valid blockhash transaction + Ok(None) => None, + + // Invalid + Err(ref err) => { + receiving_stats.add_transaction_error(err); + container.remove_by_id(transaction_id); + continue; + } + }; + + // Check the transaction's fee-payer validates. + if let Err(_err) = Consumer::check_fee_payer_unlocked( + working_bank, + transaction, + &mut error_counters, + ) { + receiving_stats.num_dropped_on_fee_payer += 1; + container.remove_by_id(transaction_id); + continue; + }; + + // Now, if this is a nonce transaction, we know it is validated and higher-priority than any + // which may exist in the priority queue. If one is queued, evict it. Regardless, record the + // incoming nonce transaction's nonce as in-use. + if let Some(nonce_address) = validated_nonce_address { + if let Some(existing_nonce_priority_id) = + container.get_nonce_transaction_priority_id(&nonce_address) + { + receiving_stats.num_evicted_on_nonce_dedup += 1; + container.remove_by_id(existing_nonce_priority_id.id); + } + container.set_nonce_transaction_priority_id(&nonce_address, priority_id); } - } - } - // Any remaining packets undergo status/age checks - check_and_push_to_queue(container, &mut transaction_priority_ids); + // Transaction is already fully validated and can be inserted into priority queue. + receiving_stats.num_dropped_on_capacity += + container.push_ids_into_queue(std::iter::once(priority_id)); - ReceivingStats { - num_received, - num_dropped_without_parsing, - num_dropped_on_parsing_and_sanitization, - num_dropped_on_lock_validation, - num_dropped_on_compute_budget, - num_dropped_on_age, - num_dropped_on_already_processed, - num_dropped_on_fee_payer, - num_dropped_on_filter_key, - num_dropped_on_capacity, - num_buffered, - receive_time_us: 0, // receive is outside this function - buffer_time_us: start.elapsed().as_micros() as u64, + receiving_stats.num_buffered += 1; + } } + + // `receive_time_us` is set outside this function + receiving_stats.buffer_time_us = start.elapsed().as_micros() as u64; + receiving_stats } fn try_handle_packet( @@ -548,32 +519,47 @@ mod tests { super::*, crate::banking_stage::tests::create_slow_genesis_config, agave_banking_stage_ingress_types::{ - to_banking_packet_batch, to_single_banking_packet_batch, + BankingPacketBatch, to_banking_packet_batch, to_single_banking_packet_batch, }, - crossbeam_channel::{Receiver, bounded}, + crossbeam_channel::{Receiver, Sender, bounded}, + solana_account::AccountSharedData, + solana_compute_budget_interface::ComputeBudgetInstruction, + solana_fee_calculator::FeeRateGovernor, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::genesis_utils::GenesisConfigInfo, solana_message::{ - AccountMeta, AddressLookupTableAccount, Instruction, VersionedMessage, v0, + AccountMeta, AddressLookupTableAccount, Instruction, Message, VersionedMessage, v0, }, + solana_nonce::{self as nonce, state::DurableNonce}, solana_packet::{Meta, PACKET_DATA_SIZE}, solana_perf::packet::{Packet, PacketBatch, RecycledPacketBatch}, solana_pubkey::Pubkey, solana_runtime::bank_forks::BankForks, + solana_sdk_ids::system_program, solana_signer::Signer, solana_system_interface::instruction as system_instruction, solana_system_transaction::transfer, - solana_transaction::versioned::VersionedTransaction, + solana_transaction::{Transaction, versioned::VersionedTransaction}, std::sync::{Arc, RwLock}, + test_case::test_case, }; fn test_bank_forks() -> (Arc>, Keypair) { + _test_bank_forks(0) + } + + fn test_bank_forks_with_fee() -> (Arc>, Keypair) { + _test_bank_forks(5_000) + } + + fn _test_bank_forks(fee: u64) -> (Arc>, Keypair) { let GenesisConfigInfo { - genesis_config, + mut genesis_config, mint_keypair, .. } = create_slow_genesis_config(u64::MAX); + genesis_config.fee_rate_governor = FeeRateGovernor::new(fee, 0); let (_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); (bank_forks, mint_keypair) @@ -615,13 +601,22 @@ mod tests { // verify container state makes sense: // 1. Number of transactions matches expectation // 2. All transactions IDs in priority queue exist in the map + // 3. Nonce transactions have a matching nonces-in-use entry. + #[track_caller] fn verify_container( container: &mut impl StateContainer, expected_length: usize, ) { let mut actual_length: usize = 0; while let Some(id) = container.pop() { - let Some(_) = container.get_transaction(id.id) else { + if let Some(state) = container.get_mut_transaction_state(id.id) { + if let Some(nonce) = state.nonce_address().cloned() { + assert_eq!( + id, + *container.get_nonce_transaction_priority_id(&nonce).unwrap() + ); + } + } else { panic!( "transaction in queue position {} with id {} must exist.", actual_length, id.id @@ -633,6 +628,19 @@ mod tests { assert_eq!(actual_length, expected_length); } + fn send_transactions(sender: &Sender, transactions: &[Transaction]) { + sender.send(to_banking_packet_batch(transactions)).unwrap(); + } + + fn receive( + receive_and_buffer: &mut TransactionViewReceiveAndBuffer, + container: &mut TransactionViewStateContainer, + ) -> ReceivingStats { + receive_and_buffer + .receive_and_buffer_packets(container, &BufferedPacketsDecision::Hold) + .unwrap() + } + #[test] fn test_calculate_max_age() { let current_slot = 100; @@ -697,7 +705,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -716,7 +726,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -752,7 +764,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -768,7 +782,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -796,7 +812,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -812,7 +830,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -839,7 +859,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -855,7 +877,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -887,7 +911,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -903,7 +929,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 1); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -950,7 +978,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -966,7 +996,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } @@ -998,7 +1030,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -1014,7 +1048,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 1); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 1); } @@ -1172,7 +1208,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -1188,7 +1226,9 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert!(num_dropped_on_capacity > 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, num_transactions); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, TEST_CONTAINER_CAPACITY); } @@ -1252,7 +1292,9 @@ mod tests { num_dropped_on_fee_payer, num_dropped_on_filter_key: _, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = receive_and_buffer @@ -1268,8 +1310,502 @@ mod tests { assert_eq!(num_dropped_on_already_processed, 0); assert_eq!(num_dropped_on_fee_payer, 0); assert_eq!(num_dropped_on_capacity, 0); + assert_eq!(num_dropped_on_nonce_dedup, 0); assert_eq!(num_buffered, 0); + assert_eq!(num_evicted_on_nonce_dedup, 0); verify_container(&mut container, 0); } + + const LOW_FEE: u64 = 1; + const HIGH_FEE: u64 = 1_000_000; + + // sets up a nonce account in the bank for a true nonce transaction + fn create_nonce_identity( + bank_forks: &RwLock, + nonce_authority: &Pubkey, + ) -> (Pubkey, Hash) { + let nonce_pubkey = Pubkey::new_unique(); + let bank = bank_forks.read().unwrap().root_bank(); + let nonce_data = nonce::state::Data::new( + *nonce_authority, + DurableNonce::from_blockhash(&Hash::new_unique()), + 5_000, + ); + let nonce_account = AccountSharedData::new_data( + bank.get_minimum_balance_for_rent_exemption(nonce::state::State::size()), + &nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())), + &system_program::id(), + ) + .unwrap(); + bank.store_account(&nonce_pubkey, &nonce_account); + (nonce_pubkey, nonce_data.blockhash()) + } + + // build a nonce-like transaction, which may be nonce- or blockhash-based, depending + // on the value of `lifetime` + fn create_nonce_transaction( + fee_payer: &Keypair, + nonce_pubkey: &Pubkey, + compute_unit_price: u64, + lifetime: Hash, + ) -> Transaction { + let ixs = [ + system_instruction::advance_nonce_account(nonce_pubkey, &fee_payer.pubkey()), + ComputeBudgetInstruction::set_compute_unit_price(compute_unit_price), + system_instruction::transfer(&fee_payer.pubkey(), &Pubkey::new_unique(), 1), + ]; + let message = Message::new(&ixs, Some(&fee_payer.pubkey())); + Transaction::new(&[fee_payer], message, lifetime) + } + + // adding nonce transactions works normally. different nonces dont conflict, + // removing a nonce transaction removes its nonces-in-use entry + #[test] + fn test_receive_and_buffer_nonce_tracked() { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey1, durable1) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + let (nonce_pubkey2, durable2) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + + send_transactions( + &sender, + &[ + create_nonce_transaction(&mint_keypair, &nonce_pubkey1, LOW_FEE, durable1), + create_nonce_transaction(&mint_keypair, &nonce_pubkey2, HIGH_FEE, durable2), + ], + ); + + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_buffered, 2); + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + + let nonce_entry1 = *container + .get_nonce_transaction_priority_id(&nonce_pubkey1) + .unwrap(); + let nonce_entry2 = *container + .get_nonce_transaction_priority_id(&nonce_pubkey2) + .unwrap(); + + assert!(container.is_queued(&nonce_entry1)); + assert!(container.is_queued(&nonce_entry2)); + + container.remove_by_id(nonce_entry1.id); + assert!(!container.is_queued(&nonce_entry1)); + assert!(container.is_queued(&nonce_entry2)); + assert!( + container + .get_nonce_transaction_priority_id(&nonce_pubkey1) + .is_none() + ); + + container.remove_by_id(nonce_entry2.id); + assert!(!container.is_queued(&nonce_entry1)); + assert!(!container.is_queued(&nonce_entry2)); + assert!( + container + .get_nonce_transaction_priority_id(&nonce_pubkey2) + .is_none() + ); + + verify_container(&mut container, 0); + } + + // a higher priority incoming nonce transaction evicts the existing transaction, + // a lower or equal priority incoming nonce transaction is dropped + #[test_case(HIGH_FEE, LOW_FEE; "hilo_drop")] + #[test_case(HIGH_FEE, HIGH_FEE; "hihi_drop")] + #[test_case(LOW_FEE, HIGH_FEE; "lohi_evict")] + fn test_receive_and_buffer_nonce_dedup_drop_evict(old_fee: u64, new_fee: u64) { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + let new_has_priority = new_fee > old_fee; + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + old_fee, + durable, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + let prior_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + new_fee, + durable, + )], + ); + + let stats = receive(&mut receive_and_buffer, &mut container); + let current_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + + if new_has_priority { + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_evicted_on_nonce_dedup, 1); + assert_eq!(stats.num_buffered, 1); + + assert_ne!(prior_nonce_entry, current_nonce_entry); + assert!(current_nonce_entry.priority > prior_nonce_entry.priority); + assert!( + container + .get_mut_transaction_state(prior_nonce_entry.id) + .is_none() + ); + } else { + assert_eq!(stats.num_dropped_on_nonce_dedup, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 0); + assert_eq!(prior_nonce_entry, current_nonce_entry); + } + + assert!(container.is_queued(¤t_nonce_entry)); + + verify_container(&mut container, 1); + } + + // a scheduled or held nonce transaction is never evicted regardless of priority + #[test_case(false; "held")] + #[test_case(true; "scheduled")] + fn test_receive_and_buffer_nonce_dedup_preserves_in_flight(is_scheduled: bool) { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + LOW_FEE, + durable, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + + let queue_entry = container.pop().unwrap(); + if is_scheduled { + container + .get_mut_transaction_state(queue_entry.id) + .unwrap() + .take_transaction_for_scheduling(); + } else { + container.hold_transaction(queue_entry); + } + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + HIGH_FEE, + durable, + )], + ); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_dropped_on_nonce_dedup, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 0); + + let nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + assert_eq!(queue_entry, nonce_entry); + assert!( + container + .get_mut_transaction_state(nonce_entry.id) + .is_some() + ); + } + + // a higher priority nonce transaction that fails validation does not evict the existing one, + // and does not affect its nonces-in-use entry + #[test] + fn test_receive_and_buffer_nonce_dedup_validation_failure() { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + LOW_FEE, + durable, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + let prior_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + + // bad blockhash + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + HIGH_FEE, + Hash::new_unique(), + )], + ); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_dropped_on_age, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 0); + + let current_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + assert_eq!(prior_nonce_entry, current_nonce_entry); + assert!(container.is_queued(¤t_nonce_entry)); + + // bad authority + let bad_authority = Keypair::new(); + let message = Message::new( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &bad_authority.pubkey()), + ComputeBudgetInstruction::set_compute_unit_price(HIGH_FEE), + system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 1), + ], + Some(&mint_keypair.pubkey()), + ); + let transaction = Transaction::new(&[&mint_keypair, &bad_authority], message, durable); + send_transactions(&sender, &[transaction]); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_dropped_on_age, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 0); + + let current_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + assert_eq!(prior_nonce_entry, current_nonce_entry); + assert!(container.is_queued(¤t_nonce_entry)); + + // bad feepayer + let bad_payer = Keypair::new(); + let message = Message::new( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &mint_keypair.pubkey()), + ComputeBudgetInstruction::set_compute_unit_price(HIGH_FEE), + system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 1), + ], + Some(&bad_payer.pubkey()), + ); + let transaction = Transaction::new(&[&bad_payer, &mint_keypair], message, durable); + send_transactions(&sender, &[transaction]); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_dropped_on_fee_payer, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 0); + + let current_nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + assert_eq!(prior_nonce_entry, current_nonce_entry); + assert!(container.is_queued(¤t_nonce_entry)); + + verify_container(&mut container, 1); + } + + // when a nonce transaction is bumped for capacity, its nonce map entry is cleared + #[test] + fn test_receive_and_buffer_nonce_capacity_eviction() { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, _container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let mut container = TransactionViewStateContainer::with_capacity(1); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + 0, + durable, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + + let nonce_entry = container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .cloned() + .unwrap(); + + // the previous txn is 0 priority, so this evicts it, even with 0 priority + let transaction = transfer( + &mint_keypair, + &Pubkey::new_unique(), + 1, + bank_forks.read().unwrap().root_bank().last_blockhash(), + ); + + send_transactions(&sender, &[transaction]); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_buffered, 1); + assert_eq!(stats.num_dropped_on_capacity, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + + assert!( + container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_none() + ); + assert!(!container.is_queued(&nonce_entry)); + + verify_container(&mut container, 1); + } + + // nonce-like blockhash transactions are queued but not tracked + #[test] + fn test_receive_and_buffer_pseudo_nonce_untracked() { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + let blockhash = bank_forks.read().unwrap().root_bank().last_blockhash(); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + HIGH_FEE, + blockhash, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + assert!( + container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_none() + ); + + // a real nonce for the same account is still admitted and tracked + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + LOW_FEE, + durable, + )], + ); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_buffered, 1); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + assert!( + container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_some() + ); + + verify_container(&mut container, 2); + } + + // nonce-like blockhash transactions never evict real nonce transactions + #[test_case(LOW_FEE, HIGH_FEE; "lohi_coexist")] + #[test_case(HIGH_FEE, LOW_FEE; "hilo_drop")] + #[test_case(HIGH_FEE, HIGH_FEE; "hihi_drop")] + fn test_receive_and_buffer_pseudo_nonce_never_evicts(real_fee: u64, pseudo_fee: u64) { + let (sender, receiver) = bounded(1024); + let (bank_forks, mint_keypair) = test_bank_forks_with_fee(); + let (mut receive_and_buffer, mut container) = + setup_transaction_view_receive_and_buffer(receiver, bank_forks.clone()); + let (nonce_pubkey, durable) = create_nonce_identity(&bank_forks, &mint_keypair.pubkey()); + let blockhash = bank_forks.read().unwrap().root_bank().last_blockhash(); + let pseudo_has_priority = pseudo_fee > real_fee; + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + real_fee, + durable, + )], + ); + assert_eq!( + receive(&mut receive_and_buffer, &mut container).num_buffered, + 1 + ); + let nonce_entry = *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(); + + send_transactions( + &sender, + &[create_nonce_transaction( + &mint_keypair, + &nonce_pubkey, + pseudo_fee, + blockhash, + )], + ); + let stats = receive(&mut receive_and_buffer, &mut container); + assert_eq!(stats.num_evicted_on_nonce_dedup, 0); + let expected_len = if pseudo_has_priority { + assert_eq!(stats.num_dropped_on_nonce_dedup, 0); + assert_eq!(stats.num_buffered, 1); + 2 + } else { + assert_eq!(stats.num_dropped_on_nonce_dedup, 1); + assert_eq!(stats.num_buffered, 0); + 1 + }; + + // the real nonce transaction still holds the nonce and is queued + assert_eq!( + *container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .unwrap(), + nonce_entry + ); + assert!(container.is_queued(&nonce_entry)); + + verify_container(&mut container, expected_len); + } } diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs index 235943c14b3..f40710c0bc3 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_controller.rs @@ -468,7 +468,9 @@ where num_dropped_on_fee_payer, num_dropped_on_filter_key, num_dropped_on_capacity, + num_dropped_on_nonce_dedup, num_buffered, + num_evicted_on_nonce_dedup, receive_time_us: _, buffer_time_us: _, } = &receiving_stats; @@ -485,7 +487,9 @@ where count_metrics.num_dropped_on_receive_fee_payer += *num_dropped_on_fee_payer; count_metrics.num_dropped_on_filter_key += *num_dropped_on_filter_key; count_metrics.num_dropped_on_capacity += *num_dropped_on_capacity; + count_metrics.num_dropped_on_nonce_dedup += *num_dropped_on_nonce_dedup; count_metrics.num_buffered += *num_buffered; + count_metrics.num_evicted_on_nonce_dedup += *num_evicted_on_nonce_dedup; }); self.timing_metrics.update(|timing_metrics| { @@ -540,16 +544,19 @@ mod tests { }, crossbeam_channel::{Receiver, Sender, bounded}, itertools::Itertools, + solana_account::AccountSharedData, solana_compute_budget_interface::ComputeBudgetInstruction, solana_fee_calculator::FeeRateGovernor, solana_hash::Hash, solana_keypair::Keypair, solana_ledger::genesis_utils::GenesisConfigInfo, solana_message::Message, + solana_nonce::{self as nonce, state::DurableNonce}, solana_poh::poh_recorder::{LeaderState, SharedLeaderState}, solana_pubkey::Pubkey, solana_runtime::{bank::Bank, bank_forks::BankForks}, solana_runtime_transaction::transaction_meta::TransactionMeta, + solana_sdk_ids::system_program, solana_signer::Signer, solana_system_interface::instruction as system_instruction, solana_transaction::Transaction, @@ -667,6 +674,108 @@ mod tests { Transaction::new(&vec![from_keypair], message, recent_blockhash) } + fn create_nonce_account_and_transaction( + bank: &Bank, + mint_keypair: &Keypair, + ) -> (Transaction, Pubkey) { + let nonce_pubkey = Pubkey::new_unique(); + let nonce_data = nonce::state::Data::new( + mint_keypair.pubkey(), + DurableNonce::from_blockhash(&Hash::new_unique()), + 5000, + ); + let nonce_account = AccountSharedData::new_data( + bank.get_minimum_balance_for_rent_exemption(nonce::state::State::size()), + &nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())), + &system_program::id(), + ) + .unwrap(); + bank.store_account(&nonce_pubkey, &nonce_account); + + let ixs = [ + system_instruction::advance_nonce_account(&nonce_pubkey, &mint_keypair.pubkey()), + system_instruction::transfer(&mint_keypair.pubkey(), &Pubkey::new_unique(), 1), + ]; + let message = Message::new(&ixs, Some(&mint_keypair.pubkey())); + let transaction = Transaction::new(&[mint_keypair], message, nonce_data.blockhash()); + (transaction, nonce_pubkey) + } + + // `clear_container()` removes nonce map entries with nonce transactions + #[test] + fn test_clear_container_clears_nonce_entry() { + let (mut test_frame, mut scheduler_controller) = + create_test_frame(1, test_create_transaction_view_receive_and_buffer); + let TestFrame { + bank, + mint_keypair, + banking_packet_sender, + .. + } = &mut test_frame; + + let (transaction, nonce_pubkey) = create_nonce_account_and_transaction(bank, mint_keypair); + banking_packet_sender + .send(to_banking_packet_batch(&[transaction])) + .unwrap(); + scheduler_controller + .receive_and_buffer_packets(&BufferedPacketsDecision::Hold) + .unwrap(); + + assert!( + scheduler_controller + .container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_some() + ); + + scheduler_controller.clear_container(); + + assert!( + scheduler_controller + .container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_none() + ); + } + + // `incremental_recheck()` removes nonce map entries with nonce transactions + #[test] + fn test_incremental_recheck_clears_nonce_entry() { + let (mut test_frame, mut scheduler_controller) = + create_test_frame(1, test_create_transaction_view_receive_and_buffer); + let TestFrame { + bank, + mint_keypair, + banking_packet_sender, + .. + } = &mut test_frame; + + let (transaction, nonce_pubkey) = create_nonce_account_and_transaction(bank, mint_keypair); + banking_packet_sender + .send(to_banking_packet_batch(std::slice::from_ref(&transaction))) + .unwrap(); + scheduler_controller + .receive_and_buffer_packets(&BufferedPacketsDecision::Hold) + .unwrap(); + + assert!( + scheduler_controller + .container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_some() + ); + + bank.process_transaction(&transaction).unwrap(); + scheduler_controller.incremental_recheck(); + + assert!( + scheduler_controller + .container + .get_nonce_transaction_priority_id(&nonce_pubkey) + .is_none() + ); + } + // Helper function to let test receive and then schedule packets. // The order of operations here is convenient for testing, but does not // match the order of operations in the actual scheduler. diff --git a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs index e4b9862f1d6..1ecb76752e3 100644 --- a/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs +++ b/core/src/banking_stage/transaction_scheduler/scheduler_metrics.rs @@ -85,6 +85,8 @@ pub struct SchedulerCountMetricsInner { pub num_dropped_on_clean: Saturating, /// Number of transactions that were dropped due to exceeded capacity. pub num_dropped_on_capacity: Saturating, + pub num_dropped_on_nonce_dedup: Saturating, + pub num_evicted_on_nonce_dedup: Saturating, /// Min prioritization fees in the transaction container pub min_prioritization_fees: u64, /// Max prioritization fees in the transaction container @@ -140,6 +142,8 @@ impl SchedulerCountMetricsInner { num_dropped_on_clear: Saturating(num_dropped_on_clear), num_dropped_on_clean: Saturating(num_dropped_on_clean), num_dropped_on_capacity: Saturating(num_dropped_on_capacity), + num_dropped_on_nonce_dedup: Saturating(num_dropped_on_nonce_dedup), + num_evicted_on_nonce_dedup: Saturating(num_evicted_on_nonce_dedup), min_prioritization_fees: _min_prioritization_fees, max_prioritization_fees: _max_prioritization_fees, } = self; @@ -187,6 +191,8 @@ impl SchedulerCountMetricsInner { i64 ), ("num_dropped_on_capacity", num_dropped_on_capacity, i64), + ("num_dropped_on_nonce_dedup", num_dropped_on_nonce_dedup, i64), + ("num_evicted_on_nonce_dedup", num_evicted_on_nonce_dedup, i64), ("min_priority", self.get_min_priority(), i64), ("max_priority", self.get_max_priority(), i64), ); @@ -226,6 +232,8 @@ impl SchedulerCountMetricsInner { self.num_dropped_on_clear = Saturating(0); self.num_dropped_on_clean = Saturating(0); self.num_dropped_on_capacity = Saturating(0); + self.num_dropped_on_nonce_dedup = Saturating(0); + self.num_evicted_on_nonce_dedup = Saturating(0); self.min_prioritization_fees = u64::MAX; self.max_prioritization_fees = 0; } diff --git a/core/src/banking_stage/transaction_scheduler/transaction_state.rs b/core/src/banking_stage/transaction_scheduler/transaction_state.rs index bfdb4e80384..0675343650a 100644 --- a/core/src/banking_stage/transaction_scheduler/transaction_state.rs +++ b/core/src/banking_stage/transaction_scheduler/transaction_state.rs @@ -1,4 +1,4 @@ -use crate::banking_stage::scheduler_messages::MaxAge; +use {crate::banking_stage::scheduler_messages::MaxAge, solana_pubkey::Pubkey}; /// TransactionState is used to track the state of a transaction in the transaction scheduler /// and banking stage as a whole. @@ -20,6 +20,8 @@ pub(crate) struct TransactionState { priority: u64, /// Estimated cost of the transaction. cost: u64, + /// Nonce address, if this is a validated nonce transaction. + nonce_address: Option, } impl TransactionState { @@ -30,6 +32,7 @@ impl TransactionState { max_age, priority, cost, + nonce_address: None, } } @@ -45,6 +48,11 @@ impl TransactionState { self.cost } + /// Return the nonce address of the transaction, if one exists. + pub(crate) fn nonce_address(&self) -> Option<&Pubkey> { + self.nonce_address.as_ref() + } + /// Intended to be called when a transaction is scheduled. This method /// takes ownership of the transaction from the state. /// @@ -79,6 +87,12 @@ impl TransactionState { .as_ref() .expect("transaction is not pending") } + + /// Set the nonce address, used in `set_nonce_transaction_priority_id()` after + /// the nonce transaction is fully validated. + pub(crate) fn set_nonce_address(&mut self, nonce_address: Option) { + self.nonce_address = nonce_address; + } } #[cfg(test)] 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 102138b1097..1631c8ba7d1 100644 --- a/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs +++ b/core/src/banking_stage/transaction_scheduler/transaction_state_container.rs @@ -4,10 +4,16 @@ use { agave_transaction_view::resolved_transaction_view::ResolvedTransactionView, slab::{Slab, VacantEntry}, solana_packet::PACKET_DATA_SIZE, + solana_pubkey::{Pubkey, PubkeyHasherBuilder}, solana_runtime_transaction::{ runtime_transaction::RuntimeTransaction, transaction_with_meta::TransactionWithMeta, }, - std::{collections::BTreeSet, iter::Rev, ops::Bound, sync::Arc}, + std::{ + collections::{BTreeSet, HashMap, hash_map::Entry}, + iter::Rev, + ops::Bound, + sync::Arc, + }, }; /// This structure will hold `TransactionState` for the entirety of a @@ -40,6 +46,7 @@ pub(crate) struct TransactionStateContainer { priority_queue: BTreeSet, id_to_transaction_state: Slab>, held_transactions: Vec, + nonces_in_use: HashMap, } pub(crate) trait StateContainer { @@ -88,8 +95,6 @@ pub(crate) trait StateContainer { /// Pushes transaction ids into the priority queue. If the queue if full, /// the lowest priority transactions will be dropped (removed from the /// queue and map) **after** all ids have been pushed. - /// To avoid allocating, the caller should not push more than - /// [`EXTRA_CAPACITY`] ids in a call. /// Returns the number of dropped transactions. fn push_ids_into_queue( &mut self, @@ -112,11 +117,24 @@ pub(crate) trait StateContainer { &self, cursor: Option<&TransactionPriorityId>, ) -> Rev>; + + fn is_queued(&self, id: &TransactionPriorityId) -> bool; + + fn get_nonce_transaction_priority_id( + &self, + nonce_address: &Pubkey, + ) -> Option<&TransactionPriorityId>; + + fn set_nonce_transaction_priority_id( + &mut self, + nonce_address: &Pubkey, + priority_id: TransactionPriorityId, + ); } -// Extra capacity is added because some additional space is needed when -// pushing a new transaction into the container to avoid reallocation. -pub(crate) const EXTRA_CAPACITY: usize = 64; +// Extra capacity is added to avoid reallocation because each new transaction +// is added to the slab before anything can be evicted from a full queue. +pub(crate) const EXTRA_CAPACITY: usize = 1; impl StateContainer for TransactionStateContainer { fn with_capacity(capacity: usize) -> Self { @@ -125,6 +143,7 @@ impl StateContainer for TransactionStateContainer StateContainer for TransactionStateContainer StateContainer for TransactionStateContainer StateContainer for TransactionStateContainer bool { + self.priority_queue.contains(id) + } + + fn get_nonce_transaction_priority_id( + &self, + nonce_address: &Pubkey, + ) -> Option<&TransactionPriorityId> { + self.nonces_in_use.get(nonce_address) + } + + fn set_nonce_transaction_priority_id( + &mut self, + nonce_address: &Pubkey, + priority_id: TransactionPriorityId, + ) { + self.nonces_in_use.insert(*nonce_address, priority_id); + if let Some(state) = self.id_to_transaction_state.get_mut(priority_id.id) { + state.set_nonce_address(Some(*nonce_address)) + } else { + debug_assert!(false, "transaction must exist"); + } + } } impl TransactionStateContainer { @@ -246,6 +288,20 @@ impl TransactionStateContainer { assert!(self.id_to_transaction_state.len() < self.id_to_transaction_state.capacity()); self.id_to_transaction_state.vacant_entry() } + + fn remove_state(&mut self, id: TransactionId) -> TransactionPriorityId { + let state = self.id_to_transaction_state.remove(id); + let priority_id = TransactionPriorityId::new(state.priority(), id); + + if let Some(nonce_address) = state.nonce_address() + && let Entry::Occupied(entry) = self.nonces_in_use.entry(*nonce_address) + && *entry.get() == priority_id + { + entry.remove(); + } + + priority_id + } } pub type SharedBytes = Arc>; @@ -383,6 +439,29 @@ impl StateContainer for TransactionViewStateContainer { ) -> Rev> { self.inner.recheck_iter(cursor) } + + #[inline] + fn is_queued(&self, id: &TransactionPriorityId) -> bool { + self.inner.is_queued(id) + } + + #[inline] + fn get_nonce_transaction_priority_id( + &self, + nonce_address: &Pubkey, + ) -> Option<&TransactionPriorityId> { + self.inner.get_nonce_transaction_priority_id(nonce_address) + } + + #[inline] + fn set_nonce_transaction_priority_id( + &mut self, + nonce_address: &Pubkey, + priority_id: TransactionPriorityId, + ) { + self.inner + .set_nonce_transaction_priority_id(nonce_address, priority_id); + } } #[cfg(test)] @@ -521,7 +600,6 @@ mod tests { } // Push 5 additional packets in. 5 should be dropped. - let mut priority_ids = Vec::with_capacity(5); for priority in [10, 11, 12, 1, 2] { let (transaction, _max_age, priority, cost) = test_transaction(priority); let packet = Packet::from_data(None, transaction.to_versioned_transaction()).unwrap(); @@ -531,9 +609,11 @@ mod tests { }) .unwrap(); let priority_id = TransactionPriorityId::new(priority, id); - priority_ids.push(priority_id); + assert_eq!( + container.push_ids_into_queue(std::iter::once(priority_id)), + 1, + ); } - assert_eq!(container.push_ids_into_queue(priority_ids.into_iter()), 5); assert_eq!(container.pop().unwrap().priority, 12); assert_eq!(container.pop().unwrap().priority, 11); assert!(container.pop().is_none()); diff --git a/runtime/src/bank/check_transactions.rs b/runtime/src/bank/check_transactions.rs index 39c697679b9..f808c03cb7c 100644 --- a/runtime/src/bank/check_transactions.rs +++ b/runtime/src/bank/check_transactions.rs @@ -6,7 +6,10 @@ use { solana_clock::{MAX_TRANSACTION_FORWARDING_DELAY, Slot}, solana_compute_budget::compute_budget::SVMTransactionExecutionBudget, solana_fee::calculate_fee_details, - solana_nonce::state::{Data as NonceData, DurableNonce, State as NonceState}, + solana_nonce::{ + NONCED_TX_MARKER_IX_INDEX, + state::{Data as NonceData, DurableNonce, State as NonceState}, + }, solana_nonce_account as nonce_account, solana_program_runtime::execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, solana_pubkey::Pubkey, @@ -66,24 +69,35 @@ impl Bank { .0 } - /// Checks a batch of sanitized transactions against the bank for age and - /// compute-budget limits, without checking the status cache. - pub fn check_transactions_without_status_cache( + /// Checks a sanitized transaction against the bank for age, + /// without checking the status cache. This is a leader-only + /// function and must not be used in replay without a feature gate. + pub fn check_transaction_without_status_cache( &self, - sanitized_txs: &[impl core::borrow::Borrow], - lock_results: &[TransactionResult<()>], + tx: &impl SVMMessage, max_age: usize, - strict_nonce_size_check: bool, error_counters: &mut TransactionErrorMetrics, - ) -> Vec { - let lock_results = self.filter_v1_transactions(sanitized_txs, lock_results); + ) -> TransactionResult> { + let feature_set: &FeatureSet = &self.feature_set; + let feature_snapshot = feature_set.snapshot(); + let enable_tx_v1 = feature_snapshot.enable_tx_v1; - self.check_age_and_compute_budget_limits( - sanitized_txs, - lock_results, + if !enable_tx_v1 && tx.version() == TransactionVersion::Number(1) { + return Err(TransactionError::UnsupportedVersion); + } + + let hash_queue = self.blockhash_queue.read().unwrap(); + let last_blockhash = hash_queue.last_hash(); + let next_durable_nonce = DurableNonce::from_blockhash(&last_blockhash); + + self.check_transaction_age( + tx, max_age, - strict_nonce_size_check, + &next_durable_nonce, + &hash_queue, error_counters, + true, // strict_nonce_size_check + true, // strict_nonce_authority_check ) } @@ -193,15 +207,21 @@ impl Bank { .inspect_err(|_err| { error_counters.invalid_compute_budget += 1; })?; - self.check_transaction_age( + + let nonce_address = self.check_transaction_age( tx.borrow(), max_age, &next_durable_nonce, &hash_queue, error_counters, - compute_budget_and_limits, strict_nonce_size_check, - ) + false, + )?; + + Ok(CheckedTransactionDetails::new( + nonce_address, + compute_budget_and_limits, + )) } Err(e) => Err(e), }) @@ -215,22 +235,22 @@ impl Bank { next_durable_nonce: &DurableNonce, hash_queue: &BlockhashQueue, error_counters: &mut TransactionErrorMetrics, - compute_budget: SVMTransactionExecutionAndFeeBudgetLimits, strict_nonce_size_check: bool, - ) -> TransactionCheckResult { + strict_nonce_authority_check: bool, + ) -> TransactionResult> { let recent_blockhash = tx.recent_blockhash(); if hash_queue .get_hash_info_if_valid(recent_blockhash, max_age) .is_some() { - Ok(CheckedTransactionDetails::new(None, compute_budget)) - } else if let Some((nonce_address, _)) = - self.check_nonce_transaction_validity(tx, next_durable_nonce, strict_nonce_size_check) - { - Ok(CheckedTransactionDetails::new( - Some(nonce_address), - compute_budget, - )) + Ok(None) + } else if let Some((nonce_address, _)) = self.check_nonce_transaction_validity( + tx, + next_durable_nonce, + strict_nonce_size_check, + strict_nonce_authority_check, + ) { + Ok(Some(nonce_address)) } else { error_counters.blockhash_not_found += 1; Err(TransactionError::BlockhashNotFound) @@ -242,6 +262,7 @@ impl Bank { message: &impl SVMMessage, next_durable_nonce: &DurableNonce, strict_nonce_size_check: bool, + strict_nonce_authority_check: bool, ) -> Option<(Pubkey, u64)> { let nonce_is_advanceable = message.recent_blockhash() != next_durable_nonce.as_hash(); if !nonce_is_advanceable { @@ -250,6 +271,15 @@ impl Bank { let (nonce_address, nonce_data) = self.load_message_nonce_data(message, strict_nonce_size_check)?; + + if strict_nonce_authority_check + && !message + .get_ix_signers(NONCED_TX_MARKER_IX_INDEX as usize) + .any(|signer| signer == &nonce_data.authority) + { + return None; + } + let previous_lamports_per_signature = nonce_data.get_lamports_per_signature(); Some((nonce_address, previous_lamports_per_signature)) @@ -352,7 +382,10 @@ mod tests { instruction::{self as system_instruction, SystemInstruction}, program as system_program, }, - solana_transaction::{sanitized::MessageHash, versioned::VersionedTransaction}, + solana_transaction::{ + sanitized::{MessageHash, SanitizedTransaction}, + versioned::VersionedTransaction, + }, std::collections::HashSet, }; @@ -387,7 +420,12 @@ mod tests { bank.store_account(&nonce_pubkey, &nonce_account); assert_eq!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false), + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + false, + false + ), Some((nonce_pubkey, STALE_LAMPORTS_PER_SIGNATURE)), ); } @@ -409,8 +447,13 @@ mod tests { &nonce_hash, )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) - .is_none() + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + false, + false + ) + .is_none() ); } @@ -442,8 +485,13 @@ mod tests { bank.store_account(&nonce_pubkey, &resized_nonce_account); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), true) - .is_none() + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + true, + false + ) + .is_none() ); } @@ -469,6 +517,7 @@ mod tests { &new_sanitized_message(message), &bank.next_durable_nonce(), false, + false, ) .is_none() ); @@ -493,8 +542,13 @@ mod tests { &nonce_hash, )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) - .is_none() + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + false, + false + ) + .is_none() ); } @@ -514,8 +568,13 @@ mod tests { &Hash::default(), )); assert!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false) - .is_none() + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + false, + false + ) + .is_none() ); } @@ -572,7 +631,12 @@ mod tests { .unwrap(); assert_eq!( - bank.check_nonce_transaction_validity(&message, &bank.next_durable_nonce(), false), + bank.check_nonce_transaction_validity( + &message, + &bank.next_durable_nonce(), + false, + false + ), None, ); } @@ -584,7 +648,7 @@ mod tests { fn make_test_tx_with_blockhash( version: TransactionVersion, recent_blockhash: Hash, - ) -> impl TransactionWithMeta { + ) -> RuntimeTransaction { let payer = Keypair::new(); let recipient = Pubkey::new_unique(); let ix = system_instruction::transfer(&payer.pubkey(), &recipient, 1); @@ -624,7 +688,7 @@ mod tests { } #[test] - fn test_check_transactions_without_status_cache_allows_already_processed() { + fn test_check_transaction_without_status_cache_allows_already_processed() { let (genesis_config, _mint_keypair) = solana_genesis_config::create_genesis_config(1); let bank = Bank::new_for_tests(&genesis_config); let tx = make_test_tx_with_blockhash(TransactionVersion::LEGACY, bank.last_blockhash()); @@ -636,11 +700,10 @@ mod tests { Ok(()), ); - let txs = [tx]; let lock_results = [Ok(())]; let mut error_counters = TransactionErrorMetrics::default(); let check_results = bank.check_transactions( - &txs, + std::slice::from_ref(&tx), &lock_results, bank.max_processing_age(), true, @@ -652,14 +715,12 @@ mod tests { )); let mut error_counters = TransactionErrorMetrics::default(); - let check_results = bank.check_transactions_without_status_cache( - &txs, - &lock_results, + let check_result = bank.check_transaction_without_status_cache( + &tx, bank.max_processing_age(), - true, &mut error_counters, ); - assert!(matches!(check_results.as_slice(), [Ok(_)])); + assert_eq!(check_result, Ok(None)); } #[test] diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index fb1af73d93d..fd9d6aee8c0 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -4675,6 +4675,7 @@ fn test_check_ro_durable_nonce_fails() { &new_sanitized_message(tx.message().clone()), &bank.next_durable_nonce(), false, + false, ), None ); From 90706554582d5ee37634c33f79b1776dd1cc9bf8 Mon Sep 17 00:00:00 2001 From: mmcgee-jump <132931789+mmcgee-jump@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:32:06 -0500 Subject: [PATCH 19/30] snapshots: produce multi frame snapshots (#13902) --- snapshots/src/archive.rs | 24 ++-- snapshots/src/lib.rs | 1 + snapshots/src/multiframe.rs | 223 ++++++++++++++++++++++++++++++++++++ 3 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 snapshots/src/multiframe.rs diff --git a/snapshots/src/archive.rs b/snapshots/src/archive.rs index 110deb0b749..a5c268e790c 100644 --- a/snapshots/src/archive.rs +++ b/snapshots/src/archive.rs @@ -1,7 +1,8 @@ use { crate::{ - ArchiveFormat, Result, SnapshotArchiveKind, error::ArchiveSnapshotPackageError, paths, - snapshot_archive_info::SnapshotArchiveInfo, snapshot_hash::SnapshotHash, + ArchiveFormat, Result, SnapshotArchiveKind, error::ArchiveSnapshotPackageError, + multiframe::MultiFrameZstdWriter, paths, snapshot_archive_info::SnapshotArchiveInfo, + snapshot_hash::SnapshotHash, }, agave_fs::{ FileSize, buffered_reader::FileBufRead as _, buffered_writer::large_file_buf_writer, @@ -35,6 +36,10 @@ const STORAGE_FILE_OPEN_CHUNK_SIZE: usize = 25 * (INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.0 + INTERLEAVE_TAR_ENTRIES_SMALL_TO_LARGE_RATIO.1); +// Uncompressed bytes per zstd frame. 32 MiB keeps compression loss below ~0.02 +// while giving parallel decompressors enough chunk size. +const ZSTD_FRAME_SIZE: u32 = 32 * 1024 * 1024; + /// Archives a snapshot into `archive_path` pub fn archive_snapshot( snapshot_archive_kind: SnapshotArchiveKind, @@ -215,11 +220,15 @@ pub fn archive_snapshot( match archive_format { ArchiveFormat::TarZstd { config } => { - let mut encoder = - zstd::stream::Encoder::new(archive_writer, config.compression_level) - .map_err(E::CreateEncoder)?; + let mut encoder = MultiFrameZstdWriter::new( + archive_writer, + config.compression_level, + ZSTD_FRAME_SIZE, + ) + .map_err(E::CreateEncoder)?; do_archive_files(&mut encoder)?; - encoder.finish().map_err(E::FinishEncoder)?; + let mut writer = encoder.finish().map_err(E::FinishEncoder)?; + writer.flush().map_err(E::FinishEncoder)?; } ArchiveFormat::TarLz4 => { let mut encoder = lz4::EncoderBuilder::new() @@ -227,8 +236,9 @@ pub fn archive_snapshot( .build(archive_writer) .map_err(E::CreateEncoder)?; do_archive_files(&mut encoder)?; - let (_output, result) = encoder.finish(); + let (mut writer, result) = encoder.finish(); result.map_err(E::FinishEncoder)?; + writer.flush().map_err(E::FinishEncoder)?; } }; } diff --git a/snapshots/src/lib.rs b/snapshots/src/lib.rs index f6f6ebe6aeb..af5d97129ff 100644 --- a/snapshots/src/lib.rs +++ b/snapshots/src/lib.rs @@ -5,6 +5,7 @@ mod archive_format; pub mod error; pub mod hardened_unpack; mod kind; +mod multiframe; pub mod paths; pub mod snapshot_archive_info; pub mod snapshot_config; diff --git a/snapshots/src/multiframe.rs b/snapshots/src/multiframe.rs new file mode 100644 index 00000000000..0243053811a --- /dev/null +++ b/snapshots/src/multiframe.rs @@ -0,0 +1,223 @@ +//! Multi-frame zstd writer. +//! +//! The payload is split into independently compressed zstd frames, written +//! back to back, so that readers can decompress frames in parallel. +use std::io::{self, Write}; + +const MAX_ZSTD_FRAME_SIZE: u32 = 1 << 30; + +/// Compresses a byte stream into independent zstd frames of `frame_size` +/// uncompressed bytes each, written back to back. +pub struct MultiFrameZstdWriter { + inner: W, + compressor: zstd::bulk::Compressor<'static>, + frame_size: usize, + buf: Vec, + compressed: Vec, +} + +impl MultiFrameZstdWriter { + pub fn new(inner: W, compression_level: i32, frame_size: u32) -> io::Result { + if frame_size == 0 || frame_size > MAX_ZSTD_FRAME_SIZE { + return Err(io::Error::new( + io::ErrorKind::InvalidInput, + format!("invalid zstd frame size: {frame_size}"), + )); + } + let frame_size = frame_size as usize; + Ok(Self { + inner, + compressor: zstd::bulk::Compressor::new(compression_level)?, + frame_size, + buf: Vec::with_capacity(frame_size), + compressed: Vec::new(), + }) + } + + fn emit_frame(&mut self) -> io::Result<()> { + if self.buf.is_empty() { + return Ok(()); + } + self.compressed.clear(); + self.compressed + .reserve(zstd::zstd_safe::compress_bound(self.buf.len())); + let compressed_size = self + .compressor + .compress_to_buffer(&self.buf, &mut self.compressed)?; + self.inner.write_all(&self.compressed[..compressed_size])?; + self.buf.clear(); + Ok(()) + } + + pub fn finish(mut self) -> io::Result { + self.emit_frame()?; + Ok(self.inner) + } +} + +impl Write for MultiFrameZstdWriter { + fn write(&mut self, data: &[u8]) -> io::Result { + if self.buf.len() == self.frame_size { + self.emit_frame()?; + } + + let take = self + .frame_size + .saturating_sub(self.buf.len()) + .min(data.len()); + self.buf.extend_from_slice(&data[..take]); + Ok(take) + } + + fn flush(&mut self) -> io::Result<()> { + self.emit_frame()?; + self.inner.flush() + } +} + +#[cfg(test)] +#[allow(clippy::arithmetic_side_effects)] +mod tests { + use { + super::*, + crate::{ArchiveFormat, ArchiveFormatDecompressor, ZstdConfig}, + std::io::{BufReader, Cursor, Read}, + }; + + const TEST_FRAME_SIZE: u32 = 64 * 1024; + + fn test_data(len: usize) -> Vec { + (0..len) + .map(|i| { + if (i / 256) % 2 == 0 { + (i / 64) as u8 + } else { + (i as u32).wrapping_mul(2654435761) as u8 + } + }) + .collect() + } + + fn compress(data: &[u8], frame_size: u32) -> Vec { + let mut writer = MultiFrameZstdWriter::new(Vec::new(), 1, frame_size).unwrap(); + writer.write_all(data).unwrap(); + writer.finish().unwrap() + } + + fn walk_frames(archive: &[u8]) -> Vec<(usize, usize)> { + let mut frames = Vec::new(); + let mut offset = 0; + while offset < archive.len() { + let rest = &archive[offset..]; + let compressed_size = zstd::zstd_safe::find_frame_compressed_size(rest).unwrap(); + let decompressed_size = zstd::zstd_safe::get_frame_content_size(rest) + .unwrap() + .expect("frame header carries decompressed size") + as usize; + frames.push((compressed_size, decompressed_size)); + offset += compressed_size; + } + frames + } + + #[test] + fn test_roundtrip_byte_identity() { + let data = test_data(10 * TEST_FRAME_SIZE as usize + 12345); + let archive = compress(&data, TEST_FRAME_SIZE); + assert_eq!(zstd::decode_all(&archive[..]).unwrap(), data); + } + + #[test] + fn test_frame_boundaries_walkable() { + let data = test_data(10 * TEST_FRAME_SIZE as usize + 12345); + let archive = compress(&data, TEST_FRAME_SIZE); + let frames = walk_frames(&archive); + assert_eq!(frames.len(), 11); + for &(_, decompressed_size) in &frames[..10] { + assert_eq!(decompressed_size, TEST_FRAME_SIZE as usize); + } + assert_eq!(frames[10].1, 12345); + let total: usize = frames.iter().map(|&(_, d)| d).sum(); + assert_eq!(total, data.len()); + } + + #[test] + fn test_frames_independently_decompressible() { + let data = test_data(4 * TEST_FRAME_SIZE as usize + 999); + let archive = compress(&data, TEST_FRAME_SIZE); + let mut offset = 0usize; + let mut decompressed_offset = 0usize; + for (compressed_size, decompressed_size) in walk_frames(&archive) { + let frame = &archive[offset..offset + compressed_size]; + assert_eq!( + zstd::bulk::decompress(frame, decompressed_size).unwrap(), + data[decompressed_offset..decompressed_offset + decompressed_size] + ); + offset += compressed_size; + decompressed_offset += decompressed_size; + } + } + + #[test] + fn test_archive_read_path_decodes_multiframe() { + let data = test_data(3 * TEST_FRAME_SIZE as usize + 777); + let archive = compress(&data, TEST_FRAME_SIZE); + let mut decompressor = ArchiveFormatDecompressor::new( + ArchiveFormat::TarZstd { + config: ZstdConfig::default(), + }, + BufReader::new(Cursor::new(archive)), + ) + .unwrap(); + let mut decompressed = Vec::new(); + decompressor.read_to_end(&mut decompressed).unwrap(); + assert_eq!(decompressed, data); + } + + #[test] + fn test_empty_input() { + let archive = compress(&[], TEST_FRAME_SIZE); + assert!(archive.is_empty()); + } + + #[test] + fn test_exact_frame_multiple() { + let data = test_data(4 * TEST_FRAME_SIZE as usize); + let archive = compress(&data, TEST_FRAME_SIZE); + let frames = walk_frames(&archive); + assert_eq!(frames.len(), 4); + assert!(frames.iter().all(|&(_, d)| d == TEST_FRAME_SIZE as usize)); + assert_eq!(zstd::decode_all(&archive[..]).unwrap(), data); + } + + #[test] + fn test_input_smaller_than_frame() { + let data = test_data(1000); + let archive = compress(&data, TEST_FRAME_SIZE); + let frames = walk_frames(&archive); + assert_eq!(frames.len(), 1); + assert_eq!(frames[0].1, 1000); + assert_eq!(zstd::decode_all(&archive[..]).unwrap(), data); + } + + #[test] + fn test_flush_cuts_frame() { + let data = test_data(1000); + let mut writer = MultiFrameZstdWriter::new(Vec::new(), 1, TEST_FRAME_SIZE).unwrap(); + writer.write_all(&data[..400]).unwrap(); + writer.flush().unwrap(); + writer.write_all(&data[400..]).unwrap(); + let archive = writer.finish().unwrap(); + let frames = walk_frames(&archive); + assert_eq!(frames.len(), 2); + assert_eq!(frames[0].1, 400); + assert_eq!(frames[1].1, 600); + assert_eq!(zstd::decode_all(&archive[..]).unwrap(), data); + } + + #[test] + fn test_invalid_frame_size_rejected() { + assert!(MultiFrameZstdWriter::new(Vec::new(), 1, 0).is_err()); + assert!(MultiFrameZstdWriter::new(Vec::new(), 1, MAX_ZSTD_FRAME_SIZE + 1).is_err()); + } +} From b0f3fe6d3d72c20ebe267c9a45f7aa3eb3ed6c2e Mon Sep 17 00:00:00 2001 From: Rory Harris Date: Fri, 17 Jul 2026 11:07:02 -0700 Subject: [PATCH 20/30] Drop zero lamport accounts during flush (#13904) * Purge zero lamport accounts during flush that do not already have an entry in the accounts index This is safe, as even during scan the account would not be returned * Fixes from review feedback - Filtered Vec pushing based on secondary indexes - Updated Comments - Renamed from purged to skipped --- accounts-db/src/accounts_db.rs | 29 +++- accounts-db/src/accounts_db/stats.rs | 2 + accounts-db/src/accounts_db/tests.rs | 241 ++++++++++++++++++++------- runtime/src/bank/tests.rs | 10 +- runtime/src/serde_snapshot/tests.rs | 2 +- 5 files changed, 224 insertions(+), 60 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 17873b415b9..13e82db1606 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4515,6 +4515,11 @@ impl AccountsDb { ), ("account_bytes_saved", flush_stats.num_bytes_purged.0, i64), ("num_accounts_saved", flush_stats.num_accounts_purged.0, i64), + ( + "num_zero_lamport_accounts_skipped", + flush_stats.num_zero_lamport_accounts_skipped.0, + i64 + ), ( "store_accounts_total_us", flush_stats.store_accounts_total_us.0, @@ -4684,6 +4689,7 @@ impl AccountsDb { ) -> FlushStats { debug_assert!(self.accounts_cache.contains_unflushed_root(slot)); let mut flush_stats = FlushStats::default(); + let mut skipped_zero_lamport_pubkeys = Vec::new(); let iter_items: Vec<_> = slot_cache.iter().collect(); let accounts: Vec<(&Pubkey, &AccountSharedData)> = iter_items @@ -4691,17 +4697,32 @@ impl AccountsDb { .filter_map(|iter_item| { let key = iter_item.key(); let account = &iter_item.value().account; - let should_flush = match pubkeys_to_flush { + let mut should_flush = match pubkeys_to_flush { PubkeysToFlush::All => true, PubkeysToFlush::Only(flush_keys) => flush_keys.contains(key), }; + // `true` keeps a disk-loaded entry in-mem for the index upsert below + if should_flush + && account.is_zero_lamport() + && !self + .accounts_index + .get_and_then(key, |entry| (true, entry.is_some())) + { + // A zero-lamport account with no index entry has no older rooted version + // in storage to shadow, so it can just be skipped + flush_stats.num_zero_lamport_accounts_skipped += 1; + if !self.account_indexes.is_empty() { + skipped_zero_lamport_pubkeys.push(*key); + } + should_flush = false; + } if should_flush { flush_stats.num_bytes_flushed += AppendVec::calculate_stored_size(account.data().len()) as u64; flush_stats.num_accounts_flushed += 1; Some((key, account)) } else { - // A newer version wins, so this one isn't written + // No need to write this account. Either superseded or zero lamport flush_stats.num_bytes_purged += AppendVec::calculate_stored_size(account.data().len()) as u64; flush_stats.num_accounts_purged += 1; @@ -4752,6 +4773,10 @@ impl AccountsDb { .remove_slot(slot) .expect("slot must be in the cache when flushing"); + // Zero-lamport accounts that were skipped above were never added to the primary + // index, so their secondary index entries may be purgeable. + self.purge_secondary_indexes_for_dead_keys(&skipped_zero_lamport_pubkeys); + // Now that this slot has left the cache, any pubkey that no longer appears // in any cached slot is eligible to be written through so its in-mem entry // becomes clean and can be evicted. diff --git a/accounts-db/src/accounts_db/stats.rs b/accounts-db/src/accounts_db/stats.rs index f54ebda9678..e64aa56b0e8 100644 --- a/accounts-db/src/accounts_db/stats.rs +++ b/accounts-db/src/accounts_db/stats.rs @@ -236,6 +236,7 @@ pub struct FlushStats { pub num_bytes_flushed: Saturating, pub num_accounts_purged: Saturating, pub num_bytes_purged: Saturating, + pub num_zero_lamport_accounts_skipped: Saturating, pub store_accounts_total_us: Saturating, pub write_accounts_us: Saturating, pub update_index_us: Saturating, @@ -273,6 +274,7 @@ impl FlushStats { self.num_bytes_flushed += other.num_bytes_flushed; self.num_accounts_purged += other.num_accounts_purged; self.num_bytes_purged += other.num_bytes_purged; + self.num_zero_lamport_accounts_skipped += other.num_zero_lamport_accounts_skipped; self.store_accounts_total_us += other.store_accounts_total_us; self.write_accounts_us += other.write_accounts_us; self.update_index_us += other.update_index_us; diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 813d89d6d9c..393da93bd9b 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -87,6 +87,25 @@ where } } +/// Stores a rooted non-zero version of each pubkey in `pubkeys` at `slot` and flushes it to +/// storage. +fn store_rooted_nonzero_accounts<'a>( + accounts_db: &AccountsDb, + slot: Slot, + pubkeys: impl IntoIterator, +) { + let predecessor_account = AccountSharedData::new(1, 0, &Pubkey::default()); + let accounts = pubkeys + .into_iter() + .map(|pubkey| (pubkey, &predecessor_account)) + .collect::>(); + if accounts.is_empty() { + return; + } + accounts_db.store_for_tests((slot, accounts.as_slice())); + accounts_db.add_root_and_flush_write_cache(slot); +} + fn create_loadable_account_with_fields( name: &str, (lamports, rent_epoch): InheritableAccountFields, @@ -1177,6 +1196,8 @@ fn test_remove_zero_lamport_single_ref_accounts_after_shrink() { let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); let slot = 1; + store_rooted_nonzero_accounts(&accounts, slot, [&pubkey_zero]); + let slot = slot + 1; accounts.store_for_tests(( slot, @@ -1284,6 +1305,9 @@ fn test_shrink_zero_lamport_single_ref_account() { let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); let slot = 1; + store_rooted_nonzero_accounts(&accounts, slot, [&pubkey_zero]); + let slot = slot + 1; + // Store a zero-lamport account and a non-zero lamport account accounts.store_for_tests(( slot, @@ -1317,7 +1341,7 @@ fn test_shrink_zero_lamport_single_ref_account() { accounts.shrink_slot_forced(slot); assert!( - accounts.storage.get_slot_storage_entry(1).is_some(), + accounts.storage.get_slot_storage_entry(slot).is_some(), "{latest_full_snapshot_slot:?}" ); @@ -1747,6 +1771,9 @@ fn test_alive_bytes_after_shrink_with_zero_lamport_single_ref_accounts() { let alive_account = AccountSharedData::new(11, 17, &Pubkey::default()); let alive_pubkey = Pubkey::new_unique(); + store_rooted_nonzero_accounts(&accounts_db, slot, &dead_pubkeys); + let slot = slot + 1; + accounts_db.store_for_tests(( slot, [ @@ -1798,46 +1825,60 @@ fn test_clean_multiple_zero_lamport_decrements_index_ref_count() { let accounts = AccountsDb::new_single_for_tests(); let pubkey1 = solana_pubkey::new_rand(); let pubkey2 = solana_pubkey::new_rand(); + let one_lamport_account = AccountSharedData::new(1, 0, AccountSharedData::default().owner()); let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); // If there is no latest full snapshot, zero lamport accounts can be cleaned and removed // immediately. Set latest full snapshot slot to zero to avoid cleaning zero lamport accounts accounts.set_latest_full_snapshot_slot(0); - // Store 2 accounts in slot 0, then update account 1 in two more slots - accounts.store_for_tests((0, [(&pubkey1, &zero_lamport_account)].as_slice())); - accounts.store_for_tests((0, [(&pubkey2, &zero_lamport_account)].as_slice())); - accounts.store_for_tests((1, [(&pubkey1, &zero_lamport_account)].as_slice())); - accounts.store_for_tests((2, [(&pubkey1, &zero_lamport_account)].as_slice())); - // Root all slots + // Store non-zero versions of both accounts in slot 0, then kill pubkey1 twice (slots 1 + // and 2) and pubkey2 once (slot 2). Each zero-lamport update is written to storage because + // the older version is in the index at flush, and reclaims the version it supersedes + accounts.store_for_tests(( + 0, + [ + (&pubkey1, &one_lamport_account), + (&pubkey2, &one_lamport_account), + ] + .as_slice(), + )); accounts.add_root_and_flush_write_cache(0); + accounts.store_for_tests((1, [(&pubkey1, &zero_lamport_account)].as_slice())); accounts.add_root_and_flush_write_cache(1); + accounts.store_for_tests(( + 2, + [ + (&pubkey1, &zero_lamport_account), + (&pubkey2, &zero_lamport_account), + ] + .as_slice(), + )); accounts.add_root_and_flush_write_cache(2); - // Ref counts are 1 as the older versions were marked obsolete during flush + // Ref counts are 1 as the older versions (including pubkey1's slot 1 tombstone) were + // reclaimed and marked obsolete during flush assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey1), 1); assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey2), 1); accounts.clean_accounts_for_tests(); - // Slots 0 and 1 should each have been cleaned because all of their - // accounts are zero lamports + // Slots 0 and 1 are cleared because all of their accounts were reclaimed by the + // zero-lamport flushes assert!(accounts.storage.get_slot_storage_entry(0).is_none()); assert!(accounts.storage.get_slot_storage_entry(1).is_none()); - // Slot 2 only has a zero lamport account as well. But, calc_delete_dependencies() - // should exclude slot 2 from the clean due to changes in other slots + // The zero-lamport accounts in slot 2 are newer than the latest full snapshot, so + // their storage is retained and the ref counts are unchanged assert!(accounts.storage.get_slot_storage_entry(2).is_some()); - // Index ref counts should be consistent with the slot stores. Account 1 ref count - // should be 1 since slot 2 is the only alive slot; account 2 should have a ref - // count of 0 due to slot 0 being dead assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey1), 1); - assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey2), 0); + assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey2), 1); // Allow clean to clean any zero lamports up to and including slot 2 accounts.set_latest_full_snapshot_slot(2); accounts.clean_accounts_for_tests(); - // Slot 2 will now be cleaned, which will leave account 1 with a ref count of 0 + // Slot 2 is now cleaned, decrementing both ref counts to 0 assert!(accounts.storage.get_slot_storage_entry(2).is_none()); assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey1), 0); + assert_eq!(accounts.accounts_index.ref_count_from_storage(&pubkey2), 0); } #[test] @@ -2044,6 +2085,9 @@ fn test_clean_retains_secondary_index_for_still_cached_key() { let zero_account = AccountSharedData::new(0, 0, &Pubkey::default()); + // Slot 0: a rooted non-zero version so the tombstone below reaches storage and the index + store_rooted_nonzero_accounts(&accounts, 0, [&pubkey]); + // Slot 1: a rooted zero-lamport tombstone. Flush with `PubkeysToFlush::All` so it is not // reclaimed accounts.store_for_tests((index_slot, [(&pubkey, &zero_account)].as_slice())); @@ -3423,22 +3467,89 @@ fn test_zero_lamport_new_root_not_cleaned() { let account_key = Pubkey::new_unique(); let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); - // Store zero lamport account into slots 0 and 1, root both slots - db.store_for_tests((0, [(&account_key, &zero_lamport_account)].as_slice())); + // Store a rooted non-zero version so the zero-lamport stores below reach storage + store_rooted_nonzero_accounts(&db, 0, [&account_key]); + + // Store zero lamport account into slots 1 and 2, root both slots db.store_for_tests((1, [(&account_key, &zero_lamport_account)].as_slice())); - db.add_root_and_flush_write_cache(0); + db.store_for_tests((2, [(&account_key, &zero_lamport_account)].as_slice())); db.add_root_and_flush_write_cache(1); + db.add_root_and_flush_write_cache(2); - // Only clean zero lamport accounts up to slot 0 - db.clean_accounts(Some(0), false); + // Only clean zero lamport accounts up to slot 1 + db.clean_accounts(Some(1), false); - // Should still be able to find zero lamport account in slot 1 + // Should still be able to find zero lamport account in slot 2 assert_eq!( db.do_load_for_tests(&Ancestors::default(), &account_key), - Some((zero_lamport_account, 1)) + Some((zero_lamport_account, 2)) ); } +/// A zero-lamport account that is not in the accounts index is purged at flush rather than +/// written to storage. The secondary index entries created when it was stored into the write +/// cache must be purged at flush, unless the key is still alive in another cached slot. +#[test] +fn test_flush_purged_zero_lamport_account_purges_secondary_index() { + let accounts = AccountsDb { + account_indexes: spl_token_mint_index_enabled(), + ..AccountsDb::new_with_config( + Vec::new(), + ACCOUNTS_DB_CONFIG_FOR_TESTING, + None, + Arc::default(), + ) + }; + let pubkey_purged = Pubkey::new_unique(); + let pubkey_cached = Pubkey::new_unique(); + let pubkey_live = Pubkey::new_unique(); + + // Set up token accounts to be added to the secondary index. + const SPL_TOKEN_INITIALIZED_OFFSET: usize = 108; + let mint_key = Pubkey::new_unique(); + let mut account_data_with_mint = vec![0; spl_generic_token::token::Account::get_packed_len()]; + account_data_with_mint[..PUBKEY_BYTES].clone_from_slice(&(mint_key.to_bytes())); + account_data_with_mint[SPL_TOKEN_INITIALIZED_OFFSET] = 1; + + let mut live_account = AccountSharedData::new(1, 0, &spl_generic_token::token::id()); + live_account.set_data(account_data_with_mint.clone()); + let mut zero_account = AccountSharedData::new(0, 0, &spl_generic_token::token::id()); + zero_account.set_data(account_data_with_mint); + + // Storing into the cache adds the secondary index entries for all three accounts + accounts.store_for_tests(( + 0, + [ + (&pubkey_purged, &zero_account), + (&pubkey_cached, &zero_account), + (&pubkey_live, &live_account), + ] + .as_slice(), + )); + // pubkey_cached is also stored in unrooted slot 1, so it stays in the cache when slot 0 + // is flushed + accounts.store_for_tests((1, [(&pubkey_cached, &zero_account)].as_slice())); + + accounts.add_root_and_flush_write_cache(0); + + // The zero-lamport accounts were not in the accounts index, so neither was written to + // storage. Only the live account was flushed + let storage = accounts.storage.get_slot_storage_entry(0).unwrap(); + assert_eq!(storage.count(), 1); + assert!(!accounts.contains(&pubkey_purged)); + assert!(accounts.accounts_cache.contains_pubkey(&pubkey_cached)); + assert!(accounts.accounts_index.contains(&pubkey_live)); + + // The purged key left the cache, so its secondary index entries were purged. The other + // keys are still alive (cached and flushed respectively) and must be retained + let mint_index_pubkeys = accounts + .accounts_index + .get_index_key_pubkeys(&IndexKey::SplTokenMint(mint_key)); + assert!(!mint_index_pubkeys.contains(&pubkey_purged)); + assert!(mint_index_pubkeys.contains(&pubkey_cached)); + assert!(mint_index_pubkeys.contains(&pubkey_live)); +} + #[test] fn test_store_load_cached() { let db = AccountsDb::new_single_for_tests(); @@ -4148,15 +4259,17 @@ define_accounts_db_test!( |accounts_db| { let slot: Slot = 0; let num_keys = 10; - let mut pubkeys = vec![]; + let pubkeys: Vec<_> = std::iter::repeat_with(Pubkey::new_unique) + .take(num_keys) + .collect(); + store_rooted_nonzero_accounts(&accounts_db, slot, &pubkeys); + + let slot = slot + 1; // populate storage with zero lamport single ref (zlsr) accounts - for _i in 0..num_keys { + for key in &pubkeys { let zero_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); - - let key = Pubkey::new_unique(); - accounts_db.store_for_tests((slot, &[(&key, &zero_account)][..])); - pubkeys.push(key); + accounts_db.store_for_tests((slot, &[(key, &zero_account)][..])); } accounts_db.add_root(slot); @@ -5478,21 +5591,25 @@ define_accounts_db_test!(test_purge_alive_unrooted_slots_after_clean, |accounts| let unrooted_key = solana_pubkey::new_rand(); let slot0 = 0; let slot1 = 1; + let slot2 = 2; + + // Rooted non-zero version of shared_key so the zero-lamport update below reaches storage + store_rooted_nonzero_accounts(&accounts, slot0, [&shared_key]); // Store accounts with greater than 0 lamports let account = AccountSharedData::new(1, 1, AccountSharedData::default().owner()); - accounts.store_for_tests((slot0, [(&shared_key, &account)].as_slice())); - accounts.store_for_tests((slot0, [(&unrooted_key, &account)].as_slice())); + accounts.store_for_tests((slot1, [(&shared_key, &account)].as_slice())); + accounts.store_for_tests((slot1, [(&unrooted_key, &account)].as_slice())); // Simulate adding dirty pubkeys on bank freeze. Note this is // not a rooted slot // On the next *rooted* slot, update the `shared_key` account to zero lamports let zero_lamport_account = AccountSharedData::new(0, 0, AccountSharedData::default().owner()); - accounts.store_for_tests((slot1, [(&shared_key, &zero_lamport_account)].as_slice())); + accounts.store_for_tests((slot2, [(&shared_key, &zero_lamport_account)].as_slice())); // Simulate adding dirty pubkeys on bank freeze, set root - accounts.add_root_and_flush_write_cache(slot1); + accounts.add_root_and_flush_write_cache(slot2); // Account is referenced in the zero lamport slot. Since the other copy is in an unflushed slot, // it does not count as a reference. @@ -5514,11 +5631,11 @@ define_accounts_db_test!(test_purge_alive_unrooted_slots_after_clean, |accounts| ); // Simulate purge_slot() all from AccountsBackgroundService - accounts.purge_slot(slot0, 0, true); + accounts.purge_slot(slot1, 0, true); // Now the key and slot are purged from the database assert!(!accounts.contains(&shared_key)); - assert_no_storages_at_slot(&accounts, slot0); + assert_no_storages_at_slot(&accounts, slot1); }); /// asserts that not only are there 0 append vecs, but there is not even an entry in the storage map for 'slot' @@ -6194,6 +6311,7 @@ fn test_shrink_collect_simple() { {normal_account_count}" ); let db = AccountsDb::new_single_for_tests(); + let slot4 = 4; let slot5 = 5; // don't do special zero lamport account handling db.set_latest_full_snapshot_slot(0); @@ -6202,6 +6320,24 @@ fn test_shrink_collect_simple() { space, AccountSharedData::default().owner(), ); + + let is_zero_lamport = |pubkey: &Pubkey| { + if Some(pubkey) == pubkey_opposite_zero_lamports { + lamports == 1 + } else { + lamports == 0 + } + }; + + store_rooted_nonzero_accounts( + &db, + slot4, + pubkeys + .iter() + .take(account_count) + .filter(|pubkey| is_zero_lamport(pubkey)), + ); + let mut to_purge = Vec::default(); for pubkey in pubkeys.iter().take(account_count) { // store in append vec and index @@ -6254,13 +6390,6 @@ fn test_shrink_collect_simple() { // a zero-lamport single-ref account is removed from the index by shrink // and carried forward as a tombstone, so it is not rewritten as alive - let is_zero_lamport = |pubkey: &Pubkey| { - if Some(pubkey) == pubkey_opposite_zero_lamports { - lamports == 1 - } else { - lamports == 0 - } - }; let expected_alive_accounts = if alive { pubkeys[..normal_account_count] .iter() @@ -6396,15 +6525,13 @@ fn test_shrink_collect_with_obsolete_accounts() { let mut regular_pubkeys = Vec::new(); let mut obsolete_pubkeys = Vec::new(); - let mut zero_lamport_pubkeys = Vec::new(); let mut unref_pubkeys = Vec::new(); for (i, pubkey) in pubkeys.iter().enumerate() { if i % 3 == 0 { // Mark third account as zero lamport - // These will be removed during shrink + // These are purged at flush since they are not in the index account.set_lamports(0); - zero_lamport_pubkeys.push(*pubkey); } else { // Regular accounts that should be kept account.set_lamports(200); @@ -6420,7 +6547,12 @@ fn test_shrink_collect_with_obsolete_accounts() { let ancestors = Ancestors::from(vec![db.max_root()]); for (i, pubkey) in pubkeys.iter().enumerate() { - // Mark Some accounts obsolete. These will include zero lamport and non zero lamport accounts + // Zero-lamport accounts (every third) are not in the index after flush, so only regular + // accounts can be marked obsolete here. + if i % 3 == 0 { + continue; + } + // Mark Some accounts obsolete. if i % 5 == 0 { // Lookup the pubkey in the database and find the AccountInfo db.accounts_index @@ -6984,19 +7116,13 @@ fn test_new_zero_lamport_accounts_skipped() { 0 ); - // 4. Flush the slot (write cache -> storage). pubkey1 (only ever written as zero) is - // absent; pubkey2 and pubkey3 are now in the index. + // 4. Flush the slot (write cache -> storage). pubkey1 (only ever written as zero) and pubkey2 + // (last written as zero in step 3) are absent; only pubkey3 is in the index. accounts_db.add_root_and_flush_write_cache(slot); assert!(!accounts_db.contains(&pubkey1)); - assert!(accounts_db.contains(&pubkey2)); + assert!(!accounts_db.contains(&pubkey2)); assert!(accounts_db.contains(&pubkey3)); - // Verify pubkey2 is present in slot in the index with a zero-lamport AccountInfo. - assert!(accounts_db.accounts_index.get_and_then(&pubkey2, |entry| { - let account_info = *entry.unwrap().slot_list_read_lock().first().unwrap(); - (false, account_info.1.is_zero_lamport()) - })); - // 5. Add a non-zero lamport account for a pubkey that was previously only written as zero // (pubkey1) and verify the pubkey is added to the write cache. let slot = slot + 1; @@ -7210,6 +7336,9 @@ fn test_is_ancestor_zero_lamport_index_only() { let pubkey = Pubkey::new_unique(); let slot = 5; + store_rooted_nonzero_accounts(&db, slot, [&pubkey]); + let slot = slot + 1; + let zero_account = AccountSharedData::new(0, 0, &Pubkey::default()); db.store_for_tests((slot, [(&pubkey, &zero_account)].as_slice())); db.add_root_and_flush_write_cache(slot); diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index fd9d6aee8c0..d63bd1bf390 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -8662,10 +8662,13 @@ fn do_test_clean_dropped_unrooted_banks(freeze_bank1: FreezeBank1) { let key2 = Keypair::new(); // only touched in bank2 let key3 = Keypair::new(); // touched in both bank1 and bank2 let key4 = Keypair::new(); // in only bank1, and has zero lamports - let key5 = Keypair::new(); // in both bank1 and bank2, and has zero lamports + let key5 = Keypair::new(); // rooted in bank0, zero lamports in bank1 and bank2 bank0 .transfer(amount, &mint_keypair, &key2.pubkey()) .unwrap(); + // key5's rooted non-zero-lamport account makes bank2's zero-lamport update reach storage as a + // kill at flush, so clean has a ref count to decrement in scenario 4 + bank0.store_account(&key5.pubkey(), &AccountSharedData::new(1, 0, &owner)); bank0.freeze(); let slot = 1; @@ -11161,6 +11164,11 @@ where let account = AccountSharedData::new(1, len1, &program); bank.store_account(&bob_pubkey, &account); + // Root and flush `slot` so bob's account is in storage and the index. + // This ensures when bob's account is closed (zero lamports) in the next slot that that version + // also reaches storage, rather than being skipped at flush. + add_root_and_flush_write_cache(&bank); + // create the next bank where we will store a zero-lamport account to be cleaned let slot = bank.slot() + 1; let bank = Bank::new_from_parent_with_bank_forks(bank_forks.as_ref(), bank, leader, slot); diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 3569fffc2d3..f6112666529 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -422,7 +422,7 @@ mod serde_snapshot_tests { let mut current_slot = 1; accounts.store_for_tests((current_slot, [(&pubkey, &account)].as_slice())); - accounts.add_root(current_slot); + accounts.add_root_and_flush_write_cache(current_slot); current_slot += 1; accounts.store_for_tests((current_slot, [(&pubkey, &zero_lamport_account)].as_slice())); From df74b5d07187de1818a673b0d150cf6a43f790b1 Mon Sep 17 00:00:00 2001 From: alex-lind1 Date: Fri, 17 Jul 2026 16:05:25 -0400 Subject: [PATCH 21/30] Clear stale dead state on no-op block switch (#13642) --- core/src/replay_stage.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index a83473825a9..138265ddb04 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -2453,6 +2453,7 @@ impl ReplayStage { let mut ancestor_slot = block.slot; let mut ancestor_block_id = block.block_id; let mut blocks_to_switch = vec![]; + let mut original_dead_slots_to_clear = BTreeSet::new(); loop { if ancestor_slot <= root { // This is either (1) an outdated attempt to switch out the @@ -2473,8 +2474,11 @@ impl ReplayStage { return Ok(()); }; - if location != BlockLocation::Original { - // Need to switch this block + if location == BlockLocation::Original { + if blockstore.is_dead(ancestor_slot) { + original_dead_slots_to_clear.insert(ancestor_slot); + } + } else { blocks_to_switch.push((ancestor_slot, location)); } @@ -2500,10 +2504,12 @@ impl ReplayStage { ancestor_slot = parent_slot; } - let slots_to_clear = bank_forks - .read() - .unwrap() - .slots_to_clear(blocks_to_switch.iter().map(|(slot, _)| *slot)); + let slots_to_clear = bank_forks.read().unwrap().slots_to_clear( + blocks_to_switch + .iter() + .map(|(slot, _)| *slot) + .chain(original_dead_slots_to_clear.iter().copied()), + ); info!("{my_pubkey}: Clearing banks for switching and descendants: {slots_to_clear:?}"); Self::clear_banks( @@ -2523,6 +2529,10 @@ impl ReplayStage { info!("{my_pubkey}: Switched {slot} from {location:?}"); } + for slot in original_dead_slots_to_clear { + blockstore.remove_dead_slot(slot)?; + } + *pending_switch = None; Ok(()) From 79d5bf2e4f4cdbe6237a682b887ffed46c729939 Mon Sep 17 00:00:00 2001 From: Brooks Date: Fri, 17 Jul 2026 16:18:08 -0400 Subject: [PATCH 22/30] accounts-db: Inlines index thread pool selection for flush (#13927) --- accounts-db/src/accounts_db.rs | 19 +++---------------- accounts-db/src/accounts_db/tests.rs | 1 - 2 files changed, 3 insertions(+), 17 deletions(-) diff --git a/accounts-db/src/accounts_db.rs b/accounts-db/src/accounts_db.rs index 13e82db1606..d2918b99819 100644 --- a/accounts-db/src/accounts_db.rs +++ b/accounts-db/src/accounts_db.rs @@ -4753,7 +4753,6 @@ impl AccountsDb { (slot, &accounts[..]), &flushed_store, reclaim_method, - UpdateIndexThreadSelection::PoolWithThreshold, )); flush_stats.accumulate_store_accounts_for_flush(store_accounts_for_flush_stats); flush_stats.store_accounts_total_us += Saturating(store_accounts_for_flush_us); @@ -5138,8 +5137,6 @@ impl AccountsDb { infos: Vec, accounts: &impl StorableAccounts<'a>, reclaim: UpsertReclaim, - update_index_thread_selection: UpdateIndexThreadSelection, - thread_pool: &ThreadPool, ) -> Vec> { let target_slot = accounts.target_slot(); let len = std::cmp::min(accounts.len(), infos.len()); @@ -5176,11 +5173,8 @@ impl AccountsDb { }; let threshold = 1; - if matches!( - update_index_thread_selection, - UpdateIndexThreadSelection::PoolWithThreshold, - ) && len > threshold - { + if len > threshold { + let thread_pool = &self.thread_pool_background; let chunk_size = len.div_ceil(thread_pool.current_num_threads()); let batches = 1 + len / chunk_size; thread_pool.install(|| { @@ -5725,7 +5719,6 @@ impl AccountsDb { accounts: impl StorableAccounts<'a>, storage: &AccountStorageEntry, reclaim_handling: UpsertReclaim, - update_index_thread_selection: UpdateIndexThreadSelection, ) -> StoreAccountsForFlushStats { let slot = accounts.target_slot(); @@ -5742,13 +5735,7 @@ impl AccountsDb { let mark_zero_lamport_us = mark_zero_lamport_time.end_as_us(); let update_index_time = Measure::start("update_index"); - let reclaims = self.update_index_for_flush( - infos, - &accounts, - reclaim_handling, - update_index_thread_selection, - &self.thread_pool_background, - ); + let reclaims = self.update_index_for_flush(infos, &accounts, reclaim_handling); let update_index_us = update_index_time.end_as_us(); // If there are any reclaims then they should be handled. Reclaims affect diff --git a/accounts-db/src/accounts_db/tests.rs b/accounts-db/src/accounts_db/tests.rs index 393da93bd9b..840d52b4f47 100644 --- a/accounts-db/src/accounts_db/tests.rs +++ b/accounts-db/src/accounts_db/tests.rs @@ -632,7 +632,6 @@ fn test_flush_slots_with_reclaim_old_slots() { (new_slot, &accounts_list[..]), &storage, UpsertReclaim::ReclaimOldSlots, - UpdateIndexThreadSelection::Inline, ); // Remove the flushed slot from the cache From a94110482eb42be9b5bc3c404038b905c7051c4b Mon Sep 17 00:00:00 2001 From: alex-lind1 Date: Fri, 17 Jul 2026 17:11:29 -0400 Subject: [PATCH 23/30] Revert "geyser: Include bank_id in transaction notifications (#13545)" (#13932) This reverts commit 8d996d99f4fcf1934412cad9038ce8a7222b80fb. --- accounts-db/src/accounts.rs | 6 - .../src/accounts_db/geyser_plugin_utils.rs | 42 +--- .../src/accounts_update_notifier_interface.rs | 3 +- core/src/banking_stage/committer.rs | 1 - core/src/cluster_info_vote_listener.rs | 2 +- core/src/replay_stage.rs | 14 +- core/src/replay_stage/tests.rs | 8 +- core/src/tpu_entry_notifier.rs | 9 +- .../src/geyser_plugin_interface.rs | 145 +----------- .../src/accounts_update_notifier.rs | 81 +------ .../src/block_metadata_notifier.rs | 97 +------- .../src/block_metadata_notifier_interface.rs | 5 +- geyser-plugin-manager/src/entry_notifier.rs | 101 +------- .../src/slot_status_notifier.rs | 115 ++------- .../src/slot_status_observer.rs | 26 +- .../src/transaction_notifier.rs | 14 +- ledger/src/blockstore_processor.rs | 2 - ledger/src/entry_notifier_interface.rs | 7 +- ledger/src/entry_notifier_service.rs | 6 +- .../optimistically_confirmed_bank_tracker.rs | 223 ++++++------------ rpc/src/rpc.rs | 13 +- rpc/src/rpc_subscriptions.rs | 52 ++-- rpc/src/slot_status_notifier.rs | 10 +- rpc/src/transaction_notifier_interface.rs | 8 +- rpc/src/transaction_status_service.rs | 14 +- runtime/src/bank.rs | 11 +- runtime/src/bank/tests.rs | 4 +- runtime/src/conformance/txn.rs | 4 +- runtime/src/serde_snapshot/tests.rs | 7 +- runtime/src/transaction_execution.rs | 6 +- votor/src/root_utils.rs | 20 +- 31 files changed, 204 insertions(+), 852 deletions(-) diff --git a/accounts-db/src/accounts.rs b/accounts-db/src/accounts.rs index e0058f77f43..94919fcd43c 100644 --- a/accounts-db/src/accounts.rs +++ b/accounts-db/src/accounts.rs @@ -499,13 +499,11 @@ impl Accounts { pub fn store_accounts_seq<'a>( &self, accounts: impl StorableAccounts<'a>, - bank_id: BankId, transactions: Option<&'a [&'a SanitizedTransaction]>, ancestors: &Ancestors, ) { self._store_accounts( accounts, - bank_id, transactions, UpdateIndexThreadSelection::Inline, ancestors, @@ -519,13 +517,11 @@ impl Accounts { pub fn store_accounts_par<'a>( &self, accounts: impl StorableAccounts<'a>, - bank_id: BankId, transactions: Option<&'a [&'a SanitizedTransaction]>, ancestors: &Ancestors, ) { self._store_accounts( accounts, - bank_id, transactions, UpdateIndexThreadSelection::PoolWithThreshold, ancestors, @@ -540,7 +536,6 @@ impl Accounts { fn _store_accounts<'a>( &self, accounts: impl StorableAccounts<'a>, - bank_id: BankId, transactions: Option<&'a [&'a SanitizedTransaction]>, update_index_thread_selection: UpdateIndexThreadSelection, ancestors: &Ancestors, @@ -557,7 +552,6 @@ impl Accounts { accounts.account_for_geyser(index, |pubkey, account_shared_data| { accounts_db.notify_account_at_accounts_update( slot, - bank_id, account_shared_data, &transaction, pubkey, diff --git a/accounts-db/src/accounts_db/geyser_plugin_utils.rs b/accounts-db/src/accounts_db/geyser_plugin_utils.rs index b6e42b6408d..ef13a07862e 100644 --- a/accounts-db/src/accounts_db/geyser_plugin_utils.rs +++ b/accounts-db/src/accounts_db/geyser_plugin_utils.rs @@ -1,16 +1,12 @@ use { - crate::accounts_db::AccountsDb, - solana_account::AccountSharedData, - solana_clock::{BankId, Slot}, - solana_pubkey::Pubkey, - solana_transaction::sanitized::SanitizedTransaction, + crate::accounts_db::AccountsDb, solana_account::AccountSharedData, solana_clock::Slot, + solana_pubkey::Pubkey, solana_transaction::sanitized::SanitizedTransaction, }; impl AccountsDb { pub fn notify_account_at_accounts_update( &self, slot: Slot, - bank_id: BankId, account: &AccountSharedData, txn: &Option<&SanitizedTransaction>, pubkey: &Pubkey, @@ -19,7 +15,6 @@ impl AccountsDb { if let Some(accounts_update_notifier) = &self.accounts_update_notifier { accounts_update_notifier.notify_account_update( slot, - bank_id, account, txn, pubkey, @@ -70,7 +65,6 @@ mod tests { fn notify_account_update( &self, slot: Slot, - _bank_id: BankId, account: &AccountSharedData, _txn: &Option<&SanitizedTransaction>, pubkey: &Pubkey, @@ -188,48 +182,26 @@ mod tests { let account1 = AccountSharedData::new(account1_lamports1, 1, AccountSharedData::default().owner()); let slot0 = 0; - let bank_id0 = 100; let mut ancestors = Ancestors::from(vec![slot0]); - accounts.store_accounts_seq( - (slot0, &[(&key1, &account1)][..]), - bank_id0, - None, - &ancestors, - ); + accounts.store_accounts_seq((slot0, &[(&key1, &account1)][..]), None, &ancestors); let key2 = solana_pubkey::new_rand(); let account2_lamports: u64 = 200; let account2 = AccountSharedData::new(account2_lamports, 1, AccountSharedData::default().owner()); - accounts.store_accounts_seq( - (slot0, &[(&key2, &account2)][..]), - bank_id0, - None, - &ancestors, - ); + accounts.store_accounts_seq((slot0, &[(&key2, &account2)][..]), None, &ancestors); let account1_lamports2 = 2; let slot1 = 1; - let bank_id1 = 101; ancestors.insert(slot1); let account1 = AccountSharedData::new(account1_lamports2, 1, account1.owner()); - accounts.store_accounts_seq( - (slot1, &[(&key1, &account1)][..]), - bank_id1, - None, - &ancestors, - ); + accounts.store_accounts_seq((slot1, &[(&key1, &account1)][..]), None, &ancestors); let key3 = solana_pubkey::new_rand(); let account3_lamports: u64 = 300; let account3 = AccountSharedData::new(account3_lamports, 1, AccountSharedData::default().owner()); - accounts.store_accounts_seq( - (slot1, &[(&key3, &account3)][..]), - bank_id1, - None, - &ancestors, - ); + accounts.store_accounts_seq((slot1, &[(&key3, &account3)][..]), None, &ancestors); assert_eq!(notifier.accounts_notified.get(&key1).unwrap().len(), 2); assert_eq!( @@ -283,13 +255,11 @@ mod tests { let slot_close = slot_open + 1; accounts.store_accounts_seq( (slot_open, [(&address, &account_open)].as_slice()), - 106, None, &Ancestors::from(vec![slot_open]), ); accounts.store_accounts_seq( (slot_close, [(&address, &account_close)].as_slice()), - 107, None, &Ancestors::from(vec![slot_open, slot_close]), ); diff --git a/accounts-db/src/accounts_update_notifier_interface.rs b/accounts-db/src/accounts_update_notifier_interface.rs index 3f742b4ef8f..99401d4585a 100644 --- a/accounts-db/src/accounts_update_notifier_interface.rs +++ b/accounts-db/src/accounts_update_notifier_interface.rs @@ -1,6 +1,6 @@ use { solana_account::{AccountSharedData, ReadableAccount}, - solana_clock::{BankId, Epoch, Slot}, + solana_clock::{Epoch, Slot}, solana_pubkey::Pubkey, solana_transaction::sanitized::SanitizedTransaction, std::sync::Arc, @@ -14,7 +14,6 @@ pub trait AccountsUpdateNotifierInterface: std::fmt::Debug { fn notify_account_update( &self, slot: Slot, - bank_id: BankId, account: &AccountSharedData, txn: &Option<&SanitizedTransaction>, pubkey: &Pubkey, diff --git a/core/src/banking_stage/committer.rs b/core/src/banking_stage/committer.rs index ea87bedd4ef..ae06aa80a28 100644 --- a/core/src/banking_stage/committer.rs +++ b/core/src/banking_stage/committer.rs @@ -178,7 +178,6 @@ impl Committer { transaction_status_sender.send_transaction_status_batch( bank.slot(), - bank.bank_id(), txs, commit_results, balances, diff --git a/core/src/cluster_info_vote_listener.rs b/core/src/cluster_info_vote_listener.rs index d7cbef4a37e..d9c28700758 100644 --- a/core/src/cluster_info_vote_listener.rs +++ b/core/src/cluster_info_vote_listener.rs @@ -770,7 +770,7 @@ impl ClusterInfoVoteListener { sender .sender .send(( - BankNotification::OptimisticallyConfirmed(last_vote_slot, last_vote_hash), + BankNotification::OptimisticallyConfirmed(last_vote_slot), dependency_work, )) .unwrap_or_else(|err| warn!("bank_notification_sender failed: {err:?}")); diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 138265ddb04..27eea15c0b8 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -4253,7 +4253,6 @@ impl ReplayStage { bank.parent_slot(), &parent_blockhash.to_string(), bank.slot(), - bank.bank_id(), &bank.last_blockhash().to_string(), &bank.get_rewards_and_num_partitions(), Some(bank.clock().unix_timestamp), @@ -5438,16 +5437,13 @@ impl ReplayStage { if let Some(rpc_subscriptions) = rpc_subscriptions { rpc_subscriptions.notify_slot(slot, parent.slot(), root_slot); } - let parent_slot = parent.slot(); - let bank = Bank::new_from_parent_with_options(parent, leader, slot, new_bank_options); if let Some(slot_status_notifier) = slot_status_notifier { - slot_status_notifier.read().unwrap().notify_created_bank( - slot, - parent_slot, - bank.bank_id(), - ); + slot_status_notifier + .read() + .unwrap() + .notify_created_bank(slot, parent.slot()); } - bank + Bank::new_from_parent_with_options(parent, leader, slot, new_bank_options) } fn log_heaviest_fork_failures( diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 5d436104c87..4d95aaa203c 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -910,17 +910,17 @@ impl SlotStatusNotifierForTest { } impl SlotStatusNotifierInterface for SlotStatusNotifierForTest { - fn notify_slot_confirmed(&self, _slot: Slot, _parent: Option, _bank_id: BankId) {} + fn notify_slot_confirmed(&self, _slot: Slot, _parent: Option) {} - fn notify_slot_processed(&self, _slot: Slot, _parent: Option, _bank_id: BankId) {} + fn notify_slot_processed(&self, _slot: Slot, _parent: Option) {} - fn notify_slot_rooted(&self, _slot: Slot, _parent: Option, _bank_id: BankId) {} + fn notify_slot_rooted(&self, _slot: Slot, _parent: Option) {} fn notify_first_shred_received(&self, _slot: Slot) {} fn notify_completed(&self, _slot: Slot) {} - fn notify_created_bank(&self, _slot: Slot, _parent: Slot, _bank_id: BankId) {} + fn notify_created_bank(&self, _slot: Slot, _parent: Slot) {} fn notify_slot_dead(&self, slot: Slot, _parent: Slot, _error: String) { self.dead_slots.lock().unwrap().insert(slot); diff --git a/core/src/tpu_entry_notifier.rs b/core/src/tpu_entry_notifier.rs index 55596839288..d95f799d025 100644 --- a/core/src/tpu_entry_notifier.rs +++ b/core/src/tpu_entry_notifier.rs @@ -1,6 +1,5 @@ use { crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, - solana_clock::BankId, solana_entry::{entry::EntrySummary, entry_or_marker::EntryOrMarker}, solana_ledger::entry_notifier_service::{EntryNotification, EntryNotifierSender}, solana_poh::poh_recorder::WorkingBankEntryOrMarker, @@ -29,7 +28,6 @@ impl TpuEntryNotifier { .name("solTpuEntry".to_string()) .spawn(move || { let mut current_slot = 0; - let mut current_bank_id = BankId::default(); let mut current_index = 0; let mut current_transaction_index = 0; loop { @@ -43,7 +41,6 @@ impl TpuEntryNotifier { &entry_notification_sender, &broadcast_entry_sender, &mut current_slot, - &mut current_bank_id, &mut current_index, &mut current_transaction_index, ) { @@ -61,19 +58,16 @@ impl TpuEntryNotifier { entry_notification_sender: &EntryNotifierSender, broadcast_entry_sender: &Sender, current_slot: &mut u64, - current_bank_id: &mut BankId, current_index: &mut usize, current_transaction_index: &mut usize, ) -> Result<(), RecvTimeoutError> { let (bank, (entry_or_marker, tick_height)) = entry_receiver.recv_timeout(Duration::from_secs(1))?; let slot = bank.slot(); - let bank_id = bank.bank_id(); - if slot != *current_slot || bank_id != *current_bank_id { + if slot != *current_slot { *current_index = 0; *current_transaction_index = 0; *current_slot = slot; - *current_bank_id = bank_id; }; let index = *current_index; @@ -85,7 +79,6 @@ impl TpuEntryNotifier { }; if let Err(err) = entry_notification_sender.send(EntryNotification { slot, - bank_id, index, entry: entry_summary, starting_transaction_index: *current_transaction_index, diff --git a/geyser-plugin-interface/src/geyser_plugin_interface.rs b/geyser-plugin-interface/src/geyser_plugin_interface.rs index cc90677e376..d50131f0c3d 100644 --- a/geyser-plugin-interface/src/geyser_plugin_interface.rs +++ b/geyser-plugin-interface/src/geyser_plugin_interface.rs @@ -3,7 +3,7 @@ //! In addition, the dynamic library must export a "C" function _create_plugin which //! creates the implementation of the plugin. use { - solana_clock::{BankId, Slot, UnixTimestamp}, + solana_clock::{Slot, UnixTimestamp}, solana_hash::Hash, solana_message::v0::LoadedAddresses, solana_signature::Signature, @@ -110,59 +110,15 @@ pub struct ReplicaAccountInfoV3<'a> { pub txn: Option<&'a SanitizedTransaction>, } -#[derive(Debug, Clone, PartialEq, Eq)] -#[repr(C)] -/// Information about an account being updated -/// (extended with reference to transaction doing this update and bank id) -pub struct ReplicaAccountInfoV4<'a> { - /// The Pubkey for the account - pub pubkey: &'a [u8], - - /// The lamports for the account - pub lamports: u64, - - /// The Pubkey of the owner program account - pub owner: &'a [u8], - - /// This account's data contains a loaded program (and is now read-only) - pub executable: bool, - - /// The epoch at which this account will next owe rent - pub rent_epoch: u64, - - /// The data held in this account. - pub data: &'a [u8], - - /// A global monotonically increasing atomic number, which can be used - /// to tell the order of the account update. For example, when an - /// account is updated in the same slot multiple times, the update - /// with higher write_version should supersede the one with lower - /// write_version. - pub write_version: u64, - - /// Reference to transaction causing this account modification - pub txn: Option<&'a SanitizedTransaction>, - - /// The id of the bank that processed the account update. - /// - /// This is `None` for account updates restored from a snapshot because they - /// are not associated with a live bank. - pub bank_id: Option, -} - /// A wrapper to future-proof ReplicaAccountInfo handling. /// If there were a change to the structure of ReplicaAccountInfo, /// there would be new enum entry for the newer version, forcing /// plugin implementations to handle the change. #[repr(u32)] pub enum ReplicaAccountInfoVersions<'a> { - #[deprecated] V0_0_1(&'a ReplicaAccountInfo<'a>), - #[deprecated] V0_0_2(&'a ReplicaAccountInfoV2<'a>), - #[deprecated] V0_0_3(&'a ReplicaAccountInfoV3<'a>), - V0_0_4(&'a ReplicaAccountInfoV4<'a>), } /// Information about a transaction @@ -225,45 +181,15 @@ pub struct ReplicaTransactionInfoV3<'a> { pub index: usize, } -/// Information about a transaction, including index in block and bank id -#[derive(Clone, Debug)] -#[repr(C)] -pub struct ReplicaTransactionInfoV4<'a> { - /// The transaction signature, used for identifying the transaction. - pub signature: &'a Signature, - - /// The transaction message hash, used for identifying the transaction. - pub message_hash: &'a Hash, - - /// Indicates if the transaction is a simple vote transaction. - pub is_vote: bool, - - /// The versioned transaction. - pub transaction: &'a VersionedTransaction, - - /// Metadata of the transaction status. - pub transaction_status_meta: &'a TransactionStatusMeta, - - /// The transaction's index in the block - pub index: usize, - - /// The id of the bank that processed the transaction. - pub bank_id: BankId, -} - /// A wrapper to future-proof ReplicaTransactionInfo handling. /// If there were a change to the structure of ReplicaTransactionInfo, /// there would be new enum entry for the newer version, forcing /// plugin implementations to handle the change. #[repr(u32)] pub enum ReplicaTransactionInfoVersions<'a> { - #[deprecated] V0_0_1(&'a ReplicaTransactionInfo<'a>), - #[deprecated] V0_0_2(&'a ReplicaTransactionInfoV2<'a>), - #[deprecated] V0_0_3(&'a ReplicaTransactionInfoV3<'a>), - V0_0_4(&'a ReplicaTransactionInfoV4<'a>), } /// Information about a transaction after deshredding (when entries are formed from shreds). @@ -361,36 +287,13 @@ pub struct ReplicaEntryInfoV2<'a> { pub starting_transaction_index: usize, } -#[derive(Clone, Debug)] -#[repr(C)] -pub struct ReplicaEntryInfoV3<'a> { - /// The slot number of the block containing this Entry - pub slot: Slot, - /// The id of the bank that executed this Entry. - pub bank_id: BankId, - /// The Entry's index in the block - pub index: usize, - /// The number of hashes since the previous Entry - pub num_hashes: u64, - /// The Entry's SHA-256 hash, generated from the previous Entry's hash with - /// `solana_entry::entry::next_hash()` - pub hash: &'a [u8], - /// The number of executed transactions in the Entry - pub executed_transaction_count: u64, - /// The index-in-block of the first executed transaction in this Entry - pub starting_transaction_index: usize, -} - /// A wrapper to future-proof ReplicaEntryInfo handling. To make a change to the structure of /// ReplicaEntryInfo, add an new enum variant wrapping a newer version, which will force plugin /// implementations to handle the change. #[repr(u32)] pub enum ReplicaEntryInfoVersions<'a> { - #[deprecated] V0_0_1(&'a ReplicaEntryInfo<'a>), - #[deprecated] V0_0_2(&'a ReplicaEntryInfoV2<'a>), - V0_0_3(&'a ReplicaEntryInfoV3<'a>), } #[derive(Clone, Debug)] @@ -447,33 +350,12 @@ pub struct ReplicaBlockInfoV4<'a> { pub entry_count: u64, } -/// Extending ReplicaBlockInfoV4 by sending bank id. -#[derive(Clone, Debug)] -#[repr(C)] -pub struct ReplicaBlockInfoV5<'a> { - pub parent_slot: Slot, - pub parent_blockhash: &'a str, - pub slot: Slot, - pub bank_id: BankId, - pub blockhash: &'a str, - pub rewards: &'a RewardsAndNumPartitions, - pub block_time: Option, - pub block_height: Option, - pub executed_transaction_count: u64, - pub entry_count: u64, -} - #[repr(u32)] pub enum ReplicaBlockInfoVersions<'a> { - #[deprecated] V0_0_1(&'a ReplicaBlockInfo<'a>), - #[deprecated] V0_0_2(&'a ReplicaBlockInfoV2<'a>), - #[deprecated] V0_0_3(&'a ReplicaBlockInfoV3<'a>), - #[deprecated] V0_0_4(&'a ReplicaBlockInfoV4<'a>), - V0_0_5(&'a ReplicaBlockInfoV5<'a>), } /// A snapshot of a validator's gossip contact info at a point in time. @@ -717,8 +599,7 @@ pub trait GeyserPlugin: Any + Send + Sync + std::fmt::Debug { Ok(()) } - /// Called when a slot status is updated. - #[deprecated] + /// Called when a slot status is updated #[allow(unused_variables)] fn update_slot_status( &self, @@ -729,28 +610,6 @@ pub trait GeyserPlugin: Any + Send + Sync + std::fmt::Debug { Ok(()) } - /// Called when a slot status is updated. - /// - /// `bank_id` identifies the concrete bank instance associated with the - /// status update. It is `Some` for bank-scoped statuses, where the slot - /// status is tied to a particular `Bank`: `CreatedBank`, `Processed`, - /// `Confirmed`, and `Rooted`. - /// - /// It is `None` for shred- or slot-level statuses that are not associated - /// with a specific bank instance: `FirstShredReceived`, `Completed`, and - /// `Dead`. - #[allow(deprecated)] - #[allow(unused_variables)] - fn update_slot_status_v2( - &self, - slot: Slot, - parent: Option, - status: &SlotStatus, - bank_id: Option, - ) -> Result<()> { - self.update_slot_status(slot, parent, status) - } - /// Called when a transaction is processed in a slot. #[allow(unused_variables)] fn notify_transaction( diff --git a/geyser-plugin-manager/src/accounts_update_notifier.rs b/geyser-plugin-manager/src/accounts_update_notifier.rs index dc41f9b3da2..f811f8f145c 100644 --- a/geyser-plugin-manager/src/accounts_update_notifier.rs +++ b/geyser-plugin-manager/src/accounts_update_notifier.rs @@ -2,7 +2,7 @@ use { crate::geyser_plugin_manager::GeyserPluginManager, agave_geyser_plugin_interface::geyser_plugin_interface::{ - ReplicaAccountInfoV4, ReplicaAccountInfoVersions, + ReplicaAccountInfoV3, ReplicaAccountInfoVersions, }, arc_swap::ArcSwap, log::*, @@ -10,7 +10,7 @@ use { solana_accounts_db::accounts_update_notifier_interface::{ AccountForGeyser, AccountsUpdateNotifierInterface, }, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_pubkey::Pubkey, solana_transaction::sanitized::SanitizedTransaction, std::sync::Arc, @@ -29,19 +29,13 @@ impl AccountsUpdateNotifierInterface for AccountsUpdateNotifierImpl { fn notify_account_update( &self, slot: Slot, - bank_id: BankId, account: &AccountSharedData, txn: &Option<&SanitizedTransaction>, pubkey: &Pubkey, write_version: u64, ) { - let account_info = self.accountinfo_from_shared_account_data( - Some(bank_id), - account, - txn, - pubkey, - write_version, - ); + let account_info = + self.accountinfo_from_shared_account_data(account, txn, pubkey, write_version); self.notify_plugins_of_account_update(account_info, slot, false); } @@ -95,13 +89,12 @@ impl AccountsUpdateNotifierImpl { fn accountinfo_from_shared_account_data<'a>( &self, - bank_id: Option, account: &'a AccountSharedData, txn: &'a Option<&'a SanitizedTransaction>, pubkey: &'a Pubkey, write_version: u64, - ) -> ReplicaAccountInfoV4<'a> { - ReplicaAccountInfoV4 { + ) -> ReplicaAccountInfoV3<'a> { + ReplicaAccountInfoV3 { pubkey: pubkey.as_ref(), lamports: account.lamports(), owner: account.owner().as_ref(), @@ -110,15 +103,14 @@ impl AccountsUpdateNotifierImpl { data: account.data(), write_version, txn: *txn, - bank_id, } } fn accountinfo_from_account_for_geyser<'a>( &self, account: &'a AccountForGeyser<'_>, - ) -> ReplicaAccountInfoV4<'a> { - ReplicaAccountInfoV4 { + ) -> ReplicaAccountInfoV3<'a> { + ReplicaAccountInfoV3 { pubkey: account.pubkey.as_ref(), lamports: account.lamports(), owner: account.owner().as_ref(), @@ -127,13 +119,12 @@ impl AccountsUpdateNotifierImpl { data: account.data(), write_version: 0, // can/will be populated afterwards txn: None, - bank_id: None, } } fn notify_plugins_of_account_update( &self, - account: ReplicaAccountInfoV4, + account: ReplicaAccountInfoV3, slot: Slot, is_startup: bool, ) { @@ -147,7 +138,7 @@ impl AccountsUpdateNotifierImpl { continue; } match plugin.update_account( - ReplicaAccountInfoVersions::V0_0_4(&account), + ReplicaAccountInfoVersions::V0_0_3(&account), slot, is_startup, ) { @@ -185,7 +176,7 @@ mod tests { libloading::Library, solana_accounts_db::accounts_update_notifier_interface::AccountsUpdateNotifierInterface, std::sync::{ - Arc, Mutex, + Arc, atomic::{AtomicUsize, Ordering}, }, }; @@ -195,7 +186,6 @@ mod tests { name: &'static str, account_updates_enabled: bool, account_update_count: Arc, - account_update_bank_ids: Arc>>>, } impl GeyserPlugin for TestAccountPlugin { @@ -205,17 +195,10 @@ mod tests { fn update_account( &self, - account: ReplicaAccountInfoVersions, + _account: ReplicaAccountInfoVersions, _slot: Slot, _is_startup: bool, ) -> agave_geyser_plugin_interface::geyser_plugin_interface::Result<()> { - let ReplicaAccountInfoVersions::V0_0_4(account) = account else { - panic!("expected V0_0_4 account info"); - }; - self.account_update_bank_ids - .lock() - .unwrap() - .push(account.bank_id); self.account_update_count.fetch_add(1, Ordering::Relaxed); Ok(()) } @@ -242,65 +225,27 @@ mod tests { fn test_notify_account_update_skips_plugins_with_account_notifications_disabled() { let enabled_count = Arc::new(AtomicUsize::new(0)); let disabled_count = Arc::new(AtomicUsize::new(0)); - let enabled_bank_ids = Arc::new(Mutex::new(Vec::new())); - let disabled_bank_ids = Arc::new(Mutex::new(Vec::new())); let plugin_manager = Arc::new(ArcSwap::from(Arc::new(GeyserPluginManager { plugins: vec![ loaded_test_plugin(TestAccountPlugin { name: "enabled", account_updates_enabled: true, account_update_count: enabled_count.clone(), - account_update_bank_ids: enabled_bank_ids.clone(), }), loaded_test_plugin(TestAccountPlugin { name: "disabled", account_updates_enabled: false, account_update_count: disabled_count.clone(), - account_update_bank_ids: disabled_bank_ids.clone(), }), ], }))); let notifier = AccountsUpdateNotifierImpl::new(plugin_manager, false); let account = AccountSharedData::new(1, 0, &Pubkey::new_unique()); let pubkey = Pubkey::new_unique(); - let bank_id = 9; - notifier.notify_account_update(42, bank_id, &account, &None, &pubkey, 7); + notifier.notify_account_update(42, &account, &None, &pubkey, 7); assert_eq!(enabled_count.load(Ordering::Relaxed), 1); assert_eq!(disabled_count.load(Ordering::Relaxed), 0); - assert_eq!(*enabled_bank_ids.lock().unwrap(), vec![Some(bank_id)]); - assert!(disabled_bank_ids.lock().unwrap().is_empty()); - } - - #[test] - fn test_notify_account_restore_from_snapshot_has_no_bank_id() { - let account_update_count = Arc::new(AtomicUsize::new(0)); - let account_update_bank_ids = Arc::new(Mutex::new(Vec::new())); - let plugin_manager = Arc::new(ArcSwap::from(Arc::new(GeyserPluginManager { - plugins: vec![loaded_test_plugin(TestAccountPlugin { - name: "enabled", - account_updates_enabled: true, - account_update_count: account_update_count.clone(), - account_update_bank_ids: account_update_bank_ids.clone(), - })], - }))); - let notifier = AccountsUpdateNotifierImpl::new(plugin_manager, true); - let pubkey = Pubkey::new_unique(); - let owner = Pubkey::new_unique(); - let data = [1, 2, 3]; - let account = AccountForGeyser { - pubkey: &pubkey, - lamports: 1, - owner: &owner, - executable: false, - rent_epoch: 0, - data: &data, - }; - - notifier.notify_account_restore_from_snapshot(42, 7, &account); - - assert_eq!(account_update_count.load(Ordering::Relaxed), 1); - assert_eq!(*account_update_bank_ids.lock().unwrap(), vec![None]); } } diff --git a/geyser-plugin-manager/src/block_metadata_notifier.rs b/geyser-plugin-manager/src/block_metadata_notifier.rs index b50f52f7cd1..0703eac8083 100644 --- a/geyser-plugin-manager/src/block_metadata_notifier.rs +++ b/geyser-plugin-manager/src/block_metadata_notifier.rs @@ -4,11 +4,11 @@ use { geyser_plugin_manager::GeyserPluginManager, }, agave_geyser_plugin_interface::geyser_plugin_interface::{ - ReplicaBlockInfoV5, ReplicaBlockInfoVersions, + ReplicaBlockInfoV4, ReplicaBlockInfoVersions, }, arc_swap::ArcSwap, log::*, - solana_clock::{BankId, UnixTimestamp}, + solana_clock::UnixTimestamp, solana_runtime::bank::KeyedRewardsAndNumPartitions, solana_transaction_status::{Reward, RewardsAndNumPartitions}, std::sync::Arc, @@ -25,7 +25,6 @@ impl BlockMetadataNotifier for BlockMetadataNotifierImpl { parent_slot: u64, parent_blockhash: &str, slot: u64, - bank_id: BankId, blockhash: &str, rewards: &KeyedRewardsAndNumPartitions, block_time: Option, @@ -44,7 +43,6 @@ impl BlockMetadataNotifier for BlockMetadataNotifierImpl { parent_slot, parent_blockhash, slot, - bank_id, blockhash, &rewards, block_time, @@ -54,7 +52,7 @@ impl BlockMetadataNotifier for BlockMetadataNotifierImpl { ); for plugin in plugin_manager.plugins.iter() { - let block_info = ReplicaBlockInfoVersions::V0_0_5(&block_info); + let block_info = ReplicaBlockInfoVersions::V0_0_4(&block_info); match plugin.notify_block_metadata(block_info) { Err(err) => { error!( @@ -106,24 +104,21 @@ impl BlockMetadataNotifierImpl { } } - #[allow(clippy::too_many_arguments)] fn build_replica_block_info<'a>( parent_slot: u64, parent_blockhash: &'a str, slot: u64, - bank_id: BankId, blockhash: &'a str, rewards: &'a RewardsAndNumPartitions, block_time: Option, block_height: Option, executed_transaction_count: u64, entry_count: u64, - ) -> ReplicaBlockInfoV5<'a> { - ReplicaBlockInfoV5 { + ) -> ReplicaBlockInfoV4<'a> { + ReplicaBlockInfoV4 { parent_slot, parent_blockhash, slot, - bank_id, blockhash, rewards, block_time, @@ -137,85 +132,3 @@ impl BlockMetadataNotifierImpl { Self { plugin_manager } } } - -#[cfg(test)] -mod tests { - use { - super::*, - crate::geyser_plugin_manager::{GeyserPluginManager, LoadedGeyserPlugin}, - agave_geyser_plugin_interface::geyser_plugin_interface::{GeyserPlugin, Result}, - arc_swap::ArcSwap, - libloading::Library, - std::sync::{Arc, Mutex}, - }; - - type BlockMetadataUpdate = (u64, BankId, u64, u64); - - #[derive(Debug)] - struct TestBlockMetadataPlugin { - updates: Arc>>, - } - - impl GeyserPlugin for TestBlockMetadataPlugin { - fn name(&self) -> &'static str { - "test-block-metadata-plugin" - } - - fn notify_block_metadata(&self, blockinfo: ReplicaBlockInfoVersions) -> Result<()> { - let ReplicaBlockInfoVersions::V0_0_5(blockinfo) = blockinfo else { - panic!("expected V0_0_5 block info"); - }; - self.updates.lock().unwrap().push(( - blockinfo.slot, - blockinfo.bank_id, - blockinfo.executed_transaction_count, - blockinfo.entry_count, - )); - Ok(()) - } - } - - fn loaded_test_plugin(plugin: TestBlockMetadataPlugin) -> Arc { - #[cfg(unix)] - let library = libloading::os::unix::Library::this(); - #[cfg(windows)] - let library = libloading::os::windows::Library::this().unwrap(); - - Arc::new(LoadedGeyserPlugin::new( - Library::from(library), - Box::new(plugin), - None, - )) - } - - #[test] - fn test_notify_block_metadata_includes_bank_id() { - let updates = Arc::new(Mutex::new(Vec::new())); - let plugin_manager = Arc::new(ArcSwap::from(Arc::new(GeyserPluginManager { - plugins: vec![loaded_test_plugin(TestBlockMetadataPlugin { - updates: updates.clone(), - })], - }))); - let notifier = BlockMetadataNotifierImpl::new(plugin_manager); - let rewards = KeyedRewardsAndNumPartitions { - keyed_rewards: Vec::new(), - num_partitions: None, - }; - - notifier.notify_block_metadata( - 41, - "parent-blockhash", - 42, - 9, - "blockhash", - &rewards, - Some(123), - Some(10), - 7, - 3, - false, - ); - - assert_eq!(*updates.lock().unwrap(), vec![(42, 9, 7, 3)]); - } -} diff --git a/geyser-plugin-manager/src/block_metadata_notifier_interface.rs b/geyser-plugin-manager/src/block_metadata_notifier_interface.rs index a89dcf4f700..3f5cb39a668 100644 --- a/geyser-plugin-manager/src/block_metadata_notifier_interface.rs +++ b/geyser-plugin-manager/src/block_metadata_notifier_interface.rs @@ -1,7 +1,5 @@ use { - solana_clock::{BankId, UnixTimestamp}, - solana_runtime::bank::KeyedRewardsAndNumPartitions, - std::sync::Arc, + solana_clock::UnixTimestamp, solana_runtime::bank::KeyedRewardsAndNumPartitions, std::sync::Arc, }; /// Interface for notifying block metadata changes @@ -13,7 +11,6 @@ pub trait BlockMetadataNotifier { parent_slot: u64, parent_blockhash: &str, slot: u64, - bank_id: BankId, blockhash: &str, rewards: &KeyedRewardsAndNumPartitions, block_time: Option, diff --git a/geyser-plugin-manager/src/entry_notifier.rs b/geyser-plugin-manager/src/entry_notifier.rs index 86cbd303205..a13064f3791 100644 --- a/geyser-plugin-manager/src/entry_notifier.rs +++ b/geyser-plugin-manager/src/entry_notifier.rs @@ -2,11 +2,11 @@ use { crate::geyser_plugin_manager::GeyserPluginManager, agave_geyser_plugin_interface::geyser_plugin_interface::{ - ReplicaEntryInfoV3, ReplicaEntryInfoVersions, + ReplicaEntryInfoV2, ReplicaEntryInfoVersions, }, arc_swap::ArcSwap, log::*, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_entry::entry::EntrySummary, solana_ledger::entry_notifier_interface::EntryNotifier, std::sync::Arc, @@ -20,7 +20,6 @@ impl EntryNotifier for EntryNotifierImpl { fn notify_entry<'a>( &'a self, slot: Slot, - bank_id: BankId, index: usize, entry: &'a EntrySummary, starting_transaction_index: usize, @@ -31,13 +30,13 @@ impl EntryNotifier for EntryNotifierImpl { } let entry_info = - Self::build_replica_entry_info(slot, bank_id, index, entry, starting_transaction_index); + Self::build_replica_entry_info(slot, index, entry, starting_transaction_index); for plugin in plugin_manager.plugins.iter() { if !plugin.entry_notifications_enabled() { continue; } - match plugin.notify_entry(ReplicaEntryInfoVersions::V0_0_3(&entry_info)) { + match plugin.notify_entry(ReplicaEntryInfoVersions::V0_0_2(&entry_info)) { Err(err) => { error!( "Failed to notify entry, error: ({}) to plugin {}", @@ -60,14 +59,12 @@ impl EntryNotifierImpl { fn build_replica_entry_info( slot: Slot, - bank_id: BankId, index: usize, entry: &'_ EntrySummary, starting_transaction_index: usize, - ) -> ReplicaEntryInfoV3<'_> { - ReplicaEntryInfoV3 { + ) -> ReplicaEntryInfoV2<'_> { + ReplicaEntryInfoV2 { slot, - bank_id, index, num_hashes: entry.num_hashes, hash: entry.hash.as_ref(), @@ -76,89 +73,3 @@ impl EntryNotifierImpl { } } } - -#[cfg(test)] -mod tests { - use { - super::*, - crate::geyser_plugin_manager::{GeyserPluginManager, LoadedGeyserPlugin}, - agave_geyser_plugin_interface::geyser_plugin_interface::{GeyserPlugin, Result}, - arc_swap::ArcSwap, - libloading::Library, - solana_hash::Hash, - std::sync::{Arc, Mutex}, - }; - - type EntryUpdate = (Slot, BankId, usize, usize); - - #[derive(Debug)] - struct TestEntryPlugin { - entry_notifications_enabled: bool, - updates: Arc>>, - } - - impl GeyserPlugin for TestEntryPlugin { - fn name(&self) -> &'static str { - "test-entry-plugin" - } - - fn notify_entry(&self, entry: ReplicaEntryInfoVersions) -> Result<()> { - let ReplicaEntryInfoVersions::V0_0_3(entry) = entry else { - panic!("expected V0_0_3 entry info"); - }; - self.updates.lock().unwrap().push(( - entry.slot, - entry.bank_id, - entry.index, - entry.starting_transaction_index, - )); - Ok(()) - } - - fn entry_notifications_enabled(&self) -> bool { - self.entry_notifications_enabled - } - } - - fn loaded_test_plugin(plugin: TestEntryPlugin) -> Arc { - #[cfg(unix)] - let library = libloading::os::unix::Library::this(); - #[cfg(windows)] - let library = libloading::os::windows::Library::this().unwrap(); - - Arc::new(LoadedGeyserPlugin::new( - Library::from(library), - Box::new(plugin), - None, - )) - } - - #[test] - fn test_notify_entry_includes_bank_id() { - let enabled_updates = Arc::new(Mutex::new(Vec::new())); - let disabled_updates = Arc::new(Mutex::new(Vec::new())); - let plugin_manager = Arc::new(ArcSwap::from(Arc::new(GeyserPluginManager { - plugins: vec![ - loaded_test_plugin(TestEntryPlugin { - entry_notifications_enabled: true, - updates: enabled_updates.clone(), - }), - loaded_test_plugin(TestEntryPlugin { - entry_notifications_enabled: false, - updates: disabled_updates.clone(), - }), - ], - }))); - let notifier = EntryNotifierImpl::new(plugin_manager); - let entry = EntrySummary { - num_hashes: 1, - hash: Hash::new_unique(), - num_transactions: 2, - }; - - notifier.notify_entry(42, 9, 3, &entry, 7); - - assert_eq!(*enabled_updates.lock().unwrap(), vec![(42, 9, 3, 7)]); - assert!(disabled_updates.lock().unwrap().is_empty()); - } -} diff --git a/geyser-plugin-manager/src/slot_status_notifier.rs b/geyser-plugin-manager/src/slot_status_notifier.rs index e62d736e49d..cf126bff775 100644 --- a/geyser-plugin-manager/src/slot_status_notifier.rs +++ b/geyser-plugin-manager/src/slot_status_notifier.rs @@ -1,10 +1,7 @@ use { crate::geyser_plugin_manager::GeyserPluginManager, - agave_geyser_plugin_interface::geyser_plugin_interface::SlotStatus, - arc_swap::ArcSwap, - log::*, - solana_clock::{BankId, Slot}, - solana_rpc::slot_status_notifier::SlotStatusNotifierInterface, + agave_geyser_plugin_interface::geyser_plugin_interface::SlotStatus, arc_swap::ArcSwap, log::*, + solana_clock::Slot, solana_rpc::slot_status_notifier::SlotStatusNotifierInterface, std::sync::Arc, }; @@ -13,32 +10,32 @@ pub struct SlotStatusNotifierImpl { } impl SlotStatusNotifierInterface for SlotStatusNotifierImpl { - fn notify_slot_confirmed(&self, slot: Slot, parent: Option, bank_id: BankId) { - self.notify_slot_status(slot, parent, SlotStatus::Confirmed, Some(bank_id)); + fn notify_slot_confirmed(&self, slot: Slot, parent: Option) { + self.notify_slot_status(slot, parent, SlotStatus::Confirmed); } - fn notify_slot_processed(&self, slot: Slot, parent: Option, bank_id: BankId) { - self.notify_slot_status(slot, parent, SlotStatus::Processed, Some(bank_id)); + fn notify_slot_processed(&self, slot: Slot, parent: Option) { + self.notify_slot_status(slot, parent, SlotStatus::Processed); } - fn notify_slot_rooted(&self, slot: Slot, parent: Option, bank_id: BankId) { - self.notify_slot_status(slot, parent, SlotStatus::Rooted, Some(bank_id)); + fn notify_slot_rooted(&self, slot: Slot, parent: Option) { + self.notify_slot_status(slot, parent, SlotStatus::Rooted); } fn notify_first_shred_received(&self, slot: Slot) { - self.notify_slot_status(slot, None, SlotStatus::FirstShredReceived, None); + self.notify_slot_status(slot, None, SlotStatus::FirstShredReceived); } fn notify_completed(&self, slot: Slot) { - self.notify_slot_status(slot, None, SlotStatus::Completed, None); + self.notify_slot_status(slot, None, SlotStatus::Completed); } - fn notify_created_bank(&self, slot: Slot, parent: Slot, bank_id: BankId) { - self.notify_slot_status(slot, Some(parent), SlotStatus::CreatedBank, Some(bank_id)); + fn notify_created_bank(&self, slot: Slot, parent: Slot) { + self.notify_slot_status(slot, Some(parent), SlotStatus::CreatedBank); } fn notify_slot_dead(&self, slot: Slot, parent: Slot, error: String) { - self.notify_slot_status(slot, Some(parent), SlotStatus::Dead(error), None); + self.notify_slot_status(slot, Some(parent), SlotStatus::Dead(error)); } } @@ -47,20 +44,14 @@ impl SlotStatusNotifierImpl { Self { plugin_manager } } - pub fn notify_slot_status( - &self, - slot: Slot, - parent: Option, - slot_status: SlotStatus, - bank_id: Option, - ) { + pub fn notify_slot_status(&self, slot: Slot, parent: Option, slot_status: SlotStatus) { let plugin_manager = self.plugin_manager.load(); if plugin_manager.plugins.is_empty() { return; } for plugin in plugin_manager.plugins.iter() { - match plugin.update_slot_status_v2(slot, parent, &slot_status, bank_id) { + match plugin.update_slot_status(slot, parent, &slot_status) { Err(err) => { error!( "Failed to update slot status at slot {}, error: {} to plugin {}", @@ -80,79 +71,3 @@ impl SlotStatusNotifierImpl { } } } - -#[cfg(test)] -mod tests { - use { - super::*, - crate::geyser_plugin_manager::{GeyserPluginManager, LoadedGeyserPlugin}, - agave_geyser_plugin_interface::geyser_plugin_interface::{GeyserPlugin, Result}, - arc_swap::ArcSwap, - libloading::Library, - std::sync::{Arc, Mutex}, - }; - - type SlotStatusUpdate = (Slot, Option, SlotStatus, Option); - - #[derive(Debug)] - struct TestSlotStatusPlugin { - updates: Arc>>, - } - - impl GeyserPlugin for TestSlotStatusPlugin { - fn name(&self) -> &'static str { - "test-slot-status-plugin" - } - - fn update_slot_status_v2( - &self, - slot: Slot, - parent: Option, - status: &SlotStatus, - bank_id: Option, - ) -> Result<()> { - self.updates - .lock() - .unwrap() - .push((slot, parent, status.clone(), bank_id)); - Ok(()) - } - } - - fn loaded_test_plugin(plugin: TestSlotStatusPlugin) -> Arc { - #[cfg(unix)] - let library = libloading::os::unix::Library::this(); - #[cfg(windows)] - let library = libloading::os::windows::Library::this().unwrap(); - - Arc::new(LoadedGeyserPlugin::new( - Library::from(library), - Box::new(plugin), - None, - )) - } - - fn create_notifier(updates: Arc>>) -> SlotStatusNotifierImpl { - let plugin_manager = Arc::new(ArcSwap::from(Arc::new(GeyserPluginManager { - plugins: vec![loaded_test_plugin(TestSlotStatusPlugin { updates })], - }))); - SlotStatusNotifierImpl::new(plugin_manager) - } - - #[test] - fn test_notify_slot_status_bank_id() { - let updates = Arc::new(Mutex::new(Vec::new())); - let notifier = create_notifier(updates.clone()); - - notifier.notify_created_bank(42, 41, 9); - notifier.notify_slot_processed(42, Some(41), 9); - - assert_eq!( - *updates.lock().unwrap(), - vec![ - (42, Some(41), SlotStatus::CreatedBank, Some(9)), - (42, Some(41), SlotStatus::Processed, Some(9)), - ] - ); - } -} diff --git a/geyser-plugin-manager/src/slot_status_observer.rs b/geyser-plugin-manager/src/slot_status_observer.rs index a8fd488899b..afe7a947141 100644 --- a/geyser-plugin-manager/src/slot_status_observer.rs +++ b/geyser-plugin-manager/src/slot_status_observer.rs @@ -55,25 +55,23 @@ impl SlotStatusObserver { while !exit.load(Ordering::Relaxed) { if let Ok(slot) = bank_notification_receiver.recv() { match slot { - SlotNotification::OptimisticallyConfirmed(slot, bank_id) => { + SlotNotification::OptimisticallyConfirmed(slot) => { slot_status_notifier .read() .unwrap() - .notify_slot_confirmed(slot, None, bank_id); + .notify_slot_confirmed(slot, None); } - SlotNotification::Frozen((slot, parent, bank_id)) => { - slot_status_notifier.read().unwrap().notify_slot_processed( - slot, - Some(parent), - bank_id, - ); + SlotNotification::Frozen((slot, parent)) => { + slot_status_notifier + .read() + .unwrap() + .notify_slot_processed(slot, Some(parent)); } - SlotNotification::Root((slot, parent, bank_id)) => { - slot_status_notifier.read().unwrap().notify_slot_rooted( - slot, - Some(parent), - bank_id, - ); + SlotNotification::Root((slot, parent)) => { + slot_status_notifier + .read() + .unwrap() + .notify_slot_rooted(slot, Some(parent)); } } } diff --git a/geyser-plugin-manager/src/transaction_notifier.rs b/geyser-plugin-manager/src/transaction_notifier.rs index 0a95941c112..7a01147e6d1 100644 --- a/geyser-plugin-manager/src/transaction_notifier.rs +++ b/geyser-plugin-manager/src/transaction_notifier.rs @@ -2,11 +2,11 @@ use { crate::geyser_plugin_manager::GeyserPluginManager, agave_geyser_plugin_interface::geyser_plugin_interface::{ - ReplicaTransactionInfoV4, ReplicaTransactionInfoVersions, + ReplicaTransactionInfoV3, ReplicaTransactionInfoVersions, }, arc_swap::ArcSwap, log::*, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_hash::Hash, solana_rpc::transaction_notifier_interface::TransactionNotifier, solana_signature::Signature, @@ -27,7 +27,6 @@ impl TransactionNotifier for TransactionNotifierImpl { fn notify_transaction( &self, slot: Slot, - bank_id: BankId, index: usize, signature: &Signature, message_hash: &Hash, @@ -36,7 +35,6 @@ impl TransactionNotifier for TransactionNotifierImpl { transaction: &VersionedTransaction, ) { let transaction_log_info = Self::build_replica_transaction_info( - bank_id, index, signature, message_hash, @@ -56,7 +54,7 @@ impl TransactionNotifier for TransactionNotifierImpl { continue; } match plugin.notify_transaction( - ReplicaTransactionInfoVersions::V0_0_4(&transaction_log_info), + ReplicaTransactionInfoVersions::V0_0_3(&transaction_log_info), slot, ) { Err(err) => { @@ -83,16 +81,14 @@ impl TransactionNotifierImpl { } fn build_replica_transaction_info<'a>( - bank_id: BankId, index: usize, signature: &'a Signature, message_hash: &'a Hash, is_vote: bool, transaction_status_meta: &'a TransactionStatusMeta, transaction: &'a VersionedTransaction, - ) -> ReplicaTransactionInfoV4<'a> { - ReplicaTransactionInfoV4 { - bank_id, + ) -> ReplicaTransactionInfoV3<'a> { + ReplicaTransactionInfoV3 { index, message_hash, signature, diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 6db8b126d62..ce135e6ba36 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -1602,7 +1602,6 @@ fn confirm_slot_entries( }; let slot = bank.slot(); - let bank_id = bank.bank_id(); let (entries, num_shreds, slot_full) = slot_entries_load_result; let num_entries = entries.len(); let mut entry_tx_starting_indexes = Vec::with_capacity(num_entries); @@ -1615,7 +1614,6 @@ fn confirm_slot_entries( let entry_index = progress.num_entries.saturating_add(i); if let Err(err) = entry_notification_sender.send(EntryNotification { slot, - bank_id, index: entry_index, entry: entry.into(), starting_transaction_index: entry_tx_starting_index, diff --git a/ledger/src/entry_notifier_interface.rs b/ledger/src/entry_notifier_interface.rs index 48755e7062e..3296b2cbadd 100644 --- a/ledger/src/entry_notifier_interface.rs +++ b/ledger/src/entry_notifier_interface.rs @@ -1,14 +1,9 @@ -use { - solana_clock::{BankId, Slot}, - solana_entry::entry::EntrySummary, - std::sync::Arc, -}; +use {solana_clock::Slot, solana_entry::entry::EntrySummary, std::sync::Arc}; pub trait EntryNotifier { fn notify_entry( &self, slot: Slot, - bank_id: BankId, index: usize, entry: &EntrySummary, starting_transaction_index: usize, diff --git a/ledger/src/entry_notifier_service.rs b/ledger/src/entry_notifier_service.rs index 9f987a308f7..4e55603c81e 100644 --- a/ledger/src/entry_notifier_service.rs +++ b/ledger/src/entry_notifier_service.rs @@ -1,7 +1,7 @@ use { crate::entry_notifier_interface::EntryNotifierArc, crossbeam_channel::{Receiver, RecvTimeoutError, Sender, unbounded}, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_entry::entry::EntrySummary, std::{ sync::{ @@ -15,7 +15,6 @@ use { pub struct EntryNotification { pub slot: Slot, - pub bank_id: BankId, pub index: usize, pub entry: EntrySummary, pub starting_transaction_index: usize, @@ -60,12 +59,11 @@ impl EntryNotifierService { ) -> Result<(), RecvTimeoutError> { let EntryNotification { slot, - bank_id, index, entry, starting_transaction_index, } = entry_notification_receiver.recv_timeout(Duration::from_secs(1))?; - entry_notifier.notify_entry(slot, bank_id, index, &entry, starting_transaction_index); + entry_notifier.notify_entry(slot, index, &entry, starting_transaction_index); Ok(()) } diff --git a/rpc/src/optimistically_confirmed_bank_tracker.rs b/rpc/src/optimistically_confirmed_bank_tracker.rs index a685d8078ec..c553b623707 100644 --- a/rpc/src/optimistically_confirmed_bank_tracker.rs +++ b/rpc/src/optimistically_confirmed_bank_tracker.rs @@ -11,8 +11,7 @@ use { crate::rpc_subscriptions::RpcSubscriptions, crossbeam_channel::{Receiver, RecvTimeoutError, Sender}, - solana_clock::{BankId, Slot}, - solana_hash::Hash, + solana_clock::Slot, solana_rpc_client_api::response::{SlotTransactionStats, SlotUpdate}, solana_runtime::{ bank::Bank, bank_forks::BankForks, dependency_tracker::DependencyTracker, @@ -44,33 +43,31 @@ impl OptimisticallyConfirmedBank { #[derive(Clone)] pub enum BankNotification { - OptimisticallyConfirmed(Slot, Hash), + OptimisticallyConfirmed(Slot), Frozen(Arc), NewRootBank(Arc), - /// The newly rooted slot chain with bank ids and the parent slot of the oldest bank in the rooted chain. - NewRootedChain(Vec<(Slot, BankId)>, Slot), + /// The newly rooted slot chain including the parent slot of the oldest bank in the rooted chain. + NewRootedChain(Vec), } #[derive(Clone, Debug)] pub enum SlotNotification { - OptimisticallyConfirmed(Slot, BankId), - /// The (Slot, Parent Slot, Bank Id) tuple for the slot frozen - Frozen((Slot, Slot, BankId)), - /// The (Slot, Parent Slot, Bank Id) tuple for the root slot - Root((Slot, Slot, BankId)), + OptimisticallyConfirmed(Slot), + /// The (Slot, Parent Slot) pair for the slot frozen + Frozen((Slot, Slot)), + /// The (Slot, Parent Slot) pair for the root slot + Root((Slot, Slot)), } impl std::fmt::Debug for BankNotification { fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { match self { - BankNotification::OptimisticallyConfirmed(slot, hash) => { - write!(f, "OptimisticallyConfirmed({slot:?}, {hash:?})") + BankNotification::OptimisticallyConfirmed(slot) => { + write!(f, "OptimisticallyConfirmed({slot:?})") } BankNotification::Frozen(bank) => write!(f, "Frozen({})", bank.slot()), BankNotification::NewRootBank(bank) => write!(f, "Root({})", bank.slot()), - BankNotification::NewRootedChain(chain, parent) => { - write!(f, "RootedChain({chain:?}, parent: {parent})") - } + BankNotification::NewRootedChain(chain) => write!(f, "RootedChain({chain:?})"), } } } @@ -92,7 +89,6 @@ pub struct BankNotificationSenderConfig { pub type SlotNotificationReceiver = Receiver; pub type SlotNotificationSender = Sender; -type PendingOptimisticallyConfirmedBanks = HashSet<(Slot, Hash)>; pub struct OptimisticallyConfirmedBankTracker { thread_hdl: JoinHandle<()>, @@ -148,7 +144,7 @@ impl OptimisticallyConfirmedBankTracker { bank_forks: &RwLock, optimistically_confirmed_bank: &RwLock, subscriptions: &RpcSubscriptions, - pending_optimistically_confirmed_banks: &mut PendingOptimisticallyConfirmedBanks, + pending_optimistically_confirmed_banks: &mut HashSet, last_notified_confirmed_slot: &mut Slot, highest_confirmed_slot: &mut Slot, newest_root_slot: &mut Slot, @@ -193,9 +189,8 @@ impl OptimisticallyConfirmedBankTracker { subscriptions: &RpcSubscriptions, bank_forks: &RwLock, bank: &Bank, - pending_confirmation: Option<(Slot, Hash)>, last_notified_confirmed_slot: &mut Slot, - pending_optimistically_confirmed_banks: &mut PendingOptimisticallyConfirmedBanks, + pending_optimistically_confirmed_banks: &mut HashSet, slot_notification_subscribers: &Option>>>, prioritization_fee_cache: Option<&PrioritizationFeeCache>, ) { @@ -209,7 +204,7 @@ impl OptimisticallyConfirmedBankTracker { *last_notified_confirmed_slot = bank.slot(); Self::notify_slot_status( slot_notification_subscribers, - SlotNotification::OptimisticallyConfirmed(bank.slot(), bank.bank_id()), + SlotNotification::OptimisticallyConfirmed(bank.slot()), ); // finalize block's minimum prioritization fee cache for this bank @@ -217,12 +212,9 @@ impl OptimisticallyConfirmedBankTracker { prioritization_fee_cache.finalize_priority_fee(bank.slot(), bank.bank_id()); } } - } else if let Some((slot, hash)) = pending_confirmation - && bank.slot() == slot - && bank.slot() > bank_forks.read().unwrap().root() - { - pending_optimistically_confirmed_banks.insert((slot, hash)); - debug!("notify_or_defer defer notifying for slot {slot:?}"); + } else if bank.slot() > bank_forks.read().unwrap().root() { + pending_optimistically_confirmed_banks.insert(bank.slot()); + debug!("notify_or_defer defer notifying for slot {:?}", bank.slot()); } } @@ -231,9 +223,8 @@ impl OptimisticallyConfirmedBankTracker { bank_forks: &RwLock, bank: Arc, slot_threshold: Slot, - pending_confirmation: Option<(Slot, Hash)>, last_notified_confirmed_slot: &mut Slot, - pending_optimistically_confirmed_banks: &mut PendingOptimisticallyConfirmedBanks, + pending_optimistically_confirmed_banks: &mut HashSet, slot_notification_subscribers: &Option>>>, prioritization_fee_cache: Option<&PrioritizationFeeCache>, ) { @@ -247,7 +238,6 @@ impl OptimisticallyConfirmedBankTracker { subscriptions, bank_forks, confirmed_bank, - pending_confirmation, last_notified_confirmed_slot, pending_optimistically_confirmed_banks, slot_notification_subscribers, @@ -258,27 +248,27 @@ impl OptimisticallyConfirmedBankTracker { } fn notify_new_root_slots( - roots: &mut [(Slot, BankId)], - oldest_parent: Slot, + roots: &mut [Slot], newest_root_slot: &mut Slot, slot_notification_subscribers: &Option>>>, ) { if slot_notification_subscribers.is_none() { return; } - roots.sort_unstable_by_key(|(root, _bank_id)| *root); - assert!(!roots.is_empty()); - let mut parent = oldest_parent; - for (root, bank_id) in roots.iter() { - if *root > *newest_root_slot { + roots.sort_unstable(); + // The chain are sorted already and must contain at least the parent of a newly rooted slot as the first element + assert!(roots.len() >= 2); + for i in 1..roots.len() { + let root = roots[i]; + if root > *newest_root_slot { + let parent = roots[i - 1]; debug!("Doing SlotNotification::Root for root {root}, parent: {parent}"); Self::notify_slot_status( slot_notification_subscribers, - SlotNotification::Root((*root, parent, *bank_id)), + SlotNotification::Root((root, parent)), ); - *newest_root_slot = *root; + *newest_root_slot = root; } - parent = *root; } } @@ -288,7 +278,7 @@ impl OptimisticallyConfirmedBankTracker { bank_forks: &RwLock, optimistically_confirmed_bank: &RwLock, subscriptions: &RpcSubscriptions, - pending_optimistically_confirmed_banks: &mut PendingOptimisticallyConfirmedBanks, + pending_optimistically_confirmed_banks: &mut HashSet, last_notified_confirmed_slot: &mut Slot, highest_confirmed_slot: &mut Slot, newest_root_slot: &mut Slot, @@ -304,63 +294,34 @@ impl OptimisticallyConfirmedBankTracker { tracker.wait_for_dependency(dependency_work); } match notification { - BankNotification::OptimisticallyConfirmed(slot, hash) => { + BankNotification::OptimisticallyConfirmed(slot) => { let bank = bank_forks.read().unwrap().get(slot); if let Some(bank) = bank { - if !bank.is_frozen() { - if slot > *highest_confirmed_slot { - Self::notify_or_defer_confirmed_banks( - subscriptions, - bank_forks, - bank, - *highest_confirmed_slot, - Some((slot, hash)), - last_notified_confirmed_slot, - pending_optimistically_confirmed_banks, - slot_notification_subscribers, - prioritization_fee_cache, - ); - *highest_confirmed_slot = slot; - } else if slot > bank_forks.read().unwrap().root() { - pending_optimistically_confirmed_banks.insert((slot, hash)); - debug!("defer notifying optimistic confirmation for slot {slot}"); - } - } else if bank.hash() != hash { - if slot > bank_forks.read().unwrap().root() { - pending_optimistically_confirmed_banks.insert((slot, hash)); - debug!( - "defer notifying optimistic confirmation for slot {slot}: local \ - bank hash {} does not match optimistic confirmation hash {hash}", - bank.hash() - ); - } - } else { - let mut w_optimistically_confirmed_bank = - optimistically_confirmed_bank.write().unwrap(); - - if bank.slot() > w_optimistically_confirmed_bank.bank.slot() { - w_optimistically_confirmed_bank.bank = bank.clone(); - } - - if slot > *highest_confirmed_slot { - Self::notify_or_defer_confirmed_banks( - subscriptions, - bank_forks, - bank, - *highest_confirmed_slot, - None, - last_notified_confirmed_slot, - pending_optimistically_confirmed_banks, - slot_notification_subscribers, - prioritization_fee_cache, - ); - - *highest_confirmed_slot = slot; - } - drop(w_optimistically_confirmed_bank); + let mut w_optimistically_confirmed_bank = + optimistically_confirmed_bank.write().unwrap(); + + if bank.slot() > w_optimistically_confirmed_bank.bank.slot() && bank.is_frozen() + { + w_optimistically_confirmed_bank.bank = bank.clone(); + } + + if slot > *highest_confirmed_slot { + Self::notify_or_defer_confirmed_banks( + subscriptions, + bank_forks, + bank, + *highest_confirmed_slot, + last_notified_confirmed_slot, + pending_optimistically_confirmed_banks, + slot_notification_subscribers, + prioritization_fee_cache, + ); + + *highest_confirmed_slot = slot; } + drop(w_optimistically_confirmed_bank); } else if slot > bank_forks.read().unwrap().root() { - pending_optimistically_confirmed_banks.insert((slot, hash)); + pending_optimistically_confirmed_banks.insert(slot); } else { inc_new_counter_info!("dropped-already-rooted-optimistic-bank-notification", 1); } @@ -393,11 +354,11 @@ impl OptimisticallyConfirmedBankTracker { Self::notify_slot_status( slot_notification_subscribers, - SlotNotification::Frozen((bank.slot(), bank.parent_slot(), bank.bank_id())), + SlotNotification::Frozen((bank.slot(), bank.parent_slot())), ); } - if pending_optimistically_confirmed_banks.remove(&(bank.slot(), bank.hash())) { + if pending_optimistically_confirmed_banks.remove(&bank.slot()) { debug!( "Calling notify_gossip_subscribers to send deferred notification \ {frozen_slot:?}" @@ -408,15 +369,11 @@ impl OptimisticallyConfirmedBankTracker { bank_forks, bank.clone(), *last_notified_confirmed_slot, - None, last_notified_confirmed_slot, pending_optimistically_confirmed_banks, slot_notification_subscribers, prioritization_fee_cache, ); - if frozen_slot > *highest_confirmed_slot { - *highest_confirmed_slot = frozen_slot; - } let mut w_optimistically_confirmed_bank = optimistically_confirmed_bank.write().unwrap(); @@ -435,12 +392,11 @@ impl OptimisticallyConfirmedBankTracker { } drop(w_optimistically_confirmed_bank); - pending_optimistically_confirmed_banks.retain(|&(slot, _hash)| slot > root_slot); + pending_optimistically_confirmed_banks.retain(|&s| s > root_slot); } - BankNotification::NewRootedChain(mut roots, oldest_parent) => { + BankNotification::NewRootedChain(mut roots) => { Self::notify_new_root_slots( &mut roots, - oldest_parent, newest_root_slot, slot_notification_subscribers, ); @@ -477,22 +433,6 @@ mod tests { notifications } - fn root_slot_notifications(root_bank: &Arc) -> (Vec<(Slot, BankId)>, Slot) { - let mut rooted_banks = root_bank.parents(); - let oldest_parent = rooted_banks - .last() - .map(|last| last.parent_slot()) - .unwrap_or_else(|| root_bank.parent_slot()); - rooted_banks.push(root_bank.clone()); - ( - rooted_banks - .iter() - .map(|bank| (bank.slot(), bank.bank_id())) - .collect(), - oldest_parent, - ) - } - #[test] fn test_process_notification() { let exit = Arc::new(AtomicBool::new(false)); @@ -508,10 +448,6 @@ mod tests { let bank2 = bank_forks.read().unwrap().get(2).unwrap(); let bank3 = Bank::new_from_parent(bank2, SlotLeader::default(), 3); bank_forks.write().unwrap().insert(bank3); - let bank1_hash = bank_forks.read().unwrap().get(1).unwrap().hash(); - let bank2 = bank_forks.read().unwrap().get(2).unwrap(); - let bank2_hash = bank2.hash(); - let bank3_pending_hash = Hash::new_unique(); let optimistically_confirmed_bank: Arc> = OptimisticallyConfirmedBank::locked_from_bank_forks_root(&bank_forks); @@ -525,7 +461,7 @@ mod tests { block_commitment_cache, optimistically_confirmed_bank.clone(), )); - let mut pending_optimistically_confirmed_banks = PendingOptimisticallyConfirmedBanks::new(); + let mut pending_optimistically_confirmed_banks: HashSet = HashSet::new(); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 0); @@ -535,7 +471,7 @@ mod tests { let mut last_notified_confirmed_slot: Slot = 0; OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(2, bank2_hash), + BankNotification::OptimisticallyConfirmed(2), None, /* no dependency work */ ), &bank_forks, @@ -555,7 +491,7 @@ mod tests { // Test max optimistically confirmed bank remains in the cache OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(1, bank1_hash), + BankNotification::OptimisticallyConfirmed(1), None, /* no dependency work */ ), &bank_forks, @@ -575,7 +511,7 @@ mod tests { // Test bank will only be cached when frozen OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(3, bank3_pending_hash), + BankNotification::OptimisticallyConfirmed(3), None, /* no dependency work */ ), &bank_forks, @@ -591,14 +527,12 @@ mod tests { ); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 2); assert_eq!(pending_optimistically_confirmed_banks.len(), 1); - assert!(pending_optimistically_confirmed_banks.contains(&(3, bank3_pending_hash))); + assert!(pending_optimistically_confirmed_banks.contains(&3)); assert_eq!(highest_confirmed_slot, 3); // Test bank will only be cached when frozen let bank3 = bank_forks.read().unwrap().get(3).unwrap(); bank3.freeze(); - assert!(pending_optimistically_confirmed_banks.remove(&(3, bank3_pending_hash))); - pending_optimistically_confirmed_banks.insert((3, bank3.hash())); OptimisticallyConfirmedBankTracker::process_notification( ( @@ -624,10 +558,9 @@ mod tests { let bank3 = bank_forks.read().unwrap().get(3).unwrap(); let bank4 = Bank::new_from_parent(bank3, SlotLeader::default(), 4); bank_forks.write().unwrap().insert(bank4); - let bank4_hash = Hash::new_unique(); OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(4, bank4_hash), + BankNotification::OptimisticallyConfirmed(4), None, /* no dependency work */ ), &bank_forks, @@ -643,7 +576,7 @@ mod tests { ); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 3); assert_eq!(pending_optimistically_confirmed_banks.len(), 1); - assert!(pending_optimistically_confirmed_banks.contains(&(4, bank4_hash))); + assert!(pending_optimistically_confirmed_banks.contains(&4)); assert_eq!(highest_confirmed_slot, 4); let bank4 = bank_forks.read().unwrap().get(4).unwrap(); @@ -656,7 +589,7 @@ mod tests { bank_notification_senders.push(sender); let subscribers = Some(Arc::new(RwLock::new(bank_notification_senders))); - let (parent_roots, oldest_parent) = root_slot_notifications(&bank5); + let parent_roots = bank5.ancestors.keys(); OptimisticallyConfirmedBankTracker::process_notification( ( @@ -676,14 +609,14 @@ mod tests { ); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 5); assert_eq!(pending_optimistically_confirmed_banks.len(), 0); - assert!(!pending_optimistically_confirmed_banks.contains(&(4, bank4_hash))); + assert!(!pending_optimistically_confirmed_banks.contains(&4)); assert_eq!(highest_confirmed_slot, 4); // The newest_root_slot is updated via NewRootedChain only assert_eq!(newest_root_slot, 0); OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::NewRootedChain(parent_roots, oldest_parent), + BankNotification::NewRootedChain(parent_roots), None, /* no dependency work */ ), &bank_forks, @@ -712,10 +645,9 @@ mod tests { let bank7 = Bank::new_from_parent(bank5, SlotLeader::default(), 7); bank_forks.write().unwrap().insert(bank7); bank_forks.write().unwrap().set_root(7, None, None); - let bank6_hash = Hash::new_unique(); OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(6, bank6_hash), + BankNotification::OptimisticallyConfirmed(6), None, /* no dependency work */ ), &bank_forks, @@ -731,12 +663,12 @@ mod tests { ); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 5); assert_eq!(pending_optimistically_confirmed_banks.len(), 0); - assert!(!pending_optimistically_confirmed_banks.contains(&(6, bank6_hash))); + assert!(!pending_optimistically_confirmed_banks.contains(&6)); assert_eq!(highest_confirmed_slot, 4); assert_eq!(newest_root_slot, 5); let bank7 = bank_forks.read().unwrap().get(7).unwrap(); - let (parent_roots, oldest_parent) = root_slot_notifications(&bank7); + let parent_roots = bank7.ancestors.keys(); OptimisticallyConfirmedBankTracker::process_notification( ( @@ -756,13 +688,13 @@ mod tests { ); assert_eq!(optimistically_confirmed_bank.read().unwrap().bank.slot(), 7); assert_eq!(pending_optimistically_confirmed_banks.len(), 0); - assert!(!pending_optimistically_confirmed_banks.contains(&(6, bank6_hash))); + assert!(!pending_optimistically_confirmed_banks.contains(&6)); assert_eq!(highest_confirmed_slot, 4); assert_eq!(newest_root_slot, 5); OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::NewRootedChain(parent_roots, oldest_parent), + BankNotification::NewRootedChain(parent_roots), None, /* no dependency work */ ), &bank_forks, @@ -801,10 +733,8 @@ mod tests { let bank0 = bank_forks.read().unwrap().get(0).unwrap(); let bank1 = Bank::new_from_parent(bank0, SlotLeader::default(), 1); bank_forks.write().unwrap().insert(bank1); - let bank1_pending_hash = Hash::new_unique(); - let mut pending_optimistically_confirmed_banks = - PendingOptimisticallyConfirmedBanks::new(); + let mut pending_optimistically_confirmed_banks: HashSet = HashSet::new(); let max_complete_transaction_status_slot = Arc::new(AtomicU64::default()); let block_commitment_cache = Arc::new(RwLock::new(BlockCommitmentCache::default())); @@ -828,7 +758,7 @@ mod tests { // confirmed without fronzen received OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(1, bank1_pending_hash), + BankNotification::OptimisticallyConfirmed(1), Some(work_id_1), /* dependency work id */ ), &bank_forks, @@ -847,12 +777,9 @@ mod tests { // highest_confirmed_slot is updated even when we have not received the frozen event assert_eq!(highest_confirmed_slot, 1); assert_eq!(pending_optimistically_confirmed_banks.len(), 1); - assert!(pending_optimistically_confirmed_banks.contains(&(1, bank1_pending_hash))); let bank1 = bank_forks.read().unwrap().get(1).unwrap(); bank1.freeze(); - assert!(pending_optimistically_confirmed_banks.remove(&(1, bank1_pending_hash))); - pending_optimistically_confirmed_banks.insert((1, bank1.hash())); OptimisticallyConfirmedBankTracker::process_notification( ( diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index c2813424c37..f9ef1557362 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -9018,10 +9018,6 @@ pub mod tests { let bank2 = bank_forks.read().unwrap().get(2).unwrap(); let bank3 = Bank::new_from_parent(bank2, SlotLeader::default(), 3); bank_forks.write().unwrap().insert(bank3); - let bank3_pending_hash = Hash::new_unique(); - let bank1_hash = bank_forks.read().unwrap().get(1).unwrap().hash(); - let bank2 = bank_forks.read().unwrap().get(2).unwrap(); - let bank2_hash = bank2.hash(); let prioritization_fee_cache_inner = None; let prioritization_fee_cache = prioritization_fee_cache_inner.as_deref(); @@ -9080,7 +9076,7 @@ pub mod tests { OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(2, bank2_hash), + BankNotification::OptimisticallyConfirmed(2), None, /* no dependency work */ ), &bank_forks, @@ -9104,7 +9100,7 @@ pub mod tests { // Test rollback does not appear to happen, even if slots are notified out of order OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(1, bank1_hash), + BankNotification::OptimisticallyConfirmed(1), None, /* no dependency work */ ), &bank_forks, @@ -9128,7 +9124,7 @@ pub mod tests { // Test bank will only be cached when frozen OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(3, bank3_pending_hash), + BankNotification::OptimisticallyConfirmed(3), None, /* no dependency work */ ), &bank_forks, @@ -9151,9 +9147,6 @@ pub mod tests { // Test freezing an optimistically confirmed bank will update cache let bank3 = bank_forks.read().unwrap().get(3).unwrap(); - bank3.freeze(); - assert!(pending_optimistically_confirmed_banks.remove(&(3, bank3_pending_hash))); - pending_optimistically_confirmed_banks.insert((3, bank3.hash())); OptimisticallyConfirmedBankTracker::process_notification( ( BankNotification::Frozen(bank3), diff --git a/rpc/src/rpc_subscriptions.rs b/rpc/src/rpc_subscriptions.rs index ea8c2b462f4..88b2b2b6e8d 100644 --- a/rpc/src/rpc_subscriptions.rs +++ b/rpc/src/rpc_subscriptions.rs @@ -1217,7 +1217,6 @@ pub(crate) mod tests { }, serial_test::serial, solana_commitment_config::CommitmentConfig, - solana_hash::Hash, solana_keypair::Keypair, solana_ledger::get_tmp_ledger_path_auto_delete, solana_message::Message, @@ -1938,7 +1937,6 @@ pub(crate) mod tests { let bank3 = bank_forks.read().unwrap().get(3).unwrap(); bank3.process_transaction(&tx).unwrap(); - let bank3_pending_hash = Hash::new_unique(); // now add programSubscribe at the "confirmed" commitment level let exit = Arc::new(AtomicBool::new(false)); @@ -1991,7 +1989,7 @@ pub(crate) mod tests { // to see transaction for alice and bob to be notified in order. OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(3, bank3_pending_hash), + BankNotification::OptimisticallyConfirmed(3), None, /* no dependency work */ ), &bank_forks, @@ -2046,8 +2044,6 @@ pub(crate) mod tests { ); bank3.freeze(); - assert!(pending_optimistically_confirmed_banks.remove(&(3, bank3_pending_hash))); - pending_optimistically_confirmed_banks.insert((3, bank3.hash())); OptimisticallyConfirmedBankTracker::process_notification( ( BankNotification::Frozen(bank3), @@ -2175,7 +2171,7 @@ pub(crate) mod tests { // expect to see any RPC notifications. OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(3, Hash::new_unique()), + BankNotification::OptimisticallyConfirmed(3), None, /* no dependency work */ ), &bank_forks, @@ -2246,22 +2242,6 @@ pub(crate) mod tests { bank2.process_transaction(&tx).unwrap(); - // Prepare bank3 and its final hash, but do not insert it into BankForks until after - // the optimistic confirmation notification. - let joe = Keypair::new(); - let tx = system_transaction::create_account( - &mint_keypair, - &joe, - blockhash, - 3, - 16, - &stake::program::id(), - ); - let bank3 = Bank::new_from_parent(bank2, SlotLeader::default(), 3); - bank3.process_transaction(&tx).unwrap(); - bank3.freeze(); - let bank3_hash = bank3.hash(); - // now add programSubscribe at the "confirmed" commitment level let exit = Arc::new(AtomicBool::new(false)); let optimistically_confirmed_bank = @@ -2313,7 +2293,7 @@ pub(crate) mod tests { // frozen. The notifications should be in the increasing order of the slot. OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(3, bank3_hash), + BankNotification::OptimisticallyConfirmed(3), None, /* no dependency work */ ), &bank_forks, @@ -2353,8 +2333,23 @@ pub(crate) mod tests { }) }; + let bank3 = Bank::new_from_parent(bank2, SlotLeader::default(), 3); bank_forks.write().unwrap().insert(bank3); + + // add account for joe and process the transaction at bank3 + let joe = Keypair::new(); + let tx = system_transaction::create_account( + &mint_keypair, + &joe, + blockhash, + 3, + 16, + &stake::program::id(), + ); let bank3 = bank_forks.read().unwrap().get(3).unwrap(); + + bank3.process_transaction(&tx).unwrap(); + bank3.freeze(); OptimisticallyConfirmedBankTracker::process_notification( ( BankNotification::Frozen(bank3), @@ -2787,7 +2782,6 @@ pub(crate) mod tests { let bank1 = bank_forks.write().unwrap().get(1).unwrap(); bank1.process_transaction(&tx).unwrap(); bank1.freeze(); - let bank1_hash = bank1.hash(); // Add the same transaction to the unfrozen 2nd bank bank_forks @@ -2797,7 +2791,6 @@ pub(crate) mod tests { .unwrap() .process_transaction(&tx) .unwrap(); - let bank2_pending_hash = Hash::new_unique(); // First, notify the unfrozen bank first to queue pending notification let mut highest_confirmed_slot: Slot = 0; @@ -2807,10 +2800,7 @@ pub(crate) mod tests { let prioritization_fee_cache = prioritization_fee_cache_inner.as_deref(); OptimisticallyConfirmedBankTracker::process_notification( - ( - BankNotification::OptimisticallyConfirmed(2, bank2_pending_hash), - None, - ), + (BankNotification::OptimisticallyConfirmed(2), None), &bank_forks, &optimistically_confirmed_bank, &subscriptions, @@ -2827,7 +2817,7 @@ pub(crate) mod tests { highest_confirmed_slot = 0; OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(1, bank1_hash), + BankNotification::OptimisticallyConfirmed(1), None, /* no dependency work */ ), &bank_forks, @@ -2883,8 +2873,6 @@ pub(crate) mod tests { let bank2 = bank_forks.read().unwrap().get(2).unwrap(); bank2.freeze(); - assert!(pending_optimistically_confirmed_banks.remove(&(2, bank2_pending_hash))); - pending_optimistically_confirmed_banks.insert((2, bank2.hash())); highest_confirmed_slot = 0; OptimisticallyConfirmedBankTracker::process_notification( ( diff --git a/rpc/src/slot_status_notifier.rs b/rpc/src/slot_status_notifier.rs index 73d90089385..9439f2809a2 100644 --- a/rpc/src/slot_status_notifier.rs +++ b/rpc/src/slot_status_notifier.rs @@ -1,17 +1,17 @@ use { - solana_clock::{BankId, Slot}, + solana_clock::Slot, std::sync::{Arc, RwLock}, }; pub trait SlotStatusNotifierInterface { /// Notified when a slot is optimistically confirmed - fn notify_slot_confirmed(&self, slot: Slot, parent: Option, bank_id: BankId); + fn notify_slot_confirmed(&self, slot: Slot, parent: Option); /// Notified when a slot is marked frozen. - fn notify_slot_processed(&self, slot: Slot, parent: Option, bank_id: BankId); + fn notify_slot_processed(&self, slot: Slot, parent: Option); /// Notified when a slot is rooted. - fn notify_slot_rooted(&self, slot: Slot, parent: Option, bank_id: BankId); + fn notify_slot_rooted(&self, slot: Slot, parent: Option); /// Notified when the first shred is received for a slot. fn notify_first_shred_received(&self, slot: Slot); @@ -20,7 +20,7 @@ pub trait SlotStatusNotifierInterface { fn notify_completed(&self, slot: Slot); /// Notified when the slot has bank created. - fn notify_created_bank(&self, slot: Slot, parent: Slot, bank_id: BankId); + fn notify_created_bank(&self, slot: Slot, parent: Slot); /// Notified when the slot is marked "Dead" fn notify_slot_dead(&self, slot: Slot, parent: Slot, error: String); diff --git a/rpc/src/transaction_notifier_interface.rs b/rpc/src/transaction_notifier_interface.rs index 22848f7d896..232ee64004d 100644 --- a/rpc/src/transaction_notifier_interface.rs +++ b/rpc/src/transaction_notifier_interface.rs @@ -1,17 +1,13 @@ use { - solana_clock::{BankId, Slot}, - solana_hash::Hash, - solana_signature::Signature, + solana_clock::Slot, solana_hash::Hash, solana_signature::Signature, solana_transaction::versioned::VersionedTransaction, - solana_transaction_status::TransactionStatusMeta, - std::sync::Arc, + solana_transaction_status::TransactionStatusMeta, std::sync::Arc, }; pub trait TransactionNotifier { fn notify_transaction( &self, slot: Slot, - bank_id: BankId, transaction_slot_index: usize, signature: &Signature, message_hash: &Hash, diff --git a/rpc/src/transaction_status_service.rs b/rpc/src/transaction_status_service.rs index 0039c2593c8..a562e0f904f 100644 --- a/rpc/src/transaction_status_service.rs +++ b/rpc/src/transaction_status_service.rs @@ -130,7 +130,6 @@ impl TransactionStatusService { TransactionStatusMessage::Batch(( TransactionStatusBatch { slot, - bank_id, transactions, commit_results, balances, @@ -211,7 +210,6 @@ impl TransactionStatusService { let transaction = transaction.to_versioned_transaction(); transaction_notifier.notify_transaction( slot, - bank_id, transaction_index, signature, message_hash, @@ -355,7 +353,7 @@ pub(crate) mod tests { solana_account_decoder::{ parse_account_data::SplTokenAdditionalDataV2, parse_token::token_amount_to_ui_amount_v3, }, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_fee_structure::FeeDetails, solana_hash::Hash, solana_keypair::Keypair, @@ -384,7 +382,6 @@ pub(crate) mod tests { #[derive(Eq, Hash, PartialEq)] struct TestNotifierKey { slot: Slot, - bank_id: BankId, transaction_index: usize, message_hash: Hash, } @@ -410,7 +407,6 @@ pub(crate) mod tests { fn notify_transaction( &self, slot: Slot, - bank_id: BankId, transaction_index: usize, _signature: &Signature, message_hash: &Hash, @@ -421,7 +417,6 @@ pub(crate) mod tests { self.notifications.insert( TestNotifierKey { slot, - bank_id, transaction_index, message_hash: *message_hash, }, @@ -519,12 +514,10 @@ pub(crate) mod tests { }; let slot = bank.slot(); - let bank_id = bank.bank_id(); let message_hash = *transaction.message_hash(); let transaction_index: usize = bank.transaction_count().try_into().unwrap(); let transaction_status_batch = TransactionStatusBatch { slot, - bank_id, transactions: vec![transaction], commit_results: vec![commit_result], balances, @@ -558,7 +551,6 @@ pub(crate) mod tests { assert_eq!(test_notifier.notifications.len(), 1); let key = TestNotifierKey { slot, - bank_id, transaction_index, message_hash, }; @@ -629,13 +621,11 @@ pub(crate) mod tests { }; let slot = bank.slot(); - let bank_id = bank.bank_id(); let transaction_index1: usize = bank.transaction_count().try_into().unwrap(); let transaction_index2: usize = transaction_index1 + 1; let transaction_status_batch = TransactionStatusBatch { slot, - bank_id, transactions: vec![transaction1, transaction2], commit_results: vec![commit_result.clone(), commit_result], balances: balances.clone(), @@ -670,13 +660,11 @@ pub(crate) mod tests { let key1 = TestNotifierKey { slot, - bank_id, transaction_index: transaction_index1, message_hash: *expected_transaction1.message_hash(), }; let key2 = TestNotifierKey { slot, - bank_id, transaction_index: transaction_index2, message_hash: *expected_transaction2.message_hash(), }; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 5c395f22ffa..2da7852b997 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -4405,12 +4405,9 @@ impl Bank { self.enqueue_on_chain_accounts_lt_hash_updates(&to_store); // See https://github.com/solana-labs/solana/pull/31455 for discussion // on *not* updating the index within a threadpool. - self.rc.accounts.store_accounts_seq( - to_store, - self.bank_id(), - transactions.as_deref(), - &self.ancestors, - ); + self.rc + .accounts + .store_accounts_seq(to_store, transactions.as_deref(), &self.ancestors); }); // Cached vote and stake accounts are synchronized with accounts-db @@ -4833,7 +4830,7 @@ impl Bank { ); self.rc .accounts - .store_accounts_par(accounts, self.bank_id(), None, &self.ancestors); + .store_accounts_par(accounts, None, &self.ancestors); } pub fn force_flush_accounts_cache(&self) { diff --git a/runtime/src/bank/tests.rs b/runtime/src/bank/tests.rs index d63bd1bf390..47a6d4bc2fd 100644 --- a/runtime/src/bank/tests.rs +++ b/runtime/src/bank/tests.rs @@ -12542,7 +12542,7 @@ fn test_new_for_txn_tests_system_transfer() { let refs: Vec<_> = owned_accounts.iter().map(|(k, v)| (k, v)).collect(); let ancestors = Ancestors::from(vec![parent_slot]); - accounts.store_accounts_seq((parent_slot, refs.as_slice()), 0, None, &ancestors); + accounts.store_accounts_seq((parent_slot, refs.as_slice()), None, &ancestors); accounts.accounts_db.add_root(parent_slot); let bank_rc = BankRc::new(accounts); @@ -12721,7 +12721,7 @@ fn test_new_for_block_tests_with_vote_account() { let refs: Vec<_> = owned_accounts.iter().map(|(k, v)| (k, v)).collect(); let ancestors = Ancestors::from(vec![parent_slot]); - accounts.store_accounts_seq((parent_slot, refs.as_slice()), 0, None, &ancestors); + accounts.store_accounts_seq((parent_slot, refs.as_slice()), None, &ancestors); accounts.accounts_db.add_root(parent_slot); let bank_rc = BankRc::new(accounts); diff --git a/runtime/src/conformance/txn.rs b/runtime/src/conformance/txn.rs index c0482dbd377..0563e3f3efe 100644 --- a/runtime/src/conformance/txn.rs +++ b/runtime/src/conformance/txn.rs @@ -56,7 +56,7 @@ use { accounts::Accounts, accounts_db::AccountsDb, ancestors::Ancestors, blockhash_queue::BlockhashQueue, }, - solana_clock::{BankId, Clock, Epoch, MAX_PROCESSING_AGE}, + solana_clock::{Clock, Epoch, MAX_PROCESSING_AGE}, solana_epoch_schedule::EpochSchedule, solana_fee_calculator::FeeRateGovernor, solana_pubkey::Pubkey, @@ -120,7 +120,7 @@ pub fn execute_txn( // Populate the accounts DB with the input accounts at the parent slot. let bank_accounts = Accounts::new(Arc::new(AccountsDb::default_for_tests())); let ancestors = Ancestors::from(vec![parent_slot]); - bank_accounts.store_accounts_seq((parent_slot, accounts), BankId::default(), None, &ancestors); + bank_accounts.store_accounts_seq((parent_slot, accounts), None, &ancestors); bank_accounts.accounts_db.add_root(parent_slot); let bank_rc = BankRc::new(bank_accounts); diff --git a/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index f6112666529..17f135357f8 100644 --- a/runtime/src/serde_snapshot/tests.rs +++ b/runtime/src/serde_snapshot/tests.rs @@ -220,12 +220,7 @@ mod serde_snapshot_tests { for (i, pubkey) in pubkeys.iter().enumerate() { let account = AccountSharedData::new(i as u64 + 1, 0, &Pubkey::default()); - accounts.store_accounts_seq( - (slot, [(pubkey, &account)].as_slice()), - 0, - None, - &ancestors, - ); + accounts.store_accounts_seq((slot, [(pubkey, &account)].as_slice()), None, &ancestors); } check_accounts_local(&accounts, &pubkeys, 100); accounts.accounts_db.add_root_and_flush_write_cache(slot); diff --git a/runtime/src/transaction_execution.rs b/runtime/src/transaction_execution.rs index d2a8c1f7837..be3cd7b8425 100644 --- a/runtime/src/transaction_execution.rs +++ b/runtime/src/transaction_execution.rs @@ -9,7 +9,7 @@ use { vote_sender_types::{ReplayVoteSendType, ReplayVoteSender}, }, log::{trace, warn}, - solana_clock::{BankId, Slot}, + solana_clock::Slot, solana_cost_model::{cost_model::CostModel, transaction_cost::TransactionCost}, solana_measure::measure::Measure, solana_runtime_transaction::transaction_with_meta::TransactionWithMeta, @@ -33,7 +33,6 @@ type WorkSequence = u64; #[derive(Debug)] pub struct TransactionStatusBatch { pub slot: Slot, - pub bank_id: BankId, pub transactions: Vec, pub commit_results: Vec, pub balances: TransactionBalancesSet, @@ -141,7 +140,6 @@ pub fn execute_batch<'a>( transaction_status_sender.send_transaction_status_batch( bank.slot(), - bank.bank_id(), transactions, commit_results, balances, @@ -238,7 +236,6 @@ impl TransactionStatusSender { pub fn send_transaction_status_batch( &self, slot: Slot, - bank_id: BankId, transactions: Vec, commit_results: Vec, balances: TransactionBalancesSet, @@ -254,7 +251,6 @@ impl TransactionStatusSender { if let Err(e) = self.sender.send(TransactionStatusMessage::Batch(( TransactionStatusBatch { slot, - bank_id, transactions, commit_results, balances, diff --git a/votor/src/root_utils.rs b/votor/src/root_utils.rs index 776a9718090..f98f468a532 100644 --- a/votor/src/root_utils.rs +++ b/votor/src/root_utils.rs @@ -83,7 +83,7 @@ pub(crate) fn set_root( .map(|s| s.get_current_declared_work()); // TODO: propagate error let _ = config.sender.send(( - BankNotification::OptimisticallyConfirmed(new_root, hash), + BankNotification::OptimisticallyConfirmed(new_root), dependency_work, )); } @@ -126,15 +126,14 @@ pub fn check_and_handle_new_root( let oldest_parent = rooted_banks.last().map(|last| last.parent_slot()); rooted_banks.push(root_bank.clone()); let rooted_slots: Vec<_> = rooted_banks.iter().map(|bank| bank.slot()).collect(); - let rooted_slot_notifications = bank_notification_sender + // The following differs from rooted_slots by including the parent slot of the oldest parent bank. + let rooted_slots_with_parents = bank_notification_sender .as_ref() .is_some_and(|sender| sender.should_send_parents) .then(|| { - let new_chain = rooted_banks - .iter() - .map(|bank| (bank.slot(), bank.bank_id())) - .collect(); - (new_chain, oldest_parent.unwrap_or(parent_slot)) + let mut new_chain = rooted_slots.clone(); + new_chain.push(oldest_parent.unwrap_or(parent_slot)); + new_chain }); // Call leader schedule_cache.set_root() before blockstore.set_root() because @@ -167,17 +166,14 @@ pub fn check_and_handle_new_root( .send((BankNotification::NewRootBank(root_bank), dependency_work)) .unwrap_or_else(|err| warn!("bank_notification_sender failed: {err:?}")); - if let Some((new_chain, oldest_parent)) = rooted_slot_notifications { + if let Some(new_chain) = rooted_slots_with_parents { let dependency_work = sender .dependency_tracker .as_ref() .map(|s| s.get_current_declared_work()); sender .sender - .send(( - BankNotification::NewRootedChain(new_chain, oldest_parent), - dependency_work, - )) + .send((BankNotification::NewRootedChain(new_chain), dependency_work)) .unwrap_or_else(|err| warn!("bank_notification_sender failed: {err:?}")); } } From 5fc30571b785d1444a59872212aab12b3db298a3 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 17 Jul 2026 17:12:01 -0400 Subject: [PATCH 24/30] chore(dep): update vendored crossbeam-epoch to 0.9.20 (#13931) chore(dep): update vendored crossbeam-epoch to 0.9.20 (RUSTSEC-2026-0204) Repoints the main-workspace [patch.crates-io] crossbeam-epoch at anza-xyz/crossbeam@5ee1c6ec (branch anza-crossbeam-epoch-0.9.20): crossbeam-epoch 0.9.20 plus our PINNINGS_BETWEEN_COLLECT patch. 0.9.20 carries the RUSTSEC-2026-0204 fmt::Pointer soundness fix. The main workspace vendors crossbeam-epoch via a git patch, so it was not covered by the earlier crates.io bump (#13686); this closes that gap. #13747 --- Cargo.lock | 25 ++++++------------------- Cargo.toml | 4 ++-- 2 files changed, 8 insertions(+), 21 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e1cbd36db32..1ce06e5eaff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2218,14 +2218,10 @@ dependencies = [ [[package]] name = "crossbeam-epoch" -version = "0.9.5" -source = "git+https://github.com/anza-xyz/crossbeam?rev=fd279d707025f0e60951e429bf778b4813d1b6bf#fd279d707025f0e60951e429bf778b4813d1b6bf" +version = "0.9.20" +source = "git+https://github.com/anza-xyz/crossbeam?rev=5ee1c6ec671d94db14ed9238eb99e59a84555aad#5ee1c6ec671d94db14ed9238eb99e59a84555aad" dependencies = [ - "cfg-if 1.0.4", "crossbeam-utils", - "lazy_static", - "memoffset 0.6.4", - "scopeguard", ] [[package]] @@ -4686,15 +4682,6 @@ dependencies = [ "libc", ] -[[package]] -name = "memoffset" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59accc507f1338036a0477ef61afdae33cde60840f4dfe481319ce3ad116ddf9" -dependencies = [ - "autocfg", -] - [[package]] name = "memoffset" version = "0.9.1" @@ -4867,7 +4854,7 @@ dependencies = [ "cfg-if 1.0.4", "cfg_aliases", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -7204,7 +7191,7 @@ dependencies = [ "dashmap", "itertools 0.14.0", "log", - "memoffset 0.9.1", + "memoffset", "modular-bitfield", "num_cpus", "qualifier_attr", @@ -9672,7 +9659,7 @@ version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "778f08fb0eaf52c9a3bef2978247f7fab0ccfddc44cfddb936d5ad9f98ede886" dependencies = [ - "memoffset 0.9.1", + "memoffset", "solana-account-info", "solana-big-mod-exp", "solana-blake3-hasher", @@ -10336,7 +10323,7 @@ dependencies = [ "libc", "libsecp256k1", "log", - "memoffset 0.9.1", + "memoffset", "mockall", "num-derive", "num-traits", diff --git a/Cargo.toml b/Cargo.toml index 53bab0d7ec2..8488bf61546 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -617,5 +617,5 @@ lto = "fat" codegen-units = 1 [patch.crates-io] -# for details, see https://github.com/anza-xyz/crossbeam/commit/fd279d707025f0e60951e429bf778b4813d1b6bf -crossbeam-epoch = { git = "https://github.com/anza-xyz/crossbeam", rev = "fd279d707025f0e60951e429bf778b4813d1b6bf" } +# for details, see https://github.com/anza-xyz/crossbeam/commit/5ee1c6ec671d94db14ed9238eb99e59a84555aad +crossbeam-epoch = { git = "https://github.com/anza-xyz/crossbeam", rev = "5ee1c6ec671d94db14ed9238eb99e59a84555aad" } From 40b907cce8934748a7d74155ecccd05a385539aa Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 10:17:06 +0800 Subject: [PATCH 25/30] chore(deps): bump lru from 0.18.0 to 0.18.1 (#13914) * chore(deps): bump lru from 0.18.0 to 0.18.1 Bumps [lru](https://github.com/jeromefroe/lru-rs) from 0.18.0 to 0.18.1. - [Changelog](https://github.com/jeromefroe/lru-rs/blob/master/CHANGELOG.md) - [Commits](https://github.com/jeromefroe/lru-rs/compare/0.18.0...0.18.1) --- updated-dependencies: - dependency-name: lru dependency-version: 0.18.1 dependency-type: direct:production update-type: version-update:semver-patch ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- dev-bins/Cargo.lock | 10 +++++----- programs/sbf/Cargo.lock | 10 +++++----- 4 files changed, 16 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ce06e5eaff..9d39a13c42a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4608,9 +4608,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.0", ] @@ -9115,7 +9115,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -11421,7 +11421,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "serde_json", @@ -11597,7 +11597,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "rand 0.9.4", "rand_chacha 0.9.0", "rayon", diff --git a/Cargo.toml b/Cargo.toml index 8488bf61546..e2464898274 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -288,7 +288,7 @@ libsecp256k1 = { version = "0.7.2", default-features = false, features = [ "static-context", ] } log = "0.4.33" -lru = "0.18.0" +lru = "0.18.1" lz4 = "1.28.1" memmap2 = "0.9.11" memoffset = "0.9.1" diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index c2b86fc3f6c..8d0ed6ee65a 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -3844,9 +3844,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.0", ] @@ -7169,7 +7169,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -8827,7 +8827,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "solana-clock", @@ -8975,7 +8975,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "rand 0.9.4", "rand_chacha 0.9.0", "rayon", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 5dc0700954d..de963ad5374 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -3864,9 +3864,9 @@ dependencies = [ [[package]] name = "lru" -version = "0.18.0" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a860605968fce16869fd239cf4237a82f3ac470723415db603b0e8b6c8d4fb9" +checksum = "0b6180140927ee907000b0aa540091f6ea512ead4447c92b8fc35bc72788a5a6" dependencies = [ "hashbrown 0.17.0", ] @@ -7354,7 +7354,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -9897,7 +9897,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "solana-clock", @@ -10045,7 +10045,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "rand 0.9.4", "rand_chacha 0.9.0", "rayon", From 4b5bfe6f7230a0f1ee7380affc39c381b9f358b5 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 18 Jul 2026 04:18:50 +0200 Subject: [PATCH 26/30] chore(deps): bump regex from 1.12.4 to 1.13.0 (#13915) * chore(deps): bump regex from 1.12.4 to 1.13.0 Bumps [regex](https://github.com/rust-lang/regex) from 1.12.4 to 1.13.0. - [Release notes](https://github.com/rust-lang/regex/releases) - [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md) - [Commits](https://github.com/rust-lang/regex/compare/1.12.4...1.13.0) --- updated-dependencies: - dependency-name: regex dependency-version: 1.13.0 dependency-type: direct:production update-type: version-update:semver-minor ... Signed-off-by: dependabot[bot] * Update all workspaces --------- Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- Cargo.lock | 10 +++++----- Cargo.toml | 2 +- ci/xtask/Cargo.lock | 4 ++-- ci/xtask/Cargo.toml | 2 +- dev-bins/Cargo.lock | 4 ++-- dev-bins/Cargo.toml | 2 +- programs/sbf/Cargo.lock | 4 ++-- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9d39a13c42a..ee98a084717 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6138,13 +6138,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d" dependencies = [ "aho-corasick 1.0.1", "memchr", - "regex-automata 0.4.13", + "regex-automata 0.4.16", "regex-syntax", ] @@ -6156,9 +6156,9 @@ checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "8fcfdb36bda0c880c5931cdc7a2bcdc8ba4556847b9d912bca70bc94708711ad" dependencies = [ "aho-corasick 1.0.1", "memchr", diff --git a/Cargo.toml b/Cargo.toml index e2464898274..8cb22e604ea 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -320,7 +320,7 @@ rand = "0.9.4" rand_chacha = "0.9.0" rayon = "1.12.0" reed-solomon-erasure = "6.0.0" -regex = "1.12.4" +regex = "1.13.0" reqwest = { version = "0.12.28", default-features = false } reqwest-middleware = "0.4.2" rolling-file = "0.2.0" diff --git a/ci/xtask/Cargo.lock b/ci/xtask/Cargo.lock index f1692c57a2c..abb4da7c59b 100644 --- a/ci/xtask/Cargo.lock +++ b/ci/xtask/Cargo.lock @@ -1611,9 +1611,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", diff --git a/ci/xtask/Cargo.toml b/ci/xtask/Cargo.toml index b2285afb7bc..5a9a17c6b82 100644 --- a/ci/xtask/Cargo.toml +++ b/ci/xtask/Cargo.toml @@ -25,7 +25,7 @@ env_logger = "0.11.11" futures-util = "0.3.31" log = "0.4.28" octocrab = { version = "0.54.0", features = ["stream"] } -regex = "1.12.4" +regex = "1.13.0" reqwest = { version = "0.12", default-features = false, features = ["rustls-tls"] } semver = { version = "1.0.27", features = ["serde"] } serde = { version = "1.0.228", features = ["derive"] } diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 8d0ed6ee65a..648bddc5401 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -5036,9 +5036,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick", "memchr", diff --git a/dev-bins/Cargo.toml b/dev-bins/Cargo.toml index d47d4bd7a42..4082604d4a8 100644 --- a/dev-bins/Cargo.toml +++ b/dev-bins/Cargo.toml @@ -61,7 +61,7 @@ prost-build = "0.14.4" protosol = "2.0.0" rand = "0.9.4" rayon = "1.12.0" -regex = "1.12.4" +regex = "1.13.0" serde = { version = "1.0.228", features = ["derive"] } serde_bytes = "0.11.19" serde_json = "1.0.150" diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index de963ad5374..a99af12465b 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -5056,9 +5056,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.12.4" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1292b7759ae1cb9ec195452d1390a074f0cd8541ab7a5a8c31cd6db45d4a6ba" +checksum = "2a0e75113e14dc5acb068cd0786884f214f1312650a3d36d269f5c4f3cdee8a2" dependencies = [ "aho-corasick 1.0.1", "memchr", From 7a7c736cbd17cee1445061c990e4624e1186b2e5 Mon Sep 17 00:00:00 2001 From: zz-sol Date: Sat, 18 Jul 2026 07:01:03 -0400 Subject: [PATCH 27/30] impl SIMD-0529 big int mod exp syscall (#12321) * wip * clean up * Update lib.rs * Update lib.rs * ci * wip * Update Cargo.lock * wip * wip * Fix compute budget formatting * update c API * address comments * charge cu before doing computation * update solana-big-mod-exp version * address comments * update solana-big-mod-exp to crates.io * Refresh Cargo.lock files * Update lib.rs * remove redundent checkes * fix ci * update solana-big-mod-exp * address comments * cargo fmt * Update Cargo.lock --- Cargo.lock | 13 +- Cargo.toml | 1 + compute-budget/src/compute_budget.rs | 12 + dev-bins/Cargo.lock | 11 + feature-set/src/lib.rs | 2 +- program-runtime/src/execution_budget.rs | 10 + programs/sbf/Cargo.lock | 29 +- programs/sbf/Cargo.toml | 2 + programs/sbf/c/inc/sol/big_mod_exp.h | 15 +- programs/sbf/c/inc/sol/inc/big_mod_exp.inc | 15 +- programs/sbf/c/src/big_mod_exp/big_mod_exp.c | 30 ++ programs/sbf/rust/big_mod_exp/Cargo.toml | 21 + programs/sbf/rust/big_mod_exp/src/lib.rs | 106 +++++ programs/sbf/tests/programs.rs | 2 + syscalls/Cargo.toml | 1 + syscalls/src/lib.rs | 409 ++++++++++++++++++- 16 files changed, 666 insertions(+), 13 deletions(-) create mode 100644 programs/sbf/c/src/big_mod_exp/big_mod_exp.c create mode 100644 programs/sbf/rust/big_mod_exp/Cargo.toml create mode 100644 programs/sbf/rust/big_mod_exp/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ee98a084717..c3daf3c5dfe 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7402,6 +7402,16 @@ dependencies = [ "solana-define-syscall 3.0.0", ] +[[package]] +name = "solana-big-mod-exp" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05789c4afc39164132db9dcc117218d412ca1f1271d2c629fc6f554c19e5a44" +dependencies = [ + "num-bigint 0.4.6", + "solana-define-syscall 5.1.0", +] + [[package]] name = "solana-bincode" version = "3.1.0" @@ -9661,7 +9671,7 @@ checksum = "778f08fb0eaf52c9a3bef2978247f7fab0ccfddc44cfddb936d5ad9f98ede886" dependencies = [ "memoffset", "solana-account-info", - "solana-big-mod-exp", + "solana-big-mod-exp 3.0.0", "solana-blake3-hasher", "solana-clock", "solana-cpi", @@ -11095,6 +11105,7 @@ dependencies = [ "num-traits", "solana-account 4.3.1", "solana-account-info", + "solana-big-mod-exp 4.0.0", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", diff --git a/Cargo.toml b/Cargo.toml index 8cb22e604ea..bec7ba0d96f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -357,6 +357,7 @@ solana-address-lookup-table-interface = "3.1.0" solana-banks-client = { path = "banks-client", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } solana-banks-interface = { path = "banks-interface", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } solana-banks-server = { path = "banks-server", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } +solana-big-mod-exp = "4.0.0" solana-bincode = "3.1.0" solana-blake3-hasher = "3.1.0" solana-bloom = { path = "bloom", version = "=4.3.0-alpha.1", features = ["agave-unstable-api"] } diff --git a/compute-budget/src/compute_budget.rs b/compute-budget/src/compute_budget.rs index 9e9f7a4e008..322ed114534 100644 --- a/compute-budget/src/compute_budget.rs +++ b/compute-budget/src/compute_budget.rs @@ -97,6 +97,14 @@ pub struct ComputeBudget { /// + alt_bn128_pairing_one_pair_cost_other * (num_elems - 1) pub alt_bn128_pairing_one_pair_cost_first: u64, pub alt_bn128_pairing_one_pair_cost_other: u64, + /// Big integer modular exponentiation base cost + pub big_modular_exponentiation_base_cost: u64, + /// Big integer modular exponentiation cost divisor. + /// The modular exponentiation cost is computed from SIMD-0529 operand + /// complexity and adjusted exponent bit length, divided by this divisor, + /// plus + /// `big_modular_exponentiation_base_cost`. + pub big_modular_exponentiation_cost_divisor: u64, /// Coefficient `a` of the quadratic function which determines the number /// of compute units consumed to call poseidon syscall for a given number /// of inputs. @@ -202,6 +210,8 @@ impl ComputeBudget { alt_bn128_g2_multiplication_cost: cost.alt_bn128_g2_multiplication_cost, alt_bn128_pairing_one_pair_cost_first: cost.alt_bn128_pairing_one_pair_cost_first, alt_bn128_pairing_one_pair_cost_other: cost.alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_base_cost: cost.big_modular_exponentiation_base_cost, + big_modular_exponentiation_cost_divisor: cost.big_modular_exponentiation_cost_divisor, poseidon_cost_coefficient_a: cost.poseidon_cost_coefficient_a, poseidon_cost_coefficient_c: cost.poseidon_cost_coefficient_c, get_remaining_compute_units_cost: cost.get_remaining_compute_units_cost, @@ -269,6 +279,8 @@ impl ComputeBudget { alt_bn128_g2_multiplication_cost: self.alt_bn128_g2_multiplication_cost, alt_bn128_pairing_one_pair_cost_first: self.alt_bn128_pairing_one_pair_cost_first, alt_bn128_pairing_one_pair_cost_other: self.alt_bn128_pairing_one_pair_cost_other, + big_modular_exponentiation_base_cost: self.big_modular_exponentiation_base_cost, + big_modular_exponentiation_cost_divisor: self.big_modular_exponentiation_cost_divisor, poseidon_cost_coefficient_a: self.poseidon_cost_coefficient_a, poseidon_cost_coefficient_c: self.poseidon_cost_coefficient_c, get_remaining_compute_units_cost: self.get_remaining_compute_units_cost, diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 648bddc5401..54fc8916c82 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -5990,6 +5990,16 @@ dependencies = [ "parking_lot 0.12.5", ] +[[package]] +name = "solana-big-mod-exp" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05789c4afc39164132db9dcc117218d412ca1f1271d2c629fc6f554c19e5a44" +dependencies = [ + "num-bigint 0.4.6", + "solana-define-syscall 5.1.0", +] + [[package]] name = "solana-bincode" version = "3.1.0" @@ -8629,6 +8639,7 @@ dependencies = [ "num-traits", "solana-account 4.3.1", "solana-account-info", + "solana-big-mod-exp", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", diff --git a/feature-set/src/lib.rs b/feature-set/src/lib.rs index f27371d8a58..2cec983054a 100644 --- a/feature-set/src/lib.rs +++ b/feature-set/src/lib.rs @@ -908,7 +908,7 @@ pub mod update_hashes_per_tick { } pub mod enable_big_mod_exp_syscall { - solana_pubkey::declare_id!("EBq48m8irRKuE7ZnMTLvLg2UuGSqhe8s8oMqnmja1fJw"); + solana_pubkey::declare_id!("expH2ppKPW2ANEdEmAjfhSEcnBQJfmoX4FjuNpe9ttg"); } pub mod disable_builtin_loader_ownership_chains { diff --git a/program-runtime/src/execution_budget.rs b/program-runtime/src/execution_budget.rs index a535f1f0186..15dd23bb0fb 100644 --- a/program-runtime/src/execution_budget.rs +++ b/program-runtime/src/execution_budget.rs @@ -152,6 +152,14 @@ pub struct SVMTransactionExecutionCost { /// + alt_bn128_pairing_one_pair_cost_other * (num_elems - 1) pub alt_bn128_pairing_one_pair_cost_first: u64, pub alt_bn128_pairing_one_pair_cost_other: u64, + /// Big integer modular exponentiation base cost + pub big_modular_exponentiation_base_cost: u64, + /// Big integer modular exponentiation cost divisor. + /// The modular exponentiation cost is computed from SIMD-0529 operand + /// complexity and adjusted exponent bit length, divided by this divisor, + /// plus + /// `big_modular_exponentiation_base_cost`. + pub big_modular_exponentiation_cost_divisor: u64, /// Coefficient `a` of the quadratic function which determines the number /// of compute units consumed to call poseidon syscall for a given number /// of inputs. @@ -229,6 +237,8 @@ impl Default for SVMTransactionExecutionCost { alt_bn128_g2_multiplication_cost: 15_670, alt_bn128_pairing_one_pair_cost_first: 36_364, alt_bn128_pairing_one_pair_cost_other: 12_121, + big_modular_exponentiation_base_cost: 422, + big_modular_exponentiation_cost_divisor: 189, poseidon_cost_coefficient_a: 61, poseidon_cost_coefficient_c: 542, get_remaining_compute_units_cost: 100, diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a99af12465b..a5f5339fa0b 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -853,6 +853,12 @@ dependencies = [ "rand 0.8.6", ] +[[package]] +name = "array-bytes" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ad284aeb45c13f2fb4f084de4a420ebf447423bdf9386c0540ce33cb3ef4b8c" + [[package]] name = "arrayref" version = "0.3.9" @@ -6140,6 +6146,16 @@ dependencies = [ "solana-define-syscall 3.0.0", ] +[[package]] +name = "solana-big-mod-exp" +version = "4.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f05789c4afc39164132db9dcc117218d412ca1f1271d2c629fc6f554c19e5a44" +dependencies = [ + "num-bigint 0.4.6", + "solana-define-syscall 5.1.0", +] + [[package]] name = "solana-bincode" version = "3.1.0" @@ -7747,7 +7763,7 @@ checksum = "778f08fb0eaf52c9a3bef2978247f7fab0ccfddc44cfddb936d5ad9f98ede886" dependencies = [ "memoffset", "solana-account-info", - "solana-big-mod-exp", + "solana-big-mod-exp 3.0.0", "solana-blake3-hasher", "solana-borsh", "solana-clock", @@ -8530,6 +8546,16 @@ dependencies = [ "solana-program-entrypoint", ] +[[package]] +name = "solana-sbf-rust-big-mod-exp" +version = "4.3.0-alpha.1" +dependencies = [ + "array-bytes", + "solana-big-mod-exp 4.0.0", + "solana-msg", + "solana-program-entrypoint", +] + [[package]] name = "solana-sbf-rust-call-args" version = "4.3.0-alpha.1" @@ -9641,6 +9667,7 @@ dependencies = [ "num-traits", "solana-account 4.3.1", "solana-account-info", + "solana-big-mod-exp 4.0.0", "solana-blake3-hasher", "solana-bls12-381-syscall", "solana-bn254", diff --git a/programs/sbf/Cargo.toml b/programs/sbf/Cargo.toml index 0cdb88412d0..94d656baadc 100644 --- a/programs/sbf/Cargo.toml +++ b/programs/sbf/Cargo.toml @@ -20,6 +20,7 @@ members = [ "rust/alloc", "rust/alt_bn128", "rust/alt_bn128_compression", + "rust/big_mod_exp", "rust/call_args", "rust/call_depth", "rust/caller_access", @@ -118,6 +119,7 @@ serde_json = "1.0.150" solana-account-info = "=3.1.1" solana-account-view = "=2.0.0" solana-address = "=2.6.1" +solana-big-mod-exp = "4.0.0" solana-blake3-hasher = { version = "=3.1.0", features = ["blake3"] } solana-bn254 = "=3.2.1" solana-clock = { version = "=3.1.1", features = ["serde", "sysvar"] } diff --git a/programs/sbf/c/inc/sol/big_mod_exp.h b/programs/sbf/c/inc/sol/big_mod_exp.h index 0db1962dae6..e0e9b3e932e 100644 --- a/programs/sbf/c/inc/sol/big_mod_exp.h +++ b/programs/sbf/c/inc/sol/big_mod_exp.h @@ -7,11 +7,22 @@ extern "C" { #endif +#define BIG_MOD_EXP_MAX_BYTES 512 + +typedef struct { + const uint8_t *base; + uint64_t base_len; + const uint8_t *exponent; + uint64_t exponent_len; + const uint8_t *modulus; + uint64_t modulus_len; +} SolBigModExpParams; + /** * Big integer modular exponentiation * - * @param bytes Pointer to BigModExpParam struct - * @param result 32 byte array to hold the result + * @param params Pointer to SolBigModExpParams bytes + * @param result Pointer to writable result buffer, at least params->modulus_len bytes * @return 0 if executed successfully */ /* DO NOT MODIFY THIS GENERATED FILE. INSTEAD CHANGE platform-tools-sdk/sbf/c/inc/sol/inc/big_mod_exp.inc AND RUN `cargo run --bin gen-headers` */ diff --git a/programs/sbf/c/inc/sol/inc/big_mod_exp.inc b/programs/sbf/c/inc/sol/inc/big_mod_exp.inc index ce5c6656296..90f8c950c31 100644 --- a/programs/sbf/c/inc/sol/inc/big_mod_exp.inc +++ b/programs/sbf/c/inc/sol/inc/big_mod_exp.inc @@ -7,11 +7,22 @@ extern "C" { #endif +#define BIG_MOD_EXP_MAX_BYTES 512 + +typedef struct { + const uint8_t *base; + uint64_t base_len; + const uint8_t *exponent; + uint64_t exponent_len; + const uint8_t *modulus; + uint64_t modulus_len; +} SolBigModExpParams; + /** * Big integer modular exponentiation * - * @param bytes Pointer to BigModExpParam struct - * @param result 32 byte array to hold the result + * @param params Pointer to SolBigModExpParams bytes + * @param result Pointer to writable result buffer, at least params->modulus_len bytes * @return 0 if executed successfully */ @SYSCALL uint64_t sol_big_mod_exp(const uint8_t *, uint8_t *); diff --git a/programs/sbf/c/src/big_mod_exp/big_mod_exp.c b/programs/sbf/c/src/big_mod_exp/big_mod_exp.c new file mode 100644 index 00000000000..5fa1c084bee --- /dev/null +++ b/programs/sbf/c/src/big_mod_exp/big_mod_exp.c @@ -0,0 +1,30 @@ +/** + * @brief Big integer modular exponentiation Syscall test + */ + +#include + +extern uint64_t entrypoint(const uint8_t *input) { + + SolBigModExpParams params; + + uint8_t base[] = { 0x05 }; + uint8_t exponent[] = { 0x02 }; + uint8_t modulus[] = { 0x07 }; + uint8_t expected[] = { 0x04 }; + uint8_t result[sizeof(modulus)]; + + params.base = base; + params.base_len = sizeof(base); + params.exponent = exponent; + params.exponent_len = sizeof(exponent); + params.modulus = modulus; + params.modulus_len = sizeof(modulus); + + uint64_t result_code = sol_big_mod_exp((const uint8_t *)¶ms, result); + + sol_assert(0 == result_code); + sol_assert(0 == sol_memcmp(result, expected, sizeof(expected))); + + return SUCCESS; +} diff --git a/programs/sbf/rust/big_mod_exp/Cargo.toml b/programs/sbf/rust/big_mod_exp/Cargo.toml new file mode 100644 index 00000000000..ac71a7d04da --- /dev/null +++ b/programs/sbf/rust/big_mod_exp/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "solana-sbf-rust-big-mod-exp" +version = { workspace = true } +description = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[lib] +crate-type = ["cdylib"] + +[dependencies] +array-bytes = { workspace = true } +solana-big-mod-exp = { workspace = true } +solana-msg = { workspace = true } +solana-program-entrypoint = { workspace = true } + +[lints] +workspace = true diff --git a/programs/sbf/rust/big_mod_exp/src/lib.rs b/programs/sbf/rust/big_mod_exp/src/lib.rs new file mode 100644 index 00000000000..af4a718e009 --- /dev/null +++ b/programs/sbf/rust/big_mod_exp/src/lib.rs @@ -0,0 +1,106 @@ +//! Big_mod_exp Syscall tests + +use { + solana_big_mod_exp::big_mod_exp, solana_msg::msg, + solana_program_entrypoint::custom_panic_default, +}; + +fn big_mod_exp_test() { + // Each case is (base, exponent, modulus, expected), given as big-endian hex. + let test_cases: &[(&str, &str, &str, &str)] = &[ + ( + "1111111111111111111111111111111111111111111111111111111111111111", + "1111111111111111111111111111111111111111111111111111111111111111", + "111111111111111111111111111111111111111111111111111111111111110A", + "0A7074864588D6847F33A168209E516F60005A0CEC3F33AAF70E8002FE964BCD", + ), + ( + "2222222222222222222222222222222222222222222222222222222222222222", + "2222222222222222222222222222222222222222222222222222222222222222", + "1111111111111111111111111111111111111111111111111111111111111111", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "3333333333333333333333333333333333333333333333333333333333333333", + "3333333333333333333333333333333333333333333333333333333333333333", + "2222222222222222222222222222222222222222222222222222222222222222", + "1111111111111111111111111111111111111111111111111111111111111111", + ), + ( + "9874231472317432847923174392874918237439287492374932871937289719", + "0948403985401232889438579475812347232099080051356165126166266222", + "25532321a214321423124212222224222b242222222222222222222222222444", + "220ECE1C42624E98AEE7EB86578B2FE5C4855DFFACCB43CCBB708A3AB37F184D", + ), + ( + "3494396663463663636363662632666565656456646566786786676786768766", + "2324324333246536456354655645656616169896565698987033121934984955", + "0218305479243590485092843590249879879842313131156656565565656566", + "012F2865E8B9E79B645FCE3A9E04156483AE1F9833F6BFCF86FCA38FC2D5BEF0", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000005", + "0000000000000000000000000000000000000000000000000000000000000002", + "0000000000000000000000000000000000000000000000000000000000000007", + "0000000000000000000000000000000000000000000000000000000000000004", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000064", + "0000000000000000000000000000000000000000000000000000000000000019", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000000", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ( + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000019", + "0000000000000000000000000000000000000000000000000000000000000001", + "0000000000000000000000000000000000000000000000000000000000000000", + ), + ]; + + test_cases + .iter() + .for_each(|(base, exponent, modulus, expected)| { + let mut base = array_bytes::hex2bytes_unchecked(base); + let mut exponent = array_bytes::hex2bytes_unchecked(exponent); + let mut modulus = array_bytes::hex2bytes_unchecked(modulus); + let mut expected = array_bytes::hex2bytes_unchecked(expected); + base.reverse(); + exponent.reverse(); + modulus.reverse(); + expected.reverse(); + + let result = big_mod_exp(base.as_slice(), exponent.as_slice(), modulus.as_slice()); + if is_supported_modulus(&modulus) { + assert_eq!(result, Some(expected)); + } else { + assert_eq!(result, None); + } + }); +} + +fn is_supported_modulus(modulus: &[u8]) -> bool { + let Some((&least_significant_byte, more_significant_bytes)) = modulus.split_first() else { + return false; + }; + + least_significant_byte & 1 == 1 + && (least_significant_byte > 1 || more_significant_bytes.iter().any(|byte| *byte != 0)) +} + +#[unsafe(no_mangle)] +pub extern "C" fn entrypoint(_input: *mut u8) -> u64 { + msg!("big_mod_exp"); + + big_mod_exp_test(); + + 0 +} + +custom_panic_default!(); diff --git a/programs/sbf/tests/programs.rs b/programs/sbf/tests/programs.rs index af3d79ed0cc..d67a3469db0 100644 --- a/programs/sbf/tests/programs.rs +++ b/programs/sbf/tests/programs.rs @@ -237,6 +237,7 @@ fn test_program_sbf_sanity() { ("alloc", true), ("alt_bn128", true), ("alt_bn128_compression", true), + ("big_mod_exp", true), ("sbf_to_sbf", true), ("float", true), ("multiple_static", true), @@ -262,6 +263,7 @@ fn test_program_sbf_sanity() { ("solana_sbf_rust_alloc", true), ("solana_sbf_rust_alt_bn128", true), ("solana_sbf_rust_alt_bn128_compression", true), + ("solana_sbf_rust_big_mod_exp", true), ("solana_sbf_rust_curve25519", true), ("solana_sbf_rust_custom_heap", true), ("solana_sbf_rust_dep_crate", true), diff --git a/syscalls/Cargo.toml b/syscalls/Cargo.toml index 57a5eea9c34..0dff07780cd 100644 --- a/syscalls/Cargo.toml +++ b/syscalls/Cargo.toml @@ -31,6 +31,7 @@ libsecp256k1 = { workspace = true } num-traits = { workspace = true } solana-account = { workspace = true } solana-account-info = { workspace = true } +solana-big-mod-exp = { workspace = true } solana-blake3-hasher = { workspace = true, features = ["blake3"] } solana-bls12-381-syscall = { workspace = true } solana-bn254 = { workspace = true } diff --git a/syscalls/src/lib.rs b/syscalls/src/lib.rs index facd61be6c4..9fddd18b984 100644 --- a/syscalls/src/lib.rs +++ b/syscalls/src/lib.rs @@ -13,6 +13,10 @@ pub use self::{ }; use { crate::mem_ops::is_nonoverlapping, + solana_big_mod_exp::{ + BIG_MOD_EXP_MAX_BYTES, BIG_MOD_EXP_MIN_EXPONENT_LENGTH, + BIG_MOD_EXP_MOD_REDUCTION_COMPLEXITY_FACTOR, BigModExpParams, big_mod_exp, + }, solana_blake3_hasher as blake3, solana_cpi::MAX_RETURN_DATA, solana_hash::Hash, @@ -2312,21 +2316,153 @@ declare_builtin_function!( } ); +fn big_mod_exp_mult_complexity(input_len: u64) -> Option { + let input_len = input_len as u128; + let input_len_squared = input_len.checked_mul(input_len)?; + if input_len <= 64 { + Some(input_len_squared) + } else if input_len <= 1024 { + input_len_squared + .checked_div(4)? + .checked_add(96_u128.checked_mul(input_len)?)? + .checked_sub(3_072) + } else { + input_len_squared + .checked_div(16)? + .checked_add(480_u128.checked_mul(input_len)?)? + .checked_sub(199_680) + } +} + +fn big_mod_exp_highest_set_bit_index_le(bytes: &[u8]) -> Option { + bytes.iter().enumerate().rev().find_map(|(index, byte)| { + (*byte != 0).then(|| { + (index as u64) + .saturating_mul(u64::from(u8::BITS)) + .saturating_add(u64::from(7_u32.saturating_sub(byte.leading_zeros()))) + }) + }) +} + +fn big_mod_exp_adjusted_exponent_length(exponent: &[u8]) -> u64 { + if exponent.len() <= 32 { + big_mod_exp_highest_set_bit_index_le(exponent).unwrap_or(0) + } else { + let trailing_bytes = exponent.len().saturating_sub(32); + let most_significant_32_bytes = &exponent[trailing_bytes..]; + (trailing_bytes as u64) + .saturating_mul(u64::from(u8::BITS)) + .saturating_add( + big_mod_exp_highest_set_bit_index_le(most_significant_32_bytes).unwrap_or(0), + ) + } +} + +fn big_mod_exp_is_one_le(bytes: &[u8]) -> bool { + matches!(bytes.first(), Some(1)) && bytes[1..].iter().all(|byte| *byte == 0) +} + +/// Compute the operation cost of a big integer modular exponentiation, i.e. the +/// cost charged on top of the flat `big_modular_exponentiation_base_cost`. +fn big_mod_exp_operation_cost( + cost_divisor: u64, + params: &BigModExpParams, + exponent: &[u8], +) -> Option { + let input_len = params.base_len.max(params.modulus_len); + let mult_complexity = big_mod_exp_mult_complexity(input_len)?; + let operation_complexity = if big_mod_exp_is_one_le(exponent) { + mult_complexity.checked_mul(u128::from(BIG_MOD_EXP_MOD_REDUCTION_COMPLEXITY_FACTOR))? + } else { + let adjusted_exponent_length = + big_mod_exp_adjusted_exponent_length(exponent).max(BIG_MOD_EXP_MIN_EXPONENT_LENGTH); + mult_complexity.checked_mul(u128::from(adjusted_exponent_length))? + }; + let divisor = u128::from(cost_divisor); + if divisor == 0 { + return None; + } + + let operation_cost = operation_complexity + .checked_add(divisor.checked_sub(1)?)? + .checked_div(divisor)?; + u64::try_from(operation_cost).ok() +} + declare_builtin_function!( /// Big integer modular exponentiation SyscallBigModExp, fn rust( - _invoke_context: &mut InvokeContext<'_, '_>, - _params: u64, - _return_value: u64, + invoke_context: &mut InvokeContext<'_, '_>, + params_addr: u64, + result_addr: u64, _arg3: u64, _arg4: u64, _arg5: u64, ) -> Result { - // The big integer modular exponentiation to be implemented once - // SIMD-529 is approved. + let check_aligned = invoke_context.get_check_aligned(); - Ok(1) + // Charge the flat base cost of the syscall up front, before doing any + // translation or work that could fail without being paid for. + let execution_cost = invoke_context.get_execution_cost(); + let base_cost = execution_cost.big_modular_exponentiation_base_cost; + let cost_divisor = execution_cost.big_modular_exponentiation_cost_divisor; + invoke_context.compute_meter.consume_checked(base_cost)?; + + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let params = + *translate_type::(memory_mapping, params_addr, check_aligned)?; + + if params.base_len > BIG_MOD_EXP_MAX_BYTES + || params.exponent_len > BIG_MOD_EXP_MAX_BYTES + || params.modulus_len > BIG_MOD_EXP_MAX_BYTES + { + return Err(SyscallError::InvalidLength.into()); + } + + // Only the exponent (and the lengths in `params`) is needed to compute + // the operation cost, so translate it and charge before translating the + // base and modulus. + let exponent = translate_slice::( + memory_mapping, + params.exponent, + params.exponent_len, + check_aligned, + )?; + let Some(cost) = big_mod_exp_operation_cost(cost_divisor, ¶ms, exponent) else { + // The operation cost cannot be represented as a `u64`, so it can + // never be paid for; drain the remaining budget and fail. + invoke_context.compute_meter.consume_checked(u64::MAX)?; + return Err(Box::new(InstructionError::ComputationalBudgetExceeded)); + }; + invoke_context.compute_meter.consume_checked(cost)?; + + let base = translate_slice::( + memory_mapping, + params.base, + params.base_len, + check_aligned, + )?; + let modulus = translate_slice::( + memory_mapping, + params.modulus, + params.modulus_len, + check_aligned, + )?; + + let Some(value) = big_mod_exp(base, exponent, modulus) else { + return Err(SyscallError::InvalidAttribute.into()); + }; + + let memory_mapping = invoke_context.memory_contexts.memory_mapping_mut()?; + translate_mut!( + memory_mapping, + check_aligned, + let result_ref_mut: (&mut [MaybeUninit]) = map(result_addr, params.modulus_len)?; + ); + result_ref_mut.write_copy_of_slice(value.as_slice()); + + Ok(SUCCESS) } ); @@ -6051,6 +6187,233 @@ mod tests { ); } + #[test] + fn test_syscall_big_mod_exp() { + let config = Config::default(); + prepare_mockup!(invoke_context, program_id, bpf_loader::id()); + + const VADDR_PARAMS: u64 = 0x100000000; + const VADDR_BASE: u64 = 0x200000000; + const VADDR_EXPONENT: u64 = 0x300000000; + const VADDR_MODULUS: u64 = 0x400000000; + const VADDR_OUT: u64 = 0x500000000; + + let base = [0x03]; + let exponent = [ + 0x2e, 0xfc, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + ]; + let modulus = [ + 0x2f, 0xfc, 0xff, 0xff, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, + 0xff, 0xff, 0xff, 0xff, + ]; + let mut data_out = [0u8; 32]; + let mut expected = [0u8; 32]; + expected[0] = 1; + assert_eq!( + big_mod_exp(&base, &exponent, &modulus), + Some(expected.to_vec()) + ); + let params = BigModExpParams { + base: VADDR_BASE, + base_len: base.len() as u64, + exponent: VADDR_EXPONENT, + exponent_len: exponent.len() as u64, + modulus: VADDR_MODULUS, + modulus_len: modulus.len() as u64, + }; + + let memory_mapping = unsafe { + MemoryMapping::new( + vec![ + MemoryRegion::new(bytes_of(¶ms), VADDR_PARAMS), + MemoryRegion::new(bytes_of_slice(&base), VADDR_BASE), + MemoryRegion::new(bytes_of_slice(&exponent), VADDR_EXPONENT), + MemoryRegion::new(bytes_of_slice(&modulus), VADDR_MODULUS), + MemoryRegion::new(bytes_of_slice_mut(&mut data_out), VADDR_OUT), + ], + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + let budget = invoke_context.get_execution_cost(); + let cost = budget.big_modular_exponentiation_base_cost + + big_mod_exp_operation_cost( + budget.big_modular_exponentiation_cost_divisor, + ¶ms, + &exponent, + ) + .unwrap(); + invoke_context.compute_meter.mock_set_remaining(cost); + + let result = SyscallBigModExp::rust(&mut invoke_context, VADDR_PARAMS, VADDR_OUT, 0, 0, 0); + + assert_eq!(result.unwrap(), SUCCESS); + assert_eq!(data_out, expected); + } + + #[test] + fn test_syscall_big_mod_exp_invalid_modulus() { + let config = Config::default(); + prepare_mockup!(invoke_context, program_id, bpf_loader::id()); + + const VADDR_PARAMS: u64 = 0x100000000; + const VADDR_BASE: u64 = 0x200000000; + const VADDR_EXPONENT: u64 = 0x300000000; + const VADDR_MODULUS: u64 = 0x400000000; + const VADDR_OUT: u64 = 0x500000000; + + let base = [0x05]; + let exponent = [0x02]; + let modulus = [0x02]; + let mut data_out = [0u8; 1]; + let params = BigModExpParams { + base: VADDR_BASE, + base_len: base.len() as u64, + exponent: VADDR_EXPONENT, + exponent_len: exponent.len() as u64, + modulus: VADDR_MODULUS, + modulus_len: modulus.len() as u64, + }; + + let memory_mapping = unsafe { + MemoryMapping::new( + vec![ + MemoryRegion::new(bytes_of(¶ms), VADDR_PARAMS), + MemoryRegion::new(bytes_of_slice(&base), VADDR_BASE), + MemoryRegion::new(bytes_of_slice(&exponent), VADDR_EXPONENT), + MemoryRegion::new(bytes_of_slice(&modulus), VADDR_MODULUS), + MemoryRegion::new(bytes_of_slice_mut(&mut data_out), VADDR_OUT), + ], + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + let budget = invoke_context.get_execution_cost(); + let cost = budget.big_modular_exponentiation_base_cost + + big_mod_exp_operation_cost( + budget.big_modular_exponentiation_cost_divisor, + ¶ms, + &exponent, + ) + .unwrap(); + invoke_context.compute_meter.mock_set_remaining(cost); + + let result = SyscallBigModExp::rust(&mut invoke_context, VADDR_PARAMS, VADDR_OUT, 0, 0, 0); + + assert_matches!( + result, + Result::Err(error) if error.downcast_ref::().unwrap() == &SyscallError::InvalidAttribute + ); + assert_eq!(data_out, [0x00]); + } + + #[test] + fn test_syscall_big_mod_exp_overlapping_result() { + let config = Config::default(); + prepare_mockup!(invoke_context, program_id, bpf_loader::id()); + + const VADDR_PARAMS: u64 = 0x100000000; + const VADDR_BASE: u64 = 0x200000000; + const VADDR_EXPONENT: u64 = 0x300000000; + const VADDR_MODULUS: u64 = 0x400000000; + let mut base = [0x05]; + let exponent = [0x02]; + let modulus = [0x07]; + assert_eq!(big_mod_exp(&[0x05], &[0x02], &[0x07]), Some(vec![0x04])); + let params = BigModExpParams { + base: VADDR_BASE, + base_len: 1, + exponent: VADDR_EXPONENT, + exponent_len: 1, + modulus: VADDR_MODULUS, + modulus_len: 1, + }; + + let memory_mapping = unsafe { + MemoryMapping::new( + vec![ + MemoryRegion::new(bytes_of(¶ms), VADDR_PARAMS), + MemoryRegion::new(bytes_of_slice_mut(&mut base), VADDR_BASE), + MemoryRegion::new(bytes_of_slice(&exponent), VADDR_EXPONENT), + MemoryRegion::new(bytes_of_slice(&modulus), VADDR_MODULUS), + ], + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + let budget = invoke_context.get_execution_cost(); + let cost = budget.big_modular_exponentiation_base_cost + + big_mod_exp_operation_cost( + budget.big_modular_exponentiation_cost_divisor, + ¶ms, + &exponent, + ) + .unwrap(); + invoke_context.compute_meter.mock_set_remaining(cost); + + let result = SyscallBigModExp::rust(&mut invoke_context, VADDR_PARAMS, VADDR_BASE, 0, 0, 0); + + assert_eq!(result.unwrap(), SUCCESS); + assert_eq!(base, [0x04]); + } + + #[test] + fn test_syscall_big_mod_exp_abort_conditions() { + let config = Config::default(); + prepare_mockup!(invoke_context, program_id, bpf_loader::id()); + + const VADDR_PARAMS: u64 = 0x100000000; + const VADDR_DATA: u64 = 0x200000000; + const VADDR_OUT: u64 = 0x300000000; + let data = [0u8; 1]; + let mut data_out = [0u8; 1]; + let params = BigModExpParams { + base: VADDR_DATA, + base_len: BIG_MOD_EXP_MAX_BYTES + 1, + exponent: VADDR_DATA, + exponent_len: 0, + modulus: VADDR_DATA, + modulus_len: 1, + }; + + let memory_mapping = unsafe { + MemoryMapping::new( + vec![ + MemoryRegion::new(bytes_of(¶ms), VADDR_PARAMS), + MemoryRegion::new(bytes_of_slice(&data), VADDR_DATA), + MemoryRegion::new(bytes_of_slice_mut(&mut data_out), VADDR_OUT), + ], + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + + let result = SyscallBigModExp::rust(&mut invoke_context, VADDR_PARAMS, VADDR_OUT, 0, 0, 0); + assert_matches!( + result, + Result::Err(error) if error.downcast_ref::().unwrap() == &SyscallError::InvalidLength + ); + } + #[test] fn test_syscall_get_epoch_stake_total_stake() { let config = Config::default(); @@ -7892,6 +8255,40 @@ mod tests { } } + #[test] + fn test_sol_big_mod_exp_registration() { + let compute_budget = SVMTransactionExecutionBudget::default(); + + let mut feature_set = SVMFeatureSet::all_enabled(); + feature_set.enable_big_mod_exp_syscall = true; + let env = create_program_runtime_environment( + &feature_set, + &compute_budget, + /* reject_deployment_of_broken_elfs */ false, + /* debugging_features */ false, + ) + .unwrap(); + assert!( + env.get_function_registry() + .lookup_by_name(b"sol_big_mod_exp") + .is_some() + ); + + feature_set.enable_big_mod_exp_syscall = false; + let env = create_program_runtime_environment( + &feature_set, + &compute_budget, + /* reject_deployment_of_broken_elfs */ false, + /* debugging_features */ false, + ) + .unwrap(); + assert!( + env.get_function_registry() + .lookup_by_name(b"sol_big_mod_exp") + .is_none() + ); + } + #[test] fn test_syscall_sha512() { let config = Config::default(); From 1236ee199e8889b32dd747246e4092b71b58caf7 Mon Sep 17 00:00:00 2001 From: Andrew Fitzgerald Date: Mon, 20 Jul 2026 07:56:00 +0800 Subject: [PATCH 28/30] poh: simplify recording to a single transaction entry (#13916) --- core/src/banking_stage.rs | 4 +- core/src/banking_stage/consumer.rs | 4 +- core/src/block_creation_loop.rs | 34 +++++------- entry/src/poh.rs | 65 ----------------------- poh/benches/poh.rs | 4 +- poh/src/poh_recorder.rs | 83 ++++++++++++------------------ poh/src/poh_service.rs | 12 ++--- poh/src/record_channels.rs | 79 +++++++++++----------------- poh/src/transaction_recorder.rs | 9 ++-- 9 files changed, 90 insertions(+), 204 deletions(-) diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 360c7fd1725..445cf8c2400 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -1201,8 +1201,8 @@ mod tests { let summary = recorder.record_transactions(bank.bank_id(), txs.clone()); assert!(summary.result.is_ok()); assert_eq!( - record_receiver.try_recv().unwrap().transaction_batches, - vec![txs.clone()] + record_receiver.try_recv().unwrap().transactions, + txs.clone() ); assert!(record_receiver.try_recv().is_err()); diff --git a/core/src/banking_stage/consumer.rs b/core/src/banking_stage/consumer.rs index f2e541d9122..0c28c64dd48 100644 --- a/core/src/banking_stage/consumer.rs +++ b/core/src/banking_stage/consumer.rs @@ -695,9 +695,7 @@ mod tests { let record = record_receiver.drain().next().unwrap(); assert_eq!(record.bank_id, bank.bank_id()); - assert_eq!(record.transaction_batches.len(), 1); - let transaction_batch = record.transaction_batches[0].clone(); - assert_eq!(transaction_batch.len(), 1); + assert_eq!(record.transactions.len(), 1); let transactions = sanitize_transactions(vec![system_transaction::transfer( &mint_keypair, diff --git a/core/src/block_creation_loop.rs b/core/src/block_creation_loop.rs index 6ff548c2e16..d333a35acb1 100644 --- a/core/src/block_creation_loop.rs +++ b/core/src/block_creation_loop.rs @@ -704,19 +704,16 @@ fn record_and_complete_block( }, recv(ctx.record_receiver.inner()) -> msg => { let record = msg.map_err(|_| PohRecorderError::ChannelDisconnected)?; - ctx.record_receiver - .on_received_record(record.transaction_batches.len() as u64); + ctx.record_receiver.on_received_record(); if optimistic_parent.is_some() { - record.transaction_batches.iter().for_each(|batch| { - accumulated_txs.extend(batch.iter().cloned()); - }); + accumulated_txs.extend(record.transactions.iter().cloned()); } ctx.poh_recorder.write().unwrap().record( record.bank_id, - record.mixins, - record.transaction_batches, + record.mixin, + record.transactions, )?; }, default(select_timeout) => {}, @@ -1002,7 +999,7 @@ fn handle_parent_ready( Ok(Some(new_bank)) } -/// Shut down record intake and synchronously record all already-reserved batches. +/// Shut down record intake and synchronously process all already-reserved records. /// /// When `accumulated_txs` is provided, the drained transactions are retained so /// sad handover can reschedule them against the recreated bank. @@ -1015,16 +1012,13 @@ fn shutdown_and_drain_record_receiver( for record in record_receiver.drain_after_shutdown() { if let Some(accumulated_txs) = accumulated_txs.as_deref_mut() { - record.transaction_batches.iter().for_each(|batch| { - accumulated_txs.extend(batch.iter().cloned()); - }); + accumulated_txs.extend(record.transactions.iter().cloned()); } - poh_recorder.write().unwrap().record( - record.bank_id, - record.mixins, - record.transaction_batches, - )?; + poh_recorder + .write() + .unwrap() + .record(record.bank_id, record.mixin, record.transactions)?; } Ok(()) @@ -1778,8 +1772,8 @@ mod tests { let bank_id = ctx.poh_recorder.read().unwrap().bank().unwrap().bank_id(); record_sender .try_send(Record::new( - vec![Hash::new_unique()], - vec![vec![versioned_transfer(1)]], + Hash::new_unique(), + vec![versioned_transfer(1)], bank_id, )) .unwrap(); @@ -1891,8 +1885,8 @@ mod tests { let drained_tx = versioned_transfer(2); record_sender .try_send(Record::new( - vec![Hash::new_unique()], - vec![vec![drained_tx.clone()]], + Hash::new_unique(), + vec![drained_tx.clone()], optimistic_bank_id, )) .unwrap(); diff --git a/entry/src/poh.rs b/entry/src/poh.rs index bdf6db6b130..16ba197a3fb 100644 --- a/entry/src/poh.rs +++ b/entry/src/poh.rs @@ -94,41 +94,6 @@ impl Poh { }) } - /// Returns `true` if the batches were recorded successfully and `false` if the batches - /// were not recorded because there were not enough hashes remaining to record all `mixins`. - /// If `true` is returned, the `entries` vector will be populated with the `PohEntry`s for each - /// batch. If `false` is returned, the `entries` vector will not be modified. - pub fn record_batches(&mut self, mixins: &[Hash], entries: &mut Vec) -> bool { - let num_mixins = mixins.len() as u64; - debug_assert_ne!(num_mixins, 0, "mixins.len() == 0"); - - if self.remaining_hashes_until_tick < num_mixins + 1 { - return false; // Not enough hashes remaining to record all mixins - } - - entries.clear(); - entries.reserve(mixins.len()); - - // The first entry will have the current number of hashes plus one. - // All subsequent entries will have 1. - let mut num_hashes = self.num_hashes + 1; - entries.extend(mixins.iter().map(|mixin| { - self.hash = hashv(&[self.hash.as_ref(), mixin.as_ref()]); - let entry = PohEntry { - num_hashes, - hash: self.hash, - }; - - num_hashes = 1; - entry - })); - - self.num_hashes = 0; - self.remaining_hashes_until_tick -= num_mixins; - - true - } - pub fn tick(&mut self) -> Option { self.hash = hash(self.hash.as_ref()); self.num_hashes += 1; @@ -385,34 +350,4 @@ mod tests { assert_eq!(poh.remaining_hashes_until_tick, 9); assert_eq!(poh.remaining_hashes_in_slot(2), 9); } - - #[test] - fn test_poh_record_batches() { - let mut poh = Poh::new(Hash::default(), Some(10)); - assert!(!poh.hash(4)); - - let mut entries = Vec::with_capacity(3); - let dummy_hashes = [Hash::default(); 4]; - assert!(poh.record_batches(&dummy_hashes[..3], &mut entries,)); - assert_eq!(entries.len(), 3); - assert_eq!(entries[0].num_hashes, 5); - assert_eq!(entries[1].num_hashes, 1); - assert_eq!(entries[2].num_hashes, 1); - assert_eq!(poh.remaining_hashes_until_tick, 3); - assert_eq!(poh.remaining_hashes_in_slot(2), 13); - - // Cannot record more than number of remaining hashes - assert!(!poh.record_batches(&dummy_hashes[..4], &mut entries,)); - - // Cannot record more than number of remaining hashes - assert!(!poh.record_batches(&dummy_hashes[..3], &mut entries,)); - - // Can record less than number of remaining hashes - assert!(poh.record_batches(&dummy_hashes[..2], &mut entries,)); - assert_eq!(entries.len(), 2); - assert_eq!(entries[0].num_hashes, 1); - assert_eq!(entries[1].num_hashes, 1); - assert_eq!(poh.remaining_hashes_until_tick, 1); - assert_eq!(poh.remaining_hashes_in_slot(2), 11); - } } diff --git a/poh/benches/poh.rs b/poh/benches/poh.rs index 4ea2c9f103e..907a175e94b 100644 --- a/poh/benches/poh.rs +++ b/poh/benches/poh.rs @@ -120,8 +120,8 @@ fn bench_poh_recorder_record(bencher: &mut Bencher) { let _record_result = poh_recorder .record( bank.slot(), - vec![test::black_box(h1)], - vec![test::black_box(txs.clone())], + test::black_box(h1), + test::black_box(txs.clone()), ) .unwrap(); }); diff --git a/poh/src/poh_recorder.rs b/poh/src/poh_recorder.rs index 80ed580447a..52125ed43e5 100644 --- a/poh/src/poh_recorder.rs +++ b/poh/src/poh_recorder.rs @@ -28,7 +28,7 @@ use { block_component::{BlockFooterV1, VersionedBlockMarker}, entry::Entry, entry_or_marker::EntryOrMarker, - poh::{Poh, PohEntry}, + poh::Poh, }, solana_hash::Hash, solana_leader_schedule::NUM_CONSECUTIVE_LEADER_SLOTS, @@ -105,20 +105,16 @@ pub struct RecordSummary { } pub struct Record { - pub mixins: Vec, - pub transaction_batches: Vec>, + pub mixin: Hash, + pub transactions: Vec, pub bank_id: BankId, } impl Record { - pub fn new( - mixins: Vec, - transaction_batches: Vec>, - bank_id: BankId, - ) -> Self { + pub fn new(mixin: Hash, transactions: Vec, bank_id: BankId) -> Self { Self { - mixins, - transaction_batches, + mixin, + transactions, bank_id, } } @@ -215,9 +211,6 @@ pub struct PohRecorder { delay_leader_block_for_pending_fork: bool, last_reported_slot_for_pending_fork: Arc>, pub is_exited: Arc, - - // Allocation to hold PohEntrys recorded into PoHStream. - entries: Vec, } impl PohRecorder { @@ -299,7 +292,6 @@ impl PohRecorder { delay_leader_block_for_pending_fork, last_reported_slot_for_pending_fork: Arc::default(), is_exited, - entries: Vec::with_capacity(64), }, working_bank_receiver, ) @@ -349,19 +341,12 @@ impl PohRecorder { pub fn record( &mut self, bank_id: BankId, - mixins: Vec, - transaction_batches: Vec>, + mixin: Hash, + transactions: Vec, ) -> Result { // Entries without transactions are used to track real-time passing in the ledger and // cannot be generated by `record()` - assert!( - mixins.len() == transaction_batches.len(), - "mismatched mixin and transaction batch lengths" - ); - assert!( - !transaction_batches.iter().any(|batch| batch.is_empty()), - "No transactions provided" - ); + assert!(!transactions.is_empty(), "No transactions provided"); if let Some(working_bank) = self.working_bank.as_ref() { let ((), report_metrics_us) = @@ -386,34 +371,30 @@ impl PohRecorder { let (mut poh_lock, poh_lock_us) = measure_us!(self.poh.lock().unwrap()); self.metrics.record_lock_contention_us += poh_lock_us; - let (mixed_in, record_mixin_us) = - measure_us!(poh_lock.record_batches(&mixins, &mut self.entries)); + let (entry, record_mixin_us) = measure_us!(poh_lock.record(mixin)); self.metrics.record_us += record_mixin_us; let remaining_hashes_in_slot = poh_lock.remaining_hashes_in_slot(working_bank.bank.ticks_per_slot()); drop(poh_lock); - if mixed_in { - debug_assert_eq!(self.entries.len(), mixins.len()); - for (entry, transactions) in self.entries.drain(..).zip(transaction_batches) { - let (send_entry_res, send_batches_us) = measure_us!( - self.working_bank_sender.send(( - working_bank.bank.clone(), - ( - Entry { - num_hashes: entry.num_hashes, - hash: entry.hash, - transactions, - } - .into(), - tick_height, // `record_batches` guarantees that mixins are **not** split across ticks. - ), - )) - ); - self.metrics.send_entry_us += send_batches_us; - send_entry_res.map_err(Box::new)?; - } + if let Some(entry) = entry { + let (send_entry_res, send_entry_us) = measure_us!( + self.working_bank_sender.send(( + working_bank.bank.clone(), + ( + Entry { + num_hashes: entry.num_hashes, + hash: entry.hash, + transactions, + } + .into(), + tick_height, + ), + )) + ); + self.metrics.send_entry_us += send_entry_us; + send_entry_res.map_err(Box::new)?; return Ok(RecordSummary { remaining_hashes_in_slot, @@ -1744,7 +1725,7 @@ mod tests { // We haven't yet reached the minimum tick height for the working bank, // so record should fail assert_matches!( - poh_recorder.record(bank1.slot(), vec![h1], vec![vec![tx.into()]]), + poh_recorder.record(bank1.slot(), h1, vec![tx.into()]), Err(PohRecorderError::MinHeightNotReached) ); assert!(entry_receiver.try_recv().is_err()); @@ -1783,7 +1764,7 @@ mod tests { // However we hand over a bad slot so record fails let bad_slot = bank.slot() + 1; assert_matches!( - poh_recorder.record(bad_slot, vec![h1], vec![vec![tx.into()]]), + poh_recorder.record(bad_slot, h1, vec![tx.into()]), Err(PohRecorderError::MaxHeightReached) ); } @@ -1833,7 +1814,7 @@ mod tests { let h1 = hash(b"hello world!"); assert!( poh_recorder - .record(bank1.slot(), vec![h1], vec![vec![tx.into()]]) + .record(bank1.slot(), h1, vec![tx.into()]) .is_ok() ); assert_eq!(poh_recorder.tick_cache.len(), 0); @@ -1877,7 +1858,7 @@ mod tests { let h1 = hash(b"hello world!"); assert!( poh_recorder - .record(bank.slot(), vec![h1], vec![vec![tx.into()]]) + .record(bank.slot(), h1, vec![tx.into()]) .is_err() ); for _ in 0..num_ticks_to_max { @@ -2115,7 +2096,7 @@ mod tests { let h1 = hash(b"hello world!"); assert!( poh_recorder - .record(bank.slot(), vec![h1], vec![vec![tx.into()]]) + .record(bank.slot(), h1, vec![tx.into()]) .is_err() ); assert!(poh_recorder.working_bank.is_none()); diff --git a/poh/src/poh_service.rs b/poh/src/poh_service.rs index fc8b1871ec8..4526069f534 100644 --- a/poh/src/poh_service.rs +++ b/poh/src/poh_service.rs @@ -315,8 +315,8 @@ impl PohService { if let Ok(record) = record { match poh_recorder.write().unwrap().record( record.bank_id, - record.mixins, - record.transaction_batches, + record.mixin, + record.transactions, ) { Ok(record_summary) => { if record_receiver @@ -462,8 +462,8 @@ impl PohService { loop { match poh_recorder_l.record( record.bank_id, - record.mixins, - std::mem::take(&mut record.transaction_batches), + record.mixin, + std::mem::take(&mut record.transactions), ) { Ok(record_summary) => { if record_receiver.should_shutdown( @@ -773,8 +773,8 @@ mod tests { poh_controller.reset(bank.clone(), None).unwrap(); record_sender .try_send(Record { - mixins: vec![Hash::new_unique()], - transaction_batches: vec![vec![VersionedTransaction::from(test_tx())]], + mixin: Hash::new_unique(), + transactions: vec![VersionedTransaction::from(test_tx())], bank_id: bank.bank_id(), }) .unwrap(); diff --git a/poh/src/record_channels.rs b/poh/src/record_channels.rs index 91328990f6c..110e7f53af5 100644 --- a/poh/src/record_channels.rs +++ b/poh/src/record_channels.rs @@ -91,11 +91,7 @@ impl RecordSender { } pub fn try_send(&self, record: Record) -> Result, RecordSenderError> { - let num_transactions: usize = record - .transaction_batches - .iter() - .map(|batch| batch.len()) - .sum(); + let num_transactions = record.transactions.len(); assert!(num_transactions > 0); loop { // Grab lock on `transaction_indexes` here to ensure we are sending @@ -106,8 +102,7 @@ impl RecordSender { .map(|transaction_indexes| transaction_indexes.lock().unwrap()); // Get the current bank_id and allowed insertions. - // If the number of allowed insertions is less than the number of - // batches, the channel is full - just return immediately. + // If there are no allowed insertions, the channel is full - just return immediately. // If the `record`'s bank_id is different from the current bank_id, // return immediately. let current_bank_id_allowed_insertions = @@ -123,14 +118,12 @@ impl RecordSender { if bank_id != record.bank_id { return Err(RecordSenderError::InactiveBankId); } - if allowed_insertions < record.transaction_batches.len() as u64 { + if allowed_insertions == 0 { return Err(RecordSenderError::Full); } - let new_bank_id_allowed_insertions = BankIdAllowedInsertions::encoded_value( - bank_id, - allowed_insertions.wrapping_sub(record.transaction_batches.len() as u64), - ); + let new_bank_id_allowed_insertions = + BankIdAllowedInsertions::encoded_value(bank_id, allowed_insertions.wrapping_sub(1)); // Increment this before CAS so the receiver can see this send is in-flight. self.active_senders.fetch_add(1, Ordering::AcqRel); @@ -174,7 +167,7 @@ impl RecordSender { /// The receiver can shutdown the channel, preventing any further sends, /// and can restart the channel for a new bank id, re-enabling sends. pub struct RecordReceiver { - /// Maximum number of record batches that may be reserved for the active bank. + /// Maximum number of records that may be reserved for the active bank. capacity: u64, /// Number of senders between reservation and channel insertion. active_senders: Arc, @@ -196,7 +189,7 @@ impl RecordReceiver { /// Returns true if the channel should be shutdown. pub fn should_shutdown(&self, remaining_hashes_in_slot: u64, ticks_per_slot: u64) -> bool { // This channel must guarantee that all sent records are recorded. - // Each batch in a record consumes one hash in the PoH stream, + // Each record consumes one hash in the PoH stream, // each tick also consumes at least one hash in the PoH stream. // As a conservative estimate, we assume no ticks have been recorded. remaining_hashes_in_slot.saturating_sub(ticks_per_slot) <= self.capacity @@ -273,7 +266,7 @@ impl RecordReceiver { loop { match self.receiver.try_recv() { Ok(record) => { - self.on_received_record(record.transaction_batches.len() as u64); + self.on_received_record(); return Ok(record); } @@ -289,7 +282,7 @@ impl RecordReceiver { // and then dropped active_senders back to 0. Re-check once. match self.receiver.try_recv() { Ok(record) => { - self.on_received_record(record.transaction_batches.len() as u64); + self.on_received_record(); return Ok(record); } Err(TryRecvError::Empty) => return Err(TryRecvError::Empty), @@ -305,27 +298,25 @@ impl RecordReceiver { /// Receive a record from the channel, waiting up to `duration`. pub fn recv_timeout(&self, duration: Duration) -> Result { let record = self.receiver.recv_timeout(duration)?; - self.on_received_record(record.transaction_batches.len() as u64); + self.on_received_record(); Ok(record) } /// Notify that a record has been received. /// Must be called after receiving a record from `inner()` directly. - pub fn on_received_record(&self, num_batches: u64) { + pub fn on_received_record(&self) { // The record has been received and processed, so increment the number // of allowed insertions, so that new records can be sent. self.bank_id_allowed_insertions .0 - .fetch_add(num_batches, Ordering::AcqRel); + .fetch_add(1, Ordering::AcqRel); } } /// Encoded u64 where the upper 54 bits are the bank_id and the lower 10 bits are /// the number of allowed insertions at the current time. -/// The number of allowed insertions is based on the number of **batches** sent, -/// not the number of [`Record`]. This is because each batch is a separate hash -/// in the PoH stream, and we must guarantee enough space for each hash, if we -/// allow a [`Record`] to be sent. +/// Each [`Record`] is a separate hash in the PoH stream, so the number of allowed +/// insertions guarantees enough space for every record that is sent. /// The allowed insertions uses 10 bits allowing up to 1023 insertions at a /// given time. This is for messages that have been sent but not yet processed /// by the receiver. @@ -381,13 +372,11 @@ impl BankIdAllowedInsertions { mod tests { use {super::*, solana_hash::Hash, solana_transaction::versioned::VersionedTransaction}; - pub(super) fn test_record(bank_id: BankId, num_batches: usize) -> Record { + pub(super) fn test_record(bank_id: BankId, num_transactions: usize) -> Record { Record { bank_id, - transaction_batches: (0..num_batches) - .map(|_| vec![VersionedTransaction::default()]) - .collect(), - mixins: (0..num_batches).map(|_| Hash::default()).collect(), + transactions: vec![VersionedTransaction::default(); num_transactions], + mixin: Hash::default(), } } @@ -410,28 +399,24 @@ mod tests { Err(RecordSenderError::InactiveBankId) )); - // Record for bank_id 1 with 1 batch succeeds. + // Record for bank_id 1 succeeds. assert!(matches!(sender.try_send(test_record(1, 1)), Ok(None))); - // Record for bank_id 1 with 1023 batches fails (channel full). - assert!(matches!( - sender.try_send(test_record(1, 1023)), - Err(RecordSenderError::Full) - )); - - // Record for bank_id 1 with 1022 batches succeeds (channel now full). - assert!(matches!(sender.try_send(test_record(1, 1022)), Ok(None))); + // Fill the rest of the channel. + for _ in 1..BankIdAllowedInsertions::MAX_ALLOWED_INSERTIONS { + assert!(matches!(sender.try_send(test_record(1, 1)), Ok(None))); + } - // Record for bank_id 1 with 1 batch fails (channel full). + // Another record for bank_id 1 fails because the channel is full. assert!(matches!( sender.try_send(test_record(1, 1)), Err(RecordSenderError::Full) )); - // Receive 1 record. - assert!(receiver.try_recv().is_ok()); assert!(!receiver.is_safe_to_restart()); - assert!(receiver.try_recv().is_ok()); + for _ in 0..BankIdAllowedInsertions::MAX_ALLOWED_INSERTIONS { + assert!(receiver.try_recv().is_ok()); + } assert!(receiver.is_safe_to_restart()); } @@ -454,17 +439,11 @@ mod tests { Err(RecordSenderError::InactiveBankId) )); - // Record for bank_id 1 with 1 batch succeeds. + // Record for bank_id 1 with 1 transaction succeeds. assert!(matches!(sender.try_send(test_record(1, 1)), Ok(Some(0)))); - // Record for bank_id 1 with 2 batches (3 transactions) succeeds. - let mut record = test_record(1, 2); - record - .transaction_batches - .last_mut() - .unwrap() - .push(VersionedTransaction::default()); - assert!(matches!(sender.try_send(record), Ok(Some(1)))); + // Record for bank_id 1 with 3 transactions succeeds. + assert!(matches!(sender.try_send(test_record(1, 3)), Ok(Some(1)))); assert!(*sender.transaction_indexes.as_ref().unwrap().lock().unwrap() == 4); } diff --git a/poh/src/transaction_recorder.rs b/poh/src/transaction_recorder.rs index d23cc245735..8487a5a8b14 100644 --- a/poh/src/transaction_recorder.rs +++ b/poh/src/transaction_recorder.rs @@ -61,8 +61,7 @@ impl TransactionRecorder { let (hash, hash_us) = measure_us!(hash_transactions(&transactions)); record_transactions_timings.hash_us = Saturating(hash_us); - let (res, poh_record_us) = - measure_us!(self.record(bank_id, vec![hash], vec![transactions])); + let (res, poh_record_us) = measure_us!(self.record(bank_id, hash, transactions)); record_transactions_timings.poh_record_us = Saturating(poh_record_us); match res { @@ -104,10 +103,10 @@ impl TransactionRecorder { pub fn record( &self, bank_id: BankId, - mixins: Vec, - transaction_batches: Vec>, + mixin: Hash, + transactions: Vec, ) -> Result, RecordSenderError> { self.record_sender - .try_send(Record::new(mixins, transaction_batches, bank_id)) + .try_send(Record::new(mixin, transactions, bank_id)) } } From 6c1b471aa3aabc5cf7ed58279663d4d739e24a0a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi <40335219+DSharifi@users.noreply.github.com> Date: Mon, 20 Jul 2026 01:59:41 +0200 Subject: [PATCH 29/30] chore: remove non-installed scheduler path in replay code (#13833) chore: remove dead code associated with non scheduler replay path --- Cargo.lock | 3 +- core/src/replay_stage.rs | 16 - core/src/replay_stage/tests.rs | 11 +- core/src/tvu.rs | 7 - core/src/validator.rs | 2 - dev-bins/Cargo.lock | 2 +- ledger/Cargo.toml | 1 + ledger/src/blockstore_processor.rs | 701 +++++++++++------------------ programs/sbf/Cargo.lock | 1 - rpc/Cargo.toml | 3 + rpc/src/rpc.rs | 34 +- unified-scheduler-pool/Cargo.toml | 3 +- unified-scheduler-pool/src/lib.rs | 17 +- 13 files changed, 315 insertions(+), 486 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c3daf3c5dfe..5a8d20e93fc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9190,6 +9190,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-transaction-status", + "solana-unified-scheduler-pool", "solana-vote", "solana-vote-program", "spl-generic-token", @@ -10123,6 +10124,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-transaction-status", + "solana-unified-scheduler-pool", "solana-validator-exit", "solana-version", "solana-vote", @@ -11695,7 +11697,6 @@ dependencies = [ "solana-entry", "solana-hash 4.5.0", "solana-keypair", - "solana-ledger", "solana-metrics", "solana-poh", "solana-pubkey 4.2.0", diff --git a/core/src/replay_stage.rs b/core/src/replay_stage.rs index 27eea15c0b8..0fd3edb1926 100644 --- a/core/src/replay_stage.rs +++ b/core/src/replay_stage.rs @@ -89,7 +89,6 @@ use { commitment::BlockCommitmentCache, installed_scheduler_pool::BankWithScheduler, leader_schedule_utils::first_of_consecutive_leader_slots, - prioritization_fee_cache::PrioritizationFeeCache, snapshot_controller::SnapshotController, transaction_execution::TransactionStatusSender, vote_sender_types::{ReplayVoteMessage, ReplayVoteSender}, @@ -261,10 +260,8 @@ struct ProcessActiveBanksContext { ancestor_hashes_replay_update_sender: AncestorHashesReplayUpdateSender, block_metadata_notifier: Option, votor_event_sender: VotorEventSender, - log_messages_bytes_limit: Option, replay_mode: ForkReplayMode, replay_tx_thread_pool: ThreadPool, - prioritization_fee_cache: Option>, migration_status: Arc, } @@ -439,8 +436,6 @@ pub struct ReplayStageConfig { pub tower: Tower, pub vote_tracker: Arc, pub cluster_slots: Arc, - pub log_messages_bytes_limit: Option, - pub prioritization_fee_cache: Option>, pub banking_tracer: Arc, pub snapshot_controller: Option>, pub replay_highest_frozen: Arc, @@ -760,8 +755,6 @@ impl ReplayStage { mut tower, vote_tracker, cluster_slots, - log_messages_bytes_limit, - prioritization_fee_cache, banking_tracer, snapshot_controller, replay_highest_frozen, @@ -923,10 +916,8 @@ impl ReplayStage { ancestor_hashes_replay_update_sender: ancestor_hashes_replay_update_sender.clone(), block_metadata_notifier: block_metadata_notifier.clone(), votor_event_sender: votor_event_sender.clone(), - log_messages_bytes_limit, replay_mode, replay_tx_thread_pool, - prioritization_fee_cache: prioritization_fee_cache.clone(), migration_status: migration_status.clone(), }; let process_bank_forks_context = ProcessBankForksContext { @@ -3025,19 +3016,12 @@ impl ReplayStage { &mut w_replay_stats, &mut w_replay_progress, false, - process_active_banks_context - .transaction_status_sender - .as_ref(), process_active_banks_context .entry_notification_sender .as_ref(), Some(&process_active_banks_context.replay_vote_sender), Some(finalization_cert_sender), false, - process_active_banks_context.log_messages_bytes_limit, - process_active_banks_context - .prioritization_fee_cache - .as_deref(), process_active_banks_context.migration_status.as_ref(), )?; let tx_count_after = w_replay_progress.num_txs; diff --git a/core/src/replay_stage/tests.rs b/core/src/replay_stage/tests.rs index 4d95aaa203c..e1c6e5d39a5 100644 --- a/core/src/replay_stage/tests.rs +++ b/core/src/replay_stage/tests.rs @@ -119,10 +119,8 @@ impl ProcessActiveBanksContext { ancestor_hashes_replay_update_sender, block_metadata_notifier: None, votor_event_sender, - log_messages_bytes_limit: None, replay_mode: ForkReplayMode::Serial, replay_tx_thread_pool, - prioritization_fee_cache: None, migration_status, } } @@ -1319,6 +1317,9 @@ where let bank0 = bank_forks.read().unwrap().get(0).unwrap(); assert!(bank0.is_frozen()); assert_eq!(bank0.tick_height(), bank0.max_tick_height()); + bank_forks.write().unwrap().install_scheduler_pool( + DefaultSchedulerPool::new_for_verification(None, None, None, None, None), + ); let bank1 = Bank::new_from_parent(bank0, SlotLeader::default(), 1); bank_forks.write().unwrap().insert(bank1); let bank1 = bank_forks.read().unwrap().get_with_scheduler(1).unwrap(); @@ -1362,6 +1363,11 @@ where stats.transaction_verify_elapsed += tx_verify_elapsed; } verify_result?; + // Transaction errors from the unified scheduler surface when waiting for its + // completion, like replay stage does before freezing the bank. + if let Some((result, _timings)) = bank1.wait_for_completed_scheduler() { + result?; + } Ok(replay_tx_count) }); let max_complete_transaction_status_slot = Arc::new(AtomicU64::default()); @@ -6295,7 +6301,6 @@ fn test_initialize_progress_and_fork_choice_with_duplicates() { &mut ConfirmationProgress::new(bank0.last_blockhash()), None, None, - None, &mut ExecuteTimings::default(), &MigrationStatus::default(), ) diff --git a/core/src/tvu.rs b/core/src/tvu.rs index a5e57d0dd54..9c49ca05bf9 100644 --- a/core/src/tvu.rs +++ b/core/src/tvu.rs @@ -71,7 +71,6 @@ use { bank_forks::BankForks, bank_forks_controller::{BankForksCommandReceiver, BankForksController}, commitment::BlockCommitmentCache, - prioritization_fee_cache::PrioritizationFeeCache, snapshot_controller::SnapshotController, transaction_execution::TransactionStatusSender, validated_block_finalization::ValidatedBlockFinalizationCert, @@ -244,8 +243,6 @@ impl Tvu { block_metadata_notifier: Option, wait_to_vote_slot: Option, snapshot_controller: Option>, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option>, banking_tracer: Arc, outstanding_repair_requests: Arc>, cluster_slots: Arc, @@ -588,8 +585,6 @@ impl Tvu { tower, vote_tracker, cluster_slots, - log_messages_bytes_limit, - prioritization_fee_cache, banking_tracer, snapshot_controller, replay_highest_frozen, @@ -896,8 +891,6 @@ pub mod tests { None, // block_metadata_notifier None, // wait_to_vote_slot None, // snapshot_controller - None, // log_messages_bytes_limit - None, // prioritization_fee_cache BankingTracer::new_disabled(), outstanding_repair_requests, cluster_slots, diff --git a/core/src/validator.rs b/core/src/validator.rs index b177c8ee1b6..9862c57a430 100644 --- a/core/src/validator.rs +++ b/core/src/validator.rs @@ -1676,8 +1676,6 @@ impl Validator { block_metadata_notifier, config.wait_to_vote_slot, Some(snapshot_controller.clone()), - config.runtime_config.log_messages_bytes_limit, - prioritization_fee_cache.clone(), banking_tracer, outstanding_repair_requests.clone(), cluster_slots.clone(), diff --git a/dev-bins/Cargo.lock b/dev-bins/Cargo.lock index 54fc8916c82..05eae8e8262 100644 --- a/dev-bins/Cargo.lock +++ b/dev-bins/Cargo.lock @@ -7857,6 +7857,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-transaction-status", + "solana-unified-scheduler-pool", "solana-validator-exit", "solana-version", "solana-vote", @@ -9061,7 +9062,6 @@ dependencies = [ "scopeguard", "solana-clock", "solana-cost-model", - "solana-ledger", "solana-metrics", "solana-poh", "solana-pubkey 4.2.0", diff --git a/ledger/Cargo.toml b/ledger/Cargo.toml index d5c2e680527..ec419404724 100644 --- a/ledger/Cargo.toml +++ b/ledger/Cargo.toml @@ -143,6 +143,7 @@ solana-program-option = { workspace = true } solana-program-pack = { workspace = true } solana-runtime = { path = "../runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-signature = { workspace = true, features = ["rand"] } +solana-unified-scheduler-pool = { path = "../unified-scheduler-pool", features = ["agave-unstable-api", "dev-context-only-utils"] } solana-vote = { path = "../vote", features = ["agave-unstable-api", "dev-context-only-utils"] } spl-generic-token = { workspace = true } spl-pod = { workspace = true } diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index ce135e6ba36..0a11462daf8 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -13,7 +13,7 @@ use { crossbeam_channel::{Receiver, Sender}, itertools::Itertools, log::*, - rayon::{ThreadPool, prelude::*}, + rayon::ThreadPool, scopeguard::defer, solana_accounts_db::{ accounts_db::AccountsDbConfig, accounts_update_notifier_interface::AccountsUpdateNotifier, @@ -26,7 +26,7 @@ use { solana_genesis_config::GenesisConfig, solana_hash::Hash, solana_keypair::Keypair, - solana_measure::{measure::Measure, measure_us}, + solana_measure::measure::Measure, solana_metrics::datapoint_error, solana_pubkey::Pubkey, solana_runtime::{ @@ -36,14 +36,10 @@ use { commitment::VOTE_THRESHOLD_SIZE, installed_scheduler_pool::BankWithScheduler, leader_schedule_utils::leader_slot_index, - prioritization_fee_cache::PrioritizationFeeCache, runtime_config::RuntimeConfig, snapshot_controller::SnapshotController, - transaction_batch::{OwnedOrBorrowed, TransactionBatch}, - transaction_execution::{ - TransactionBatchWithIndexes, TransactionStatusSender, execute_batch, - }, - vote_sender_types::{ReplayVoteMessage, ReplayVoteSendType, ReplayVoteSender}, + transaction_execution::TransactionStatusSender, + vote_sender_types::{ReplayVoteMessage, ReplayVoteSender}, }, solana_runtime_transaction::runtime_transaction::RuntimeTransaction, solana_shred_version::compute_shred_version, @@ -62,7 +58,7 @@ use { ops::Index, path::PathBuf, result, - sync::{Arc, Mutex, OnceLock, RwLock, atomic::AtomicBool}, + sync::{Arc, OnceLock, RwLock, atomic::AtomicBool}, time::{Duration, Instant}, vec::Drain, }, @@ -148,148 +144,33 @@ impl ExecuteBatchesInternalMetrics { } } -fn execute_batches_internal( - bank: &Arc, - replay_tx_thread_pool: &ThreadPool, - batches: &[TransactionBatchWithIndexes>], - transaction_status_sender: Option<&TransactionStatusSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, -) -> Result { - assert!(!batches.is_empty()); - let execution_timings_per_thread: Mutex> = - Mutex::new(HashMap::new()); - - let mut execute_batches_elapsed = Measure::start("execute_batches_elapsed"); - let results: Vec> = replay_tx_thread_pool.install(|| { - batches - .into_par_iter() - .map(|transaction_batch| { - let transaction_count = - transaction_batch.batch.sanitized_transactions().len() as u64; - let mut timings = ExecuteTimings::default(); - let (result, execute_batches_us) = measure_us!(execute_batch( - transaction_batch, - bank, - transaction_status_sender, - replay_vote_sender, - ReplayVoteSendType::Executed { - replay_bank_id: bank.bank_id(), - replay_slot: bank.slot(), - }, - &mut timings, - log_messages_bytes_limit, - prioritization_fee_cache, - )); - - let thread_index = replay_tx_thread_pool.current_thread_index().unwrap(); - execution_timings_per_thread - .lock() - .unwrap() - .entry(thread_index) - .and_modify(|thread_execution_time| { - let ThreadExecuteTimings { - total_thread_us, - total_transactions_executed, - execute_timings: total_thread_execute_timings, - } = thread_execution_time; - *total_thread_us += execute_batches_us; - *total_transactions_executed += transaction_count; - total_thread_execute_timings - .saturating_add_in_place(ExecuteTimingType::TotalBatchesLen, 1); - total_thread_execute_timings.accumulate(&timings); - }) - .or_insert(ThreadExecuteTimings { - total_thread_us: Saturating(execute_batches_us), - total_transactions_executed: Saturating(transaction_count), - execute_timings: timings, - }); - result - }) - .collect() - }); - execute_batches_elapsed.stop(); - - first_err(&results)?; - - Ok(ExecuteBatchesInternalMetrics { - execution_timings_per_thread: execution_timings_per_thread.into_inner().unwrap(), - total_batches_len: batches.len() as u64, - execute_batches_us: execute_batches_elapsed.as_us(), - }) -} - -// This fn diverts the code-path into two variants. Both must provide exactly the same set of -// validations. For this reason, this fn is deliberately inserted into the code path to be called -// inside process_entries(), so that Bank::prepare_sanitized_batch() has been called on all of -// batches already, while minimizing code duplication (thus divergent behavior risk) at the cost of -// acceptable overhead of meaningless buffering of batches for the scheduler variant. +// Scheduling usually succeeds (immediately returns `Ok(())`) here without being blocked on the +// actual transaction executions. // -// Also note that the scheduler variant can't implement the batch-level sanitization naively, due -// to the nature of individual tx processing. That's another reason of this particular placement of -// divergent point in the code-path (i.e. not one layer up with its own prepare_sanitized_batch() -// invocation). +// As an exception, this fn could propagate the transaction execution _errors of +// previously-scheduled transactions_ to notify the replay stage. Then, the replay stage will bail +// out the further processing of the malformed (possibly malicious) block immediately, not to +// waste any system resources. Note that this propagation is of early hints; the returned error is +// completely unrelated to the `locked_entries` at hand. Even if errors won't be propagated in +// this way, they are guaranteed to be propagated eventually via the blocking fn called +// BankWithScheduler::wait_for_completed_scheduler(). fn process_batches( bank: &BankWithScheduler, - replay_tx_thread_pool: &ThreadPool, locked_entries: impl ExactSizeIterator>, - transaction_status_sender: Option<&TransactionStatusSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - batch_execution_timing: &mut BatchExecutionTiming, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, ) -> Result<()> { - if bank.has_installed_scheduler() { - debug!( - "process_batches()/schedule_batches_for_execution({} batches)", - locked_entries.len() - ); - // Scheduling usually succeeds (immediately returns `Ok(())`) here without being blocked on - // the actual transaction executions. - // - // As an exception, this code path could propagate the transaction execution _errors of - // previously-scheduled transactions_ to notify the replay stage. Then, the replay stage - // will bail out the further processing of the malformed (possibly malicious) block - // immediately, not to waste any system resources. Note that this propagation is of early - // hints. Even if errors won't be propagated in this way, they are guaranteed to be - // propagated eventually via the blocking fn called - // BankWithScheduler::wait_for_completed_scheduler(). - // - // To recite, the returned error is completely unrelated to the argument's `locked_entries` - // at the hand. While being awkward, the _async_ unified scheduler is abusing this existing - // error propagation code path to the replay stage for compatibility and ease of - // integration, exploiting the fact that the replay stage doesn't care _which transaction - // the returned error is originating from_. - // - // In the future, more proper error propagation mechanism will be introduced once after we - // fully transition to the unified scheduler for the block verification. That one would be - // a push based one from the unified scheduler to the replay stage to eliminate the current - // overhead: 1 read lock per batch in - // `BankWithScheduler::schedule_transaction_executions()`. - schedule_batches_for_execution(bank, locked_entries) - } else { - debug!( - "process_batches()/execute_batches({} batches)", - locked_entries.len() - ); - execute_batches( - bank, - replay_tx_thread_pool, - locked_entries, - transaction_status_sender, - replay_vote_sender, - batch_execution_timing, - log_messages_bytes_limit, - prioritization_fee_cache, - ) - } -} + // Tick-only flushes and the empty lock-retry flush are no-ops; this also + // covers slot 0, the only bank replayed before the scheduler pool is installed. + if locked_entries.len() == 0 { + return Ok(()); + }; + assert!( + bank.has_installed_scheduler(), + "no scheduler installed for bank of slot {} during replay", + bank.slot() + ); + + debug!("process_batches({} batches)", locked_entries.len()); -fn schedule_batches_for_execution( - bank: &BankWithScheduler, - locked_entries: impl Iterator>, -) -> Result<()> { // Track the first error encountered in the loop below, if any. // This error will be propagated to the replay stage, or Ok(()). let mut first_err = Ok(()); @@ -315,70 +196,33 @@ fn schedule_batches_for_execution( first_err } -fn execute_batches( - bank: &Arc, - replay_tx_thread_pool: &ThreadPool, - locked_entries: impl ExactSizeIterator>, - transaction_status_sender: Option<&TransactionStatusSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - timing: &mut BatchExecutionTiming, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, -) -> Result<()> { - if locked_entries.len() == 0 { - return Ok(()); - } - - let tx_batches: Vec<_> = locked_entries - .into_iter() - .map( - |LockedTransactionsWithIndexes { - lock_results, - transactions, - starting_index, - }| { - let ending_index = starting_index + transactions.len(); - TransactionBatchWithIndexes { - batch: TransactionBatch::new( - lock_results, - bank, - OwnedOrBorrowed::Owned(transactions), - ), - transaction_indexes: (starting_index..ending_index).collect(), - } - }, - ) - .collect(); - - let execute_batches_internal_metrics = execute_batches_internal( - bank, - replay_tx_thread_pool, - &tx_batches, - transaction_status_sender, - replay_vote_sender, - log_messages_bytes_limit, - prioritization_fee_cache, - )?; - - // Pass false because this code-path is never touched by unified scheduler. - timing.accumulate(execute_batches_internal_metrics, false); - Ok(()) -} - -/// Process an ordered list of entries in parallel +/// Process an ordered list of entries and wait for their completed execution /// 1. In order lock accounts for each entry while the lock succeeds, up to a Tick entry -/// 2. Process the locked group in parallel +/// 2. Schedule the locked group for execution on `bank`'s installed unified scheduler /// 3. Register the `Tick` if it's available -/// 4. Update the leader scheduler, goto 1 +/// 4. goto 1 +/// 5. Wait for the scheduler's completed execution, so that `Ok(())` means the entries executed +/// successfully +/// +/// Waiting ends the bank's scheduler session; processing further entries against the same bank +/// requires wrapping it with a freshly taken scheduler again. /// /// This method is for use testing against a single Bank, and assumes `Bank::transaction_count()` /// represents the number of transactions executed in this Bank -pub fn process_entries_for_tests( - bank: &BankWithScheduler, - entries: Vec, - transaction_status_sender: Option<&TransactionStatusSender>, - replay_vote_sender: Option<&ReplayVoteSender>, -) -> Result<()> { +pub fn process_entries_for_tests(bank: &BankWithScheduler, entries: Vec) -> Result<()> { + let result = schedule_entries_for_tests(bank, entries); + + // Wait even if scheduling failed, both to surface any transaction execution error like the + // replay stage does before freezing the bank, and to return the scheduler to its pool before + // `bank` is dropped. + let wait_result = bank + .wait_for_completed_scheduler() + .map_or(Ok(()), |(wait_result, _timings)| wait_result); + + result.and(wait_result) +} + +fn schedule_entries_for_tests(bank: &BankWithScheduler, entries: Vec) -> Result<()> { let replay_tx_thread_pool = create_thread_pool(1); let validate_and_hash_transaction = { let bank = bank.clone_with_scheduler(); @@ -406,7 +250,6 @@ pub fn process_entries_for_tests( unverified_signatures.verify()?; let mut entry_starting_index: usize = bank.transaction_count().try_into().unwrap(); - let mut batch_timing = BatchExecutionTiming::default(); let replay_entries: Vec<_> = entries .into_iter() .map(|entry| { @@ -421,31 +264,10 @@ pub fn process_entries_for_tests( }) .collect(); - let result = process_entries( - bank, - &replay_tx_thread_pool, - replay_entries, - transaction_status_sender, - replay_vote_sender, - &mut batch_timing, - None, - None, - ); - - debug!("process_entries: {batch_timing:?}"); - result + process_entries(bank, replay_entries) } -fn process_entries( - bank: &BankWithScheduler, - replay_tx_thread_pool: &ThreadPool, - entries: Vec, - transaction_status_sender: Option<&TransactionStatusSender>, - replay_vote_sender: Option<&ReplayVoteSender>, - batch_timing: &mut BatchExecutionTiming, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, -) -> Result<()> { +fn process_entries(bank: &BankWithScheduler, entries: Vec) -> Result<()> { // accumulator for entries that can be processed in parallel let mut batches = vec![]; let mut tick_hashes = vec![]; @@ -469,32 +291,12 @@ fn process_entries( starting_index, transactions, &mut batches, - |batches| { - process_batches( - bank, - replay_tx_thread_pool, - batches, - transaction_status_sender, - replay_vote_sender, - batch_timing, - log_messages_bytes_limit, - prioritization_fee_cache, - ) - }, + |batches| process_batches(bank, batches), )?; } } } - process_batches( - bank, - replay_tx_thread_pool, - batches.into_iter(), - transaction_status_sender, - replay_vote_sender, - batch_timing, - log_messages_bytes_limit, - prioritization_fee_cache, - )?; + process_batches(bank, batches.into_iter())?; for hash in tick_hashes { bank.register_tick(&hash); } @@ -876,7 +678,6 @@ fn confirm_full_slot( replay_tx_thread_pool: &ThreadPool, opts: &ProcessOptions, progress: &mut ConfirmationProgress, - transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, timing: &mut ExecuteTimings, @@ -903,13 +704,10 @@ fn confirm_full_slot( &mut confirmation_timing, progress, skip_verification, - transaction_status_sender, entry_notification_sender, replay_vote_sender, None, opts.allow_dead_slots, - opts.runtime_config.log_messages_bytes_limit, - None, migration_status, )?; @@ -1429,13 +1227,10 @@ pub fn confirm_slot( timing: &mut ConfirmationTiming, progress: &mut ConfirmationProgress, skip_verification: bool, - transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, finalization_cert_sender: Option<&Sender>, allow_dead_slots: bool, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, migration_status: &MigrationStatus, ) -> result::Result<(), BlockstoreProcessorError> { let slot = bank.slot(); @@ -1518,11 +1313,8 @@ pub fn confirm_slot( timing, progress, skip_verification, - transaction_status_sender, entry_notification_sender, replay_vote_sender, - log_messages_bytes_limit, - prioritization_fee_cache, migration_status, )?; } @@ -1580,11 +1372,8 @@ fn confirm_slot_entries( timing: &mut ConfirmationTiming, progress: &mut ConfirmationProgress, skip_verification: bool, - transaction_status_sender: Option<&TransactionStatusSender>, entry_notification_sender: Option<&EntryNotifierSender>, replay_vote_sender: Option<&ReplayVoteSender>, - log_messages_bytes_limit: Option, - prioritization_fee_cache: Option<&PrioritizationFeeCache>, migration_status: &MigrationStatus, ) -> result::Result<(), BlockstoreProcessorError> { let ConfirmationTiming { @@ -1592,7 +1381,6 @@ fn confirm_slot_entries( replay_elapsed, poh_verify_elapsed, transaction_verify_elapsed, - batch_execute: batch_execute_timing, .. } = timing; @@ -1798,17 +1586,8 @@ fn confirm_slot_entries( }) .collect::, _>>()?; - let process_result = process_entries( - bank, - replay_tx_thread_pool, - replay_entries, - transaction_status_sender, - replay_vote_sender, - batch_execute_timing, - log_messages_bytes_limit, - prioritization_fee_cache, - ) - .map_err(BlockstoreProcessorError::from); + let process_result = + process_entries(bank, replay_entries).map_err(BlockstoreProcessorError::from); replay_timer.stop(); *replay_elapsed += replay_timer.as_us(); @@ -1847,7 +1626,6 @@ fn process_bank_0( replay_tx_thread_pool, opts, &mut progress, - None, entry_notification_sender, None, &mut ExecuteTimings::default(), @@ -2482,7 +2260,6 @@ pub fn process_single_slot( replay_tx_thread_pool, opts, progress, - transaction_status_sender, entry_notification_sender, replay_vote_sender, timing, @@ -2601,8 +2378,8 @@ pub mod tests { self, ValidatorVoteKeypairs, create_genesis_config_with_vote_accounts, }, installed_scheduler_pool::{ - MockInstalledScheduler, MockUninstalledScheduler, SchedulerAborted, - SchedulingContext, + InstalledSchedulerPool, MockInstalledScheduler, MockUninstalledScheduler, + SchedulerAborted, SchedulingContext, }, transaction_execution::TransactionStatusMessage, }, @@ -2611,6 +2388,7 @@ pub mod tests { solana_system_transaction as system_transaction, solana_transaction::Transaction, solana_transaction_error::TransactionError, + solana_unified_scheduler_pool::DefaultSchedulerPool, solana_vote::{vote_account::VoteAccount, vote_transaction}, solana_vote_program::{ self, @@ -2618,7 +2396,7 @@ pub mod tests { }, std::{ collections::BTreeSet, - sync::{Arc, Barrier, RwLock, atomic::Ordering}, + sync::{Arc, Barrier, Mutex, RwLock, atomic::Ordering}, thread, }, test_case::test_case, @@ -2695,6 +2473,9 @@ pub mod tests { exit, ) .unwrap(); + bank_forks.write().unwrap().install_scheduler_pool( + DefaultSchedulerPool::new_for_verification(None, None, None, None, None), + ); let leader_schedule_cache = LeaderScheduleCache::new_from_bank(&bank_forks.read().unwrap().root_bank()); @@ -2746,16 +2527,30 @@ pub mod tests { } } - fn process_entries_for_tests_without_scheduler( + fn take_bank_with_scheduler_for_tests( + pool: &Arc, + bank: Arc, + ) -> BankWithScheduler { + let context = SchedulingContext::new(bank.clone()); + let scheduler = pool.take_scheduler(context).unwrap(); + BankWithScheduler::new(bank, Some(scheduler)) + } + + fn process_entries_with_pool_for_tests( + pool: &Arc, bank: &Arc, entries: Vec, ) -> Result<()> { - process_entries_for_tests( - &BankWithScheduler::new_without_scheduler(bank.clone()), - entries, - None, - None, - ) + let bank = take_bank_with_scheduler_for_tests(pool, bank.clone()); + process_entries_for_tests(&bank, entries) + } + + fn process_entries_for_tests_with_scheduler( + bank: &Arc, + entries: Vec, + ) -> Result<()> { + let pool = DefaultSchedulerPool::new_for_verification(None, None, None, None, None); + process_entries_with_pool_for_tests(&pool, bank, entries) } #[test] @@ -3431,7 +3226,7 @@ pub mod tests { ); // Now ensure the TX is accepted despite pointing to the ID of an empty entry. - process_entries_for_tests_without_scheduler(&bank, slot_entries).unwrap(); + process_entries_for_tests_with_scheduler(&bank, slot_entries).unwrap(); assert_eq!(bank.process_transaction(&tx), Ok(())); } @@ -3553,7 +3348,7 @@ pub mod tests { assert_eq!(bank.tick_height(), 0); let tick = next_entry(&genesis_config.hash(), 1, vec![]); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![tick]), + process_entries_for_tests_with_scheduler(&bank, vec![tick]), Ok(()) ); assert_eq!(bank.tick_height(), 1); @@ -3588,7 +3383,7 @@ pub mod tests { ); let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![entry_1, entry_2]), + process_entries_for_tests_with_scheduler(&bank, vec![entry_1, entry_2]), Ok(()) ); assert_eq!(bank.get_balance(&keypair1.pubkey()), 2); @@ -3644,7 +3439,7 @@ pub mod tests { ); assert_eq!( - process_entries_for_tests_without_scheduler( + process_entries_for_tests_with_scheduler( &bank, vec![entry_1_to_mint, entry_2_to_3_mint_to_1], ), @@ -3686,7 +3481,7 @@ pub mod tests { &bank.last_blockhash(), 1, vec![ - good_tx.clone(), + good_tx, system_transaction::transfer( &keypair4, &keypair4.pubkey(), @@ -3716,16 +3511,18 @@ pub mod tests { ); assert_matches!( - process_entries_for_tests_without_scheduler( + process_entries_for_tests_with_scheduler( &bank, vec![entry_1_to_mint.clone(), entry_2_to_3_mint_to_1.clone()], ), Err(TransactionError::BlockhashNotFound) ); - // First transaction in first entry was rolled-back, so keypair1 didn't lost 1 lamport - assert_eq!(bank.get_balance(&keypair1.pubkey()), 4); - assert_eq!(bank.get_balance(&keypair2.pubkey()), 4); + // The scheduler commits each transaction individually and aborts asynchronously, + // so the other transactions may or may not have been committed by now; only the failing + // transaction is guaranteed not to have landed. In production such a block is marked + // dead and its bank discarded, so any partial commit is never visible. + assert_eq!(bank.get_balance(&keypair4.pubkey()), 4); // Check all accounts are unlocked let txs1 = entry_1_to_mint.transactions; @@ -3742,14 +3539,26 @@ pub mod tests { } drop(batch2); - // ensure good_tx will succeed and was just rolled back above due to other failing tx - let entry_3 = next_entry(&entry_2_to_3_mint_to_1.hash, 1, vec![good_tx]); + // ensure the bank still processes new entries after the aborted scheduler; keypair4's + // only prior transaction was the one guaranteed to have failed, so this outcome is + // deterministic + let pubkey5 = Pubkey::new_unique(); + let entry_3 = next_entry( + &entry_2_to_3_mint_to_1.hash, + 1, + vec![system_transaction::transfer( + &keypair4, + &pubkey5, + 1, + bank.last_blockhash(), + )], + ); assert_matches!( - process_entries_for_tests_without_scheduler(&bank, vec![entry_3]), + process_entries_for_tests_with_scheduler(&bank, vec![entry_3]), Ok(()) ); - // First transaction in third entry succeeded, so keypair1 lost 1 lamport - assert_eq!(bank.get_balance(&keypair1.pubkey()), 3); + assert_eq!(bank.get_balance(&keypair4.pubkey()), 3); + assert_eq!(bank.get_balance(&pubkey5), 1); } #[test] @@ -3846,7 +3655,7 @@ pub mod tests { ); let entry = next_entry(&bank.last_blockhash(), 1, vec![tx]); - let result = process_entries_for_tests_without_scheduler(&bank, vec![entry]); + let result = process_entries_for_tests_with_scheduler(&bank, vec![entry]); bank.freeze(); let ok_bank_details = SlotDetails::new_from_bank(&bank, true).unwrap(); assert!(result.is_ok()); @@ -3891,7 +3700,7 @@ pub mod tests { let entry = next_entry(&bank.last_blockhash(), 1, vec![tx]); let bank = Arc::new(bank); - let result = process_entries_for_tests_without_scheduler(&bank, vec![entry]); + let result = process_entries_for_tests_with_scheduler(&bank, vec![entry]); assert!(result.is_ok()); // No failing transaction error - only instruction errors bank.freeze(); let bank_details = SlotDetails::new_from_bank(&bank, true).unwrap(); @@ -4007,7 +3816,7 @@ pub mod tests { // keypair3=3 // succeeds following simd83 locking, fails otherwise - let result = process_entries_for_tests_without_scheduler( + let result = process_entries_for_tests_with_scheduler( &bank, vec![ entry_1_to_mint, @@ -4070,7 +3879,7 @@ pub mod tests { // keypair2=5 // succeeds following simd83 locking, fails otherwise - let result = process_entries_for_tests_without_scheduler(&bank, vec![entry_1_to_2_twice]); + let result = process_entries_for_tests_with_scheduler(&bank, vec![entry_1_to_2_twice]); let balances = [ bank.get_balance(&keypair1.pubkey()), @@ -4119,7 +3928,7 @@ pub mod tests { system_transaction::transfer(&keypair2, &keypair4.pubkey(), 1, bank.last_blockhash()); let entry_2 = next_entry(&entry_1.hash, 1, vec![tx]); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![entry_1, entry_2]), + process_entries_for_tests_with_scheduler(&bank, vec![entry_1, entry_2]), Ok(()) ); assert_eq!(bank.get_balance(&keypair3.pubkey()), 1); @@ -4180,7 +3989,7 @@ pub mod tests { }) .collect(); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, entries), + process_entries_for_tests_with_scheduler(&bank, entries), Ok(()) ); } @@ -4243,7 +4052,7 @@ pub mod tests { // Transfer lamports to each other let entry = next_entry(&bank.last_blockhash(), 1, tx_vector); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![entry]), + process_entries_for_tests_with_scheduler(&bank, vec![entry]), Ok(()) ); bank.squash(); @@ -4308,10 +4117,7 @@ pub mod tests { system_transaction::transfer(&keypair1, &keypair4.pubkey(), 1, bank.last_blockhash()); let entry_2 = next_entry(&tick.hash, 1, vec![tx]); assert_eq!( - process_entries_for_tests_without_scheduler( - &bank, - vec![entry_1, tick, entry_2.clone()], - ), + process_entries_for_tests_with_scheduler(&bank, vec![entry_1, tick, entry_2.clone()],), Ok(()) ); assert_eq!(bank.get_balance(&keypair3.pubkey()), 1); @@ -4322,7 +4128,7 @@ pub mod tests { system_transaction::transfer(&keypair2, &keypair3.pubkey(), 1, bank.last_blockhash()); let entry_3 = next_entry(&entry_2.hash, 1, vec![tx]); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![entry_3]), + process_entries_for_tests_with_scheduler(&bank, vec![entry_3]), if relax_fee_payer_constraint { Ok(()) } else { @@ -4431,7 +4237,7 @@ pub mod tests { ); assert_eq!( - process_entries_for_tests_without_scheduler(&bank, vec![entry_1_to_mint]), + process_entries_for_tests_with_scheduler(&bank, vec![entry_1_to_mint]), Ok(()) ); @@ -4540,7 +4346,6 @@ pub mod tests { &mut ConfirmationProgress::new(bank0_last_blockhash), None, None, - None, &mut ExecuteTimings::default(), &MigrationStatus::default(), ) @@ -4643,7 +4448,7 @@ pub mod tests { }) .collect(); info!("paying iteration {i}"); - process_entries_for_tests_without_scheduler(&bank, entries).expect("paying failed"); + process_entries_for_tests_with_scheduler(&bank, entries).expect("paying failed"); let entries: Vec<_> = (0..NUM_TRANSFERS) .step_by(NUM_TRANSFERS_PER_ENTRY) @@ -4666,10 +4471,10 @@ pub mod tests { .collect(); info!("refunding iteration {i}"); - process_entries_for_tests_without_scheduler(&bank, entries).expect("refunding failed"); + process_entries_for_tests_with_scheduler(&bank, entries).expect("refunding failed"); // advance to next block - process_entries_for_tests_without_scheduler( + process_entries_for_tests_with_scheduler( &bank, (0..bank.ticks_per_slot()) .map(|_| next_entry_mut(&mut hash, 1, vec![])) @@ -4789,12 +4594,14 @@ pub mod tests { .collect(); let entry = next_entry(&bank_1_blockhash, 1, vote_txs); let (replay_vote_sender, replay_vote_receiver) = bounded(1024); - let _ = process_entries_for_tests( - &BankWithScheduler::new_without_scheduler(bank1), - vec![entry], + let pool = DefaultSchedulerPool::new_for_verification( + None, + None, + None, + Some(replay_vote_sender), None, - Some(&replay_vote_sender), ); + let _ = process_entries_with_pool_for_tests(&pool, &bank1, vec![entry]); let successes: BTreeSet = replay_vote_receiver .try_iter() .filter_map(|replay_vote| match replay_vote { @@ -5081,29 +4888,47 @@ pub mod tests { ); } - fn confirm_slot_entries_for_tests( + fn confirm_slot_entries_with_pool_for_tests( + pool: &Arc, bank: &Arc, slot_entries: Vec, slot_full: bool, - prev_entry_hash: Hash, + progress: &mut ConfirmationProgress, ) -> result::Result<(), BlockstoreProcessorError> { + let bank = take_bank_with_scheduler_for_tests(pool, bank.clone()); let replay_tx_thread_pool = create_thread_pool(1); - let mut progress = ConfirmationProgress::new(prev_entry_hash); - confirm_slot_entries( - &BankWithScheduler::new_without_scheduler(bank.clone()), + let result = confirm_slot_entries( + &bank, &replay_tx_thread_pool, (slot_entries, 0, slot_full), &mut ConfirmationTiming::default(), - &mut progress, + progress, false, None, None, - None, - None, - None, &MigrationStatus::default(), - )?; - progress.wait_for_all_verification_results(&mut 0, &mut 0) + ); + let (wait_result, _timings) = bank.wait_for_completed_scheduler().unwrap(); + result?; + progress.wait_for_all_verification_results(&mut 0, &mut 0)?; + Ok(wait_result?) + } + + fn confirm_slot_entries_for_tests( + bank: &Arc, + slot_entries: Vec, + slot_full: bool, + prev_entry_hash: Hash, + ) -> result::Result<(), BlockstoreProcessorError> { + let pool = DefaultSchedulerPool::new_for_verification(None, None, None, None, None); + let mut progress = ConfirmationProgress::new(prev_entry_hash); + confirm_slot_entries_with_pool_for_tests( + &pool, + bank, + slot_entries, + slot_full, + &mut progress, + ) } fn create_test_transactions( @@ -5147,9 +4972,18 @@ pub mod tests { } = create_genesis_config(100 * LAMPORTS_PER_SOL); let genesis_hash = genesis_config.hash(); let (bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); - let bank = BankWithScheduler::new_without_scheduler(bank); - let replay_tx_thread_pool = create_thread_pool(1); - let mut timing = ConfirmationTiming::default(); + let (transaction_status_sender, transaction_status_receiver) = bounded(1024); + let transaction_status_sender = TransactionStatusSender { + sender: transaction_status_sender, + dependency_tracker: None, + }; + let pool = DefaultSchedulerPool::new_for_verification( + None, + None, + Some(transaction_status_sender), + None, + None, + ); let mut progress = ConfirmationProgress::new(genesis_hash); let amount = genesis_config.rent.minimum_balance(0); let keypair1 = Keypair::new(); @@ -5161,12 +4995,6 @@ pub mod tests { bank.transfer(LAMPORTS_PER_SOL, &mint_keypair, &keypair2.pubkey()) .unwrap(); - let (transaction_status_sender, transaction_status_receiver) = bounded(1024); - let transaction_status_sender = TransactionStatusSender { - sender: transaction_status_sender, - dependency_tracker: None, - }; - let blockhash = bank.last_blockhash(); let tx1 = system_transaction::transfer( &keypair1, @@ -5183,33 +5011,13 @@ pub mod tests { let entry = next_entry(&blockhash, 1, vec![tx1, tx2]); let new_hash = entry.hash; - confirm_slot_entries( - &bank, - &replay_tx_thread_pool, - (vec![entry], 0, false), - &mut timing, - &mut progress, - false, - Some(&transaction_status_sender), - None, - None, - None, - None, - &MigrationStatus::default(), - ) - .unwrap(); - progress - .wait_for_all_verification_results(&mut 0, &mut 0) + confirm_slot_entries_with_pool_for_tests(&pool, &bank, vec![entry], false, &mut progress) .unwrap(); assert_eq!(progress.num_txs, 2); - let batch = transaction_status_receiver.recv().unwrap(); - if let TransactionStatusMessage::Batch((batch, _sequence)) = batch { - assert_eq!(batch.transactions.len(), 2); - assert_eq!(batch.transaction_indexes.len(), 2); - assert_eq!(batch.transaction_indexes, [0, 1]); - } else { - panic!("batch should have been sent"); - } + // The unified scheduler executes each transaction as its own task, so statuses arrive + // as multiple batches in no particular order. + let indexes = receive_transaction_indexes(&transaction_status_receiver); + assert_eq!(indexes, [0, 1]); let tx1 = system_transaction::transfer( &keypair1, @@ -5231,33 +5039,28 @@ pub mod tests { ); let entry = next_entry(&new_hash, 1, vec![tx1, tx2, tx3]); - confirm_slot_entries( - &bank, - &replay_tx_thread_pool, - (vec![entry], 0, false), - &mut timing, - &mut progress, - false, - Some(&transaction_status_sender), - None, - None, - None, - None, - &MigrationStatus::default(), - ) - .unwrap(); - progress - .wait_for_all_verification_results(&mut 0, &mut 0) + confirm_slot_entries_with_pool_for_tests(&pool, &bank, vec![entry], false, &mut progress) .unwrap(); assert_eq!(progress.num_txs, 5); - let batch = transaction_status_receiver.recv().unwrap(); - if let TransactionStatusMessage::Batch((batch, _sequnce)) = batch { - assert_eq!(batch.transactions.len(), 3); - assert_eq!(batch.transaction_indexes.len(), 3); - assert_eq!(batch.transaction_indexes, [2, 3, 4]); - } else { - panic!("batch should have been sent"); + let indexes = receive_transaction_indexes(&transaction_status_receiver); + assert_eq!(indexes, [2, 3, 4]); + } + + fn receive_transaction_indexes( + receiver: &crossbeam_channel::Receiver, + ) -> Vec { + let mut indexes = vec![]; + while let Ok(message) = receiver.try_recv() { + match message { + TransactionStatusMessage::Batch((batch, _sequence)) => { + assert_eq!(batch.transactions.len(), batch.transaction_indexes.len()); + indexes.extend_from_slice(&batch.transaction_indexes); + } + TransactionStatusMessage::Freeze(_) => {} + } } + indexes.sort(); + indexes } #[test] @@ -5324,7 +5127,7 @@ pub mod tests { exit_barrier.wait(); } - fn do_test_schedule_batches_for_execution(should_succeed: bool) { + fn do_test_process_batches(should_succeed: bool) { agave_logger::setup(); let dummy_leader_pubkey = solana_pubkey::new_rand(); let GenesisConfigInfo { @@ -5387,18 +5190,7 @@ pub mod tests { starting_index: 0, }; - let replay_tx_thread_pool = create_thread_pool(1); - let mut batch_execution_timing = BatchExecutionTiming::default(); - let result = process_batches( - &bank, - &replay_tx_thread_pool, - [locked_entry].into_iter(), - None, - None, - &mut batch_execution_timing, - None, - None, - ); + let result = process_batches(&bank, [locked_entry].into_iter()); if should_succeed { assert_matches!(result, Ok(())); } else { @@ -5407,13 +5199,59 @@ pub mod tests { } #[test] - fn test_schedule_batches_for_execution_success() { - do_test_schedule_batches_for_execution(true); + fn test_process_batches_success() { + do_test_process_batches(true); + } + + #[test] + fn test_process_batches_failure() { + do_test_process_batches(false); + } + + #[test] + fn process_batches_is_noop_for_empty_batches_without_scheduler() { + // Given: a bank with no scheduler installed + let genesis_config = create_genesis_config(100).genesis_config; + let bank = BankWithScheduler::new_without_scheduler(Arc::new(Bank::new_for_tests( + &genesis_config, + ))); + + // When: processing an empty set of batches, like those of a tick-only slot + let result = process_batches(&bank, std::iter::empty()); + + // Then: nothing needs to be scheduled, so no scheduler is required + assert_eq!(result, Ok(())); } #[test] - fn test_schedule_batches_for_execution_failure() { - do_test_schedule_batches_for_execution(false); + #[should_panic(expected = "no scheduler installed for bank of slot 0")] + fn process_batches_asserts_installed_scheduler() { + // Given: a bank with no scheduler installed and a single batch of one transaction + let GenesisConfigInfo { + genesis_config, + mint_keypair, + .. + } = create_genesis_config(100); + let bank = BankWithScheduler::new_without_scheduler(Arc::new(Bank::new_for_tests( + &genesis_config, + ))); + let transactions = vec![RuntimeTransaction::from_transaction_for_tests( + system_transaction::transfer( + &mint_keypair, + &solana_pubkey::new_rand(), + 1, + genesis_config.hash(), + ), + )]; + let batch = LockedTransactionsWithIndexes { + lock_results: bank.try_lock_accounts(&transactions), + transactions, + starting_index: 0, + }; + + // When: processing the batch + // Then: unreachable; the missing-scheduler assert must have fired + let _ = process_batches(&bank, [batch].into_iter()); } #[test] @@ -5432,7 +5270,7 @@ pub mod tests { genesis_config.ticks_per_slot = TICKS_PER_SLOT; let genesis_hash = genesis_config.hash(); - let (slot_0_bank, bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); + let (slot_0_bank, _bank_forks) = Bank::new_with_bank_forks_for_tests(&genesis_config); let hashes_per_tick = slot_0_bank.hashes_per_tick().unwrap(); assert_eq!(slot_0_bank.slot(), 0); assert_eq!(slot_0_bank.tick_height(), 0); @@ -5448,24 +5286,8 @@ pub mod tests { assert_eq!(slot_0_bank.get_hash_age(&genesis_hash), Some(1)); assert_eq!(slot_0_bank.get_hash_age(&slot_0_hash), Some(0)); - let new_bank = Bank::new_from_parent(slot_0_bank, leader, 2); - let slot_2_bank = bank_forks - .write() - .unwrap() - .insert(new_bank) - .clone_without_scheduler(); - assert_eq!(slot_2_bank.slot(), 2); - assert_eq!(slot_2_bank.tick_height(), 2); - assert_eq!(slot_2_bank.max_tick_height(), 6); - assert_eq!(slot_2_bank.last_blockhash(), slot_0_hash); - let slot_1_entries = entry::create_ticks(TICKS_PER_SLOT, hashes_per_tick, slot_0_hash); let slot_1_hash = slot_1_entries.last().unwrap().hash; - confirm_slot_entries_for_tests(&slot_2_bank, slot_1_entries, false, slot_0_hash).unwrap(); - assert_eq!(slot_2_bank.tick_height(), 4); - assert_eq!(slot_2_bank.last_blockhash(), slot_0_hash); - assert_eq!(slot_2_bank.get_hash_age(&genesis_hash), Some(1)); - assert_eq!(slot_2_bank.get_hash_age(&slot_0_hash), Some(0)); struct TestCase { recent_blockhash: Hash, @@ -5485,12 +5307,30 @@ pub mod tests { }, ]; - // Check that slot 2 transactions can only use hashes for completed blocks. + // Check that slot 2 transactions can only use hashes for completed blocks. The unified + // scheduler surfaces transaction errors only when waiting for its completion, after the + // slot's ticks have already been registered on the bank, so use a fresh bank per test + // case to keep a failing case from tainting the following one. for TestCase { recent_blockhash, expected_result, } in test_cases { + let slot_2_bank = Arc::new(Bank::new_from_parent(slot_0_bank.clone(), leader, 2)); + assert_eq!(slot_2_bank.slot(), 2); + assert_eq!(slot_2_bank.tick_height(), 2); + assert_eq!(slot_2_bank.max_tick_height(), 6); + assert_eq!(slot_2_bank.last_blockhash(), slot_0_hash); + + let slot_1_entries = entry::create_ticks(TICKS_PER_SLOT, hashes_per_tick, slot_0_hash); + assert_eq!(slot_1_entries.last().unwrap().hash, slot_1_hash); + confirm_slot_entries_for_tests(&slot_2_bank, slot_1_entries, false, slot_0_hash) + .unwrap(); + assert_eq!(slot_2_bank.tick_height(), 4); + assert_eq!(slot_2_bank.last_blockhash(), slot_0_hash); + assert_eq!(slot_2_bank.get_hash_age(&genesis_hash), Some(1)); + assert_eq!(slot_2_bank.get_hash_age(&slot_0_hash), Some(0)); + let slot_2_entries = { let to_pubkey = Pubkey::new_unique(); let mut prev_entry_hash = slot_1_hash; @@ -5700,10 +5540,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::default(), ) .unwrap_err(); @@ -5736,10 +5573,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::post_migration_status(), ) .unwrap(); @@ -5767,10 +5601,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::post_migration_status(), ); assert_matches!( diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index a5f5339fa0b..a0879b3d9c8 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -10147,7 +10147,6 @@ dependencies = [ "scopeguard", "solana-clock", "solana-cost-model", - "solana-ledger", "solana-metrics", "solana-poh", "solana-pubkey 4.2.0", diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index a851f17b867..1d128672ab8 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -22,8 +22,10 @@ name = "solana_rpc" agave-unstable-api = [] dev-context-only-utils = [ "agave-votor-messages/dev-context-only-utils", + "dep:solana-unified-scheduler-pool", "solana-ledger/dev-context-only-utils", "solana-rpc/dev-context-only-utils", + "solana-unified-scheduler-pool/dev-context-only-utils", ] [dependencies] @@ -91,6 +93,7 @@ solana-transaction = { workspace = true } solana-transaction-context = { workspace = true } solana-transaction-error = { workspace = true } solana-transaction-status = { workspace = true } +solana-unified-scheduler-pool = { workspace = true, optional = true } solana-validator-exit = { workspace = true } solana-version = { workspace = true } solana-vote = { workspace = true } diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index f9ef1557362..640c9a4b5e9 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -1,6 +1,8 @@ //! The `rpc` module implements the Solana RPC interface. #[cfg(feature = "dev-context-only-utils")] -use solana_runtime::installed_scheduler_pool::BankWithScheduler; +use solana_runtime::installed_scheduler_pool::{ + BankWithScheduler, InstalledSchedulerPool, SchedulingContext, +}; use { crate::{ filter::filter_allows, max_slots::MaxSlots, @@ -4541,7 +4543,6 @@ pub fn populate_blockstore_for_tests( blockstore.set_roots(std::iter::once(&slot)).unwrap(); let (transaction_status_sender, transaction_status_receiver) = bounded(1024); - let (replay_vote_sender, _replay_vote_receiver) = bounded(1024); let tss_exit = Arc::new(AtomicBool::new(false)); let transaction_status_service = crate::transaction_status_service::TransactionStatusService::new( @@ -4555,20 +4556,27 @@ pub fn populate_blockstore_for_tests( tss_exit.clone(), ); + let transaction_status_sender = + solana_runtime::transaction_execution::TransactionStatusSender { + sender: transaction_status_sender, + dependency_tracker: None, + }; + let pool = solana_unified_scheduler_pool::DefaultSchedulerPool::new_for_verification( + None, + None, + Some(transaction_status_sender), + None, + None, + ); + + let context = SchedulingContext::new(bank.clone()); + let scheduler = pool.take_scheduler(context).unwrap(); + let bank = BankWithScheduler::new(bank, Some(scheduler)); + // Check that process_entries successfully writes can_commit transactions statuses, and // that they are matched properly by get_rooted_block assert_eq!( - solana_ledger::blockstore_processor::process_entries_for_tests( - &BankWithScheduler::new_without_scheduler(bank), - entries, - Some( - &solana_runtime::transaction_execution::TransactionStatusSender { - sender: transaction_status_sender, - dependency_tracker: None, - }, - ), - Some(&replay_vote_sender), - ), + solana_ledger::blockstore_processor::process_entries_for_tests(&bank, entries), Ok(()) ); diff --git a/unified-scheduler-pool/Cargo.toml b/unified-scheduler-pool/Cargo.toml index c81163fcddc..18c81e653e4 100644 --- a/unified-scheduler-pool/Cargo.toml +++ b/unified-scheduler-pool/Cargo.toml @@ -16,7 +16,7 @@ rustdoc-args = ["--cfg=docsrs"] [features] agave-unstable-api = [] -dev-context-only-utils = [] +dev-context-only-utils = ["solana-runtime/dev-context-only-utils"] [dependencies] agave-banking-stage-ingress-types = { workspace = true } @@ -30,7 +30,6 @@ log = { workspace = true } scopeguard = { workspace = true } solana-clock = { workspace = true } solana-cost-model = { workspace = true } -solana-ledger = { workspace = true } solana-metrics = { workspace = true } solana-poh = { workspace = true } solana-pubkey = { workspace = true } diff --git a/unified-scheduler-pool/src/lib.rs b/unified-scheduler-pool/src/lib.rs index ee55dd45e59..b43010df8ac 100644 --- a/unified-scheduler-pool/src/lib.rs +++ b/unified-scheduler-pool/src/lib.rs @@ -1909,7 +1909,10 @@ mod tests { solana_runtime::{ bank::{Bank, SlotLeader}, bank_forks::BankForks, - genesis_utils::{GenesisConfigInfo, create_genesis_config}, + genesis_utils::{ + GenesisConfigInfo, bootstrap_validator_stake_lamports, create_genesis_config, + create_genesis_config_with_leader, + }, installed_scheduler_pool::{ BankWithScheduler, InstalledSchedulerPoolArc, SchedulingContext, }, @@ -2980,13 +2983,17 @@ mod tests { } fn create_genesis_config_for_block_production(lamports: u64) -> GenesisConfigInfo { - // The in-scope create_genesis_config(), which is imported from the `solana-runtime`, - // doesn't properly setup leader schedule, causing the following panic if used for poh - // recorder, so use the one from the `solana-ledger` crate: + // The in-scope create_genesis_config() doesn't properly setup leader schedule, causing + // the following panic if used for poh recorder, so set up a bootstrap validator stake + // like `solana-ledger`'s genesis_utils does: // // thread 'tests::...' panicked at ledger/src/leader_schedule.rs:LL:CC: // called `Result::unwrap()` on an `Err` value: NoItem - solana_ledger::genesis_utils::create_genesis_config(lamports) + create_genesis_config_with_leader( + lamports, + &Pubkey::new_unique(), + bootstrap_validator_stake_lamports(), + ) } #[test] From 6b496d05b845ef8cb4f3eaaff5b394ecf4e2abe1 Mon Sep 17 00:00:00 2001 From: Dmitry Adamushka Date: Thu, 16 Jul 2026 17:14:21 +0200 Subject: [PATCH 30/30] streamer: revalidate cached QUIC stake --- streamer/src/nonblocking/qos.rs | 77 +++++++++++++++++++++++++- streamer/src/nonblocking/quic.rs | 35 +++++++++++- streamer/src/nonblocking/simple_qos.rs | 6 +- streamer/src/nonblocking/swqos.rs | 6 +- 4 files changed, 117 insertions(+), 7 deletions(-) diff --git a/streamer/src/nonblocking/qos.rs b/streamer/src/nonblocking/qos.rs index 3dddfcc8e15..32990b70f33 100644 --- a/streamer/src/nonblocking/qos.rs +++ b/streamer/src/nonblocking/qos.rs @@ -1,7 +1,10 @@ use { - crate::nonblocking::quic::{ClientConnectionTracker, ConnectionPeerType}, + crate::{ + nonblocking::quic::{ClientConnectionTracker, ConnectionPeerType, get_pubkey_stake}, + streamer::StakedNodes, + }, quinn::Connection, - std::future::Future, + std::{future::Future, sync::RwLock}, tokio_util::sync::CancellationToken, }; @@ -13,6 +16,19 @@ pub(crate) trait ConnectionContext: Clone + Send + Sync { fn remote_pubkey(&self) -> Option; } +pub(crate) fn is_stake_current( + context: &C, + staked_nodes: &RwLock, +) -> bool { + let ConnectionPeerType::Staked(cached_stake) = context.peer_type() else { + return true; + }; + context.remote_pubkey().is_some_and(|pubkey| { + get_pubkey_stake(&pubkey, staked_nodes) + .is_some_and(|(stake, _total_stake)| stake == cached_stake) + }) +} + /// A trait to manage QoS for connections. This includes /// 1) deriving the ConnectionContext for a connection /// 2) managing connection caching and connection limits, stream limits @@ -20,6 +36,9 @@ pub(crate) trait QosController { /// Build the ConnectionContext for a connection fn build_connection_context(&self, connection: &Connection) -> C; + /// Returns whether the stake cached in the connection context is still current. + fn is_stake_current(&self, context: &C) -> bool; + /// Try to add a new connection to the connection table. This is an async operation that /// returns a Future. If successful, the Future resolves to Some containing a CancellationToken. /// Otherwise, the Future resolves to None. @@ -67,3 +86,57 @@ pub(crate) struct NullStreamerCounter; #[cfg(test)] impl OpaqueStreamerCounter for NullStreamerCounter {} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_pubkey::Pubkey, + std::{collections::HashMap, sync::Arc}, + }; + + #[derive(Clone)] + struct TestConnectionContext { + peer_type: ConnectionPeerType, + remote_pubkey: Option, + } + + impl ConnectionContext for TestConnectionContext { + fn peer_type(&self) -> ConnectionPeerType { + self.peer_type + } + + fn remote_pubkey(&self) -> Option { + self.remote_pubkey + } + } + + #[test] + fn test_is_stake_current() { + let pubkey = Pubkey::new_unique(); + let other_pubkey = Pubkey::new_unique(); + let staked_nodes = RwLock::new(StakedNodes::new( + Arc::new(HashMap::from([(pubkey, 100), (other_pubkey, 900)])), + HashMap::new(), + )); + let context = TestConnectionContext { + peer_type: ConnectionPeerType::Staked(100), + remote_pubkey: Some(pubkey), + }; + let other_context = TestConnectionContext { + peer_type: ConnectionPeerType::Staked(900), + remote_pubkey: Some(other_pubkey), + }; + + assert!(is_stake_current(&context, &staked_nodes)); + assert!(is_stake_current(&other_context, &staked_nodes)); + *staked_nodes.write().unwrap() = StakedNodes::new( + Arc::new(HashMap::from([(pubkey, 100), (other_pubkey, 1_900)])), + HashMap::new(), + ); + assert!(is_stake_current(&context, &staked_nodes)); + assert!(!is_stake_current(&other_context, &staked_nodes)); + *staked_nodes.write().unwrap() = StakedNodes::default(); + assert!(!is_stake_current(&context, &staked_nodes)); + } +} diff --git a/streamer/src/nonblocking/quic.rs b/streamer/src/nonblocking/quic.rs index 88a97e18c12..85911061ce2 100644 --- a/streamer/src/nonblocking/quic.rs +++ b/streamer/src/nonblocking/quic.rs @@ -46,7 +46,7 @@ use { // introduce any other awaits while holding the RwLock. select, task::JoinHandle, - time::timeout, + time::{sleep, timeout}, }, tokio_util::{sync::CancellationToken, task::TaskTracker}, }; @@ -79,6 +79,10 @@ const MAX_CONNECTION_BURST: u64 = 1000; /// peer, and is canceled when we get a Handshake packet from them. const QUIC_CONNECTION_HANDSHAKE_TIMEOUT: Duration = Duration::from_secs(2); +// Bounds for the randomized interval between checks for stale cached stake. +const MIN_STAKE_REVALIDATION_INTERVAL: Duration = Duration::from_secs(60 * 60); +const MAX_STAKE_REVALIDATION_INTERVAL: Duration = Duration::from_secs(2 * 60 * 60); + /// Absolute max RTT to allow for a legitimate connection. /// Enough to cover any non-malicious link on Earth. pub(crate) const MAX_RTT: Duration = Duration::from_millis(320); @@ -419,14 +423,27 @@ pub fn get_connection_stake( ) -> Option<(Pubkey, u64, u64)> { let pubkey = get_remote_pubkey(connection)?; debug!("Peer public key is {pubkey:?}"); + let (stake, total_stake) = get_pubkey_stake(&pubkey, staked_nodes)?; + Some((pubkey, stake, total_stake)) +} + +pub(crate) fn get_pubkey_stake( + pubkey: &Pubkey, + staked_nodes: &RwLock, +) -> Option<(u64, u64)> { let staked_nodes = staked_nodes.read().unwrap(); Some(( - pubkey, - staked_nodes.get_node_stake(&pubkey)?, + staked_nodes.get_node_stake(pubkey)?, staked_nodes.total_stake(), )) } +fn stake_revalidation_interval() -> Duration { + Duration::from_secs(rng().random_range( + MIN_STAKE_REVALIDATION_INTERVAL.as_secs()..=MAX_STAKE_REVALIDATION_INTERVAL.as_secs(), + )) +} + #[derive(Debug)] pub(crate) enum ConnectionHandlerError { ConnectionAddError, @@ -607,6 +624,8 @@ async fn handle_connection( // we only use that for some stats here, so if it gets stale during connection lifetime // it is not the end of the world. let rtt = connection.rtt(); + let stake_revalidation_timer = sleep(stake_revalidation_interval()); + tokio::pin!(stake_revalidation_timer); 'conn: loop { // Wait for new streams. If the peer is disconnected we get a cancellation signal and stop // the connection task. @@ -619,6 +638,16 @@ async fn handle_connection( } }, _ = cancel.cancelled() => break, + _ = &mut stake_revalidation_timer, if peer_type.is_staked() => { + if !qos.is_stake_current(&context) { + debug!("Closing connection from {remote_address}: stake changed"); + break; + } + stake_revalidation_timer + .as_mut() + .reset(tokio::time::Instant::now() + stake_revalidation_interval()); + continue; + }, }; qos.on_new_stream(&context).await; diff --git a/streamer/src/nonblocking/simple_qos.rs b/streamer/src/nonblocking/simple_qos.rs index cd26c5e391f..e5b714a24c4 100644 --- a/streamer/src/nonblocking/simple_qos.rs +++ b/streamer/src/nonblocking/simple_qos.rs @@ -1,7 +1,7 @@ use { crate::{ nonblocking::{ - qos::{ConnectionContext, OpaqueStreamerCounter, QosController}, + qos::{ConnectionContext, OpaqueStreamerCounter, QosController, is_stake_current}, quic::{ CONNECTION_CLOSE_CODE_DISALLOWED, CONNECTION_CLOSE_REASON_DISALLOWED, ClientConnectionTracker, ConnectionHandlerError, ConnectionPeerType, @@ -272,6 +272,10 @@ impl QosController for SimpleQos { } } + fn is_stake_current(&self, context: &SimpleQosConnectionContext) -> bool { + is_stake_current(context, &self.staked_nodes) + } + fn spawn_background_tasks(&mut self) { let eviction_receiver = self .banlist_eviction_receiver diff --git a/streamer/src/nonblocking/swqos.rs b/streamer/src/nonblocking/swqos.rs index 8ec246f5c60..ef7603c52bc 100644 --- a/streamer/src/nonblocking/swqos.rs +++ b/streamer/src/nonblocking/swqos.rs @@ -1,7 +1,7 @@ use { crate::{ nonblocking::{ - qos::{ConnectionContext, QosController}, + qos::{ConnectionContext, QosController, is_stake_current}, quic::{ CONNECTION_CLOSE_CODE_DISALLOWED, CONNECTION_CLOSE_REASON_DISALLOWED, ClientConnectionTracker, ConnectionHandlerError, ConnectionPeerType, @@ -341,6 +341,10 @@ impl QosController for SwQos { ) } + fn is_stake_current(&self, context: &SwQosConnectionContext) -> bool { + is_stake_current(context, &self.staked_nodes) + } + #[allow(clippy::manual_async_fn)] fn try_add_connection( &self,