diff --git a/core/src/banking_stage.rs b/core/src/banking_stage.rs index 499305d58f..c0316daa13 100644 --- a/core/src/banking_stage.rs +++ b/core/src/banking_stage.rs @@ -531,8 +531,19 @@ impl BankingStage { } else { vec![] }; - let (mut loaded_accounts, results, mut retryable_txs, tx_count, signature_count) = - bank.load_and_execute_transactions(batch, MAX_PROCESSING_AGE, None); + let ( + mut loaded_accounts, + results, + inner_instructions, + mut retryable_txs, + tx_count, + signature_count, + ) = bank.load_and_execute_transactions( + batch, + MAX_PROCESSING_AGE, + None, + transaction_status_sender.is_some(), + ); load_execute_time.stop(); let freeze_lock = bank.freeze_lock(); @@ -569,6 +580,7 @@ impl BankingStage { batch.iteration_order_vec(), tx_results.processing_results, TransactionBalancesSet::new(pre_balances, post_balances), + inner_instructions, sender, ); } diff --git a/core/src/transaction_status_service.rs b/core/src/transaction_status_service.rs index 86e4a467e3..108f482f71 100644 --- a/core/src/transaction_status_service.rs +++ b/core/src/transaction_status_service.rs @@ -1,11 +1,12 @@ use crossbeam_channel::{Receiver, RecvTimeoutError}; +use itertools::izip; use solana_ledger::{blockstore::Blockstore, blockstore_processor::TransactionStatusBatch}; use solana_runtime::{ bank::{Bank, HashAgeKind}, nonce_utils, transaction_utils::OrderedIterator, }; -use solana_transaction_status::TransactionStatusMeta; +use solana_transaction_status::{InnerInstructions, TransactionStatusMeta}; use std::{ sync::{ atomic::{AtomicBool, Ordering}, @@ -54,15 +55,23 @@ impl TransactionStatusService { iteration_order, statuses, balances, + inner_instructions, } = write_transaction_status_receiver.recv_timeout(Duration::from_secs(1))?; let slot = bank.slot(); - for ((((_, transaction), (status, hash_age_kind)), pre_balances), post_balances) in - OrderedIterator::new(&transactions, iteration_order.as_deref()) - .zip(statuses) - .zip(balances.pre_balances) - .zip(balances.post_balances) - { + for ( + (_, transaction), + (status, hash_age_kind), + pre_balances, + post_balances, + inner_instructions, + ) in izip!( + OrderedIterator::new(&transactions, iteration_order.as_deref()), + statuses, + balances.pre_balances, + balances.post_balances, + inner_instructions + ) { if Bank::can_commit(&status) && !transaction.signatures.is_empty() { let fee_calculator = match hash_age_kind { Some(HashAgeKind::DurableNonce(_, account)) => { @@ -77,6 +86,19 @@ impl TransactionStatusService { ); let (writable_keys, readonly_keys) = transaction.message.get_account_keys_by_lock_type(); + + let inner_instructions = inner_instructions.map(|inner_instructions| { + inner_instructions + .into_iter() + .enumerate() + .map(|(index, instructions)| InnerInstructions { + index: index as u8, + instructions, + }) + .filter(|i| !i.instructions.is_empty()) + .collect() + }); + blockstore .write_transaction_status( slot, @@ -88,6 +110,7 @@ impl TransactionStatusService { fee, pre_balances, post_balances, + inner_instructions, }, ) .expect("Expect database write to succeed"); diff --git a/docs/src/apps/jsonrpc-api.md b/docs/src/apps/jsonrpc-api.md index fc358dc289..d9176468a7 100644 --- a/docs/src/apps/jsonrpc-api.md +++ b/docs/src/apps/jsonrpc-api.md @@ -332,6 +332,7 @@ The result field will be an object with the following fields: - `fee: ` - fee this transaction was charged, as u64 integer - `preBalances: ` - array of u64 account balances from before the transaction was processed - `postBalances: ` - array of u64 account balances after the transaction was processed + - `innerInstructions: ` - List of [inner instructions](#inner-instructions-structure) or omitted if inner instruction recording was not yet enabled during this transaction - DEPRECATED: `status: ` - Transaction status - `"Ok": ` - Transaction was successful - `"Err": ` - Transaction failed with TransactionError @@ -347,13 +348,13 @@ The result field will be an object with the following fields: curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedBlock","params":[430, "json"]}' localhost:8899 // Result -{"jsonrpc":"2.0","result":{"blockTime":null,"blockhash":"3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA","parentSlot":429,"previousBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B","rewards":[],"transactions":[{"meta":{"err":null,"fee":5000,"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"transaction":{"message":{"accountKeys":["3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe","AjozzgE83A3x1sHNUR64hfH7zaEBWeMaFuAN9kQgujrc","SysvarS1otHashes111111111111111111111111111","SysvarC1ock11111111111111111111111111111111","Vote111111111111111111111111111111111111111"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":3,"numRequiredSignatures":1},"instructions":[{"accounts":[1,2,3,0],"data":"37u9WtQpcm6ULa3WRQHmj49EPs4if7o9f1jSRVZpm2dvihR9C8jY4NqEwXUbLwx15HBSNcP1","programIdIndex":4}],"recentBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B"},"signatures":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv"]}}]},"id":1} +{"jsonrpc":"2.0","result":{"blockTime":null,"blockhash":"3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA","parentSlot":429,"previousBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B","rewards":[],"transactions":[{"meta":{"err":null,"fee":5000,"innerInstructions":[],"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"transaction":{"message":{"accountKeys":["3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe","AjozzgE83A3x1sHNUR64hfH7zaEBWeMaFuAN9kQgujrc","SysvarS1otHashes111111111111111111111111111","SysvarC1ock11111111111111111111111111111111","Vote111111111111111111111111111111111111111"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":3,"numRequiredSignatures":1},"instructions":[{"accounts":[1,2,3,0],"data":"37u9WtQpcm6ULa3WRQHmj49EPs4if7o9f1jSRVZpm2dvihR9C8jY4NqEwXUbLwx15HBSNcP1","programIdIndex":4}],"recentBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B"},"signatures":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv"]}}]},"id":1} // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedBlock","params":[430, "base64"]}' localhost:8899 // Result -{"jsonrpc":"2.0","result":{"blockTime":null,"blockhash":"3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA","parentSlot":429,"previousBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B","rewards":[],"transactions":[{"meta":{"err":null,"fee":5000,"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"transaction":["AVj7dxHlQ9IrvdYVIjuiRFs1jLaDMHixgrv+qtHBwz51L4/ImLZhszwiyEJDIp7xeBSpm/TX5B7mYzxa+fPOMw0BAAMFJMJVqLw+hJYheizSoYlLm53KzgT82cDVmazarqQKG2GQsLgiqktA+a+FDR4/7xnDX7rsusMwryYVUdixfz1B1Qan1RcZLwqvxvJl4/t3zHragsUp0L47E24tAFUgAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAHYUgdNXR0u3xNdiTr072z2DVec9EQQ/wNo1OAAAAAAAtxOUhPBp2WSjUNJEgfvy70BbxI00fZyEPvFHNfxrtEAQQEAQIDADUCAAAAAQAAAAAAAACtAQAAAAAAAAdUE18R96XTJCe+YfRfUp6WP+YKCy/72ucOL8AoBFSpAA==","base64"]}]},"id":1} +{"jsonrpc":"2.0","result":{"blockTime":null,"blockhash":"3Eq21vXNB5s86c62bVuUfTeaMif1N2kUqRPBmGRJhyTA","parentSlot":429,"previousBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B","rewards":[],"transactions":[{"meta":{"err":null,"fee":5000,"innerInstructions":[],"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"transaction":["AVj7dxHlQ9IrvdYVIjuiRFs1jLaDMHixgrv+qtHBwz51L4/ImLZhszwiyEJDIp7xeBSpm/TX5B7mYzxa+fPOMw0BAAMFJMJVqLw+hJYheizSoYlLm53KzgT82cDVmazarqQKG2GQsLgiqktA+a+FDR4/7xnDX7rsusMwryYVUdixfz1B1Qan1RcZLwqvxvJl4/t3zHragsUp0L47E24tAFUgAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAHYUgdNXR0u3xNdiTr072z2DVec9EQQ/wNo1OAAAAAAAtxOUhPBp2WSjUNJEgfvy70BbxI00fZyEPvFHNfxrtEAQQEAQIDADUCAAAAAQAAAAAAAACtAQAAAAAAAAdUE18R96XTJCe+YfRfUp6WP+YKCy/72ucOL8AoBFSpAA==","base64"]}]},"id":1} ``` #### Transaction Structure @@ -375,6 +376,18 @@ The JSON structure of a transaction is defined as follows: - `accounts: ` - List of ordered indices into the `message.accountKeys` array indicating which accounts to pass to the program. - `data: ` - The program input data encoded in a base-58 string. +#### Inner Instructions Structure + +The Solana runtime records the cross-program instructions that are invoked during transaction processing and makes these available for greater transparency of what was executed on-chain per transaction instruction. Invoked instructions are grouped by the originating transaction instruction and are listed in order of processing. + +The JSON structure of inner instructions is defined as a list of objects in the following structure: + +- `index: number` - Index of the transaction instruction from which the inner instruction(s) originated +- `instructions: ` - Ordered list of inner program instructions that were invoked during a single transaction instruction. + - `programIdIndex: ` - Index into the `message.accountKeys` array indicating the program account that executes this instruction. + - `accounts: ` - List of ordered indices into the `message.accountKeys` array indicating which accounts to pass to the program. + - `data: ` - The program input data encoded in a base-58 string. + ### getConfirmedBlocks Returns a list of confirmed blocks @@ -485,6 +498,7 @@ N encoding attempts to use program-specific instruction parsers to return more h - `fee: ` - fee this transaction was charged, as u64 integer - `preBalances: ` - array of u64 account balances from before the transaction was processed - `postBalances: ` - array of u64 account balances after the transaction was processed + - `innerInstructions: ` - List of [inner instructions](#inner-instructions-structure) or omitted if inner instruction recording was not yet enabled during this transaction - DEPRECATED: `status: ` - Transaction status - `"Ok": ` - Transaction was successful - `"Err": ` - Transaction failed with TransactionError @@ -496,13 +510,13 @@ N encoding attempts to use program-specific instruction parsers to return more h curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedTransaction","params":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv", "json"]}' localhost:8899 // Result -{"jsonrpc":"2.0","result":{"meta":{"err":null,"fee":5000,"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"slot":430,"transaction":{"message":{"accountKeys":["3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe","AjozzgE83A3x1sHNUR64hfH7zaEBWeMaFuAN9kQgujrc","SysvarS1otHashes111111111111111111111111111","SysvarC1ock11111111111111111111111111111111","Vote111111111111111111111111111111111111111"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":3,"numRequiredSignatures":1},"instructions":[{"accounts":[1,2,3,0],"data":"37u9WtQpcm6ULa3WRQHmj49EPs4if7o9f1jSRVZpm2dvihR9C8jY4NqEwXUbLwx15HBSNcP1","programIdIndex":4}],"recentBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B"},"signatures":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv"]}},"id":1} +{"jsonrpc":"2.0","result":{"meta":{"err":null,"fee":5000,"innerInstructions":[],"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"slot":430,"transaction":{"message":{"accountKeys":["3UVYmECPPMZSCqWKfENfuoTv51fTDTWicX9xmBD2euKe","AjozzgE83A3x1sHNUR64hfH7zaEBWeMaFuAN9kQgujrc","SysvarS1otHashes111111111111111111111111111","SysvarC1ock11111111111111111111111111111111","Vote111111111111111111111111111111111111111"],"header":{"numReadonlySignedAccounts":0,"numReadonlyUnsignedAccounts":3,"numRequiredSignatures":1},"instructions":[{"accounts":[1,2,3,0],"data":"37u9WtQpcm6ULa3WRQHmj49EPs4if7o9f1jSRVZpm2dvihR9C8jY4NqEwXUbLwx15HBSNcP1","programIdIndex":4}],"recentBlockhash":"mfcyqEXB3DnHXki6KjjmZck6YjmZLvpAByy2fj4nh6B"},"signatures":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv"]}},"id":1} // Request curl -X POST -H "Content-Type: application/json" -d '{"jsonrpc": "2.0","id":1,"method":"getConfirmedTransaction","params":["2nBhEBYYvfaAe16UMNqRHre4YNSskvuYgx3M6E4JP1oDYvZEJHvoPzyUidNgNX5r9sTyN1J9UxtbCXy2rqYcuyuv", "base64"]}' localhost:8899 // Result -{"jsonrpc":"2.0","result":{"meta":{"err":null,"fee":5000,"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"slot":430,"transaction":["AVj7dxHlQ9IrvdYVIjuiRFs1jLaDMHixgrv+qtHBwz51L4/ImLZhszwiyEJDIp7xeBSpm/TX5B7mYzxa+fPOMw0BAAMFJMJVqLw+hJYheizSoYlLm53KzgT82cDVmazarqQKG2GQsLgiqktA+a+FDR4/7xnDX7rsusMwryYVUdixfz1B1Qan1RcZLwqvxvJl4/t3zHragsUp0L47E24tAFUgAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAHYUgdNXR0u3xNdiTr072z2DVec9EQQ/wNo1OAAAAAAAtxOUhPBp2WSjUNJEgfvy70BbxI00fZyEPvFHNfxrtEAQQEAQIDADUCAAAAAQAAAAAAAACtAQAAAAAAAAdUE18R96XTJCe+YfRfUp6WP+YKCy/72ucOL8AoBFSpAA==","base64"]},"id":1} +{"jsonrpc":"2.0","result":{"meta":{"err":null,"fee":5000,"innerInstructions":[],"postBalances":[499998932500,26858640,1,1,1],"preBalances":[499998937500,26858640,1,1,1],"status":{"Ok":null}},"slot":430,"transaction":["AVj7dxHlQ9IrvdYVIjuiRFs1jLaDMHixgrv+qtHBwz51L4/ImLZhszwiyEJDIp7xeBSpm/TX5B7mYzxa+fPOMw0BAAMFJMJVqLw+hJYheizSoYlLm53KzgT82cDVmazarqQKG2GQsLgiqktA+a+FDR4/7xnDX7rsusMwryYVUdixfz1B1Qan1RcZLwqvxvJl4/t3zHragsUp0L47E24tAFUgAAAABqfVFxjHdMkoVmOYaR1etoteuKObS21cc1VbIQAAAAAHYUgdNXR0u3xNdiTr072z2DVec9EQQ/wNo1OAAAAAAAtxOUhPBp2WSjUNJEgfvy70BbxI00fZyEPvFHNfxrtEAQQEAQIDADUCAAAAAQAAAAAAAACtAQAAAAAAAAdUE18R96XTJCe+YfRfUp6WP+YKCy/72ucOL8AoBFSpAA==","base64"]},"id":1} ``` ### getEpochInfo diff --git a/ledger/src/blockstore.rs b/ledger/src/blockstore.rs index e8fa6d2086..f1368fcd55 100644 --- a/ledger/src/blockstore.rs +++ b/ledger/src/blockstore.rs @@ -3474,6 +3474,7 @@ pub mod tests { signature::Signature, transaction::TransactionError, }; + use solana_transaction_status::InnerInstructions; use solana_vote_program::{vote_instruction, vote_state::Vote}; use std::{iter::FromIterator, time::Duration}; @@ -5671,6 +5672,7 @@ pub mod tests { fee: 42, pre_balances: pre_balances.clone(), post_balances: post_balances.clone(), + inner_instructions: Some(vec![]), }, ) .unwrap(); @@ -5683,6 +5685,7 @@ pub mod tests { fee: 42, pre_balances: pre_balances.clone(), post_balances: post_balances.clone(), + inner_instructions: Some(vec![]), }, ) .unwrap(); @@ -5693,6 +5696,7 @@ pub mod tests { fee: 42, pre_balances, post_balances, + inner_instructions: Some(vec![]), }), } }) @@ -5985,6 +5989,10 @@ pub mod tests { let pre_balances_vec = vec![1, 2, 3]; let post_balances_vec = vec![3, 2, 1]; + let inner_instructions_vec = vec![InnerInstructions { + index: 0, + instructions: vec![CompiledInstruction::new(1, &(), vec![0])], + }]; // result not found assert!(transaction_status_cf @@ -6003,6 +6011,7 @@ pub mod tests { fee: 5u64, pre_balances: pre_balances_vec.clone(), post_balances: post_balances_vec.clone(), + inner_instructions: Some(inner_instructions_vec.clone()), }, ) .is_ok()); @@ -6013,6 +6022,7 @@ pub mod tests { fee, pre_balances, post_balances, + inner_instructions, } = transaction_status_cf .get((0, Signature::default(), 0)) .unwrap() @@ -6021,6 +6031,7 @@ pub mod tests { assert_eq!(fee, 5u64); assert_eq!(pre_balances, pre_balances_vec); assert_eq!(post_balances, post_balances_vec); + assert_eq!(inner_instructions.unwrap(), inner_instructions_vec); // insert value assert!(transaction_status_cf @@ -6031,6 +6042,7 @@ pub mod tests { fee: 9u64, pre_balances: pre_balances_vec.clone(), post_balances: post_balances_vec.clone(), + inner_instructions: Some(inner_instructions_vec.clone()), }, ) .is_ok()); @@ -6041,6 +6053,7 @@ pub mod tests { fee, pre_balances, post_balances, + inner_instructions, } = transaction_status_cf .get((0, Signature::new(&[2u8; 64]), 9)) .unwrap() @@ -6051,6 +6064,7 @@ pub mod tests { assert_eq!(fee, 9u64); assert_eq!(pre_balances, pre_balances_vec); assert_eq!(post_balances, post_balances_vec); + assert_eq!(inner_instructions.unwrap(), inner_instructions_vec); } Blockstore::destroy(&blockstore_path).expect("Expected successful database destruction"); } @@ -6277,6 +6291,7 @@ pub mod tests { fee: 42u64, pre_balances: pre_balances_vec, post_balances: post_balances_vec, + inner_instructions: Some(vec![]), }; let signature1 = Signature::new(&[1u8; 64]); @@ -6406,6 +6421,10 @@ pub mod tests { pre_balances.push(i as u64 * 10); post_balances.push(i as u64 * 11); } + let inner_instructions = Some(vec![InnerInstructions { + index: 0, + instructions: vec![CompiledInstruction::new(1, &(), vec![0])], + }]); let signature = transaction.signatures[0]; blockstore .transaction_status_cf @@ -6416,6 +6435,7 @@ pub mod tests { fee: 42, pre_balances: pre_balances.clone(), post_balances: post_balances.clone(), + inner_instructions: inner_instructions.clone(), }, ) .unwrap(); @@ -6426,6 +6446,7 @@ pub mod tests { fee: 42, pre_balances, post_balances, + inner_instructions, }), } }) @@ -6865,6 +6886,7 @@ pub mod tests { fee: x, pre_balances: vec![], post_balances: vec![], + inner_instructions: Some(vec![]), }, ) .unwrap(); diff --git a/ledger/src/blockstore_processor.rs b/ledger/src/blockstore_processor.rs index 115e824640..3c20ede8ec 100644 --- a/ledger/src/blockstore_processor.rs +++ b/ledger/src/blockstore_processor.rs @@ -15,7 +15,10 @@ use solana_measure::{measure::Measure, thread_mem_usage}; use solana_metrics::{datapoint_error, inc_new_counter_debug}; use solana_rayon_threadlimit::get_thread_count; use solana_runtime::{ - bank::{Bank, TransactionBalancesSet, TransactionProcessResult, TransactionResults}, + bank::{ + Bank, InnerInstructionsList, TransactionBalancesSet, TransactionProcessResult, + TransactionResults, + }, bank_forks::BankForks, bank_utils, commitment::VOTE_THRESHOLD_SIZE, @@ -100,11 +103,13 @@ fn execute_batch( transaction_status_sender: Option, replay_vote_sender: Option<&ReplayVoteSender>, ) -> Result<()> { - let (tx_results, balances) = batch.bank().load_execute_and_commit_transactions( - batch, - MAX_PROCESSING_AGE, - transaction_status_sender.is_some(), - ); + let (tx_results, balances, inner_instructions) = + batch.bank().load_execute_and_commit_transactions( + batch, + MAX_PROCESSING_AGE, + transaction_status_sender.is_some(), + transaction_status_sender.is_some(), + ); bank_utils::find_and_send_votes(batch.transactions(), &tx_results, replay_vote_sender); @@ -121,6 +126,7 @@ fn execute_batch( batch.iteration_order_vec(), processing_results, balances, + inner_instructions, sender, ); } @@ -1048,7 +1054,9 @@ pub struct TransactionStatusBatch { pub iteration_order: Option>, pub statuses: Vec, pub balances: TransactionBalancesSet, + pub inner_instructions: Vec>, } + pub type TransactionStatusSender = Sender; pub fn send_transaction_status_batch( @@ -1057,6 +1065,7 @@ pub fn send_transaction_status_batch( iteration_order: Option>, statuses: Vec, balances: TransactionBalancesSet, + inner_instructions: Vec>, transaction_status_sender: TransactionStatusSender, ) { let slot = bank.slot(); @@ -1066,6 +1075,7 @@ pub fn send_transaction_status_batch( iteration_order, statuses, balances, + inner_instructions, }) { trace!( "Slot {} transaction_status send batch failed: {:?}", @@ -2913,9 +2923,13 @@ pub mod tests { .. }, _balances, - ) = batch - .bank() - .load_execute_and_commit_transactions(&batch, MAX_PROCESSING_AGE, false); + _inner_instructions, + ) = batch.bank().load_execute_and_commit_transactions( + &batch, + MAX_PROCESSING_AGE, + false, + false, + ); let (err, signature) = get_first_error(&batch, fee_collection_results).unwrap(); // First error found should be for the 2nd transaction, due to iteration_order assert_eq!(err.unwrap_err(), TransactionError::AccountNotFound); diff --git a/programs/bpf/benches/bpf_loader.rs b/programs/bpf/benches/bpf_loader.rs index 4c76fd969e..214887ccab 100644 --- a/programs/bpf/benches/bpf_loader.rs +++ b/programs/bpf/benches/bpf_loader.rs @@ -227,6 +227,7 @@ impl InvokeContext for MockInvokeContext { fn get_executor(&mut self, _pubkey: &Pubkey) -> Option> { None } + fn record_instruction(&self, _instruction: &Instruction) {} } #[derive(Debug, Default, Clone)] pub struct MockLogger { diff --git a/programs/bpf/c/src/invoke/invoke.c b/programs/bpf/c/src/invoke/invoke.c index 4c67e36a66..88c745f015 100644 --- a/programs/bpf/c/src/invoke/invoke.c +++ b/programs/bpf/c/src/invoke/invoke.c @@ -251,9 +251,9 @@ extern uint64_t entrypoint(const uint8_t *input) { sol_assert(SUCCESS == sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts))); + // Signer privilege escalation will always fail the whole transaction instruction.accounts[0].is_signer = true; - sol_assert(SUCCESS != - sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts))); + sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts)); break; } case TEST_PRIVILEGE_ESCALATION_WRITABLE: { @@ -267,9 +267,9 @@ extern uint64_t entrypoint(const uint8_t *input) { sol_assert(SUCCESS == sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts))); + // Writable privilege escalation will always fail the whole transaction instruction.accounts[0].is_writable = true; - sol_assert(SUCCESS != - sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts))); + sol_invoke(&instruction, accounts, SOL_ARRAY_SIZE(accounts)); break; } default: diff --git a/programs/bpf/rust/invoke/src/lib.rs b/programs/bpf/rust/invoke/src/lib.rs index 561d6403f2..d1ba88fe86 100644 --- a/programs/bpf/rust/invoke/src/lib.rs +++ b/programs/bpf/rust/invoke/src/lib.rs @@ -239,6 +239,7 @@ fn process_instruction( ); invoke(&invoked_instruction, accounts)?; + // Signer privilege escalation will always fail the whole transaction invoked_instruction.accounts[0].is_signer = true; assert_eq!( invoke(&invoked_instruction, accounts), @@ -254,6 +255,7 @@ fn process_instruction( ); invoke(&invoked_instruction, accounts)?; + // Writable privilege escalation will always fail the whole transaction invoked_instruction.accounts[0].is_writable = true; assert_eq!( invoke(&invoked_instruction, accounts), diff --git a/programs/bpf/tests/programs.rs b/programs/bpf/tests/programs.rs index f9d8fd5a8c..71173fd634 100644 --- a/programs/bpf/tests/programs.rs +++ b/programs/bpf/tests/programs.rs @@ -18,7 +18,7 @@ use solana_sdk::{ account::{Account, KeyedAccount}, bpf_loader, bpf_loader_deprecated, client::SyncClient, - clock::DEFAULT_SLOTS_PER_EPOCH, + clock::{DEFAULT_SLOTS_PER_EPOCH, MAX_PROCESSING_AGE}, entrypoint::{MAX_PERMITTED_DATA_INCREASE, SUCCESS}, entrypoint_native::{ ComputeBudget, ComputeMeter, Executor, InvokeContext, Logger, ProcessInstruction, @@ -28,7 +28,7 @@ use solana_sdk::{ pubkey::Pubkey, signature::{Keypair, Signer}, sysvar::{clock, fees, rent, rewards, slot_hashes, stake_history}, - transaction::TransactionError, + transaction::{Transaction, TransactionError}, }; use std::{cell::RefCell, env, fs::File, io::Read, path::PathBuf, rc::Rc, sync::Arc}; @@ -98,6 +98,26 @@ fn run_program( Ok(vm.get_total_instruction_count()) } +fn process_transaction_and_record_inner( + bank: &Bank, + tx: Transaction, +) -> (Result<(), TransactionError>, Vec>) { + let signature = tx.signatures.get(0).unwrap().clone(); + let txs = vec![tx]; + let tx_batch = bank.prepare_batch(&txs, None); + let (mut results, _, mut inner) = + bank.load_execute_and_commit_transactions(&tx_batch, MAX_PROCESSING_AGE, false, true); + let inner_instructions = inner.swap_remove(0); + let result = results + .fee_collection_results + .swap_remove(0) + .and_then(|_| bank.get_signature_status(&signature).unwrap()); + ( + result, + inner_instructions.expect("cpi recording should be enabled"), + ) +} + #[test] #[cfg(any(feature = "bpf_c", feature = "bpf_rust"))] fn test_program_bpf_sanity() { @@ -482,61 +502,91 @@ fn test_program_bpf_invoke() { account_metas.clone(), ); let message = Message::new(&[instruction], Some(&mint_pubkey)); - assert!(bank_client - .send_and_confirm_message( - &[ - &mint_keypair, - &argument_keypair, - &invoked_argument_keypair, - &from_keypair - ], - message, - ) - .is_ok()); + let tx = Transaction::new( + &[ + &mint_keypair, + &argument_keypair, + &invoked_argument_keypair, + &from_keypair, + ], + message.clone(), + bank.last_blockhash(), + ); + let (result, inner_instructions) = process_transaction_and_record_inner(&bank, tx); + assert!(result.is_ok()); + let invoked_programs: Vec = inner_instructions[0] + .iter() + .map(|ix| message.account_keys[ix.program_id_index as usize].clone()) + .collect(); + assert_eq!( + invoked_programs, + vec![ + solana_sdk::system_program::id(), + solana_sdk::system_program::id(), + invoked_program_id.clone(), + invoked_program_id.clone(), + invoked_program_id.clone(), + invoked_program_id.clone(), + invoked_program_id.clone(), + invoked_program_id.clone(), + invoked_program_id.clone(), + ] + ); // failure cases let instruction = Instruction::new( invoke_program_id, - &TEST_PRIVILEGE_ESCALATION_SIGNER, + &[TEST_PRIVILEGE_ESCALATION_SIGNER, nonce1, nonce2, nonce3], account_metas.clone(), ); let message = Message::new(&[instruction], Some(&mint_pubkey)); + let tx = Transaction::new( + &[ + &mint_keypair, + &argument_keypair, + &invoked_argument_keypair, + &from_keypair, + ], + message.clone(), + bank.last_blockhash(), + ); + + let (result, inner_instructions) = process_transaction_and_record_inner(&bank, tx); + let invoked_programs: Vec = inner_instructions[0] + .iter() + .map(|ix| message.account_keys[ix.program_id_index as usize].clone()) + .collect(); + assert_eq!(invoked_programs, vec![invoked_program_id.clone()]); assert_eq!( - bank_client - .send_and_confirm_message( - &[ - &mint_keypair, - &argument_keypair, - &invoked_argument_keypair, - &from_keypair - ], - message, - ) - .unwrap_err() - .unwrap(), + result.unwrap_err(), TransactionError::InstructionError(0, InstructionError::Custom(194969602)) ); let instruction = Instruction::new( invoke_program_id, - &TEST_PRIVILEGE_ESCALATION_WRITABLE, + &[TEST_PRIVILEGE_ESCALATION_WRITABLE, nonce1, nonce2, nonce3], account_metas.clone(), ); let message = Message::new(&[instruction], Some(&mint_pubkey)); + let tx = Transaction::new( + &[ + &mint_keypair, + &argument_keypair, + &invoked_argument_keypair, + &from_keypair, + ], + message.clone(), + bank.last_blockhash(), + ); + let (result, inner_instructions) = process_transaction_and_record_inner(&bank, tx); + let invoked_programs: Vec = inner_instructions[0] + .iter() + .map(|ix| message.account_keys[ix.program_id_index as usize].clone()) + .collect(); + assert_eq!(invoked_programs, vec![invoked_program_id.clone()]); assert_eq!( - bank_client - .send_and_confirm_message( - &[ - &mint_keypair, - &argument_keypair, - &invoked_argument_keypair, - &from_keypair - ], - message, - ) - .unwrap_err() - .unwrap(), + result.unwrap_err(), TransactionError::InstructionError(0, InstructionError::Custom(194969602)) ); @@ -639,6 +689,7 @@ impl InvokeContext for MockInvokeContext { fn get_executor(&mut self, _pubkey: &Pubkey) -> Option> { None } + fn record_instruction(&self, _instruction: &Instruction) {} } #[derive(Debug, Default, Clone)] diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index a7d5590464..5e0319444d 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -275,6 +275,7 @@ mod tests { account::Account, entrypoint_native::{ComputeBudget, Logger, ProcessInstruction}, instruction::CompiledInstruction, + instruction::Instruction, message::Message, rent::Rent, }; @@ -360,6 +361,7 @@ mod tests { fn get_executor(&mut self, _pubkey: &Pubkey) -> Option> { None } + fn record_instruction(&self, _instruction: &Instruction) {} } struct TestInstructionMeter { @@ -587,6 +589,7 @@ mod tests { max_invoke_depth: 2, }, Rc::new(RefCell::new(Executors::default())), + None, ); assert_eq!( Err(InstructionError::Custom(194969602)), diff --git a/programs/bpf_loader/src/syscalls.rs b/programs/bpf_loader/src/syscalls.rs index 78954ae600..1510524309 100644 --- a/programs/bpf_loader/src/syscalls.rs +++ b/programs/bpf_loader/src/syscalls.rs @@ -1020,6 +1020,7 @@ fn call<'a>( ro_regions, )?; verify_instruction(syscall, &instruction, &signers)?; + invoke_context.record_instruction(&instruction); let message = Message::new(&[instruction], None); let callee_program_id_index = message.instructions[0].program_id_index as usize; let callee_program_id = message.account_keys[callee_program_id_index]; diff --git a/runtime/src/bank.rs b/runtime/src/bank.rs index 4da18f215f..ec259d7e15 100644 --- a/runtime/src/bank.rs +++ b/runtime/src/bank.rs @@ -12,6 +12,7 @@ use crate::{ blockhash_queue::BlockhashQueue, builtins::get_builtins, epoch_stakes::{EpochStakes, NodeVoteAccounts}, + instruction_recorder::InstructionRecorder, log_collector::LogCollector, message_processor::{Executors, MessageProcessor}, nonce_utils, @@ -46,6 +47,7 @@ use solana_sdk::{ hash::{extend_and_hash, hashv, Hash}, incinerator, inflation::Inflation, + instruction::CompiledInstruction, message::Message, native_loader, native_token::sol_to_lamports, @@ -299,6 +301,12 @@ impl TransactionBalancesSet { } pub type TransactionBalances = Vec>; +/// An ordered list of instructions that were invoked during a transaction instruction +pub type InnerInstructions = Vec; + +/// A list of instructions that were invoked during each instruction of a transaction +pub type InnerInstructionsList = Vec; + #[derive(Clone, Debug, Eq, PartialEq)] pub enum HashAgeKind { Extant, @@ -1535,6 +1543,7 @@ impl Bank { let ( _loaded_accounts, executed, + _inner_instructions, _retryable_transactions, _transaction_count, _signature_count, @@ -1542,6 +1551,7 @@ impl Bank { &batch, MAX_PROCESSING_AGE, Some(log_collector.clone()), + false, ); let transaction_result = executed[0].0.clone().map(|_| ()); let log_messages = Rc::try_unwrap(log_collector).unwrap_or_default().into(); @@ -1877,6 +1887,19 @@ impl Bank { }); } + fn compile_recorded_instructions( + inner_instructions: &mut Vec>, + instruction_recorders: Option>, + message: &Message, + ) { + inner_instructions.push(instruction_recorders.map(|instruction_recorders| { + instruction_recorders + .into_iter() + .map(|r| r.compile_instructions(message)) + .collect() + })); + } + /// Get any cached executors needed by the transaction fn get_executors( &self, @@ -1926,9 +1949,11 @@ impl Bank { batch: &TransactionBatch, max_age: usize, log_collector: Option>, + enable_cpi_recording: bool, ) -> ( Vec<(Result, Option)>, Vec, + Vec>, Vec, u64, u64, @@ -1969,6 +1994,8 @@ impl Bank { let mut execution_time = Measure::start("execution_time"); let mut signature_count: u64 = 0; + let mut inner_instructions: Vec> = + Vec::with_capacity(txs.len()); let executed: Vec = loaded_accounts .iter_mut() .zip(OrderedIterator::new(txs, batch.iteration_order())) @@ -1982,6 +2009,11 @@ impl Bank { let (account_refcells, loader_refcells) = Self::accounts_to_refcells(accounts, loaders); + let mut instruction_recorders = if enable_cpi_recording { + Some(Vec::new()) + } else { + None + }; let process_result = self.message_processor.process_message( tx.message(), &loader_refcells, @@ -1989,10 +2021,17 @@ impl Bank { &self.rent_collector, log_collector.clone(), executors.clone(), + instruction_recorders.as_mut(), self.cluster_type(), self.epoch(), ); + Self::compile_recorded_instructions( + &mut inner_instructions, + instruction_recorders, + &tx.message, + ); + Self::refcells_to_accounts( accounts, loaders, @@ -2050,6 +2089,7 @@ impl Bank { ( loaded_accounts, executed, + inner_instructions, retryable_txs, tx_count, signature_count, @@ -2676,14 +2716,19 @@ impl Bank { batch: &TransactionBatch, max_age: usize, collect_balances: bool, - ) -> (TransactionResults, TransactionBalancesSet) { + enable_cpi_recording: bool, + ) -> ( + TransactionResults, + TransactionBalancesSet, + Vec>, + ) { let pre_balances = if collect_balances { self.collect_balances(batch) } else { vec![] }; - let (mut loaded_accounts, executed, _, tx_count, signature_count) = - self.load_and_execute_transactions(batch, max_age, None); + let (mut loaded_accounts, executed, inner_instructions, _, tx_count, signature_count) = + self.load_and_execute_transactions(batch, max_age, None, enable_cpi_recording); let results = self.commit_transactions( batch.transactions(), @@ -2701,13 +2746,14 @@ impl Bank { ( results, TransactionBalancesSet::new(pre_balances, post_balances), + inner_instructions, ) } #[must_use] pub fn process_transactions(&self, txs: &[Transaction]) -> Vec> { let batch = self.prepare_batch(txs, None); - self.load_execute_and_commit_transactions(&batch, MAX_PROCESSING_AGE, false) + self.load_execute_and_commit_transactions(&batch, MAX_PROCESSING_AGE, false, false) .0 .fee_collection_results } @@ -6010,7 +6056,7 @@ mod tests { let lock_result = bank.prepare_batch(&pay_alice, None); let results_alice = bank - .load_execute_and_commit_transactions(&lock_result, MAX_PROCESSING_AGE, false) + .load_execute_and_commit_transactions(&lock_result, MAX_PROCESSING_AGE, false, false) .0 .fee_collection_results; assert_eq!(results_alice[0], Ok(())); @@ -7821,9 +7867,10 @@ mod tests { let txs = vec![tx0, tx1, tx2]; let lock_result = bank0.prepare_batch(&txs, None); - let (transaction_results, transaction_balances_set) = - bank0.load_execute_and_commit_transactions(&lock_result, MAX_PROCESSING_AGE, true); + let (transaction_results, transaction_balances_set, inner_instructions) = bank0 + .load_execute_and_commit_transactions(&lock_result, MAX_PROCESSING_AGE, true, false); + assert!(inner_instructions[0].iter().all(|ix| ix.is_empty())); assert_eq!(transaction_balances_set.pre_balances.len(), 3); assert_eq!(transaction_balances_set.post_balances.len(), 3); diff --git a/runtime/src/instruction_recorder.rs b/runtime/src/instruction_recorder.rs new file mode 100644 index 0000000000..f314b7c8bd --- /dev/null +++ b/runtime/src/instruction_recorder.rs @@ -0,0 +1,26 @@ +use std::{cell::RefCell, rc::Rc}; + +use solana_sdk::{ + instruction::{CompiledInstruction, Instruction}, + message::Message, +}; + +/// Records and compiles cross-program invoked instructions +#[derive(Clone, Default)] +pub struct InstructionRecorder { + inner: Rc>>, +} + +impl InstructionRecorder { + pub fn compile_instructions(&self, message: &Message) -> Vec { + self.inner + .borrow() + .iter() + .map(|ix| message.compile_instruction(ix)) + .collect() + } + + pub fn record_instruction(&self, instruction: Instruction) { + self.inner.borrow_mut().push(instruction); + } +} diff --git a/runtime/src/lib.rs b/runtime/src/lib.rs index ef33a47e92..47104635e3 100644 --- a/runtime/src/lib.rs +++ b/runtime/src/lib.rs @@ -14,6 +14,7 @@ pub mod commitment; pub mod epoch_stakes; pub mod genesis_utils; pub mod hardened_unpack; +pub mod instruction_recorder; pub mod loader_utils; pub mod log_collector; pub mod message_processor; diff --git a/runtime/src/message_processor.rs b/runtime/src/message_processor.rs index 4edca80ecf..5ab1c86f2e 100644 --- a/runtime/src/message_processor.rs +++ b/runtime/src/message_processor.rs @@ -1,5 +1,6 @@ use crate::{ - log_collector::LogCollector, native_loader::NativeLoader, rent_collector::RentCollector, + instruction_recorder::InstructionRecorder, log_collector::LogCollector, + native_loader::NativeLoader, rent_collector::RentCollector, }; use log::*; use serde::{Deserialize, Serialize}; @@ -11,7 +12,7 @@ use solana_sdk::{ Executor, InvokeContext, Logger, ProcessInstruction, ProcessInstructionWithContext, }, genesis_config::ClusterType, - instruction::{CompiledInstruction, InstructionError}, + instruction::{CompiledInstruction, Instruction, InstructionError}, message::Message, native_loader, pubkey::Pubkey, @@ -206,6 +207,7 @@ pub struct ThisInvokeContext { compute_budget: ComputeBudget, compute_meter: Rc>, executors: Rc>, + instruction_recorder: Option, } impl ThisInvokeContext { pub fn new( @@ -217,6 +219,7 @@ impl ThisInvokeContext { is_cross_program_supported: bool, compute_budget: ComputeBudget, executors: Rc>, + instruction_recorder: Option, ) -> Self { let mut program_ids = Vec::with_capacity(compute_budget.max_invoke_depth); program_ids.push(*program_id); @@ -232,6 +235,7 @@ impl ThisInvokeContext { remaining: compute_budget.max_units, })), executors, + instruction_recorder, } } } @@ -294,6 +298,11 @@ impl InvokeContext for ThisInvokeContext { fn get_executor(&mut self, pubkey: &Pubkey) -> Option> { self.executors.borrow().get(&pubkey) } + fn record_instruction(&self, instruction: &Instruction) { + if let Some(recorder) = &self.instruction_recorder { + recorder.record_instruction(instruction.clone()); + } + } } pub struct ThisLogger { log_collector: Option>, @@ -667,6 +676,7 @@ impl MessageProcessor { rent_collector: &RentCollector, log_collector: Option>, executors: Rc>, + instruction_recorder: Option, instruction_index: usize, cluster_type: ClusterType, epoch: Epoch, @@ -696,6 +706,7 @@ impl MessageProcessor { self.is_cross_program_supported, self.compute_budget, executors, + instruction_recorder, ); let keyed_accounts = Self::create_keyed_accounts(message, instruction, executable_accounts, accounts)?; @@ -714,6 +725,7 @@ impl MessageProcessor { /// Process a message. /// This method calls each instruction in the message over the set of loaded Accounts /// The accounts are committed back to the bank only if every instruction succeeds + #[allow(clippy::too_many_arguments)] pub fn process_message( &self, message: &Message, @@ -722,10 +734,16 @@ impl MessageProcessor { rent_collector: &RentCollector, log_collector: Option>, executors: Rc>, + mut instruction_recorders: Option<&mut Vec>, cluster_type: ClusterType, epoch: Epoch, ) -> Result<(), TransactionError> { for (instruction_index, instruction) in message.instructions.iter().enumerate() { + let instruction_recorder = instruction_recorders.as_mut().map(|recorders| { + let instruction_recorder = InstructionRecorder::default(); + recorders.push(instruction_recorder.clone()); + instruction_recorder + }); self.execute_instruction( message, instruction, @@ -734,6 +752,7 @@ impl MessageProcessor { rent_collector, log_collector.clone(), executors.clone(), + instruction_recorder, instruction_index, cluster_type, epoch, @@ -794,6 +813,7 @@ mod tests { true, ComputeBudget::default(), Rc::new(RefCell::new(Executors::default())), + None, ); // Check call depth increases and has a limit @@ -1329,6 +1349,7 @@ mod tests { &rent_collector, None, executors.clone(), + None, ClusterType::Development, 0, ); @@ -1352,6 +1373,7 @@ mod tests { &rent_collector, None, executors.clone(), + None, ClusterType::Development, 0, ); @@ -1379,6 +1401,7 @@ mod tests { &rent_collector, None, executors, + None, ClusterType::Development, 0, ); @@ -1489,6 +1512,7 @@ mod tests { &rent_collector, None, executors.clone(), + None, ClusterType::Development, 0, ); @@ -1516,6 +1540,7 @@ mod tests { &rent_collector, None, executors.clone(), + None, ClusterType::Development, 0, ); @@ -1540,6 +1565,7 @@ mod tests { &rent_collector, None, executors, + None, ClusterType::Development, 0, ); @@ -1618,6 +1644,7 @@ mod tests { true, ComputeBudget::default(), Rc::new(RefCell::new(Executors::default())), + None, ); let metas = vec![ AccountMeta::new(owned_key, false), diff --git a/sdk/src/entrypoint_native.rs b/sdk/src/entrypoint_native.rs index 64994de89a..dda3911834 100644 --- a/sdk/src/entrypoint_native.rs +++ b/sdk/src/entrypoint_native.rs @@ -3,8 +3,10 @@ #[cfg(RUSTC_WITH_SPECIALIZATION)] use crate::abi_example::AbiExample; use crate::{ - account::Account, account::KeyedAccount, instruction::CompiledInstruction, - instruction::InstructionError, message::Message, pubkey::Pubkey, + account::{Account, KeyedAccount}, + instruction::{CompiledInstruction, Instruction, InstructionError}, + message::Message, + pubkey::Pubkey, }; use std::{cell::RefCell, rc::Rc, sync::Arc}; @@ -225,6 +227,8 @@ pub trait InvokeContext { fn add_executor(&mut self, pubkey: &Pubkey, executor: Arc); /// Get the completed loader work that can be re-used across executions fn get_executor(&mut self, pubkey: &Pubkey) -> Option>; + /// Record invoked instruction + fn record_instruction(&self, instruction: &Instruction); } #[derive(Clone, Copy, Debug)] diff --git a/sdk/src/message.rs b/sdk/src/message.rs index fe3be66b2f..fed7537c37 100644 --- a/sdk/src/message.rs +++ b/sdk/src/message.rs @@ -272,6 +272,10 @@ impl Message { Self::new(&instructions, payer) } + pub fn compile_instruction(&self, ix: &Instruction) -> CompiledInstruction { + compile_instruction(ix, &self.account_keys) + } + pub fn serialize(&self) -> Vec { bincode::serialize(self).unwrap() } diff --git a/storage-bigtable/src/lib.rs b/storage-bigtable/src/lib.rs index 1133a5a6ca..bc2bc4cf86 100644 --- a/storage-bigtable/src/lib.rs +++ b/storage-bigtable/src/lib.rs @@ -8,8 +8,9 @@ use solana_sdk::{ transaction::{Transaction, TransactionError}, }; use solana_transaction_status::{ - ConfirmedBlock, ConfirmedTransaction, ConfirmedTransactionStatusWithSignature, Rewards, - TransactionStatus, TransactionStatusMeta, TransactionWithStatusMeta, + ConfirmedBlock, ConfirmedTransaction, ConfirmedTransactionStatusWithSignature, + InnerInstructions, Rewards, TransactionStatus, TransactionStatusMeta, + TransactionWithStatusMeta, }; use std::collections::HashMap; use thiserror::Error; @@ -161,6 +162,7 @@ struct StoredConfirmedBlockTransactionStatusMeta { fee: u64, pre_balances: Vec, post_balances: Vec, + inner_instructions: Option>, } impl From for TransactionStatusMeta { @@ -170,6 +172,7 @@ impl From for TransactionStatusMeta { fee, pre_balances, post_balances, + inner_instructions, } = value; let status = match &err { None => Ok(()), @@ -180,6 +183,7 @@ impl From for TransactionStatusMeta { fee, pre_balances, post_balances, + inner_instructions, } } } @@ -191,6 +195,7 @@ impl From for StoredConfirmedBlockTransactionStatusMeta { fee, pre_balances, post_balances, + inner_instructions, .. } = value; Self { @@ -198,6 +203,7 @@ impl From for StoredConfirmedBlockTransactionStatusMeta { fee, pre_balances, post_balances, + inner_instructions, } } } diff --git a/transaction-status/src/lib.rs b/transaction-status/src/lib.rs index d61a020df4..e7ee0c10bb 100644 --- a/transaction-status/src/lib.rs +++ b/transaction-status/src/lib.rs @@ -15,7 +15,7 @@ use solana_sdk::{ clock::{Slot, UnixTimestamp}, commitment_config::CommitmentConfig, instruction::CompiledInstruction, - message::MessageHeader, + message::{Message, MessageHeader}, pubkey::Pubkey, signature::Signature, transaction::{Result, Transaction, TransactionError}, @@ -36,6 +36,19 @@ pub enum UiParsedInstruction { PartiallyDecoded(UiPartiallyDecodedInstruction), } +impl UiInstruction { + fn parse(instruction: &CompiledInstruction, message: &Message) -> Self { + let program_id = instruction.program_id(&message.account_keys); + if let Ok(parsed_instruction) = parse(program_id, instruction, &message.account_keys) { + UiInstruction::Parsed(UiParsedInstruction::Parsed(parsed_instruction)) + } else { + UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded( + UiPartiallyDecodedInstruction::from(instruction, &message.account_keys), + )) + } + } +} + /// A duplicate representation of a CompiledInstruction for pretty JSON serialization #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] @@ -78,6 +91,49 @@ impl UiPartiallyDecodedInstruction { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +pub struct InnerInstructions { + /// Transaction instruction index + pub index: u8, + /// List of inner instructions + pub instructions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct UiInnerInstructions { + /// Transaction instruction index + pub index: u8, + /// List of inner instructions + pub instructions: Vec, +} + +impl UiInnerInstructions { + fn parse(inner_instructions: InnerInstructions, message: &Message) -> Self { + Self { + index: inner_instructions.index, + instructions: inner_instructions + .instructions + .iter() + .map(|ix| UiInstruction::parse(ix, message)) + .collect(), + } + } +} + +impl From for UiInnerInstructions { + fn from(inner_instructions: InnerInstructions) -> Self { + Self { + index: inner_instructions.index, + instructions: inner_instructions + .instructions + .iter() + .map(|ix| UiInstruction::Compiled(ix.into())) + .collect(), + } + } +} + #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct TransactionStatusMeta { @@ -85,6 +141,7 @@ pub struct TransactionStatusMeta { pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, + pub inner_instructions: Option>, } impl Default for TransactionStatusMeta { @@ -94,6 +151,7 @@ impl Default for TransactionStatusMeta { fee: 0, pre_balances: vec![], post_balances: vec![], + inner_instructions: None, } } } @@ -107,6 +165,24 @@ pub struct UiTransactionStatusMeta { pub fee: u64, pub pre_balances: Vec, pub post_balances: Vec, + pub inner_instructions: Option>, +} + +impl UiTransactionStatusMeta { + fn parse(meta: TransactionStatusMeta, message: &Message) -> Self { + Self { + err: meta.status.clone().err(), + status: meta.status, + fee: meta.fee, + pre_balances: meta.pre_balances, + post_balances: meta.post_balances, + inner_instructions: meta.inner_instructions.map(|ixs| { + ixs.into_iter() + .map(|ix| UiInnerInstructions::parse(ix, message)) + .collect() + }), + } + } } impl From for UiTransactionStatusMeta { @@ -117,6 +193,9 @@ impl From for UiTransactionStatusMeta { fee: meta.fee, pre_balances: meta.pre_balances, post_balances: meta.post_balances, + inner_instructions: meta + .inner_instructions + .map(|ixs| ixs.into_iter().map(|ix| ix.into()).collect()), } } } @@ -261,9 +340,11 @@ pub struct TransactionWithStatusMeta { impl TransactionWithStatusMeta { fn encode(self, encoding: UiTransactionEncoding) -> EncodedTransactionWithStatusMeta { + let message = self.transaction.message(); + let meta = self.meta.map(|meta| meta.encode(encoding, message)); EncodedTransactionWithStatusMeta { transaction: EncodedTransaction::encode(self.transaction, encoding), - meta: self.meta.map(|meta| meta.into()), + meta, } } } @@ -275,6 +356,15 @@ pub struct EncodedTransactionWithStatusMeta { pub meta: Option, } +impl TransactionStatusMeta { + fn encode(self, encoding: UiTransactionEncoding, message: &Message) -> UiTransactionStatusMeta { + match encoding { + UiTransactionEncoding::JsonParsed => UiTransactionStatusMeta::parse(self, message), + _ => self.into(), + } + } +} + #[derive(Serialize, Deserialize, Clone, Copy, Debug, PartialEq)] #[serde(rename_all = "camelCase")] pub enum UiTransactionEncoding { @@ -334,24 +424,7 @@ impl EncodedTransaction { .instructions .iter() .map(|instruction| { - let program_id = - instruction.program_id(&transaction.message.account_keys); - if let Ok(parsed_instruction) = parse( - program_id, - instruction, - &transaction.message.account_keys, - ) { - UiInstruction::Parsed(UiParsedInstruction::Parsed( - parsed_instruction, - )) - } else { - UiInstruction::Parsed(UiParsedInstruction::PartiallyDecoded( - UiPartiallyDecodedInstruction::from( - instruction, - &transaction.message.account_keys, - ), - )) - } + UiInstruction::parse(instruction, &transaction.message) }) .collect(), })