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/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/Cargo.lock b/Cargo.lock index 6447d0bcd8f..5a8d20e93fc 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]] @@ -2224,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]] @@ -4618,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", ] @@ -4692,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" @@ -4873,7 +4854,7 @@ dependencies = [ "cfg-if 1.0.4", "cfg_aliases", "libc", - "memoffset 0.9.1", + "memoffset", ] [[package]] @@ -6157,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", ] @@ -6175,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", @@ -7210,7 +7191,7 @@ dependencies = [ "dashmap", "itertools 0.14.0", "log", - "memoffset 0.9.1", + "memoffset", "modular-bitfield", "num_cpus", "qualifier_attr", @@ -7421,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" @@ -7893,6 +7884,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", @@ -7904,7 +7896,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", @@ -7914,6 +7908,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", @@ -7924,8 +7919,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", ] @@ -9127,7 +9125,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -9192,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", @@ -9671,9 +9670,9 @@ 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-big-mod-exp 3.0.0", "solana-blake3-hasher", "solana-clock", "solana-cpi", @@ -10125,6 +10124,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-transaction-status", + "solana-unified-scheduler-pool", "solana-validator-exit", "solana-version", "solana-vote", @@ -10335,7 +10335,7 @@ dependencies = [ "libc", "libsecp256k1", "log", - "memoffset 0.9.1", + "memoffset", "mockall", "num-derive", "num-traits", @@ -11107,6 +11107,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", @@ -11433,7 +11434,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "serde_json", @@ -11609,7 +11610,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", @@ -11696,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/Cargo.toml b/Cargo.toml index fcfcffb07f3..bec7ba0d96f 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"] } @@ -289,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" @@ -321,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" @@ -358,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"] } @@ -618,5 +618,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" } 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/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.rs b/accounts-db/src/accounts_db.rs index c72652a710d..d2918b99819 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(), @@ -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; @@ -4732,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); @@ -4752,6 +4772,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. @@ -5113,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()); @@ -5151,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(|| { @@ -5700,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(); @@ -5717,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 @@ -6825,33 +6837,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/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_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 5abbd7a7228..840d52b4f47 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, @@ -149,9 +168,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); }; @@ -610,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 @@ -640,8 +661,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 +749,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 { @@ -1174,6 +1195,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, @@ -1281,6 +1304,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, @@ -1314,7 +1340,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:?}" ); @@ -1744,6 +1770,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, [ @@ -1795,46 +1824,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] @@ -2041,6 +2084,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())); @@ -3420,22 +3466,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(); @@ -4145,15 +4258,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); @@ -5475,21 +5590,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. @@ -5511,11 +5630,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' @@ -6191,6 +6310,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); @@ -6199,6 +6319,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 @@ -6251,13 +6389,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() @@ -6393,15 +6524,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); @@ -6417,7 +6546,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 @@ -6981,19 +7115,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; @@ -7207,6 +7335,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/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/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/bls-sigverify/src/bls_sigverifier.rs b/bls-sigverify/src/bls_sigverifier.rs index 9e5edfdc15a..783c14e244f 100644 --- a/bls-sigverify/src/bls_sigverifier.rs +++ b/bls-sigverify/src/bls_sigverifier.rs @@ -16,24 +16,24 @@ use { migration::MigrationStatus, reward_certificate::AddVoteMessage, sig_verified_messages::SigVerifiedBatch, - unverified_vote_message::{DecodedWireConsensusMessage, UnverifiedVoteMessage}, - vote::Vote, + unverified_vote_message::{ + DecodedWireConsensusMessage, UnverifiedCertificate, UnverifiedVoteMessage, + }, 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}, - 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}, @@ -47,12 +47,18 @@ 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 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 +71,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, @@ -85,6 +92,11 @@ pub fn spawn_service( .unwrap() } +struct ExtractedMsgs { + certs: HashMap>, + votes: HashMap>, +} + struct SigVerifier { migration_status: Arc, banlist: Arc, @@ -96,11 +108,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 { @@ -128,25 +142,35 @@ 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(), } } 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 +183,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()); + self.maybe_prune_caches(&root_bank); - let ((cert_groups, votes_to_verify), extract_msgs_us) = - measure_us!(self.extract_and_filter_msgs(batches, &root_bank)); + let (extracted_msgs, extract_msgs_us) = + measure_us!(self.extract_and_filter_msgs(batches, certificates, &root_bank)); self.stats .extract_filter_msgs_us .add_sample(extract_msgs_us); @@ -172,7 +205,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, @@ -184,7 +217,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, @@ -201,21 +234,50 @@ 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( + &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>, - HashMap>, - ) { + ) -> ExtractedMsgs { let root_slot = root_bank.slot(); let mut cert_groups = HashMap::>::new(); let mut votes: HashMap> = HashMap::new(); @@ -248,21 +310,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) => { @@ -270,93 +325,132 @@ 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) + 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, + }) } } -/// 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 +485,7 @@ mod tests { }, solana_signer::Signer, solana_signer_store::encode_base2, + std::sync::RwLock, }; fn new_test_banlist() -> Arc { @@ -409,6 +504,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 +547,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 +564,7 @@ mod tests { }, SigVerifierChannels { packet_receiver, + certificate_receiver, channel_to_repair, channel_to_reward, channel_to_pool, @@ -482,6 +581,8 @@ mod tests { pool_receiver, _metrics_receiver: metrics_receiver, generated_cert_types, + _certificate_sender: certificate_sender, + _bank_forks: bank_forks, } } } @@ -522,6 +623,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 +655,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 extracted_msgs = + ctx.verifier + .extract_and_filter_msgs(vec![], vec![certificate], &root_bank); + assert!(extracted_msgs.certs.is_empty()); + assert!(extracted_msgs.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(); @@ -691,7 +863,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); @@ -1486,6 +1658,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 +1671,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, @@ -1935,7 +2109,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), 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/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); 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/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/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/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/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/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/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/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/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/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/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/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/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/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/core/src/replay_stage.rs b/core/src/replay_stage.rs index a83473825a9..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 { @@ -2453,6 +2444,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 +2465,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 +2495,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 +2520,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(()) @@ -3015,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; @@ -4243,7 +4237,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), @@ -5428,16 +5421,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..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, } } @@ -910,17 +908,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); @@ -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/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/core/src/tvu.rs b/core/src/tvu.rs index 9f2b18f0c7d..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, @@ -109,6 +108,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<()>, @@ -241,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, @@ -286,6 +286,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 +340,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, @@ -582,8 +585,6 @@ impl Tvu { tower, vote_tracker, cluster_slots, - log_messages_bytes_limit, - prioritization_fee_cache, banking_tracer, snapshot_controller, replay_highest_frozen, @@ -890,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 28838b45942..05eae8e8262 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", @@ -367,7 +368,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", @@ -3841,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", ] @@ -5033,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", @@ -5987,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" @@ -6272,6 +6285,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", @@ -7165,7 +7179,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -7843,6 +7857,7 @@ dependencies = [ "solana-transaction-context", "solana-transaction-error", "solana-transaction-status", + "solana-unified-scheduler-pool", "solana-validator-exit", "solana-version", "solana-vote", @@ -8625,6 +8640,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", @@ -8823,7 +8839,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "solana-clock", @@ -8971,7 +8987,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", @@ -9046,7 +9062,6 @@ dependencies = [ "scopeguard", "solana-clock", "solana-cost-model", - "solana-ledger", "solana-metrics", "solana-poh", "solana-pubkey 4.2.0", 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/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/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/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/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) } 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 = 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/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/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/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.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, diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 6db8b126d62..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; @@ -1602,7 +1390,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 +1402,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, @@ -1800,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(); @@ -1849,7 +1626,6 @@ fn process_bank_0( replay_tx_thread_pool, opts, &mut progress, - None, entry_notification_sender, None, &mut ExecuteTimings::default(), @@ -2484,7 +2260,6 @@ pub fn process_single_slot( replay_tx_thread_pool, opts, progress, - transaction_status_sender, entry_notification_sender, replay_vote_sender, timing, @@ -2603,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, }, @@ -2613,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, @@ -2620,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, @@ -2697,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()); @@ -2748,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] @@ -3433,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(())); } @@ -3555,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); @@ -3590,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); @@ -3646,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], ), @@ -3688,7 +3481,7 @@ pub mod tests { &bank.last_blockhash(), 1, vec![ - good_tx.clone(), + good_tx, system_transaction::transfer( &keypair4, &keypair4.pubkey(), @@ -3718,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; @@ -3744,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] @@ -3848,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()); @@ -3893,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(); @@ -4009,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, @@ -4072,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()), @@ -4121,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); @@ -4182,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(()) ); } @@ -4245,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(); @@ -4310,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); @@ -4324,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 { @@ -4433,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(()) ); @@ -4542,7 +4346,6 @@ pub mod tests { &mut ConfirmationProgress::new(bank0_last_blockhash), None, None, - None, &mut ExecuteTimings::default(), &MigrationStatus::default(), ) @@ -4645,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) @@ -4668,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![])) @@ -4791,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 { @@ -5083,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( @@ -5149,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(); @@ -5163,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, @@ -5185,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, @@ -5233,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] @@ -5326,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 { @@ -5389,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 { @@ -5409,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] @@ -5434,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); @@ -5450,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, @@ -5487,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; @@ -5702,10 +5540,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::default(), ) .unwrap_err(); @@ -5738,10 +5573,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::post_migration_status(), ) .unwrap(); @@ -5769,10 +5601,7 @@ pub mod tests { None, None, None, - None, false, - None, - None, &MigrationStatus::post_migration_status(), ); assert_matches!( 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/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)) } } 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/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/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 539ae9f5b39..a0879b3d9c8 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", @@ -851,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" @@ -3862,9 +3870,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", ] @@ -5054,9 +5062,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", @@ -6138,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" @@ -6423,6 +6441,7 @@ dependencies = [ "solana-time-utils", "solana-tls-utils", "solana-tpu-client", + "solana-tpu-client-next", "solana-transaction", "solana-transaction-error", "solana-udp-client", @@ -7351,7 +7370,7 @@ dependencies = [ "itertools 0.14.0", "lazy-lru", "log", - "lru 0.18.0", + "lru 0.18.1", "mockall", "num_cpus", "num_enum", @@ -7744,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", @@ -8527,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" @@ -9638,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", @@ -9894,7 +9924,7 @@ dependencies = [ "futures 0.3.32", "futures-util", "log", - "lru 0.18.0", + "lru 0.18.1", "quinn", "rustls", "solana-clock", @@ -10042,7 +10072,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", @@ -10117,7 +10147,6 @@ dependencies = [ "scopeguard", "solana-clock", "solana-cost-model", - "solana-ledger", "solana-metrics", "solana-poh", "solana-pubkey 4.2.0", 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/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/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 e6927478af2..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, @@ -479,12 +481,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, @@ -4540,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( @@ -4554,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(()) ); @@ -6978,11 +6987,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 +7280,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 +7305,7 @@ pub mod tests { SendTransactionService::new( bank_forks, receiver, - client, + tpu_sender, SendTransactionServiceConfig { retry_rate_ms: 1_000, leader_forward_count: 1, @@ -9015,10 +9026,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(); @@ -9077,7 +9084,7 @@ pub mod tests { OptimisticallyConfirmedBankTracker::process_notification( ( - BankNotification::OptimisticallyConfirmed(2, bank2_hash), + BankNotification::OptimisticallyConfirmed(2), None, /* no dependency work */ ), &bank_forks, @@ -9101,7 +9108,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, @@ -9125,7 +9132,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, @@ -9148,9 +9155,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_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/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-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/runtime/src/bank.rs b/runtime/src/bank.rs index 2470dcfccb0..2da7852b997 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(), + ); + } } } } @@ -4390,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 @@ -4818,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/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/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/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/tests.rs b/runtime/src/bank/tests.rs index aad6e5dcc65..47a6d4bc2fd 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() { @@ -4623,6 +4675,7 @@ fn test_check_ro_durable_nonce_fails() { &new_sanitized_message(tx.message().clone()), &bank.next_durable_nonce(), false, + false, ), None ); @@ -5862,12 +5915,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); @@ -5973,11 +6028,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()..) @@ -6101,7 +6158,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 { @@ -6114,7 +6171,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 { @@ -8604,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; @@ -11103,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); @@ -11517,11 +11583,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()..) @@ -11972,11 +12040,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()..) @@ -12472,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); @@ -12651,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/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(); 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/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/runtime/src/serde_snapshot/tests.rs b/runtime/src/serde_snapshot/tests.rs index 7a335eb9140..17f135357f8 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)?; @@ -215,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); @@ -417,7 +417,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())); 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/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, )?) }, ); 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, 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/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 diff --git a/snapshots/src/archive.rs b/snapshots/src/archive.rs index 2759bec5d13..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, @@ -12,8 +13,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, }, @@ -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, @@ -138,6 +143,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 +189,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()) @@ -203,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() @@ -215,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()); + } +} 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, 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); } 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(); 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/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, 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), ) 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]); - } -} 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] diff --git a/validator/src/commands/run/execute.rs b/validator/src/commands/run/execute.rs index afba484fe6e..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") { @@ -758,6 +760,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 +785,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 +842,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, 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:?}")); } }