diff --git a/Cargo.lock b/Cargo.lock index a41ce70c2ae6bc..84336c91b81118 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4352,6 +4352,22 @@ dependencies = [ "spl-token", ] +[[package]] +name = "solana-address-map-program" +version = "1.8.0" +dependencies = [ + "bincode", + "log 0.4.14", + "num-derive", + "num-traits", + "serde", + "serde_derive", + "solana-frozen-abi 1.8.0", + "solana-frozen-abi-macro 1.8.0", + "solana-sdk", + "thiserror", +] + [[package]] name = "solana-banking-bench" version = "1.8.0" @@ -5598,6 +5614,7 @@ dependencies = [ "rustc_version 0.4.0", "serde", "serde_derive", + "solana-address-map-program", "solana-compute-budget-program", "solana-config-program", "solana-frozen-abi 1.8.0", diff --git a/Cargo.toml b/Cargo.toml index 8abf20d5754513..b4eb97beb43b66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ members = [ "poh", "poh-bench", "program-test", + "programs/address_map", "programs/bpf_loader", "programs/compute-budget", "programs/config", diff --git a/cli-output/src/cli_output.rs b/cli-output/src/cli_output.rs index 31d26c664a330c..66b222b3c87d92 100644 --- a/cli-output/src/cli_output.rs +++ b/cli-output/src/cli_output.rs @@ -2262,7 +2262,7 @@ impl fmt::Display for CliBlock { writeln!(f, "Transaction {}:", index)?; writeln_transaction( f, - &transaction_with_meta.transaction.decode().unwrap(), + transaction_with_meta.transaction.decode().unwrap(), &transaction_with_meta.meta, " ", None, @@ -2296,7 +2296,7 @@ impl fmt::Display for CliTransaction { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { writeln_transaction( f, - &self.decoded_transaction, + self.decoded_transaction.clone(), &self.meta, &self.prefix, if !self.sigverify_status.is_empty() { diff --git a/cli-output/src/display.rs b/cli-output/src/display.rs index 3d1c3e74fcb494..72090a0ace5ede 100644 --- a/cli-output/src/display.rs +++ b/cli-output/src/display.rs @@ -4,13 +4,19 @@ use { console::style, indicatif::{ProgressBar, ProgressStyle}, solana_sdk::{ - clock::UnixTimestamp, hash::Hash, message::Message, native_token::lamports_to_sol, - program_utils::limited_deserialize, pubkey::Pubkey, stake, transaction::Transaction, + clock::UnixTimestamp, + hash::Hash, + message::SanitizedMessage, + native_token::lamports_to_sol, + program_utils::limited_deserialize, + pubkey::Pubkey, + stake, + transaction::{SanitizedTransaction, Transaction}, }, solana_transaction_status::UiTransactionStatusMeta, spl_memo::id as spl_memo_id, spl_memo::v1::id as spl_memo_v1_id, - std::{collections::HashMap, fmt, io}, + std::{collections::HashMap, convert::TryFrom, fmt, io}, }; #[derive(Clone, Debug)] @@ -132,7 +138,7 @@ pub fn println_signers( println!(); } -fn format_account_mode(message: &Message, index: usize) -> String { +fn format_account_mode(message: &SanitizedMessage, index: usize) -> String { format!( "{}r{}{}", // accounts are always readable... if message.is_signer(index) { @@ -147,7 +153,7 @@ fn format_account_mode(message: &Message, index: usize) -> String { }, // account may be executable on-chain while not being // designated as a program-id in the message - if message.maybe_executable(index) { + if message.is_invoked(index) { "x" } else { // programs to be executed via CPI cannot be identified as @@ -159,13 +165,13 @@ fn format_account_mode(message: &Message, index: usize) -> String { pub fn write_transaction( w: &mut W, - transaction: &Transaction, + transaction: &SanitizedTransaction, transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, block_time: Option, ) -> io::Result<()> { - let message = &transaction.message; + let message = transaction.message(); if let Some(block_time) = block_time { writeln!( w, @@ -177,7 +183,8 @@ pub fn write_transaction( writeln!( w, "{}Recent Blockhash: {:?}", - prefix, message.recent_blockhash + prefix, + message.recent_blockhash() )?; let sigverify_statuses = if let Some(sigverify_status) = sigverify_status { sigverify_status @@ -185,10 +192,10 @@ pub fn write_transaction( .map(|s| format!(" ({})", s)) .collect() } else { - vec!["".to_string(); transaction.signatures.len()] + vec!["".to_string(); transaction.signatures().len()] }; for (signature_index, (signature, sigverify_status)) in transaction - .signatures + .signatures() .iter() .zip(&sigverify_statuses) .enumerate() @@ -200,7 +207,7 @@ pub fn write_transaction( )?; } let mut fee_payer_index = None; - for (account_index, account) in message.account_keys.iter().enumerate() { + for (account_index, account) in message.account_keys_iter().enumerate() { if fee_payer_index.is_none() && message.is_non_loader_key(account_index) { fee_payer_index = Some(account_index) } @@ -218,8 +225,9 @@ pub fn write_transaction( }, )?; } - for (instruction_index, instruction) in message.instructions.iter().enumerate() { - let program_pubkey = message.account_keys[instruction.program_id_index as usize]; + for (instruction_index, (program_pubkey, instruction)) in + message.program_instructions_iter().enumerate() + { writeln!(w, "{}Instruction {}", prefix, instruction_index)?; writeln!( w, @@ -227,7 +235,9 @@ pub fn write_transaction( prefix, program_pubkey, instruction.program_id_index )?; for (account_index, account) in instruction.accounts.iter().enumerate() { - let account_pubkey = message.account_keys[*account as usize]; + let account_pubkey = message + .get_account_key(*account as usize) + .expect("account index is sanitized"); writeln!( w, "{} Account {}: {} ({})", @@ -236,7 +246,7 @@ pub fn write_transaction( } let mut raw = true; - if program_pubkey == solana_vote_program::id() { + if program_pubkey == &solana_vote_program::id() { if let Ok(vote_instruction) = limited_deserialize::< solana_vote_program::vote_instruction::VoteInstruction, >(&instruction.data) @@ -244,14 +254,14 @@ pub fn write_transaction( writeln!(w, "{} {:?}", prefix, vote_instruction)?; raw = false; } - } else if program_pubkey == stake::program::id() { + } 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() { + } else if program_pubkey == &solana_sdk::system_program::id() { if let Ok(system_instruction) = limited_deserialize::< solana_sdk::system_instruction::SystemInstruction, >(&instruction.data) @@ -259,7 +269,7 @@ pub fn write_transaction( writeln!(w, "{} {:?}", prefix, system_instruction)?; raw = false; } - } else if is_memo_program(&program_pubkey) { + } else if is_memo_program(program_pubkey) { if let Ok(s) = std::str::from_utf8(&instruction.data) { writeln!(w, "{} Data: \"{}\"", prefix, s)?; raw = false; @@ -364,7 +374,7 @@ pub fn write_transaction( } pub fn println_transaction( - transaction: &Transaction, + transaction: &SanitizedTransaction, transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, @@ -389,25 +399,27 @@ pub fn println_transaction( pub fn writeln_transaction( f: &mut dyn fmt::Write, - transaction: &Transaction, + transaction: Transaction, transaction_status: &Option, prefix: &str, sigverify_status: Option<&[CliSignatureVerificationStatus]>, block_time: Option, ) -> fmt::Result { let mut w = Vec::new(); - if write_transaction( - &mut w, - transaction, - transaction_status, - prefix, - sigverify_status, - block_time, - ) - .is_ok() - { - if let Ok(s) = String::from_utf8(w) { - write!(f, "{}", s)?; + if let Ok(transaction) = SanitizedTransaction::try_from(transaction) { + if write_transaction( + &mut w, + &transaction, + transaction_status, + prefix, + sigverify_status, + block_time, + ) + .is_ok() + { + if let Ok(s) = String::from_utf8(w) { + write!(f, "{}", s)?; + } } } Ok(()) diff --git a/cli/src/cluster_query.rs b/cli/src/cluster_query.rs index 59efb36daa7faf..a55752b00f4a18 100644 --- a/cli/src/cluster_query.rs +++ b/cli/src/cluster_query.rs @@ -56,12 +56,13 @@ use solana_sdk::{ stake_history::{self}, }, timing, - transaction::Transaction, + transaction::{SanitizedTransaction, Transaction}, }; use solana_transaction_status::UiTransactionEncoding; use solana_vote_program::vote_state::VoteState; use std::{ collections::{BTreeMap, HashMap, VecDeque}, + convert::TryFrom, fmt, str::FromStr, sync::{ @@ -2022,17 +2023,26 @@ pub fn process_transaction_history( }, ) { Ok(confirmed_transaction) => { - println_transaction( - &confirmed_transaction - .transaction - .transaction - .decode() - .expect("Successful decode"), - &confirmed_transaction.transaction.meta, - " ", - None, - None, - ); + let transaction = confirmed_transaction + .transaction + .transaction + .decode() + .expect("Successful decode"); + + match SanitizedTransaction::try_from(transaction) { + Ok(transaction) => { + println_transaction( + &transaction, + &confirmed_transaction.transaction.meta, + " ", + None, + None, + ); + } + Err(err) => { + println!(" Unable to construct sanitized transaction: {}", err) + } + } } Err(err) => println!(" Unable to get confirmed transaction details: {}", err), } diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 1ae719e0691371..056c21a2522e91 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -1043,14 +1043,15 @@ impl BankingStage { // Also returned is packet indexes for transaction should be retried due to cost limits. #[allow(clippy::needless_collect)] fn transactions_from_packets( + bank: &Arc, msgs: &Packets, transaction_indexes: &[usize], - libsecp256k1_0_5_upgrade_enabled: bool, cost_tracker: &Arc>, banking_stage_stats: &BankingStageStats, ) -> (Vec, Vec, Vec) { let mut retryable_transaction_packet_indexes: Vec = vec![]; + let libsecp256k1_0_5_upgrade_enabled = bank.libsecp256k1_0_5_upgrade_enabled(); let verified_transactions_with_packet_indexes: Vec<_> = transaction_indexes .iter() .filter_map(|tx_index| { @@ -1058,8 +1059,10 @@ impl BankingStage { let tx: VersionedTransaction = limited_deserialize(&p.data[0..p.meta.size]).ok()?; let message_bytes = Self::packet_message(p)?; let message_hash = Message::hash_raw_message(message_bytes); - let tx = SanitizedTransaction::try_create(tx, message_hash, |_| { - Err(TransactionError::UnsupportedVersion) + let tx = SanitizedTransaction::try_create(tx, message_hash, |message| { + bank.address_map_cache() + .map_message_addresses(message) + .ok_or(TransactionError::AccountNotFound) }) .ok()?; tx.verify_precompiles(libsecp256k1_0_5_upgrade_enabled) @@ -1155,9 +1158,9 @@ impl BankingStage { let mut packet_conversion_time = Measure::start("packet_conversion"); let (transactions, transaction_to_packet_indexes, retryable_packet_indexes) = Self::transactions_from_packets( + bank, msgs, &packet_indexes, - bank.libsecp256k1_0_5_upgrade_enabled(), cost_tracker, banking_stage_stats, ); @@ -1257,9 +1260,9 @@ impl BankingStage { Measure::start("unprocessed_packet_conversion"); let (transactions, transaction_to_packet_indexes, retry_packet_indexes) = Self::transactions_from_packets( + bank, msgs, transaction_indexes, - bank.libsecp256k1_0_5_upgrade_enabled(), cost_tracker, banking_stage_stats, ); diff --git a/ledger-tool/src/bigtable.rs b/ledger-tool/src/bigtable.rs index b856ef13114ab3..e755f369dbb80e 100644 --- a/ledger-tool/src/bigtable.rs +++ b/ledger-tool/src/bigtable.rs @@ -9,9 +9,12 @@ use solana_cli_output::{ OutputFormat, }; use solana_ledger::{blockstore::Blockstore, blockstore_db::AccessType}; -use solana_sdk::{clock::Slot, pubkey::Pubkey, signature::Signature}; +use solana_sdk::{ + clock::Slot, pubkey::Pubkey, signature::Signature, transaction::SanitizedTransaction, +}; use solana_transaction_status::{ConfirmedBlock, EncodedTransaction, UiTransactionEncoding}; use std::{ + convert::TryFrom, path::Path, process::exit, result::Result, @@ -183,13 +186,19 @@ pub async fn transaction_history( ); } Some(transaction_with_meta) => { - println_transaction( - &transaction_with_meta.transaction, - &transaction_with_meta.meta.clone().map(|m| m.into()), - " ", - None, - None, - ); + let transaction = transaction_with_meta.transaction.clone(); + match SanitizedTransaction::try_from(transaction) { + Ok(transaction) => println_transaction( + &transaction, + &transaction_with_meta.meta.clone().map(|m| m.into()), + " ", + None, + None, + ), + Err(err) => { + println!(" Unable to construct sanitized transaction for {}: {}", result.signature, err) + } + } } } break; diff --git a/ledger-tool/src/main.rs b/ledger-tool/src/main.rs index b9f177dcc36843..d5c7dccef1db7f 100644 --- a/ledger-tool/src/main.rs +++ b/ledger-tool/src/main.rs @@ -52,6 +52,7 @@ use solana_sdk::{ transaction::{SanitizedTransaction, TransactionError}, }; use solana_stake_program::stake_state::{self, PointValue}; +use solana_transaction_status::UiTransactionStatusMeta; use solana_vote_program::{ self, vote_state::{self, VoteState}, @@ -144,15 +145,33 @@ fn output_entry( }) .map(|transaction_status| transaction_status.into()); - if let Some(legacy_tx) = transaction.legacy_transaction() { - solana_cli_output::display::println_transaction( - &legacy_tx, &tx_status, " ", None, None, - ); - } else { - eprintln!( - "Failed to print unsupported transaction for {} at slot {}", - tx_signature, slot - ); + let message_hash = transaction.message.hash(); + let sanitize_result = + SanitizedTransaction::try_create(transaction, message_hash, |_| { + tx_status + .as_ref() + .and_then(|meta: &UiTransactionStatusMeta| { + meta.mapped_addresses.clone() + }) + .ok_or(TransactionError::AccountNotFound) + }); + + match sanitize_result { + Ok(transaction) => { + solana_cli_output::display::println_transaction( + &transaction, + &tx_status, + " ", + None, + None, + ); + } + Err(err) => { + eprintln!( + "Failed to construct sanitized transaction for {} at slot {}: {}", + tx_signature, slot, err + ); + } } } } @@ -223,20 +242,17 @@ fn output_slot( let tx_signature = transaction.signatures[0]; let sanitize_result = SanitizedTransaction::try_create(transaction, Hash::default(), |_| { - Err(TransactionError::UnsupportedVersion) + blockstore + .read_transaction_status((tx_signature, slot)) + .ok() + .flatten() + .and_then(|meta| meta.mapped_addresses) + .ok_or(TransactionError::AccountNotFound) }); - match sanitize_result { - Ok(transaction) => { - for (program_id, _) in transaction.message().program_instructions_iter() { - *program_ids.entry(*program_id).or_insert(0) += 1; - } - } - Err(err) => { - warn!( - "Failed to analyze unsupported transaction {}: {:?}", - tx_signature, err - ); + if let Ok(transaction) = sanitize_result { + for (program_id, _) in transaction.message().program_instructions_iter() { + *program_ids.entry(*program_id).or_insert(0) += 1; } } } @@ -779,8 +795,14 @@ fn compute_slot_cost(blockstore: &Blockstore, slot: Slot) -> Result<(), String> .transactions .into_iter() .filter_map(|transaction| { + let tx_signature = transaction.signatures[0]; SanitizedTransaction::try_create(transaction, Hash::default(), |_| { - Err(TransactionError::UnsupportedVersion) + blockstore + .read_transaction_status((tx_signature, slot)) + .ok() + .flatten() + .and_then(|meta| meta.mapped_addresses) + .ok_or(TransactionError::AccountNotFound) }) .map_err(|err| { warn!("Failed to compute cost of transaction: {:?}", err); diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index 8e8b2edccff65c..b8448881f3c993 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -4021,6 +4021,7 @@ pub mod tests { use solana_sdk::{ hash::{self, hash, Hash}, instruction::CompiledInstruction, + message::MappedAddresses, packet::PACKET_DATA_SIZE, pubkey::Pubkey, signature::Signature, @@ -6225,6 +6226,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: None, } .into(); ledger @@ -6241,6 +6243,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: None, } .into(); ledger @@ -6257,6 +6260,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: None, } .into(); ledger @@ -6275,6 +6279,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: None, }), } }) @@ -6379,6 +6384,10 @@ pub mod tests { let pre_token_balances_vec = vec![]; let post_token_balances_vec = vec![]; let rewards_vec = vec![]; + let expected_mapped_addresses = MappedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![Pubkey::new_unique()], + }; // result not found assert!(transaction_status_cf @@ -6403,6 +6412,7 @@ pub mod tests { pre_token_balances: Some(pre_token_balances_vec.clone()), post_token_balances: Some(post_token_balances_vec.clone()), rewards: Some(rewards_vec.clone()), + mapped_addresses: Some(expected_mapped_addresses.clone()), } .into(); assert!(transaction_status_cf @@ -6420,6 +6430,7 @@ pub mod tests { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = transaction_status_cf .get_protobuf_or_bincode::(( 0, @@ -6439,6 +6450,7 @@ pub mod tests { assert_eq!(pre_token_balances.unwrap(), pre_token_balances_vec); assert_eq!(post_token_balances.unwrap(), post_token_balances_vec); assert_eq!(rewards.unwrap(), rewards_vec); + assert_eq!(mapped_addresses.unwrap(), expected_mapped_addresses); // insert value let status = TransactionStatusMeta { @@ -6451,6 +6463,7 @@ pub mod tests { pre_token_balances: Some(pre_token_balances_vec.clone()), post_token_balances: Some(post_token_balances_vec.clone()), rewards: Some(rewards_vec.clone()), + mapped_addresses: Some(expected_mapped_addresses.clone()), } .into(); assert!(transaction_status_cf @@ -6468,6 +6481,7 @@ pub mod tests { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = transaction_status_cf .get_protobuf_or_bincode::(( 0, @@ -6489,6 +6503,7 @@ pub mod tests { assert_eq!(pre_token_balances.unwrap(), pre_token_balances_vec); assert_eq!(post_token_balances.unwrap(), post_token_balances_vec); assert_eq!(rewards.unwrap(), rewards_vec); + assert_eq!(mapped_addresses.unwrap(), expected_mapped_addresses); } Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } @@ -6720,6 +6735,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: Some(MappedAddresses::default()), } .into(); @@ -6916,6 +6932,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: Some(MappedAddresses::default()), } .into(); @@ -7097,6 +7114,7 @@ pub mod tests { let post_token_balances = Some(vec![]); let rewards = Some(vec![]); let signature = transaction.signatures[0]; + let mapped_addresses = Some(MappedAddresses::default()); let status = TransactionStatusMeta { status: Ok(()), fee: 42, @@ -7107,6 +7125,7 @@ pub mod tests { pre_token_balances: pre_token_balances.clone(), post_token_balances: post_token_balances.clone(), rewards: rewards.clone(), + mapped_addresses: mapped_addresses.clone(), } .into(); blockstore @@ -7125,6 +7144,7 @@ pub mod tests { pre_token_balances, post_token_balances, rewards, + mapped_addresses, }), } }) @@ -7199,6 +7219,7 @@ pub mod tests { let pre_token_balances = Some(vec![]); let post_token_balances = Some(vec![]); let rewards = Some(vec![]); + let mapped_addresses = Some(MappedAddresses::default()); let signature = transaction.signatures[0]; let status = TransactionStatusMeta { status: Ok(()), @@ -7210,6 +7231,7 @@ pub mod tests { pre_token_balances: pre_token_balances.clone(), post_token_balances: post_token_balances.clone(), rewards: rewards.clone(), + mapped_addresses: mapped_addresses.clone(), } .into(); blockstore @@ -7228,6 +7250,7 @@ pub mod tests { pre_token_balances, post_token_balances, rewards, + mapped_addresses, }), } }) @@ -7964,6 +7987,7 @@ pub mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: Some(MappedAddresses::default()), } .into(); transaction_status_cf @@ -8517,6 +8541,10 @@ pub mod tests { reward_type: Some(RewardType::Rent), commission: None, }]), + mapped_addresses: Some(MappedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![Pubkey::new_unique()], + }), }; let deprecated_status: StoredTransactionStatusMeta = status.clone().into(); let protobuf_status: generated::TransactionStatusMeta = status.into(); diff --git a/ledger/src/blockstore/blockstore_purge.rs b/ledger/src/blockstore/blockstore_purge.rs index 769b16520e62fc..cb16f78ce74dee 100644 --- a/ledger/src/blockstore/blockstore_purge.rs +++ b/ledger/src/blockstore/blockstore_purge.rs @@ -334,13 +334,26 @@ impl Blockstore { .flat_map(|entry| entry.transactions) { if let Some(&signature) = transaction.signatures.get(0) { + let mut delete_addresses = |pubkeys: Vec| -> Result<()> { + for pubkey in pubkeys { + batch.delete::((0, pubkey, slot, signature))?; + batch.delete::((1, pubkey, slot, signature))?; + } + Ok(()) + }; + + delete_addresses(transaction.message.unmapped_keys())?; + // Mapped keys are recorded in transaction status meta and + // are necessary for cleaning up the remaining address rows + if let Some(status) = self.read_transaction_status((signature, slot))? { + if let Some(mapped_addresses) = status.mapped_addresses { + delete_addresses(mapped_addresses.writable)?; + delete_addresses(mapped_addresses.readonly)?; + } + } + batch.delete::((0, signature, slot))?; batch.delete::((1, signature, slot))?; - // TODO: support purging mapped addresses from versioned transactions - for pubkey in transaction.message.unmapped_keys() { - batch.delete::((0, pubkey, slot, signature))?; - batch.delete::((1, pubkey, slot, signature))?; - } } } } diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index f206780d1e2b7e..9bb57f94e61a7f 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -27,7 +27,7 @@ use { hash::Hash, instruction::Instruction, instruction::InstructionError, - message::Message, + message::{Message, SanitizedMessage}, native_token::sol_to_lamports, process_instruction::{stable_log, InvokeContext, ProcessInstructionWithContext}, program_error::{ProgramError, ACCOUNT_BORROW_FAILED, UNSUPPORTED_SYSVAR}, @@ -324,6 +324,7 @@ impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { invoke_context.record_instruction(instruction); + let message = SanitizedMessage::Legacy(message); solana_runtime::message_processor::MessageProcessor::process_cross_program_instruction( &message, &executables, @@ -334,7 +335,7 @@ impl solana_sdk::program_stubs::SyscallStubs for SyscallStubs { .map_err(|err| ProgramError::try_from(err).unwrap_or_else(|err| panic!("{}", err)))?; // Copy writeable account modifications back into the caller's AccountInfos - for (i, (pubkey, account)) in accounts.iter().enumerate().take(message.account_keys.len()) { + for (i, (pubkey, account)) in accounts.iter().enumerate().take(message.account_keys_len()) { if !message.is_writable(i) { continue; } diff --git a/programs/address_map/Cargo.toml b/programs/address_map/Cargo.toml new file mode 100644 index 00000000000000..caa99b15253be0 --- /dev/null +++ b/programs/address_map/Cargo.toml @@ -0,0 +1,29 @@ +[package] +name = "solana-address-map-program" +version = "1.8.0" +description = "Solana address map program" +authors = ["Solana Maintainers "] +repository = "https://github.com/solana-labs/solana" +license = "Apache-2.0" +homepage = "https://solana.com/" +documentation = "https://docs.rs/solana-address-map-program" +edition = "2018" + +[dependencies] +bincode = "1.3.3" +log = "0.4.14" +num-derive = "0.3" +num-traits = "0.2" +serde = "1.0.126" +serde_derive = "1.0.103" +solana-frozen-abi = { path = "../../frozen-abi", version = "=1.8.0" } +solana-frozen-abi-macro = { path = "../../frozen-abi/macro", version = "=1.8.0" } +solana-sdk = { path = "../../sdk", version = "=1.8.0" } +thiserror = "1.0" + +[lib] +crate-type = ["lib"] +name = "solana_address_map_program" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] diff --git a/programs/address_map/src/lib.rs b/programs/address_map/src/lib.rs new file mode 100644 index 00000000000000..044619bbe55f31 --- /dev/null +++ b/programs/address_map/src/lib.rs @@ -0,0 +1,24 @@ +use { + serde::{Deserialize, Serialize}, + solana_frozen_abi_macro::AbiExample, + solana_sdk::{clock::Epoch, declare_id, pubkey::Pubkey, short_vec}, +}; + +declare_id!("AddressMap111111111111111111111111111111111"); + +pub const DEACTIVATION_COOLDOWN: Epoch = 2; + +/// Data structue of address map +#[derive(Debug, Serialize, Deserialize, PartialEq, Clone, AbiExample)] +pub struct AddressMap { + // authority must sign for each addition and to close the map account + pub authority: Pubkey, + // record a deactivation epoch to help validators know when to remove + // the map from their caches. + pub deactivation_epoch: Epoch, + // entries may not be modified once activated + pub activated: bool, + // list of entries, max capacity of u8::MAX + #[serde(with = "short_vec")] + pub entries: Vec, +} diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index 42ef572e04e97d..df4e415a3dc31e 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -293,7 +293,7 @@ fn process_transaction_and_record_inner( ) -> (Result<(), TransactionError>, Vec>) { let signature = tx.signatures.get(0).unwrap().clone(); let txs = vec![tx]; - let tx_batch = bank.prepare_batch(txs.iter()).unwrap(); + let tx_batch = bank.prepare_batch(txs).unwrap(); let (mut results, _, mut inner_instructions, _transaction_logs) = bank .load_execute_and_commit_transactions( &tx_batch, @@ -316,7 +316,7 @@ fn process_transaction_and_record_inner( } fn execute_transactions(bank: &Bank, txs: &[Transaction]) -> Vec { - let batch = bank.prepare_batch(txs.iter()).unwrap(); + let batch = bank.prepare_batch(txs.to_vec()).unwrap(); let mut timings = ExecuteTimings::default(); let mut mint_decimals = HashMap::new(); let tx_pre_token_balances = collect_token_balances(&bank, &batch, &mut mint_decimals); diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs index 401ca8c1197e12..9267f84584fbfa 100644 --- a/programs/bpf_loader/src/syscalls.rs +++ b/programs/bpf_loader/src/syscalls.rs @@ -29,6 +29,7 @@ use solana_sdk::{ instruction::{AccountMeta, Instruction, InstructionError}, keccak, keyed_account::KeyedAccount, + message::SanitizedMessage, native_loader, process_instruction::{self, stable_log, ComputeMeter, InvokeContext, Logger}, pubkey::{Pubkey, PubkeyError, MAX_SEEDS}, @@ -2370,7 +2371,7 @@ fn call<'a>( invoke_context.record_instruction(&instruction); ( - message, + SanitizedMessage::Legacy(message), executables, accounts, account_refs, diff --git a/rpc/src/rpc.rs b/rpc/src/rpc.rs index 17aa48f037cf54..4c74167070fdd8 100644 --- a/rpc/src/rpc.rs +++ b/rpc/src/rpc.rs @@ -3276,7 +3276,7 @@ pub mod rpc_full { .preflight_commitment .map(|commitment| CommitmentConfig { commitment }); let preflight_bank = &*meta.bank(preflight_commitment); - let transaction = sanitize_transaction(unsanitized_tx)?; + let transaction = sanitize_transaction(preflight_bank, unsanitized_tx)?; let signature = *transaction.signature(); let mut last_valid_block_height = preflight_bank @@ -3381,7 +3381,7 @@ pub mod rpc_full { .set_recent_blockhash(bank.last_blockhash()); } - let transaction = sanitize_transaction(unsanitized_tx)?; + let transaction = sanitize_transaction(bank, unsanitized_tx)?; if config.sig_verify { verify_transaction(&transaction, bank.libsecp256k1_0_5_upgrade_enabled())?; } @@ -3924,10 +3924,15 @@ fn deserialize_transaction( .map(|transaction| (wire_transaction, transaction)) } -fn sanitize_transaction(transaction: VersionedTransaction) -> Result { +fn sanitize_transaction( + bank: &Bank, + transaction: VersionedTransaction, +) -> Result { let message_hash = transaction.message.hash(); - SanitizedTransaction::try_create(transaction, message_hash, |_| { - Err(TransactionError::UnsupportedVersion) + SanitizedTransaction::try_create(transaction, message_hash, |message| { + bank.address_map_cache() + .map_message_addresses(message) + .ok_or(TransactionError::AccountNotFound) }) .map_err(|err| Error::invalid_params(format!("invalid transaction: {}", err))) } diff --git a/rpc/src/transaction_status_service.rs b/rpc/src/transaction_status_service.rs index 77cb3051702ce2..5f0b96c2d461ac 100644 --- a/rpc/src/transaction_status_service.rs +++ b/rpc/src/transaction_status_service.rs @@ -140,6 +140,7 @@ impl TransactionStatusService { .collect(), ); + let mapped_addresses = transaction.message().mapped_addresses().cloned(); blockstore .write_transaction_status( slot, @@ -156,6 +157,7 @@ impl TransactionStatusService { pre_token_balances, post_token_balances, rewards, + mapped_addresses, }, ) .expect("Expect database write to succeed"); diff --git a/runtime/Cargo.toml b/runtime/Cargo.toml index 1dd0ab6c9913bd..f7eb43572fabec 100644 --- a/runtime/Cargo.toml +++ b/runtime/Cargo.toml @@ -36,6 +36,7 @@ rayon = "1.5.1" regex = "1.5.4" serde = { version = "1.0.127", features = ["rc"] } serde_derive = "1.0.103" +solana-address-map-program = { path = "../programs/address_map", version = "=1.8.0" } solana-config-program = { path = "../programs/config", version = "=1.8.0" } solana-compute-budget-program = { path = "../programs/compute-budget", version = "=1.8.0" } solana-frozen-abi = { path = "../frozen-abi", version = "=1.8.0" } diff --git a/runtime/src/accounts_db.rs b/runtime/src/accounts_db.rs index ebdf41a6981cbc..d52619d6edbd0a 100644 --- a/runtime/src/accounts_db.rs +++ b/runtime/src/accounts_db.rs @@ -26,6 +26,7 @@ use crate::{ AccountIndexGetResult, AccountSecondaryIndexes, AccountsIndex, AccountsIndexRootsStats, IndexKey, IsCached, ScanResult, SlotList, SlotSlice, ZeroLamport, BINS_FOR_TESTING, }, + address_map_cache::AddressMapCache, ancestors::Ancestors, append_vec::{AppendVec, StoredAccountMeta, StoredMeta, StoredMetaWriteVersion}, contains::Contains, @@ -897,6 +898,9 @@ pub struct AccountsDb { pub accounts_cache: AccountsCache, + /// Cache for fast address lookups required by mapped transactions + pub address_map_cache: AddressMapCache, + sender_bg_hasher: Option>, pub read_only_accounts_cache: ReadOnlyAccountsCache, @@ -1410,6 +1414,7 @@ impl AccountsDb { remove_unrooted_slots_synchronization: RemoveUnrootedSlotsSynchronization::default(), shrink_ratio: AccountShrinkThreshold::default(), dirty_stores: DashMap::default(), + address_map_cache: AddressMapCache::default(), } } @@ -5922,6 +5927,11 @@ impl AccountsDb { stored_account, }, )| { + if stored_account.account_meta.owner == solana_address_map_program::id() { + self.address_map_cache + .update_cache(&pubkey, stored_account.data); + } + if secondary { self.accounts_index.update_secondary_indexes( &pubkey, diff --git a/runtime/src/address_map_cache.rs b/runtime/src/address_map_cache.rs new file mode 100644 index 00000000000000..175305f39ce95b --- /dev/null +++ b/runtime/src/address_map_cache.rs @@ -0,0 +1,102 @@ +//! Cache of finalized address maps + +use { + dashmap::DashMap, + solana_address_map_program::{AddressMap, DEACTIVATION_COOLDOWN}, + solana_sdk::{ + clock::Epoch, + message::{v0, MappedAddresses}, + pubkey::Pubkey, + }, +}; + +#[derive(Debug)] +pub enum AddressMapChange { + Activation(Pubkey), + Deactivation(Pubkey), +} + +// Address maps can be removed but they have to be deactivated. We need to store +// an activation and deactivation epoch. + +/// Cached address map with an optional deactivation epoch. +#[derive(Debug)] +struct CachedAddressMap { + entries: Vec, + deactivation_epoch: Option, +} + +#[derive(Debug, Default)] +pub struct AddressMapCache { + address_maps: DashMap, +} + +impl AddressMapCache { + pub fn map_message_addresses(&self, message: &v0::Message) -> Option { + let mut mapped_addresses = MappedAddresses { + writable: Vec::with_capacity(message.num_writable_map_indexes()), + readonly: Vec::with_capacity(message.num_readonly_map_indexes()), + }; + + for (key, indexes) in message.address_map_indexes_iter() { + let address_map = self.address_maps.get(key)?; + let lookup_address = |index: &u8| address_map.entries.get(*index as usize).cloned(); + + mapped_addresses.writable.extend( + indexes + .writable + .iter() + .map(lookup_address) + .collect::>>()?, + ); + + mapped_addresses.readonly.extend( + indexes + .readonly + .iter() + .map(lookup_address) + .collect::>>()?, + ); + } + + Some(mapped_addresses) + } + + pub(crate) fn update_cache(&self, pubkey: &Pubkey, account_data: &[u8]) { + if let Ok(address_map) = bincode::deserialize::(account_data) { + let deactivation_epoch = match address_map.deactivation_epoch { + Epoch::MAX => None, + epoch => Some(epoch), + }; + + // assumes that address maps cannot be recreated at the same account address + if address_map.activated { + self.address_maps + .entry(*pubkey) + .and_modify(|cached_address_map| { + // assumes that address map entries cannot be modified after activation + cached_address_map.deactivation_epoch = deactivation_epoch; + }) + .or_insert(CachedAddressMap { + entries: address_map.entries, + deactivation_epoch, + }); + } + } else { + // remove closed address map + self.address_maps.remove(pubkey); + } + } + + /// Purge all cached address maps that have been deactivated and passed the cooldown period + pub fn purge_deactivated_maps(&self, current_epoch: Epoch) { + self.address_maps.retain(|_address, address_map| { + address_map + .deactivation_epoch + .map(|deactivation_epoch| { + deactivation_epoch + DEACTIVATION_COOLDOWN > current_epoch + }) + .unwrap_or(true) + }); + } +} diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index c73788c9bc3ad2..09eb7916fb2a16 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -42,6 +42,7 @@ use crate::{ accounts_index::{ AccountSecondaryIndexes, IndexKey, ScanResult, BINS_FOR_BENCHMARKS, BINS_FOR_TESTING, }, + address_map_cache::{AddressMapCache, AddressMapChange}, ancestors::{Ancestors, AncestorsForSerialization}, blockhash_queue::BlockhashQueue, builtins::{self, ActivationType}, @@ -1042,6 +1043,9 @@ pub struct Bank { pub drop_callback: RwLock, pub freeze_started: AtomicBool, + + /// Pending address map changes queued by this bank + pub pending_address_map_changes: RwLock>, } impl Default for BlockhashQueue { @@ -1168,6 +1172,7 @@ impl Bank { feature_set: Arc::::default(), drop_callback: RwLock::::default(), freeze_started: AtomicBool::default(), + pending_address_map_changes: RwLock::>::default(), } } @@ -1387,6 +1392,7 @@ impl Bank { .map(|drop_callback| drop_callback.clone_box()), )), freeze_started: AtomicBool::new(false), + pending_address_map_changes: RwLock::new(vec![]), }; datapoint_info!( @@ -1534,6 +1540,7 @@ impl Bank { feature_set: new(), drop_callback: RwLock::new(OptionalDropCallback(None)), freeze_started: AtomicBool::new(fields.hash != Hash::default()), + pending_address_map_changes: new(), }; bank.finish_init( genesis_config, @@ -2811,6 +2818,10 @@ impl Bank { tick_height % self.ticks_per_slot == 0 } + pub fn address_map_cache(&self) -> &AddressMapCache { + &self.rc.accounts.accounts_db.address_map_cache + } + /// Prepare a transaction batch from a list of legacy transactionsy. Used for tests only. pub fn prepare_batch(&self, txs: Vec) -> Result { let sanitized_txs = txs @@ -2832,8 +2843,10 @@ impl Bank { .into_iter() .map(|tx| { let message_hash = tx.message.hash(); - SanitizedTransaction::try_create(tx, message_hash, |_| { - Err(TransactionError::UnsupportedVersion) + SanitizedTransaction::try_create(tx, message_hash, |message| { + self.address_map_cache() + .map_message_addresses(message) + .ok_or(TransactionError::AccountNotFound) }) }) .collect::>>()?; @@ -3418,28 +3431,23 @@ impl Bank { ) }; - if let Some(legacy_message) = tx.message().legacy_message() { - process_result = self.message_processor.process_message( - legacy_message, - &loader_refcells, - &account_refcells, - &self.rent_collector, - log_collector.clone(), - executors.clone(), - instruction_recorders.as_deref(), - feature_set, - compute_budget, - compute_meter, - &mut timings.details, - self.rc.accounts.clone(), - &self.ancestors, - blockhash, - fee_calculator, - ); - } else { - // TODO: support versioned messages - process_result = Err(TransactionError::UnsupportedVersion); - } + process_result = self.message_processor.process_message( + tx.message(), + &loader_refcells, + &account_refcells, + &self.rent_collector, + log_collector.clone(), + executors.clone(), + instruction_recorders.as_deref(), + feature_set, + compute_budget, + compute_meter, + &mut timings.details, + self.rc.accounts.clone(), + &self.ancestors, + blockhash, + fee_calculator, + ); transaction_log_messages.push(Self::collect_log_messages(log_collector)); inner_instructions.push(Self::compile_recorded_instructions( @@ -4954,8 +4962,10 @@ impl Bank { tx.message.hash() }; - SanitizedTransaction::try_create(tx, message_hash, |_| { - Err(TransactionError::UnsupportedVersion) + SanitizedTransaction::try_create(tx, message_hash, |message| { + self.address_map_cache() + .map_message_addresses(message) + .ok_or(TransactionError::AccountNotFound) }) }?; diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index e6896837625148..b084b988cc1338 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -6,6 +6,7 @@ pub mod accounts_cache; pub mod accounts_db; pub mod accounts_hash; pub mod accounts_index; +pub mod address_map_cache; pub mod ancestors; pub mod append_vec; pub mod bank; diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 32acb5c5ee7cf3..9bdd5a3524ff7e 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -19,7 +19,7 @@ use solana_sdk::{ ic_logger_msg, ic_msg, instruction::{CompiledInstruction, Instruction, InstructionError}, keyed_account::{create_keyed_accounts_unified, keyed_account_at_index, KeyedAccount}, - message::Message, + message::{Message, SanitizedMessage}, native_loader, process_instruction::{ ComputeMeter, Executor, InvokeContext, InvokeContextStackFrame, Logger, @@ -313,7 +313,7 @@ impl<'a> ThisInvokeContext<'a> { pub fn new( program_id: &Pubkey, rent: Rent, - message: &'a Message, + message: &'a SanitizedMessage, instruction: &'a CompiledInstruction, executable_accounts: &'a [(Pubkey, Rc>)], accounts: &'a [(Pubkey, Rc>)], @@ -660,7 +660,7 @@ impl MessageProcessor { /// Create the KeyedAccounts that will be passed to the program fn create_keyed_accounts<'a>( - message: &'a Message, + message: &'a SanitizedMessage, instruction: &'a CompiledInstruction, executable_accounts: &'a [(Pubkey, Rc>)], accounts: &'a [(Pubkey, Rc>)], @@ -896,7 +896,7 @@ impl MessageProcessor { executable_accounts.push(programdata); } ( - message, + SanitizedMessage::Legacy(message), executable_accounts, accounts, keyed_account_indices_reordered, @@ -956,15 +956,13 @@ impl MessageProcessor { /// Process a cross-program instruction /// This method calls the instruction's program entrypoint function pub fn process_cross_program_instruction( - message: &Message, + message: &SanitizedMessage, executable_accounts: &[(Pubkey, Rc>)], accounts: &[(Pubkey, Rc>)], caller_write_privileges: &[bool], invoke_context: &mut dyn InvokeContext, ) -> Result<(), InstructionError> { - if let Some(instruction) = message.instructions.get(0) { - let program_id = instruction.program_id(&message.account_keys); - + if let Some((program_id, instruction)) = message.program_instructions_iter().next() { // Verify the calling program hasn't misbehaved invoke_context.verify_and_update(instruction, accounts, caller_write_privileges)?; @@ -987,7 +985,7 @@ impl MessageProcessor { ); if result.is_ok() { // Verify the called program has not misbehaved - let write_privileges: Vec = (0..message.account_keys.len()) + let write_privileges: Vec = (0..message.account_keys_len()) .map(|i| message.is_writable(i)) .collect(); result = invoke_context.verify_and_update(instruction, accounts, &write_privileges); @@ -1005,14 +1003,14 @@ impl MessageProcessor { /// Record the initial state of the accounts so that they can be compared /// after the instruction is processed pub fn create_pre_accounts( - message: &Message, + message: &SanitizedMessage, instruction: &CompiledInstruction, accounts: &[(Pubkey, Rc>)], ) -> Vec { let mut pre_accounts = Vec::with_capacity(instruction.accounts.len()); { let mut work = |_unique_index: usize, account_index: usize| { - if account_index < message.account_keys.len() && account_index < accounts.len() { + if account_index < message.account_keys_len() && account_index < accounts.len() { let account = accounts[account_index].1.borrow(); pre_accounts.push(PreAccount::new(&accounts[account_index].0, &account)); return Ok(()); @@ -1038,7 +1036,7 @@ impl MessageProcessor { /// Verify the results of an instruction pub fn verify( - message: &Message, + message: &SanitizedMessage, instruction: &CompiledInstruction, pre_accounts: &[PreAccount], executable_accounts: &[(Pubkey, Rc>)], @@ -1054,7 +1052,9 @@ impl MessageProcessor { // Verify the per-account instruction results let (mut pre_sum, mut post_sum) = (0_u128, 0_u128); { - let program_id = instruction.program_id(&message.account_keys); + let program_id = message + .get_account_key(instruction.program_id_index as usize) + .expect("message to be sanitized"); let mut work = |unique_index: usize, account_index: usize| { { // Verify account has no outstanding references @@ -1167,7 +1167,7 @@ impl MessageProcessor { #[allow(clippy::too_many_arguments)] fn execute_instruction( &self, - message: &Message, + message: &SanitizedMessage, instruction: &CompiledInstruction, executable_accounts: &[(Pubkey, Rc>)], accounts: &[(Pubkey, Rc>)], @@ -1188,7 +1188,7 @@ impl MessageProcessor { // Fixup the special instructions key if present // before the account pre-values are taken care of if feature_set.is_active(&instructions_sysvar_enabled::id()) { - for (pubkey, accont) in accounts.iter().take(message.account_keys.len()) { + for (pubkey, accont) in accounts.iter().take(message.account_keys_len()) { if instructions::check_id(pubkey) { let mut mut_account_ref = accont.borrow_mut(); instructions::store_current_index( @@ -1200,7 +1200,9 @@ impl MessageProcessor { } } - let program_id = instruction.program_id(&message.account_keys); + let program_id = message + .get_account_key(instruction.program_id_index as usize) + .expect("message to be sanitized"); let mut compute_budget = compute_budget; if feature_set.is_active(&neon_evm_compute_budget::id()) @@ -1255,7 +1257,7 @@ impl MessageProcessor { #[allow(clippy::type_complexity)] pub fn process_message( &self, - message: &Message, + message: &SanitizedMessage, loaders: &[Vec<(Pubkey, Rc>)>], accounts: &[(Pubkey, Rc>)], rent_collector: &RentCollector, @@ -1271,7 +1273,9 @@ impl MessageProcessor { blockhash: Hash, fee_calculator: FeeCalculator, ) -> Result<(), TransactionError> { - for (instruction_index, instruction) in message.instructions.iter().enumerate() { + for (instruction_index, (program_id, instruction)) in + message.program_instructions_iter().enumerate() + { let mut time = Measure::start("execute_instruction"); let pre_remaining_units = compute_meter.borrow().get_remaining(); let instruction_recorder = instruction_recorders @@ -1302,7 +1306,7 @@ impl MessageProcessor { let post_remaining_units = compute_meter.borrow().get_remaining(); timings.accumulate_program( - instruction.program_id(&message.account_keys), + program_id, time.as_us(), pre_remaining_units - post_remaining_units, ); @@ -1323,6 +1327,11 @@ mod tests { native_loader::create_loadable_account_for_test, process_instruction::MockComputeMeter, }; + use std::convert::TryInto; + + fn new_sanitized_message(instructions: &[Instruction], payer: &Pubkey) -> SanitizedMessage { + Message::new(instructions, Some(payer)).try_into().unwrap() + } #[test] fn test_invoke_context() { @@ -1354,9 +1363,9 @@ mod tests { metas.push(AccountMeta::new(*program_id, false)); } - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bytes(invoke_stack[0], &[0], metas)], - None, + &Pubkey::new_unique(), ); let ancestors = Ancestors::default(); let blockhash = Hash::default(); @@ -1365,7 +1374,7 @@ mod tests { &invoke_stack[0], Rent::default(), &message, - &message.instructions[0], + message.instructions().get(0).unwrap(), &[], &accounts, &[], @@ -1399,13 +1408,13 @@ mod tests { AccountMeta::new(accounts[not_owned_index].0, false), AccountMeta::new(accounts[owned_index].0, false), ]; - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bytes( invoke_stack[owned_index], &[0], metas, )], - None, + &Pubkey::new_unique(), ); // modify account owned by the program @@ -1413,18 +1422,22 @@ mod tests { (MAX_DEPTH + owned_index) as u8; let mut these_accounts = accounts[not_owned_index..owned_index + 1].to_vec(); these_accounts.push(( - message.account_keys[2], + *message.get_account_key(2).unwrap(), Rc::new(RefCell::new(AccountSharedData::new( 1, 1, &solana_sdk::pubkey::Pubkey::default(), ))), )); - let write_privileges: Vec = (0..message.account_keys.len()) + let write_privileges: Vec = (0..message.account_keys_len()) .map(|i| message.is_writable(i)) .collect(); invoke_context - .verify_and_update(&message.instructions[0], &these_accounts, &write_privileges) + .verify_and_update( + message.instructions().get(0).unwrap(), + &these_accounts, + &write_privileges, + ) .unwrap(); assert_eq!( invoke_context.pre_accounts[owned_index] @@ -1440,7 +1453,7 @@ mod tests { (MAX_DEPTH + not_owned_index) as u8; assert_eq!( invoke_context.verify_and_update( - &message.instructions[0], + message.instructions().get(0).unwrap(), &accounts[not_owned_index..owned_index + 1], &write_privileges, ), @@ -1968,13 +1981,13 @@ mod tests { AccountMeta::new(accounts[0].0, true), AccountMeta::new_readonly(accounts[1].0, false), ]; - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_system_program_id, &MockSystemInstruction::Correct, account_metas.clone(), )], - Some(&accounts[0].0), + &accounts[0].0, ); let result = message_processor.process_message( @@ -1998,13 +2011,13 @@ mod tests { assert_eq!(accounts[0].1.borrow().lamports(), 100); assert_eq!(accounts[1].1.borrow().lamports(), 0); - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_system_program_id, &MockSystemInstruction::AttemptCredit { lamports: 50 }, account_metas.clone(), )], - Some(&accounts[0].0), + &accounts[0].0, ); let result = message_processor.process_message( @@ -2032,13 +2045,13 @@ mod tests { )) ); - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_system_program_id, &MockSystemInstruction::AttemptDataChange { data: 50 }, account_metas, )], - Some(&accounts[0].0), + &accounts[0].0, ); let result = message_processor.process_message( @@ -2159,13 +2172,13 @@ mod tests { ]; // Try to borrow mut the same account - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_program_id, &MockSystemInstruction::BorrowFail, account_metas.clone(), )], - Some(&accounts[0].0), + &accounts[0].0, ); let result = message_processor.process_message( &message, @@ -2193,13 +2206,13 @@ mod tests { ); // Try to borrow mut the same account in a safe way - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_program_id, &MockSystemInstruction::MultiBorrowMut, account_metas.clone(), )], - Some(&accounts[0].0), + &accounts[0].0, ); let result = message_processor.process_message( &message, @@ -2221,7 +2234,7 @@ mod tests { assert_eq!(result, Ok(())); // Do work on the same account but at different location in keyed_accounts[] - let message = Message::new( + let message = new_sanitized_message( &[Instruction::new_with_bincode( mock_program_id, &MockSystemInstruction::DoWork { @@ -2230,7 +2243,7 @@ mod tests { }, account_metas, )], - Some(&accounts[0].0), + &accounts[0].0, ); let ancestors = Ancestors::default(); let result = message_processor.process_message( @@ -2334,7 +2347,7 @@ mod tests { &MockInstruction::NoopSuccess, metas.clone(), ); - let message = Message::new(&[instruction], None); + let message = new_sanitized_message(&[instruction], &accounts[0].0); let ancestors = Ancestors::default(); let blockhash = Hash::default(); @@ -2361,8 +2374,7 @@ mod tests { // not owned account modified by the caller (before the invoke) let caller_write_privileges = message - .account_keys - .iter() + .account_keys_iter() .enumerate() .map(|(i, _)| message.is_writable(i)) .collect::>(); @@ -2395,7 +2407,7 @@ mod tests { for case in cases { let instruction = Instruction::new_with_bincode(callee_program_id, &case.0, metas.clone()); - let message = Message::new(&[instruction], None); + let message = new_sanitized_message(&[instruction], &accounts[0].0); let ancestors = Ancestors::default(); let blockhash = Hash::default(); @@ -2421,8 +2433,7 @@ mod tests { ); let caller_write_privileges = message - .account_keys - .iter() + .account_keys_iter() .enumerate() .map(|(i, _)| message.is_writable(i)) .collect::>(); diff --git a/sdk/program/src/message/legacy.rs b/sdk/program/src/message/legacy.rs index 7279eb5e5fa5a4..8b3fae59359955 100644 --- a/sdk/program/src/message/legacy.rs +++ b/sdk/program/src/message/legacy.rs @@ -318,6 +318,7 @@ impl Message { .collect() } + #[deprecated] pub fn is_key_passed_to_program(&self, key_index: usize) -> bool { if let Ok(key_index) = u8::try_from(key_index) { self.instructions @@ -328,6 +329,7 @@ impl Message { } } + #[deprecated] pub fn is_key_called_as_program(&self, key_index: usize) -> bool { if let Ok(key_index) = u8::try_from(key_index) { self.instructions @@ -338,10 +340,13 @@ impl Message { } } + #[deprecated] + #[allow(deprecated)] pub fn is_non_loader_key(&self, key_index: usize) -> bool { !self.is_key_called_as_program(key_index) || self.is_key_passed_to_program(key_index) } + #[deprecated] pub fn program_position(&self, index: usize) -> Option { let program_ids = self.program_ids(); program_ids @@ -349,6 +354,8 @@ impl Message { .position(|&&pubkey| pubkey == self.account_keys[index]) } + #[deprecated] + #[allow(deprecated)] pub fn maybe_executable(&self, i: usize) -> bool { self.program_position(i).is_some() } diff --git a/sdk/program/src/message/mod.rs b/sdk/program/src/message/mod.rs index 2507536f1a1019..b6baa87e0a1a51 100644 --- a/sdk/program/src/message/mod.rs +++ b/sdk/program/src/message/mod.rs @@ -8,6 +8,7 @@ mod versions; pub use legacy::Message; pub use mapped::*; +pub use mapped::*; pub use sanitized::SanitizedMessage; pub use versions::*; diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 13e575e85e3065..fb216493ec8b21 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -220,7 +220,7 @@ impl SanitizedMessage { } /// Return the mapped addresses for this message if it has any. - fn mapped_addresses(&self) -> Option<&MappedAddresses> { + pub fn mapped_addresses(&self) -> Option<&MappedAddresses> { match &self { SanitizedMessage::V0(message) => Some(&message.mapped_addresses), _ => None, diff --git a/sdk/src/lib.rs b/sdk/src/lib.rs index 15492a534af82c..f658a818118d20 100644 --- a/sdk/src/lib.rs +++ b/sdk/src/lib.rs @@ -48,6 +48,7 @@ pub mod system_transaction; pub mod timing; pub mod transaction; pub mod transport; +pub mod versioned_transaction; /// Same as `declare_id` except report that this id has been deprecated pub use solana_sdk_macro::declare_deprecated_id; diff --git a/sdk/src/sanitized_transaction.rs b/sdk/src/sanitized_transaction.rs new file mode 100644 index 00000000000000..8b0e6e6904bfb1 --- /dev/null +++ b/sdk/src/sanitized_transaction.rs @@ -0,0 +1,224 @@ +#![cfg(feature = "full")] + +use { + crate::{ + fee_calculator::FeeCalculator, + hash::Hash, + message::{v0, MappedAddresses, MappedMessage, SanitizedMessage, VersionedMessage}, + nonce::NONCED_TX_MARKER_IX_INDEX, + program_utils::limited_deserialize, + pubkey::Pubkey, + sanitize::Sanitize, + secp256k1_instruction::verify_eth_addresses, + secp256k1_program, + signature::Signature, + transaction::{Result, Transaction, TransactionError}, + versioned_transaction::VersionedTransaction, + }, + solana_program::{system_instruction::SystemInstruction, system_program}, + std::convert::TryFrom, +}; + +/// Sanitized transaction and the hash of its message +#[derive(Debug, Clone)] +pub struct SanitizedTransaction { + message: SanitizedMessage, + message_hash: Hash, + signatures: Vec, +} + +#[derive(Debug, Clone, Default)] +pub struct TransactionAccountLocks<'a> { + pub readonly: Vec<&'a Pubkey>, + pub writable: Vec<&'a Pubkey>, +} + +impl TryFrom for SanitizedTransaction { + type Error = TransactionError; + fn try_from(tx: Transaction) -> Result { + tx.sanitize()?; + + if tx.message.has_duplicates() { + return Err(TransactionError::AccountLoadedTwice); + } + + Ok(Self { + message_hash: tx.message.hash(), + message: SanitizedMessage::Legacy(tx.message), + signatures: tx.signatures, + }) + } +} + +impl SanitizedTransaction { + pub fn try_create( + tx: VersionedTransaction, + message_hash: Hash, + address_mapper: impl Fn(&v0::Message) -> Option, + ) -> Result { + tx.sanitize()?; + + let signatures = tx.signatures; + let message = match tx.message { + VersionedMessage::Legacy(message) => SanitizedMessage::Legacy(message), + VersionedMessage::V0(message) => SanitizedMessage::V0(MappedMessage { + mapped_addresses: address_mapper(&message) + .ok_or(TransactionError::AccountNotFound)?, + message, + }), + }; + + if message.has_duplicates() { + return Err(TransactionError::AccountLoadedTwice); + } + + Ok(Self { + signatures, + message, + message_hash, + }) + } + + /// Sanitized transactions must have at least one RW signature because + /// the number of signatures must be greater than or equal to the message + /// header value `num_required_signatures` which must be greater than 0 + /// itself. + pub fn signature(&self) -> &Signature { + &self.signatures[0] + } + + pub fn signatures(&self) -> &[Signature] { + &self.signatures + } + + pub fn message(&self) -> &SanitizedMessage { + &self.message + } + + pub fn message_hash(&self) -> &Hash { + &self.message_hash + } + + pub fn to_versioned_transaction(&self) -> VersionedTransaction { + let signatures = self.signatures.clone(); + match &self.message { + SanitizedMessage::V0(mapped_msg) => VersionedTransaction { + signatures, + message: VersionedMessage::V0(mapped_msg.message.clone()), + }, + SanitizedMessage::Legacy(message) => VersionedTransaction { + signatures, + message: VersionedMessage::Legacy(message.clone()), + }, + } + } + + pub fn get_account_locks(&self) -> TransactionAccountLocks { + let message = &self.message; + let num_readonly_accounts = message.num_readonly_accounts(); + let num_writable_accounts = message + .account_keys_len() + .saturating_sub(num_readonly_accounts); + + let mut account_locks = TransactionAccountLocks { + writable: Vec::with_capacity(num_writable_accounts), + readonly: Vec::with_capacity(num_readonly_accounts), + }; + + for (i, key) in message.account_keys_iter().enumerate() { + if message.is_writable(i) { + account_locks.writable.push(key); + } else { + account_locks.readonly.push(key); + } + } + + account_locks + } + + pub fn get_durable_nonce(&self) -> Option<&Pubkey> { + self.message + .instructions() + .get(NONCED_TX_MARKER_IX_INDEX as usize) + .filter( + |ix| match self.message.get_account_key(ix.program_id_index as usize) { + Some(program_id) => system_program::check_id(program_id), + _ => false, + }, + ) + .filter(|ix| { + matches!( + limited_deserialize(&ix.data), + Ok(SystemInstruction::AdvanceNonceAccount) + ) + }) + .and_then(|ix| { + ix.accounts.get(0).and_then(|idx| { + let idx = *idx as usize; + self.message.get_account_key(idx) + }) + }) + } + + /// Return the serialized message data to sign. + fn message_data(&self) -> Vec { + match &self.message { + SanitizedMessage::Legacy(message) => message.serialize(), + SanitizedMessage::V0(mapped_msg) => mapped_msg.message.serialize(), + } + } + + /// Verify the length of signatures matches the value in the message header + pub fn verify_signatures_len(&self) -> bool { + self.signatures.len() == self.message.header().num_required_signatures as usize + } + + /// Verify the transaction + pub fn verify(&self) -> Result<()> { + let message_bytes = self.message_data(); + if self + .signatures + .iter() + .zip(self.message.account_keys_iter()) + .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes)) + .any(|verified| !verified) + { + Err(TransactionError::SignatureFailure) + } else { + Ok(()) + } + } + + pub fn verify_precompiles(&self, libsecp256k1_0_5_upgrade_enabled: bool) -> Result<()> { + for (program_id, instruction) in self.message.program_instructions_iter() { + if secp256k1_program::check_id(program_id) { + let instruction_datas: Vec<_> = self + .message + .instructions() + .iter() + .map(|instruction| instruction.data.as_ref()) + .collect(); + let data = &instruction.data; + let e = verify_eth_addresses( + data, + &instruction_datas, + libsecp256k1_0_5_upgrade_enabled, + ); + e.map_err(|_| TransactionError::InvalidAccountIndex)?; + } + } + Ok(()) + } + + pub fn calculate_fee(&self, fee_calculator: &FeeCalculator) -> u64 { + let mut num_secp256k1_signatures: u64 = 0; + for (program_id, instruction) in self.message.program_instructions_iter() { + if secp256k1_program::check_id(program_id) && !instruction.data.is_empty() { + num_secp256k1_signatures += instruction.data[0] as u64; + } + } + + fee_calculator.lamports_per_signature + * (u64::from(self.message.header().num_required_signatures) + num_secp256k1_signatures) + } +} diff --git a/sdk/src/versioned_transaction.rs b/sdk/src/versioned_transaction.rs new file mode 100644 index 00000000000000..631d42c1a09926 --- /dev/null +++ b/sdk/src/versioned_transaction.rs @@ -0,0 +1,73 @@ +//! Defines a transaction which supports multiple versions of messages. + +#![cfg(feature = "full")] + +use { + crate::{ + hash::Hash, + message::VersionedMessage, + sanitize::{Sanitize, SanitizeError}, + short_vec, + signature::Signature, + transaction::{Result, Transaction, TransactionError}, + }, + serde::Serialize, +}; + +#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)] +pub struct VersionedTransaction { + #[serde(with = "short_vec")] + pub signatures: Vec, + pub message: VersionedMessage, +} + +impl Sanitize for VersionedTransaction { + fn sanitize(&self) -> std::result::Result<(), SanitizeError> { + if self.message.header().num_required_signatures as usize > self.signatures.len() { + return Err(SanitizeError::IndexOutOfBounds); + } + if self.signatures.len() > self.message.unmapped_keys_len() { + return Err(SanitizeError::IndexOutOfBounds); + } + self.message.sanitize() + } +} + +impl From for VersionedTransaction { + fn from(transaction: Transaction) -> Self { + Self { + signatures: transaction.signatures, + message: VersionedMessage::Legacy(transaction.message), + } + } +} + +impl VersionedTransaction { + /// Returns a version-less transaction if the transaction message is an + /// original version-less message. + pub fn original_transaction(self) -> Option { + match self.message { + VersionedMessage::Legacy(message) => Some(Transaction { + signatures: self.signatures, + message, + }), + _ => None, + } + } + + /// Verify the transaction and hash its message + pub fn verify_and_hash_message(&self) -> Result { + let message_bytes = self.message.serialize(); + if self + .signatures + .iter() + .zip(self.message.unmapped_keys_iter()) + .map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes)) + .any(|verified| !verified) + { + Err(TransactionError::SignatureFailure) + } else { + Ok(VersionedMessage::hash_raw_message(&message_bytes)) + } + } +} diff --git a/storage-bigtable/src/bigtable.rs b/storage-bigtable/src/bigtable.rs index a3d4b172d5d473..e1f61a50ad4831 100644 --- a/storage-bigtable/src/bigtable.rs +++ b/storage-bigtable/src/bigtable.rs @@ -665,7 +665,9 @@ mod tests { use super::*; use crate::StoredConfirmedBlock; use prost::Message; - use solana_sdk::{hash::Hash, signature::Keypair, system_transaction}; + use solana_sdk::{ + hash::Hash, message::MappedAddresses, signature::Keypair, system_transaction, + }; use solana_storage_proto::convert::generated; use solana_transaction_status::{ ConfirmedBlock, TransactionStatusMeta, TransactionWithStatusMeta, @@ -689,6 +691,7 @@ mod tests { pre_token_balances: Some(vec![]), post_token_balances: Some(vec![]), rewards: Some(vec![]), + mapped_addresses: Some(MappedAddresses::default()), }), }; let block = ConfirmedBlock { diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index d4e4c763d5ef69..306debc4150450 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -200,6 +200,7 @@ impl From for TransactionStatusMeta { pre_token_balances: None, post_token_balances: None, rewards: None, + mapped_addresses: None, } } } diff --git a/storage-proto/proto/confirmed_block.proto b/storage-proto/proto/confirmed_block.proto index 57f119febe1cdd..980d6767dc9444 100644 --- a/storage-proto/proto/confirmed_block.proto +++ b/storage-proto/proto/confirmed_block.proto @@ -45,6 +45,7 @@ message TransactionStatusMeta { repeated TokenBalance pre_token_balances = 7; repeated TokenBalance post_token_balances = 8; repeated Reward rewards = 9; + MappedAddresses mapped_addresses = 10; } message TransactionError { @@ -102,3 +103,8 @@ message UnixTimestamp { message BlockHeight { uint64 block_height = 1; } + +message MappedAddresses { + repeated bytes writable = 1; + repeated bytes readonly = 2; +} diff --git a/storage-proto/src/convert.rs b/storage-proto/src/convert.rs index 8e33dbc93dd1be..fa6c2a73fc88cd 100644 --- a/storage-proto/src/convert.rs +++ b/storage-proto/src/convert.rs @@ -5,7 +5,7 @@ use { hash::Hash, instruction::CompiledInstruction, instruction::InstructionError, - message::{Message, MessageHeader}, + message::{MappedAddresses, Message, MessageHeader}, pubkey::Pubkey, signature::Signature, transaction::Transaction, @@ -113,6 +113,40 @@ impl From for Reward { } } +impl From for generated::MappedAddresses { + fn from(mapped_addresses: MappedAddresses) -> Self { + Self { + writable: mapped_addresses + .writable + .into_iter() + .map(|key| >::as_ref(&key).into()) + .collect(), + readonly: mapped_addresses + .readonly + .into_iter() + .map(|key| >::as_ref(&key).into()) + .collect(), + } + } +} + +impl From for MappedAddresses { + fn from(value: generated::MappedAddresses) -> Self { + Self { + writable: value + .writable + .into_iter() + .map(|key| Pubkey::new(&key)) + .collect(), + readonly: value + .readonly + .into_iter() + .map(|key| Pubkey::new(&key)) + .collect(), + } + } +} + impl From for generated::ConfirmedBlock { fn from(confirmed_block: ConfirmedBlock) -> Self { let ConfirmedBlock { @@ -276,6 +310,7 @@ impl From for generated::TransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = value; let err = match status { Ok(()) => None, @@ -304,6 +339,7 @@ impl From for generated::TransactionStatusMeta { .into_iter() .map(|reward| reward.into()) .collect(); + let mapped_addresses = mapped_addresses.map(|mapped_addresses| mapped_addresses.into()); Self { err, @@ -315,6 +351,7 @@ impl From for generated::TransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } } } @@ -340,6 +377,7 @@ impl TryFrom for TransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = value; let status = match &err { None => Ok(()), @@ -365,6 +403,7 @@ impl TryFrom for TransactionStatusMeta { .collect(), ); let rewards = Some(rewards.into_iter().map(|reward| reward.into()).collect()); + let mapped_addresses = mapped_addresses.map(|mapped_addresses| mapped_addresses.into()); Ok(Self { status, fee, @@ -375,6 +414,7 @@ impl TryFrom for TransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, }) } } diff --git a/storage-proto/src/lib.rs b/storage-proto/src/lib.rs index bcb876e65ad9b6..63eaaa6ce21e14 100644 --- a/storage-proto/src/lib.rs +++ b/storage-proto/src/lib.rs @@ -4,7 +4,9 @@ use { parse_token::{real_number_string_trimmed, UiTokenAmount}, StringAmount, }, - solana_sdk::{deserialize_utils::default_on_eof, transaction::Result}, + solana_sdk::{ + deserialize_utils::default_on_eof, message::MappedAddresses, transaction::Result, + }, solana_transaction_status::{ InnerInstructions, Reward, RewardType, TransactionStatusMeta, TransactionTokenBalance, }, @@ -159,6 +161,8 @@ pub struct StoredTransactionStatusMeta { pub post_token_balances: Option>, #[serde(deserialize_with = "default_on_eof")] pub rewards: Option>, + #[serde(deserialize_with = "default_on_eof")] + pub mapped_addresses: Option, } impl From for TransactionStatusMeta { @@ -173,6 +177,7 @@ impl From for TransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = value; Self { status, @@ -187,6 +192,7 @@ impl From for TransactionStatusMeta { .map(|balances| balances.into_iter().map(|balance| balance.into()).collect()), rewards: rewards .map(|rewards| rewards.into_iter().map(|reward| reward.into()).collect()), + mapped_addresses, } } } @@ -203,6 +209,7 @@ impl From for StoredTransactionStatusMeta { pre_token_balances, post_token_balances, rewards, + mapped_addresses, } = value; Self { status, @@ -217,6 +224,7 @@ impl From for StoredTransactionStatusMeta { .map(|balances| balances.into_iter().map(|balance| balance.into()).collect()), rewards: rewards .map(|rewards| rewards.into_iter().map(|reward| reward.into()).collect()), + mapped_addresses, } } } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index 6a145dbc080e36..8e6bdc3d3ba0a4 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -25,7 +25,7 @@ use solana_sdk::{ commitment_config::CommitmentConfig, deserialize_utils::default_on_eof, instruction::CompiledInstruction, - message::{Message, MessageHeader}, + message::{MappedAddresses, Message, MessageHeader}, pubkey::Pubkey, sanitize::Sanitize, signature::Signature, @@ -187,6 +187,8 @@ pub struct TransactionStatusMeta { pub post_token_balances: Option>, #[serde(deserialize_with = "default_on_eof")] pub rewards: Option, + #[serde(deserialize_with = "default_on_eof")] + pub mapped_addresses: Option, } impl Default for TransactionStatusMeta { @@ -201,6 +203,7 @@ impl Default for TransactionStatusMeta { pre_token_balances: None, post_token_balances: None, rewards: None, + mapped_addresses: None, } } } @@ -219,6 +222,7 @@ pub struct UiTransactionStatusMeta { pub pre_token_balances: Option>, pub post_token_balances: Option>, pub rewards: Option, + pub mapped_addresses: Option, } impl UiTransactionStatusMeta { @@ -242,6 +246,7 @@ impl UiTransactionStatusMeta { .post_token_balances .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), rewards: meta.rewards, + mapped_addresses: meta.mapped_addresses, } } } @@ -265,6 +270,7 @@ impl From for UiTransactionStatusMeta { .post_token_balances .map(|balance| balance.into_iter().map(|balance| balance.into()).collect()), rewards: meta.rewards, + mapped_addresses: meta.mapped_addresses, } } } diff --git a/transaction-status/src/token_balances.rs b/transaction-status/src/token_balances.rs index ed5f72711d5608..e48a44c7931f75 100644 --- a/transaction-status/src/token_balances.rs +++ b/transaction-status/src/token_balances.rs @@ -67,7 +67,6 @@ pub fn collect_token_balances( if transaction.message().is_invoked(index) || is_token_program(account_id) { continue; } - if let Some((mint, ui_token_amount)) = collect_token_balance_from_account(bank, account_id, &mut mint_decimals) {