diff --git a/Cargo.lock b/Cargo.lock index 30757f132ffe69..2b11aaaaf152a8 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7080,6 +7080,7 @@ dependencies = [ "solana-stake-program", "solana-svm", "solana-system-program", + "solana-transaction-status", "solana-version", "solana-vote", "solana-vote-program", diff --git a/ledger-tool/src/ledger_utils.rs b/ledger-tool/src/ledger_utils.rs index 31a91a0d161b48..44b6667f0516f5 100644 --- a/ledger-tool/src/ledger_utils.rs +++ b/ledger-tool/src/ledger_utils.rs @@ -100,6 +100,7 @@ pub fn load_and_process_ledger_or_exit( process_options: ProcessOptions, snapshot_archive_path: Option, incremental_snapshot_archive_path: Option, + transaction_status_sender: Option, ) -> (Arc>, Option) { load_and_process_ledger( arg_matches, @@ -108,6 +109,7 @@ pub fn load_and_process_ledger_or_exit( process_options, snapshot_archive_path, incremental_snapshot_archive_path, + transaction_status_sender, ) .unwrap_or_else(|err| { eprintln!("Exiting. Failed to load and process ledger: {err}"); @@ -122,6 +124,7 @@ pub fn load_and_process_ledger( process_options: ProcessOptions, snapshot_archive_path: Option, incremental_snapshot_archive_path: Option, + transaction_status_sender: Option, ) -> Result<(Arc>, Option), LoadAndProcessLedgerError> { let bank_snapshots_dir = if blockstore.is_primary_access() { blockstore.ledger_path().join("snapshot") @@ -387,7 +390,7 @@ pub fn load_and_process_ledger( Some(transaction_status_service), ) } else { - (None, None) + (transaction_status_sender, None) }; let result = blockstore_processor::process_blockstore_from_root( diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index 9bbaaddc8d9e0a..00255581f79052 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -40,12 +40,17 @@ use { solana_ledger::{ blockstore::{create_new_ledger, Blockstore}, blockstore_options::{AccessType, LedgerColumnOptions}, - blockstore_processor::ProcessSlotCallback, + blockstore_processor::{ + ProcessSlotCallback, TransactionStatusMessage, TransactionStatusSender, + }, use_snapshot_archives_at_startup, }, solana_measure::{measure, measure::Measure}, solana_runtime::{ - bank::{bank_hash_details, Bank, RewardCalculationEvent}, + bank::{ + bank_hash_details::{self, SlotDetails, TransactionDetails}, + Bank, RewardCalculationEvent, + }, bank_forks::BankForks, snapshot_archive_info::SnapshotArchiveInfoGetter, snapshot_bank_utils, @@ -73,6 +78,7 @@ use { transaction::{MessageHash, SanitizedTransaction, SimpleAddressLoader}, }, solana_stake_program::{points::PointValue, stake_state}, + solana_transaction_status::UiInstruction, solana_unified_scheduler_pool::DefaultSchedulerPool, solana_vote_program::{ self, @@ -83,6 +89,7 @@ use { ffi::OsStr, fs::File, io::{self, Write}, + mem::swap, path::{Path, PathBuf}, process::{exit, Command, Stdio}, str::FromStr, @@ -1067,10 +1074,15 @@ fn main() { .arg( Arg::with_name("record_slots_config") .long("record-slots-config") - .default_value("hash-only") - .possible_values(&["hash-only", "accounts"]) + .multiple(true) + .takes_value(true) + .possible_values(&["accounts", "tx"]) .requires("record_slots") - .help("In the slot recording, include bank details or not"), + .conflicts_with_all(&[ + "enable_rpc_transaction_history", + "geyser_plugin_config", + ]) + .help("In addition to the bank hash, optionally include accounts and/or transactions details for the slot"), ), ) .subcommand( @@ -1597,6 +1609,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); println!( @@ -1622,6 +1635,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); println!("{}", &bank_forks.read().unwrap().working_bank().hash()); } @@ -1654,6 +1668,9 @@ fn main() { exit(1); } + let mut transaction_status_sender = None; + let mut tx_receiver = None; + let (slot_callback, record_slots_file, recorded_slots) = if arg_matches .occurrences_of("record_slots") > 0 @@ -1665,29 +1682,61 @@ fn main() { exit(1); }); - let include_bank = - match arg_matches.value_of("record_slots_config").unwrap() { - "hash-only" => false, - "accounts" => true, - _ => unreachable!(), - }; + let mut include_bank = false; + let mut include_tx = false; + + if let Some(args) = arg_matches.values_of("record_slots_config") { + for arg in args { + match arg { + "tx" => include_tx = true, + "accounts" => include_bank = true, + _ => unreachable!(), + } + } + } let slot_hashes = Arc::new(Mutex::new(Vec::new())); + if include_tx { + let (sender, receiver) = crossbeam_channel::unbounded(); + + transaction_status_sender = Some(TransactionStatusSender { sender }); + + let slots = Arc::clone(&slot_hashes); + + tx_receiver = Some(std::thread::spawn(move || { + record_transactions(receiver, slots); + })); + } + let slot_callback = Arc::new({ let slots = Arc::clone(&slot_hashes); move |bank: &Bank| { - let slot_details = if include_bank { - bank_hash_details::BankHashSlotDetails::try_from(bank).unwrap() + let mut details = if include_bank { + bank_hash_details::SlotDetails::try_from(bank).unwrap() } else { - bank_hash_details::BankHashSlotDetails { + bank_hash_details::SlotDetails { slot: bank.slot(), bank_hash: bank.hash().to_string(), ..Default::default() } }; - slots.lock().unwrap().push(slot_details); + let mut slots = slots.lock().unwrap(); + + if let Some(recorded_slot) = + slots.iter_mut().find(|f| f.slot == details.slot) + { + // copy all fields except transactions + swap( + &mut recorded_slot.transactions, + &mut details.transactions, + ); + + *recorded_slot = details; + } else { + slots.push(details); + } } }); @@ -1722,7 +1771,7 @@ fn main() { bank.hash() ); } else { - let bank_hash_details::BankHashSlotDetails { + let bank_hash_details::SlotDetails { slot: expected_slot, bank_hash: expected_hash, .. @@ -1764,6 +1813,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + transaction_status_sender, ); if print_accounts_stats { @@ -1779,6 +1829,10 @@ fn main() { .ok(); } + if let Some(tx_receiver) = tx_receiver { + tx_receiver.join().unwrap(); + } + if let Some(recorded_slots_file) = record_slots_file { if let Ok(recorded_slots) = recorded_slots.clone().unwrap().lock() { let bank_hashes = @@ -1821,6 +1875,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); let dot = graph_forks(&bank_forks.read().unwrap(), &graph_config); @@ -1984,6 +2039,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); let mut bank = bank_forks .read() @@ -2373,6 +2429,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); let bank = bank_forks.read().unwrap().working_bank(); @@ -2425,6 +2482,7 @@ fn main() { process_options, snapshot_archive_path, incremental_snapshot_archive_path, + None, ); let bank_forks = bank_forks.read().unwrap(); let slot = bank_forks.working_bank().slot(); @@ -2947,3 +3005,64 @@ fn main() { measure_total_execution_time.stop(); info!("{}", measure_total_execution_time); } + +fn record_transactions( + recv: crossbeam_channel::Receiver, + slots: Arc>>, +) { + for tsm in recv { + if let TransactionStatusMessage::Batch(batch) = tsm { + let slot = batch.bank.slot(); + + assert_eq!(batch.transactions.len(), batch.execution_results.len()); + + let transactions: Vec<_> = batch + .transactions + .iter() + .zip(batch.execution_results) + .zip(batch.transaction_indexes) + .map(|((tx, execution_results), index)| { + let message = tx.message(); + + let accounts: Vec = message + .account_keys() + .iter() + .map(|acc| acc.to_string()) + .collect(); + + let instructions = message + .instructions() + .iter() + .map(|ix| UiInstruction::parse(ix, &message.account_keys(), None)) + .collect(); + + let is_simple_vote_tx = tx.is_simple_vote_transaction(); + + TransactionDetails { + accounts, + instructions, + is_simple_vote_tx, + execution_results, + index, + } + }) + .collect(); + + let mut slots = slots.lock().unwrap(); + + if let Some(recorded_slot) = slots.iter_mut().find(|f| f.slot == slot) { + recorded_slot.transactions.extend(transactions); + } else { + slots.push(SlotDetails { + slot, + transactions, + ..Default::default() + }); + } + } + } + + for slot in slots.lock().unwrap().iter_mut() { + slot.transactions.sort_by(|a, b| a.index.cmp(&b.index)); + } +} diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs index 78d5d5bfff6712..15750290fbed2a 100644 --- a/ledger-tool/src/program.rs +++ b/ledger-tool/src/program.rs @@ -93,6 +93,7 @@ fn load_blockstore(ledger_path: &Path, arg_matches: &ArgMatches<'_>) -> Arc, + pub bank_hash_details: Vec, } impl BankHashDetails { - pub fn new(bank_hash_details: Vec) -> Self { + pub fn new(bank_hash_details: Vec) -> Self { Self { version: solana_version::version!().to_string(), account_data_encoding: "base64".to_string(), @@ -65,9 +67,18 @@ impl BankHashDetails { } } +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] +pub struct TransactionDetails { + pub index: usize, + pub accounts: Vec, + pub instructions: Vec, + pub is_simple_vote_tx: bool, + pub execution_results: Option, +} + /// The components that go into a bank hash calculation for a single bank/slot. #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize, Default)] -pub struct BankHashSlotDetails { +pub struct SlotDetails { pub slot: Slot, pub bank_hash: String, #[serde(skip_serializing_if = "String::is_empty")] @@ -82,20 +93,23 @@ pub struct BankHashSlotDetails { #[serde(skip_serializing_if = "String::is_empty")] #[serde(default)] pub last_blockhash: String, - #[serde(skip_serializing_if = "bankhashaccounts_is_empty")] + #[serde(skip_serializing_if = "accounts_is_empty")] + #[serde(default)] + pub accounts: AccountsDetails, + #[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default)] - pub accounts: BankHashAccounts, + pub transactions: Vec, } fn u64_is_zero(val: &u64) -> bool { *val == 0 } -fn bankhashaccounts_is_empty(accounts: &BankHashAccounts) -> bool { +fn accounts_is_empty(accounts: &AccountsDetails) -> bool { accounts.accounts.is_empty() } -impl BankHashSlotDetails { +impl SlotDetails { pub fn new( slot: Slot, bank_hash: Hash, @@ -103,7 +117,7 @@ impl BankHashSlotDetails { accounts_delta_hash: Hash, signature_count: u64, last_blockhash: Hash, - accounts: BankHashAccounts, + accounts: AccountsDetails, ) -> Self { Self { slot, @@ -113,11 +127,12 @@ impl BankHashSlotDetails { signature_count, last_blockhash: last_blockhash.to_string(), accounts, + transactions: Vec::new(), } } } -impl TryFrom<&Bank> for BankHashSlotDetails { +impl TryFrom<&Bank> for SlotDetails { type Error = String; fn try_from(bank: &Bank) -> Result { @@ -152,7 +167,7 @@ impl TryFrom<&Bank> for BankHashSlotDetails { accounts_delta_hash, bank.signature_count(), bank.last_blockhash(), - BankHashAccounts { accounts }, + AccountsDetails { accounts }, )) } } @@ -160,7 +175,7 @@ impl TryFrom<&Bank> for BankHashSlotDetails { /// Wrapper around a Vec<_> to facilitate custom Serialize/Deserialize trait /// implementations. #[derive(Clone, Debug, Eq, PartialEq, Default)] -pub struct BankHashAccounts { +pub struct AccountsDetails { pub accounts: Vec, } @@ -221,7 +236,7 @@ impl TryFrom for PubkeyHashAccount { } } -impl Serialize for BankHashAccounts { +impl Serialize for AccountsDetails { fn serialize(&self, serializer: S) -> Result where S: Serializer, @@ -235,7 +250,7 @@ impl Serialize for BankHashAccounts { } } -impl<'de> Deserialize<'de> for BankHashAccounts { +impl<'de> Deserialize<'de> for AccountsDetails { fn deserialize(deserializer: D) -> Result where D: Deserializer<'de>, @@ -246,7 +261,7 @@ impl<'de> Deserialize<'de> for BankHashAccounts { .map(PubkeyHashAccount::try_from) .collect(); let pubkey_hash_accounts = pubkey_hash_accounts.map_err(de::Error::custom)?; - Ok(BankHashAccounts { + Ok(AccountsDetails { accounts: pubkey_hash_accounts, }) } @@ -254,7 +269,7 @@ impl<'de> Deserialize<'de> for BankHashAccounts { /// Output the components that comprise the overall bank hash for the supplied `Bank` pub fn write_bank_hash_details_file(bank: &Bank) -> std::result::Result<(), String> { - let slot_details = BankHashSlotDetails::try_from(bank)?; + let slot_details = SlotDetails::try_from(bank)?; let details = BankHashDetails::new(vec![slot_details]); let parent_dir = bank @@ -306,7 +321,7 @@ pub mod tests { }); let account_pubkey = Pubkey::new_unique(); let account_hash = AccountHash(hash("account".as_bytes())); - let accounts = BankHashAccounts { + let accounts = AccountsDetails { accounts: vec![PubkeyHashAccount { pubkey: account_pubkey, hash: account_hash, @@ -319,7 +334,7 @@ pub mod tests { let accounts_delta_hash = hash("accounts_delta".as_bytes()); let last_blockhash = hash("last_blockhash".as_bytes()); - BankHashSlotDetails::new( + SlotDetails::new( slot as Slot, bank_hash, parent_bank_hash, diff --git a/sdk/src/fee.rs b/sdk/src/fee.rs index 5edba38ee4f60f..e7bbaa15ebec93 100644 --- a/sdk/src/fee.rs +++ b/sdk/src/fee.rs @@ -31,7 +31,7 @@ pub struct FeeStructure { pub compute_fee_bins: Vec, } -#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)] +#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Deserialize, Serialize)] pub struct FeeDetails { transaction_fee: u64, prioritization_fee: u64, diff --git a/svm/src/transaction_results.rs b/svm/src/transaction_results.rs index 8cf40a39d3eb57..c02187028e860b 100644 --- a/svm/src/transaction_results.rs +++ b/svm/src/transaction_results.rs @@ -5,6 +5,7 @@ )] pub use solana_sdk::inner_instruction::{InnerInstruction, InnerInstructionsList}; use { + serde::{Deserialize, Serialize}, solana_program_runtime::loaded_programs::ProgramCacheForTxBatch, solana_sdk::{ fee::FeeDetails, @@ -76,7 +77,7 @@ impl TransactionExecutionResult { } } -#[derive(Debug, Clone)] +#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] pub struct TransactionExecutionDetails { pub status: transaction::Result<()>, pub log_messages: Option>, diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 4028ff14bb62bb..7779cfc5ae9353 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -142,7 +142,7 @@ pub enum UiInstruction { } impl UiInstruction { - fn parse( + pub fn parse( instruction: &CompiledInstruction, account_keys: &AccountKeys, stack_height: Option,