From 0eccacbd5b6b62b473e2eee8af2091a6495af0e6 Mon Sep 17 00:00:00 2001 From: Justin Starry Date: Thu, 17 Mar 2022 11:43:04 +0800 Subject: [PATCH] Add CLI support for versioned transactions (#23606) --- cli-output/src/cli_output.rs | 6 +- cli-output/src/display.rs | 332 +++++++++++++++++---- cli/src/cli.rs | 4 +- cli/src/cluster_query.rs | 3 +- cli/src/wallet.rs | 36 ++- ledger-tool/src/bigtable.rs | 50 ++-- ledger-tool/src/main.rs | 20 +- sdk/program/src/message/versions/mod.rs | 53 +++- sdk/program/src/message/versions/v0/mod.rs | 69 ++++- transaction-status/src/lib.rs | 203 +++++-------- 10 files changed, 538 insertions(+), 238 deletions(-) diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index c9e0310ca..046ec3a08 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -28,7 +28,7 @@ use { signature::Signature, stake::state::{Authorized, Lockup}, stake_history::StakeHistoryEntry, - transaction::{Transaction, TransactionError}, + transaction::{Transaction, TransactionError, VersionedTransaction}, }, solana_transaction_status::{ EncodedConfirmedBlock, EncodedTransaction, TransactionConfirmationStatus, @@ -2218,7 +2218,7 @@ pub enum CliSignatureVerificationStatus { } impl CliSignatureVerificationStatus { - pub fn verify_transaction(tx: &Transaction) -> Vec { + pub fn verify_transaction(tx: &VersionedTransaction) -> Vec { tx.verify_with_results() .iter() .zip(&tx.signatures) @@ -2354,7 +2354,7 @@ pub struct CliTransaction { #[serde(skip_serializing)] pub slot: Option, #[serde(skip_serializing)] - pub decoded_transaction: Transaction, + pub decoded_transaction: VersionedTransaction, #[serde(skip_serializing)] pub prefix: String, #[serde(skip_serializing_if = "Vec::is_empty")] diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index c7c222b68..52ae71ccc 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -7,12 +7,13 @@ use { clock::UnixTimestamp, hash::Hash, instruction::CompiledInstruction, + message::v0::MessageAddressTableLookup, native_token::lamports_to_sol, program_utils::limited_deserialize, pubkey::Pubkey, signature::Signature, stake, - transaction::{Transaction, TransactionError}, + transaction::{TransactionError, TransactionVersion, VersionedTransaction}, }, solana_transaction_status::{Rewards, UiTransactionStatusMeta}, spl_memo::{id as spl_memo_id, v1::id as spl_memo_v1_id}, @@ -171,7 +172,7 @@ fn format_account_mode(meta: CliAccountMeta) -> String { fn write_transaction( w: &mut W, - transaction: &Transaction, + transaction: &VersionedTransaction, transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, @@ -181,48 +182,65 @@ fn write_transaction( write_block_time(w, block_time, timezone, prefix)?; let message = &transaction.message; - write_recent_blockhash(w, &message.recent_blockhash, prefix)?; + let account_keys: Vec = { + let static_keys_iter = message + .static_account_keys() + .iter() + .map(AccountKeyType::Known); + let dynamic_keys: Vec = message + .address_table_lookups() + .map(transform_lookups_to_unknown_keys) + .unwrap_or_default(); + static_keys_iter.chain(dynamic_keys).collect() + }; + + write_version(w, transaction.version(), prefix)?; + write_recent_blockhash(w, message.recent_blockhash(), prefix)?; write_signatures(w, &transaction.signatures, sigverify_status, prefix)?; let mut fee_payer_index = None; - for (account_index, account) in message.account_keys.iter().enumerate() { + for (account_index, account) in account_keys.iter().enumerate() { if fee_payer_index.is_none() && message.is_non_loader_key(account_index) { fee_payer_index = Some(account_index) } let account_meta = CliAccountMeta { is_signer: message.is_signer(account_index), - is_writable: message.is_writable(account_index), - is_invoked: message.maybe_executable(account_index), + is_writable: message.is_maybe_writable(account_index), + is_invoked: message.is_invoked(account_index), }; write_account( w, account_index, - account, + *account, format_account_mode(account_meta), Some(account_index) == fee_payer_index, prefix, )?; } - for (instruction_index, instruction) in message.instructions.iter().enumerate() { - let program_pubkey = message.account_keys[instruction.program_id_index as usize]; - let instruction_accounts = instruction.accounts.iter().map(|account_index| { - let account_pubkey = &message.account_keys[*account_index as usize]; - (account_pubkey, *account_index) - }); + for (instruction_index, instruction) in message.instructions().iter().enumerate() { + let program_pubkey = account_keys[instruction.program_id_index as usize]; + let instruction_accounts = instruction + .accounts + .iter() + .map(|account_index| (account_keys[*account_index as usize], *account_index)); write_instruction( w, instruction_index, - &program_pubkey, + program_pubkey, instruction, instruction_accounts, prefix, )?; } + if let Some(address_table_lookups) = message.address_table_lookups() { + write_address_table_lookups(w, address_table_lookups, prefix)?; + } + if let Some(transaction_status) = transaction_status { write_status(w, &transaction_status.status, prefix)?; write_fees(w, transaction_status.fee, prefix)?; @@ -236,6 +254,36 @@ fn write_transaction( Ok(()) } +fn transform_lookups_to_unknown_keys(lookups: &[MessageAddressTableLookup]) -> Vec { + let unknown_writable_keys = lookups + .iter() + .enumerate() + .flat_map(|(lookup_index, lookup)| { + lookup + .writable_indexes + .iter() + .map(move |table_index| AccountKeyType::Unknown { + lookup_index, + table_index: *table_index, + }) + }); + + let unknown_readonly_keys = lookups + .iter() + .enumerate() + .flat_map(|(lookup_index, lookup)| { + lookup + .readonly_indexes + .iter() + .map(move |table_index| AccountKeyType::Unknown { + lookup_index, + table_index: *table_index, + }) + }); + + unknown_writable_keys.chain(unknown_readonly_keys).collect() +} + enum CliTimezone { Local, #[allow(dead_code)] @@ -258,6 +306,18 @@ fn write_block_time( Ok(()) } +fn write_version( + w: &mut W, + version: TransactionVersion, + prefix: &str, +) -> io::Result<()> { + let version = match version { + TransactionVersion::Legacy(_) => "legacy".to_string(), + TransactionVersion::Number(number) => number.to_string(), + }; + writeln!(w, "{}Version: {}", prefix, version) +} + fn write_recent_blockhash( w: &mut W, recent_blockhash: &Hash, @@ -292,10 +352,37 @@ fn write_signatures( Ok(()) } +#[derive(Debug, Clone, Copy, PartialEq)] +enum AccountKeyType<'a> { + Known(&'a Pubkey), + Unknown { + lookup_index: usize, + table_index: u8, + }, +} + +impl fmt::Display for AccountKeyType<'_> { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + Self::Known(address) => write!(f, "{}", address), + Self::Unknown { + lookup_index, + table_index, + } => { + write!( + f, + "Unknown Address (uses lookup {} and index {})", + lookup_index, table_index + ) + } + } + } +} + fn write_account( w: &mut W, account_index: usize, - account_address: &Pubkey, + account_address: AccountKeyType, account_mode: String, is_fee_payer: bool, prefix: &str, @@ -314,9 +401,9 @@ fn write_account( fn write_instruction<'a, W: io::Write>( w: &mut W, instruction_index: usize, - program_pubkey: &Pubkey, + program_pubkey: AccountKeyType, instruction: &CompiledInstruction, - instruction_accounts: impl Iterator, + instruction_accounts: impl Iterator, u8)>, prefix: &str, ) -> io::Result<()> { writeln!(w, "{}Instruction {}", prefix, instruction_index)?; @@ -334,33 +421,35 @@ fn write_instruction<'a, W: io::Write>( } let mut raw = true; - if program_pubkey == &solana_vote_program::id() { - if let Ok(vote_instruction) = limited_deserialize::< - solana_vote_program::vote_instruction::VoteInstruction, - >(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, vote_instruction)?; - raw = false; - } - } else if program_pubkey == &stake::program::id() { - if let Ok(stake_instruction) = - limited_deserialize::(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, stake_instruction)?; - raw = false; - } - } else if program_pubkey == &solana_sdk::system_program::id() { - if let Ok(system_instruction) = limited_deserialize::< - solana_sdk::system_instruction::SystemInstruction, - >(&instruction.data) - { - writeln!(w, "{} {:?}", prefix, system_instruction)?; - raw = false; - } - } else if is_memo_program(program_pubkey) { - if let Ok(s) = std::str::from_utf8(&instruction.data) { - writeln!(w, "{} Data: \"{}\"", prefix, s)?; - raw = false; + if let AccountKeyType::Known(program_pubkey) = program_pubkey { + if program_pubkey == &solana_vote_program::id() { + if let Ok(vote_instruction) = limited_deserialize::< + solana_vote_program::vote_instruction::VoteInstruction, + >(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, vote_instruction)?; + raw = false; + } + } else if program_pubkey == &stake::program::id() { + if let Ok(stake_instruction) = + limited_deserialize::(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, stake_instruction)?; + raw = false; + } + } else if program_pubkey == &solana_sdk::system_program::id() { + if let Ok(system_instruction) = limited_deserialize::< + solana_sdk::system_instruction::SystemInstruction, + >(&instruction.data) + { + writeln!(w, "{} {:?}", prefix, system_instruction)?; + raw = false; + } + } else if is_memo_program(program_pubkey) { + if let Ok(s) = std::str::from_utf8(&instruction.data) { + writeln!(w, "{} Data: \"{}\"", prefix, s)?; + raw = false; + } } } @@ -371,6 +460,30 @@ fn write_instruction<'a, W: io::Write>( Ok(()) } +fn write_address_table_lookups( + w: &mut W, + address_table_lookups: &[MessageAddressTableLookup], + prefix: &str, +) -> io::Result<()> { + for (lookup_index, lookup) in address_table_lookups.iter().enumerate() { + writeln!(w, "{}Address Table Lookup {}", prefix, lookup_index,)?; + writeln!(w, "{} Table Account: {}", prefix, lookup.account_key,)?; + writeln!( + w, + "{} Writable Indexes: {:?}", + prefix, + &lookup.writable_indexes[..], + )?; + writeln!( + w, + "{} Readonly Indexes: {:?}", + prefix, + &lookup.readonly_indexes[..], + )?; + } + Ok(()) +} + fn write_rewards( w: &mut W, rewards: Option<&Rewards>, @@ -480,7 +593,7 @@ fn write_log_messages( } pub fn println_transaction( - transaction: &Transaction, + transaction: &VersionedTransaction, transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, @@ -506,7 +619,7 @@ pub fn println_transaction( pub fn writeln_transaction( f: &mut dyn fmt::Write, - transaction: &Transaction, + transaction: &VersionedTransaction, transaction_status: Option<&UiTransactionStatusMeta>, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, @@ -552,26 +665,59 @@ mod test { use { super::*, solana_sdk::{ - message::{v0::LoadedAddresses, Message as LegacyMessage, MessageHeader}, + message::{ + v0::{self, LoadedAddresses}, + Message as LegacyMessage, MessageHeader, VersionedMessage, + }, pubkey::Pubkey, signature::{Keypair, Signer}, + transaction::Transaction, }, solana_transaction_status::{Reward, RewardType, TransactionStatusMeta}, std::io::BufWriter, }; - fn test_keypair() -> Keypair { + fn new_test_keypair() -> Keypair { let secret = ed25519_dalek::SecretKey::from_bytes(&[0u8; 32]).unwrap(); let public = ed25519_dalek::PublicKey::from(&secret); let keypair = ed25519_dalek::Keypair { secret, public }; Keypair::from_bytes(&keypair.to_bytes()).unwrap() } - #[test] - fn test_write_transaction() { - let keypair = test_keypair(); + fn new_test_v0_transaction() -> VersionedTransaction { + let keypair = new_test_keypair(); let account_key = Pubkey::new_from_array([1u8; 32]); - let transaction = Transaction::new( + let address_table_key = Pubkey::new_from_array([2u8; 32]); + VersionedTransaction::try_new( + VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + recent_blockhash: Hash::default(), + account_keys: vec![keypair.pubkey(), account_key], + address_table_lookups: vec![MessageAddressTableLookup { + account_key: address_table_key, + writable_indexes: vec![0], + readonly_indexes: vec![1], + }], + instructions: vec![CompiledInstruction::new_from_raw_parts( + 3, + vec![], + vec![1, 2], + )], + }), + &[&keypair], + ) + .unwrap() + } + + #[test] + fn test_write_legacy_transaction() { + let keypair = new_test_keypair(); + let account_key = Pubkey::new_from_array([1u8; 32]); + let transaction = VersionedTransaction::from(Transaction::new( &[&keypair], LegacyMessage { header: MessageHeader { @@ -584,7 +730,7 @@ mod test { instructions: vec![CompiledInstruction::new_from_raw_parts(1, vec![], vec![0])], }, Hash::default(), - ); + )); let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&transaction); let meta = TransactionStatusMeta { @@ -625,6 +771,7 @@ mod test { assert_eq!( output, r#"Block Time: 2021-08-10T22:16:31Z +Version: legacy Recent Blockhash: 11111111111111111111111111111111 Signature 0: 5pkjrE4VBa3Bu9CMKXgh1U345cT1gGo8QBVRTzHAo6gHeiPae5BTbShP15g6NgqRMNqu8Qrhph1ATmrfC1Ley3rx (pass) Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer) @@ -646,6 +793,85 @@ Rewards: ); } + #[test] + fn test_write_v0_transaction() { + let versioned_tx = new_test_v0_transaction(); + let sigverify_status = CliSignatureVerificationStatus::verify_transaction(&versioned_tx); + let address_table_entry1 = Pubkey::new_from_array([3u8; 32]); + let address_table_entry2 = Pubkey::new_from_array([4u8; 32]); + let loaded_addresses = LoadedAddresses { + writable: vec![address_table_entry1], + readonly: vec![address_table_entry2], + }; + let meta = TransactionStatusMeta { + status: Ok(()), + fee: 5000, + pre_balances: vec![5000, 10_000, 15_000, 20_000], + post_balances: vec![0, 10_000, 14_900, 20_000], + inner_instructions: None, + log_messages: Some(vec!["Test message".to_string()]), + pre_token_balances: None, + post_token_balances: None, + rewards: Some(vec![Reward { + pubkey: address_table_entry1.to_string(), + lamports: -100, + post_balance: 14_900, + reward_type: Some(RewardType::Rent), + commission: None, + }]), + loaded_addresses, + }; + + let output = { + let mut write_buffer = BufWriter::new(Vec::new()); + write_transaction( + &mut write_buffer, + &versioned_tx, + Some(&meta.into()), + "", + Some(&sigverify_status), + Some(1628633791), + CliTimezone::Utc, + ) + .unwrap(); + let bytes = write_buffer.into_inner().unwrap(); + String::from_utf8(bytes).unwrap() + }; + + assert_eq!( + output, + r#"Block Time: 2021-08-10T22:16:31Z +Version: 0 +Recent Blockhash: 11111111111111111111111111111111 +Signature 0: 5iEy3TT3ZhTA1NkuCY8GrQGNVY8d5m1bpjdh5FT3Ca4Py81fMipAZjafDuKJKrkw5q5UAAd8oPcgZ4nyXpHt4Fp7 (pass) +Account 0: srw- 4zvwRjXUKGfvwnParsHAS3HuSVzV5cA4McphgmoCtajS (fee payer) +Account 1: -r-- 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi +Account 2: -rw- Unknown Address (uses lookup 0 and index 0) +Account 3: -r-x Unknown Address (uses lookup 0 and index 1) +Instruction 0 + Program: Unknown Address (uses lookup 0 and index 1) (3) + Account 0: 4vJ9JU1bJJE96FWSJKvHsmmFADCg4gpZQff4P3bkLKi (1) + Account 1: Unknown Address (uses lookup 0 and index 0) (2) + Data: [] +Address Table Lookup 0 + Table Account: 8qbHbw2BbbTHBW1sbeqakYXVKRQM8Ne7pLK7m6CVfeR + Writable Indexes: [0] + Readonly Indexes: [1] +Status: Ok + Fee: ◎0.000005 + Account 0 balance: ◎0.000005 -> ◎0 + Account 1 balance: ◎0.00001 + Account 2 balance: ◎0.000015 -> ◎0.0000149 + Account 3 balance: ◎0.00002 +Log Messages: + Test message +Rewards: + Address Type Amount New Balance \0 + CktRuQ2mttgRGkXJtyksdKHjUdc2C4TgDzyB98oEzy8 rent -◎0.000000100 ◎0.000014900 \0 +"#.replace("\\0", "") // replace marker used to subvert trailing whitespace linter on CI + ); + } + #[test] fn test_format_labeled_address() { let pubkey = Pubkey::default().to_string(); diff --git a/cli/src/cli.rs b/cli/src/cli.rs index 6890de9cf..ac5aae1e1 100644 --- a/cli/src/cli.rs +++ b/cli/src/cli.rs @@ -30,7 +30,7 @@ use { pubkey::Pubkey, signature::{Signature, Signer, SignerError}, stake::{instruction::LockupArgs, state::Lockup}, - transaction::{Transaction, TransactionError}, + transaction::{TransactionError, VersionedTransaction}, }, solana_vote_program::vote_state::VoteAuthorize, std::{collections::HashMap, error, io::stdout, str::FromStr, sync::Arc, time::Duration}, @@ -385,7 +385,7 @@ pub enum CliCommand { seed: String, program_id: Pubkey, }, - DecodeTransaction(Transaction), + DecodeTransaction(VersionedTransaction), ResolveSigner(Option), ShowAccount { pubkey: Pubkey, diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 24aa2442e..8835868a3 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -1058,6 +1058,7 @@ pub fn process_get_block( RpcBlockConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), + max_supported_transaction_version: Some(0), ..RpcBlockConfig::default() }, )? @@ -2042,7 +2043,7 @@ pub fn process_transaction_history( RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), - max_supported_transaction_version: None, + max_supported_transaction_version: Some(0), }, ) { Ok(confirmed_transaction) => { diff --git a/cli/src/wallet.rs b/cli/src/wallet.rs index 35002cede..8d50b8ce5 100644 --- a/cli/src/wallet.rs +++ b/cli/src/wallet.rs @@ -37,10 +37,11 @@ use { stake, system_instruction::{self, SystemError}, system_program, - transaction::Transaction, + transaction::{Transaction, VersionedTransaction}, }, solana_transaction_status::{ - Encodable, EncodedTransaction, TransactionBinaryEncoding, UiTransactionEncoding, + EncodableWithMeta, EncodedConfirmedTransactionWithStatusMeta, EncodedTransaction, + TransactionBinaryEncoding, UiTransactionEncoding, }, std::{fmt::Write as FmtWrite, fs::File, io::Write, sync::Arc}, }; @@ -561,23 +562,25 @@ pub fn process_confirm( RpcTransactionConfig { encoding: Some(UiTransactionEncoding::Base64), commitment: Some(CommitmentConfig::confirmed()), - max_supported_transaction_version: None, + max_supported_transaction_version: Some(0), }, ) { Ok(confirmed_transaction) => { - let decoded_transaction = confirmed_transaction - .transaction - .transaction - .decode() - .expect("Successful decode"); - let json_transaction = - decoded_transaction.encode(UiTransactionEncoding::Json); + let EncodedConfirmedTransactionWithStatusMeta { + block_time, + slot, + transaction: transaction_with_meta, + } = confirmed_transaction; + + let decoded_transaction = + transaction_with_meta.transaction.decode().unwrap(); + let json_transaction = decoded_transaction.json_encode(); transaction = Some(CliTransaction { transaction: json_transaction, - meta: confirmed_transaction.transaction.meta, - block_time: confirmed_transaction.block_time, - slot: Some(confirmed_transaction.slot), + meta: transaction_with_meta.meta, + block_time, + slot: Some(slot), decoded_transaction, prefix: " ".to_string(), sigverify_status: vec![], @@ -609,11 +612,14 @@ pub fn process_confirm( } #[allow(clippy::unnecessary_wraps)] -pub fn process_decode_transaction(config: &CliConfig, transaction: &Transaction) -> ProcessResult { +pub fn process_decode_transaction( + config: &CliConfig, + transaction: &VersionedTransaction, +) -> ProcessResult { let sigverify_status = CliSignatureVerificationStatus::verify_transaction(transaction); let decode_transaction = CliTransaction { decoded_transaction: transaction.clone(), - transaction: transaction.encode(UiTransactionEncoding::Json), + transaction: transaction.json_encode(), meta: None, block_time: None, slot: None, diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index 53a204548..e1061242a 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -1,4 +1,4 @@ -/// The `bigtable` subcommand +//! The `bigtable` subcommand use { crate::ledger_path::canonicalize_ledger_path, clap::{ @@ -17,7 +17,7 @@ use { solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}, solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}, solana_transaction_status::{ - BlockEncodingOptions, Encodable, EncodeError, LegacyConfirmedBlock, TransactionDetails, + BlockEncodingOptions, ConfirmedBlock, EncodeError, TransactionDetails, UiTransactionEncoding, }, std::{ @@ -172,19 +172,17 @@ async fn confirm( if verbose { match bigtable.get_confirmed_transaction(signature).await { Ok(Some(confirmed_tx)) => { - let legacy_confirmed_tx = confirmed_tx - .into_legacy_confirmed_transaction() - .ok_or_else(|| "Failed to read versioned transaction in block".to_string())?; - + let decoded_tx = confirmed_tx.get_transaction(); + let encoded_tx_with_meta = confirmed_tx + .tx_with_meta + .encode(UiTransactionEncoding::Json, Some(0)) + .map_err(|_| "Failed to encode transaction in block".to_string())?; transaction = Some(CliTransaction { - transaction: legacy_confirmed_tx - .tx_with_meta - .transaction - .encode(UiTransactionEncoding::Json), - meta: legacy_confirmed_tx.tx_with_meta.meta.map(|m| m.into()), - block_time: legacy_confirmed_tx.block_time, - slot: Some(legacy_confirmed_tx.slot), - decoded_transaction: legacy_confirmed_tx.tx_with_meta.transaction, + transaction: encoded_tx_with_meta.transaction, + meta: encoded_tx_with_meta.meta, + block_time: confirmed_tx.block_time, + slot: Some(confirmed_tx.slot), + decoded_transaction: decoded_tx, prefix: " ".to_string(), sigverify_status: vec![], }); @@ -216,7 +214,7 @@ pub async fn transaction_history( ) -> Result<(), Box> { let bigtable = solana_storage_bigtable::LedgerStorage::new(true, None, None).await?; - let mut loaded_block: Option<(Slot, LegacyConfirmedBlock)> = None; + let mut loaded_block: Option<(Slot, ConfirmedBlock)> = None; while limit > 0 { let results = bigtable .get_confirmed_signatures_for_address( @@ -257,21 +255,22 @@ pub async fn transaction_history( loop { if let Some((slot, block)) = &loaded_block { if *slot == result.slot { - match block.transactions.get(index as usize) { + match block.transactions.get(index as usize).map(|tx_with_meta| { + ( + tx_with_meta.get_transaction(), + tx_with_meta.get_status_meta(), + ) + }) { None => { println!( " Transaction info for {} is corrupt", result.signature ); } - Some(transaction_with_meta) => { + Some((transaction, meta)) => { println_transaction( - &transaction_with_meta.transaction, - transaction_with_meta - .meta - .clone() - .map(|m| m.into()) - .as_ref(), + &transaction, + meta.map(|m| m.into()).as_ref(), " ", None, None, @@ -287,10 +286,7 @@ pub async fn transaction_history( break; } Ok(confirmed_block) => { - let block = confirmed_block.into_legacy_block().ok_or_else(|| { - "Failed to read versioned transaction in block".to_string() - })?; - loaded_block = Some((result.slot, block)); + loaded_block = Some((result.slot, confirmed_block)); } } } diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index cff88d8e5..beaf74d7c 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -1,5 +1,6 @@ #![allow(clippy::integer_arithmetic)] use { + crate::{bigtable::*, ledger_path::*}, clap::{ crate_description, crate_name, value_t, value_t_or_exit, values_t_or_exit, App, AppSettings, Arg, ArgMatches, SubCommand, @@ -63,6 +64,7 @@ use { transaction::{MessageHash, SanitizedTransaction, SimpleAddressLoader}, }, solana_stake_program::stake_state::{self, PointValue}, + solana_transaction_status::VersionedTransactionWithStatusMeta, solana_vote_program::{ self, vote_state::{self, VoteState}, @@ -83,9 +85,7 @@ use { }; mod bigtable; -use bigtable::*; mod ledger_path; -use ledger_path::*; #[derive(PartialEq)] enum LedgerOutputMethod { @@ -147,7 +147,7 @@ fn output_entry( for (transactions_index, transaction) in entry.transactions.into_iter().enumerate() { println!(" Transaction {}", transactions_index); let tx_signature = transaction.signatures[0]; - let tx_status = blockstore + let tx_with_meta = blockstore .read_transaction_status((tx_signature, slot)) .unwrap_or_else(|err| { eprintln!( @@ -156,21 +156,17 @@ fn output_entry( ); None }) - .map(|transaction_status| transaction_status.into()); + .map(|meta| VersionedTransactionWithStatusMeta { transaction, meta }); - if let Some(legacy_tx) = transaction.into_legacy_transaction() { + if let Some(tx_with_meta) = tx_with_meta { + let status = tx_with_meta.meta.into(); solana_cli_output::display::println_transaction( - &legacy_tx, - tx_status.as_ref(), + &tx_with_meta.transaction, + Some(&status), " ", None, None, ); - } else { - eprintln!( - "Failed to print unsupported transaction for {} at slot {}", - tx_signature, slot - ); } } } diff --git a/sdk/program/src/message/versions/mod.rs b/sdk/program/src/message/versions/mod.rs index 12807863e..28cd284b7 100644 --- a/sdk/program/src/message/versions/mod.rs +++ b/sdk/program/src/message/versions/mod.rs @@ -2,7 +2,7 @@ use { crate::{ hash::Hash, instruction::CompiledInstruction, - message::{legacy::Message as LegacyMessage, MessageHeader}, + message::{legacy::Message as LegacyMessage, v0::MessageAddressTableLookup, MessageHeader}, pubkey::Pubkey, sanitize::{Sanitize, SanitizeError}, short_vec, @@ -50,6 +50,55 @@ impl VersionedMessage { } } + pub fn address_table_lookups(&self) -> Option<&[MessageAddressTableLookup]> { + match self { + Self::Legacy(_) => None, + Self::V0(message) => Some(&message.address_table_lookups), + } + } + + /// Returns true if the account at the specified index signed this + /// message. + pub fn is_signer(&self, index: usize) -> bool { + index < usize::from(self.header().num_required_signatures) + } + + /// Returns true if the account at the specified index is writable by the + /// instructions in this message. Since dynamically loaded addresses can't + /// have write locks demoted without loading addresses, this shouldn't be + /// used in the runtime. + pub fn is_maybe_writable(&self, index: usize) -> bool { + match self { + Self::Legacy(message) => message.is_writable(index), + Self::V0(message) => message.is_maybe_writable(index), + } + } + + /// Returns true if the account at the specified index is an input to some + /// program instruction in this message. + fn is_key_passed_to_program(&self, key_index: usize) -> bool { + if let Ok(key_index) = u8::try_from(key_index) { + self.instructions() + .iter() + .any(|ix| ix.accounts.contains(&key_index)) + } else { + false + } + } + + pub fn is_invoked(&self, key_index: usize) -> bool { + match self { + Self::Legacy(message) => message.is_key_called_as_program(key_index), + Self::V0(message) => message.is_key_called_as_program(key_index), + } + } + + /// Returns true if the account at the specified index is not invoked as a + /// program or, if invoked, is passed to a program. + pub fn is_non_loader_key(&self, key_index: usize) -> bool { + !self.is_invoked(key_index) || self.is_key_passed_to_program(key_index) + } + pub fn recent_blockhash(&self) -> &Hash { match self { Self::Legacy(message) => &message.recent_blockhash, @@ -64,6 +113,8 @@ impl VersionedMessage { } } + /// Program instructions that will be executed in sequence and committed in + /// one atomic transaction if all succeed. pub fn instructions(&self) -> &[CompiledInstruction] { match self { Self::Legacy(message) => &message.instructions, diff --git a/sdk/program/src/message/versions/v0/mod.rs b/sdk/program/src/message/versions/v0/mod.rs index 3a03e1ea7..85ed791ae 100644 --- a/sdk/program/src/message/versions/v0/mod.rs +++ b/sdk/program/src/message/versions/v0/mod.rs @@ -10,12 +10,13 @@ //! [future message format]: https://docs.solana.com/proposals/transactions-v2 use crate::{ + bpf_loader_upgradeable, hash::Hash, instruction::CompiledInstruction, - message::{MessageHeader, MESSAGE_VERSION_PREFIX}, + message::{legacy::BUILTIN_PROGRAMS_KEYS, MessageHeader, MESSAGE_VERSION_PREFIX}, pubkey::Pubkey, sanitize::{Sanitize, SanitizeError}, - short_vec, + short_vec, sysvar, }; mod loaded; @@ -138,6 +139,70 @@ impl Message { pub fn serialize(&self) -> Vec { bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap() } + + /// Returns true if the account at the specified index is called as a program by an instruction + pub fn is_key_called_as_program(&self, key_index: usize) -> bool { + if let Ok(key_index) = u8::try_from(key_index) { + self.instructions + .iter() + .any(|ix| ix.program_id_index == key_index) + } else { + false + } + } + + /// Returns true if the account at the specified index was requested to be + /// writable. This method should not be used directly. + fn is_writable_index(&self, key_index: usize) -> bool { + let header = &self.header; + let num_account_keys = self.account_keys.len(); + let num_signed_accounts = usize::from(header.num_required_signatures); + if key_index >= num_account_keys { + let loaded_addresses_index = key_index.saturating_sub(num_account_keys); + let num_writable_dynamic_addresses = self + .address_table_lookups + .iter() + .map(|lookup| lookup.writable_indexes.len()) + .sum(); + loaded_addresses_index < num_writable_dynamic_addresses + } else if key_index >= num_signed_accounts { + let num_unsigned_accounts = num_account_keys.saturating_sub(num_signed_accounts); + let num_writable_unsigned_accounts = num_unsigned_accounts + .saturating_sub(usize::from(header.num_readonly_unsigned_accounts)); + let unsigned_account_index = key_index.saturating_sub(num_signed_accounts); + unsigned_account_index < num_writable_unsigned_accounts + } else { + let num_writable_signed_accounts = num_signed_accounts + .saturating_sub(usize::from(header.num_readonly_signed_accounts)); + key_index < num_writable_signed_accounts + } + } + + /// Returns true if any static account key is the bpf upgradeable loader + fn is_upgradeable_loader_in_static_keys(&self) -> bool { + self.account_keys + .iter() + .any(|&key| key == bpf_loader_upgradeable::id()) + } + + /// Returns true if the account at the specified index was requested as writable. + /// Before loading addresses, we can't demote write locks for dynamically loaded + /// addresses so this should not be used by the runtime. + pub fn is_maybe_writable(&self, key_index: usize) -> bool { + self.is_writable_index(key_index) + && !{ + // demote reserved ids + self.account_keys + .get(key_index) + .map(|key| sysvar::is_sysvar_id(key) || BUILTIN_PROGRAMS_KEYS.contains(key)) + .unwrap_or_default() + } + && !{ + // demote program ids + self.is_key_called_as_program(key_index) + && !self.is_upgradeable_loader_in_static_keys() + } + } } #[cfg(test)] diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index b539da3cd..24fcf3077 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -1,19 +1,4 @@ #![allow(clippy::integer_arithmetic)] -#[macro_use] -extern crate lazy_static; -#[macro_use] -extern crate serde_derive; - -pub mod extract_memos; -pub mod parse_accounts; -pub mod parse_associated_token; -pub mod parse_bpf_loader; -pub mod parse_instruction; -pub mod parse_stake; -pub mod parse_system; -pub mod parse_token; -pub mod parse_vote; -pub mod token_balances; pub use {crate::extract_memos::extract_and_fmt_memos, solana_runtime::bank::RewardType}; use { @@ -42,6 +27,22 @@ use { thiserror::Error, }; +#[macro_use] +extern crate lazy_static; +#[macro_use] +extern crate serde_derive; + +pub mod extract_memos; +pub mod parse_accounts; +pub mod parse_associated_token; +pub mod parse_bpf_loader; +pub mod parse_instruction; +pub mod parse_stake; +pub mod parse_system; +pub mod parse_token; +pub mod parse_vote; +pub mod token_balances; + pub struct BlockEncodingOptions { pub transaction_details: TransactionDetails, pub show_rewards: bool, @@ -68,6 +69,7 @@ pub trait EncodableWithMeta { encoding: UiTransactionEncoding, meta: &TransactionStatusMeta, ) -> Self::Encoded; + fn json_encode(&self) -> Self::Encoded; } #[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)] @@ -488,38 +490,6 @@ pub struct VersionedConfirmedBlock { pub block_height: Option, } -// Confirmed block which only supports legacy transactions. Used -// until migration to versioned transactions is completed. -pub struct LegacyConfirmedBlock { - pub previous_blockhash: String, - pub blockhash: String, - pub parent_slot: Slot, - pub transactions: Vec, - pub rewards: Rewards, - pub block_time: Option, - pub block_height: Option, -} - -impl ConfirmedBlock { - /// Downgrades a versioned block into a legacy block type - /// if it only contains legacy transactions - pub fn into_legacy_block(self) -> Option { - Some(LegacyConfirmedBlock { - previous_blockhash: self.previous_blockhash, - blockhash: self.blockhash, - parent_slot: self.parent_slot, - transactions: self - .transactions - .into_iter() - .map(|tx_with_meta| tx_with_meta.into_legacy_transaction_with_meta()) - .collect::>>()?, - rewards: self.rewards, - block_time: self.block_time, - block_height: self.block_height, - }) - } -} - impl From for ConfirmedBlock { fn from(block: VersionedConfirmedBlock) -> Self { Self { @@ -641,12 +611,21 @@ pub struct VersionedTransactionWithStatusMeta { pub meta: TransactionStatusMeta, } -pub struct LegacyTransactionWithStatusMeta { - pub transaction: Transaction, - pub meta: Option, -} - impl TransactionWithStatusMeta { + pub fn get_status_meta(&self) -> Option { + match self { + Self::MissingMetadata(_) => None, + Self::Complete(tx_with_meta) => Some(tx_with_meta.meta.clone()), + } + } + + pub fn get_transaction(&self) -> VersionedTransaction { + match self { + Self::MissingMetadata(transaction) => VersionedTransaction::from(transaction.clone()), + Self::Complete(tx_with_meta) => tx_with_meta.transaction.clone(), + } + } + pub fn transaction_signature(&self) -> &Signature { match self { Self::MissingMetadata(transaction) => &transaction.signatures[0], @@ -673,20 +652,6 @@ impl TransactionWithStatusMeta { } } - pub fn into_legacy_transaction_with_meta(self) -> Option { - match self { - TransactionWithStatusMeta::MissingMetadata(transaction) => { - Some(LegacyTransactionWithStatusMeta { - transaction, - meta: None, - }) - } - TransactionWithStatusMeta::Complete(tx_with_meta) => { - tx_with_meta.into_legacy_transaction_with_meta() - } - } - } - pub fn account_keys(&self) -> AccountKeys { match self { Self::MissingMetadata(tx) => AccountKeys::new(&tx.message.account_keys, None), @@ -739,13 +704,6 @@ impl VersionedTransactionWithStatusMeta { Some(&self.meta.loaded_addresses), ) } - - fn into_legacy_transaction_with_meta(self) -> Option { - Some(LegacyTransactionWithStatusMeta { - transaction: self.transaction.into_legacy_transaction()?, - meta: Some(self.meta), - }) - } } #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] @@ -771,25 +729,7 @@ pub struct VersionedConfirmedTransactionWithStatusMeta { pub block_time: Option, } -pub struct LegacyConfirmedTransactionWithStatusMeta { - pub slot: Slot, - pub tx_with_meta: LegacyTransactionWithStatusMeta, - pub block_time: Option, -} - impl ConfirmedTransactionWithStatusMeta { - /// Downgrades a versioned confirmed transaction into a legacy - /// confirmed transaction if it contains a legacy transaction. - pub fn into_legacy_confirmed_transaction( - self, - ) -> Option { - Some(LegacyConfirmedTransactionWithStatusMeta { - tx_with_meta: self.tx_with_meta.into_legacy_transaction_with_meta()?, - block_time: self.block_time, - slot: self.slot, - }) - } - pub fn encode( self, encoding: UiTransactionEncoding, @@ -803,6 +743,10 @@ impl ConfirmedTransactionWithStatusMeta { block_time: self.block_time, }) } + + pub fn get_transaction(&self) -> VersionedTransaction { + self.tx_with_meta.get_transaction() + } } #[derive(Debug, PartialEq, Serialize, Deserialize)] @@ -841,17 +785,29 @@ impl EncodableWithMeta for VersionedTransaction { base64::encode(bincode::serialize(self).unwrap()), TransactionBinaryEncoding::Base64, ), - UiTransactionEncoding::Json | UiTransactionEncoding::JsonParsed => { - EncodedTransaction::Json(UiTransaction { - signatures: self.signatures.iter().map(ToString::to_string).collect(), - message: match &self.message { - VersionedMessage::Legacy(message) => message.encode(encoding), - VersionedMessage::V0(message) => message.encode_with_meta(encoding, meta), - }, - }) - } + UiTransactionEncoding::Json => self.json_encode(), + UiTransactionEncoding::JsonParsed => EncodedTransaction::Json(UiTransaction { + signatures: self.signatures.iter().map(ToString::to_string).collect(), + message: match &self.message { + VersionedMessage::Legacy(message) => { + message.encode(UiTransactionEncoding::JsonParsed) + } + VersionedMessage::V0(message) => { + message.encode_with_meta(UiTransactionEncoding::JsonParsed, meta) + } + }, + }), } } + fn json_encode(&self) -> Self::Encoded { + EncodedTransaction::Json(UiTransaction { + signatures: self.signatures.iter().map(ToString::to_string).collect(), + message: match &self.message { + VersionedMessage::Legacy(message) => message.encode(UiTransactionEncoding::Json), + VersionedMessage::V0(message) => message.json_encode(), + }, + }) + } } impl Encodable for Transaction { @@ -880,23 +836,23 @@ impl Encodable for Transaction { } impl EncodedTransaction { - pub fn decode(&self) -> Option { - let transaction: Option = match self { - EncodedTransaction::Json(_) => None, - EncodedTransaction::LegacyBinary(blob) => bs58::decode(blob) + pub fn decode(&self) -> Option { + let (blob, encoding) = match self { + Self::Json(_) => return None, + Self::LegacyBinary(blob) => (blob, TransactionBinaryEncoding::Base58), + Self::Binary(blob, encoding) => (blob, *encoding), + }; + + let transaction: Option = match encoding { + TransactionBinaryEncoding::Base58 => bs58::decode(blob) .into_vec() .ok() .and_then(|bytes| bincode::deserialize(&bytes).ok()), - EncodedTransaction::Binary(blob, encoding) => match *encoding { - TransactionBinaryEncoding::Base58 => bs58::decode(blob) - .into_vec() - .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), - TransactionBinaryEncoding::Base64 => base64::decode(blob) - .ok() - .and_then(|bytes| bincode::deserialize(&bytes).ok()), - }, + TransactionBinaryEncoding::Base64 => base64::decode(blob) + .ok() + .and_then(|bytes| bincode::deserialize(&bytes).ok()), }; + transaction.filter(|transaction| transaction.sanitize().is_ok()) } } @@ -966,17 +922,20 @@ impl EncodableWithMeta for v0::Message { ), }) } else { - UiMessage::Raw(UiRawMessage { - header: self.header, - account_keys: self.account_keys.iter().map(ToString::to_string).collect(), - recent_blockhash: self.recent_blockhash.to_string(), - instructions: self.instructions.iter().map(Into::into).collect(), - address_table_lookups: Some( - self.address_table_lookups.iter().map(Into::into).collect(), - ), - }) + self.json_encode() } } + fn json_encode(&self) -> Self::Encoded { + UiMessage::Raw(UiRawMessage { + header: self.header, + account_keys: self.account_keys.iter().map(ToString::to_string).collect(), + recent_blockhash: self.recent_blockhash.to_string(), + instructions: self.instructions.iter().map(Into::into).collect(), + address_table_lookups: Some( + self.address_table_lookups.iter().map(Into::into).collect(), + ), + }) + } } /// A duplicate representation of a Message, in raw format, for pretty JSON serialization