RPC: Add inner instructions to simulate transaction response (#34313)

* rpc: add optional `innerInstructions: bool` arg to `simulateTransaction`

* bank: enable cpi recording in simulate

* sdk: move `InnerInstructions` into SDK from accounts DB

* bank: return inner instructions from simulate tx

* rpc: return inner instructions from simulate tx

* rpc: simulate tx: add `jsonParsed` support for inner instructions

* accounts db: add deprecated attribute to re-exported inner instructions

* rpc: de-dupe inner instruction mapping

* update deprecated comment

Co-authored-by: Tyera <teulberg@gmail.com>

---------

Co-authored-by: Tyera <teulberg@gmail.com>
This commit is contained in:
Joe C 2023-12-16 07:49:22 -05:00 committed by GitHub
parent 1f2b72b6e3
commit 171c58c5c0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 111 additions and 71 deletions

View File

@ -1,3 +1,9 @@
// Re-exported since these have moved to `solana_sdk`.
#[deprecated(
since = "1.18.0",
note = "Please use `solana_sdk::inner_instruction` types instead"
)]
pub use solana_sdk::inner_instruction::{InnerInstruction, InnerInstructionsList};
use { use {
crate::{ crate::{
nonce_info::{NonceFull, NonceInfo, NoncePartial}, nonce_info::{NonceFull, NonceInfo, NoncePartial},
@ -105,22 +111,6 @@ impl DurableNonceFee {
} }
} }
/// An ordered list of compiled instructions that were invoked during a
/// transaction instruction
pub type InnerInstructions = Vec<InnerInstruction>;
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct InnerInstruction {
pub instruction: CompiledInstruction,
/// Invocation stack height of this instruction. Instruction stack height
/// starts at 1 for transaction instructions.
pub stack_height: u8,
}
/// A list of compiled instructions that were invoked during each instruction of
/// a transaction
pub type InnerInstructionsList = Vec<InnerInstructions>;
/// Extract the InnerInstructionsList from a TransactionContext /// Extract the InnerInstructionsList from a TransactionContext
pub fn inner_instructions_list_from_instruction_trace( pub fn inner_instructions_list_from_instruction_trace(
transaction_context: &TransactionContext, transaction_context: &TransactionContext,

View File

@ -8,6 +8,7 @@ use {
commitment_config::CommitmentLevel, commitment_config::CommitmentLevel,
fee_calculator::FeeCalculator, fee_calculator::FeeCalculator,
hash::Hash, hash::Hash,
inner_instruction::InnerInstructions,
message::Message, message::Message,
pubkey::Pubkey, pubkey::Pubkey,
signature::Signature, signature::Signature,
@ -37,6 +38,7 @@ pub struct TransactionSimulationDetails {
pub logs: Vec<String>, pub logs: Vec<String>,
pub units_consumed: u64, pub units_consumed: u64,
pub return_data: Option<TransactionReturnData>, pub return_data: Option<TransactionReturnData>,
pub inner_instructions: Option<Vec<InnerInstructions>>,
} }
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -194,11 +194,14 @@ fn simulate_transaction(
post_simulation_accounts: _, post_simulation_accounts: _,
units_consumed, units_consumed,
return_data, return_data,
} = bank.simulate_transaction_unchecked(sanitized_transaction); inner_instructions,
} = bank.simulate_transaction_unchecked(&sanitized_transaction, false);
let simulation_details = TransactionSimulationDetails { let simulation_details = TransactionSimulationDetails {
logs, logs,
units_consumed, units_consumed,
return_data, return_data,
inner_instructions,
}; };
BanksTransactionResultWithSimulation { BanksTransactionResultWithSimulation {
result: Some(result), result: Some(result),

View File

@ -54,7 +54,7 @@ use {
transaction::VersionedTransaction, transaction::VersionedTransaction,
}, },
solana_transaction_status::{ solana_transaction_status::{
ConfirmedTransactionWithStatusMeta, InnerInstructions, TransactionStatusMeta, map_inner_instructions, ConfirmedTransactionWithStatusMeta, TransactionStatusMeta,
TransactionWithStatusMeta, VersionedTransactionWithStatusMeta, TransactionWithStatusMeta, VersionedTransactionWithStatusMeta,
}, },
std::collections::HashMap, std::collections::HashMap,
@ -212,21 +212,7 @@ fn execute_transactions(
); );
let inner_instructions = inner_instructions.map(|inner_instructions| { let inner_instructions = inner_instructions.map(|inner_instructions| {
inner_instructions map_inner_instructions(inner_instructions).collect()
.into_iter()
.enumerate()
.map(|(index, instructions)| InnerInstructions {
index: index as u8,
instructions: instructions
.into_iter()
.map(|ix| solana_transaction_status::InnerInstruction {
instruction: ix.instruction,
stack_height: Some(u32::from(ix.stack_height)),
})
.collect(),
})
.filter(|i| !i.instructions.is_empty())
.collect()
}); });
let tx_status_meta = TransactionStatusMeta { let tx_status_meta = TransactionStatusMeta {
@ -766,7 +752,7 @@ fn test_return_data_and_log_data_syscall() {
let transaction = Transaction::new(&[&mint_keypair], message, blockhash); let transaction = Transaction::new(&[&mint_keypair], message, blockhash);
let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(transaction); let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(transaction);
let result = bank.simulate_transaction(sanitized_tx); let result = bank.simulate_transaction(&sanitized_tx, false);
assert!(result.result.is_ok()); assert!(result.result.is_ok());

View File

@ -44,6 +44,8 @@ pub struct RpcSimulateTransactionConfig {
pub encoding: Option<UiTransactionEncoding>, pub encoding: Option<UiTransactionEncoding>,
pub accounts: Option<RpcSimulateTransactionAccountsConfig>, pub accounts: Option<RpcSimulateTransactionAccountsConfig>,
pub min_context_slot: Option<Slot>, pub min_context_slot: Option<Slot>,
#[serde(default)]
pub inner_instructions: bool,
} }
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]

View File

@ -11,7 +11,7 @@ use {
}, },
solana_transaction_status::{ solana_transaction_status::{
ConfirmedTransactionStatusWithSignature, TransactionConfirmationStatus, UiConfirmedBlock, ConfirmedTransactionStatusWithSignature, TransactionConfirmationStatus, UiConfirmedBlock,
UiTransactionReturnData, UiInnerInstructions, UiTransactionReturnData,
}, },
std::{collections::HashMap, fmt, net::SocketAddr, str::FromStr}, std::{collections::HashMap, fmt, net::SocketAddr, str::FromStr},
thiserror::Error, thiserror::Error,
@ -423,6 +423,7 @@ pub struct RpcSimulateTransactionResult {
pub accounts: Option<Vec<Option<UiAccount>>>, pub accounts: Option<Vec<Option<UiAccount>>>,
pub units_consumed: Option<u64>, pub units_consumed: Option<u64>,
pub return_data: Option<UiTransactionReturnData>, pub return_data: Option<UiTransactionReturnData>,
pub inner_instructions: Option<Vec<UiInnerInstructions>>,
} }
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Eq)]

View File

@ -350,6 +350,7 @@ impl RpcSender for MockSender {
accounts: None, accounts: None,
units_consumed: None, units_consumed: None,
return_data: None, return_data: None,
inner_instructions: None,
}, },
})?, })?,
"getMinimumBalanceForRentExemption" => json![20], "getMinimumBalanceForRentExemption" => json![20],

View File

@ -87,10 +87,10 @@ use {
solana_storage_bigtable::Error as StorageError, solana_storage_bigtable::Error as StorageError,
solana_streamer::socket::SocketAddrSpace, solana_streamer::socket::SocketAddrSpace,
solana_transaction_status::{ solana_transaction_status::{
BlockEncodingOptions, ConfirmedBlock, ConfirmedTransactionStatusWithSignature, map_inner_instructions, BlockEncodingOptions, ConfirmedBlock,
ConfirmedTransactionWithStatusMeta, EncodedConfirmedTransactionWithStatusMeta, Reward, ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta,
RewardType, TransactionBinaryEncoding, TransactionConfirmationStatus, TransactionStatus, EncodedConfirmedTransactionWithStatusMeta, Reward, RewardType, TransactionBinaryEncoding,
UiConfirmedBlock, UiTransactionEncoding, TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, UiTransactionEncoding,
}, },
solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY}, solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY},
spl_token_2022::{ spl_token_2022::{
@ -3266,6 +3266,7 @@ pub mod rpc_full {
use { use {
super::*, super::*,
solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage}, solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage},
solana_transaction_status::UiInnerInstructions,
}; };
#[rpc] #[rpc]
pub trait Full { pub trait Full {
@ -3676,7 +3677,8 @@ pub mod rpc_full {
post_simulation_accounts: _, post_simulation_accounts: _,
units_consumed, units_consumed,
return_data, return_data,
} = preflight_bank.simulate_transaction(transaction) inner_instructions: _, // Always `None` due to `enable_cpi_recording = false`
} = preflight_bank.simulate_transaction(&transaction, false)
{ {
match err { match err {
TransactionError::BlockhashNotFound => { TransactionError::BlockhashNotFound => {
@ -3694,6 +3696,7 @@ pub mod rpc_full {
accounts: None, accounts: None,
units_consumed: Some(units_consumed), units_consumed: Some(units_consumed),
return_data: return_data.map(|return_data| return_data.into()), return_data: return_data.map(|return_data| return_data.into()),
inner_instructions: None,
}, },
} }
.into()); .into());
@ -3724,6 +3727,7 @@ pub mod rpc_full {
encoding, encoding,
accounts: config_accounts, accounts: config_accounts,
min_context_slot, min_context_slot,
inner_instructions: enable_cpi_recording,
} = config.unwrap_or_default(); } = config.unwrap_or_default();
let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58); let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58);
let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| { let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
@ -3753,7 +3757,6 @@ pub mod rpc_full {
if sig_verify { if sig_verify {
verify_transaction(&transaction, &bank.feature_set)?; verify_transaction(&transaction, &bank.feature_set)?;
} }
let number_of_accounts = transaction.message().account_keys().len();
let TransactionSimulationResult { let TransactionSimulationResult {
result, result,
@ -3761,7 +3764,11 @@ pub mod rpc_full {
post_simulation_accounts, post_simulation_accounts,
units_consumed, units_consumed,
return_data, return_data,
} = bank.simulate_transaction(transaction); inner_instructions,
} = bank.simulate_transaction(&transaction, enable_cpi_recording);
let account_keys = transaction.message().account_keys();
let number_of_accounts = account_keys.len();
let accounts = if let Some(config_accounts) = config_accounts { let accounts = if let Some(config_accounts) = config_accounts {
let accounts_encoding = config_accounts let accounts_encoding = config_accounts
@ -3804,6 +3811,12 @@ pub mod rpc_full {
None None
}; };
let inner_instructions = inner_instructions.map(|info| {
map_inner_instructions(info)
.map(|converted| UiInnerInstructions::parse(converted, &account_keys))
.collect()
});
Ok(new_response( Ok(new_response(
bank, bank,
RpcSimulateTransactionResult { RpcSimulateTransactionResult {
@ -3812,6 +3825,7 @@ pub mod rpc_full {
accounts, accounts,
units_consumed: Some(units_consumed), units_consumed: Some(units_consumed),
return_data: return_data.map(|return_data| return_data.into()), return_data: return_data.map(|return_data| return_data.into()),
inner_instructions,
}, },
)) ))
} }
@ -5913,6 +5927,7 @@ pub mod tests {
} }
], ],
"err":null, "err":null,
"innerInstructions": null,
"logs":[ "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
@ -5997,6 +6012,7 @@ pub mod tests {
"value":{ "value":{
"accounts":null, "accounts":null,
"err":null, "err":null,
"innerInstructions":null,
"logs":[ "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
@ -6025,6 +6041,7 @@ pub mod tests {
"value":{ "value":{
"accounts":null, "accounts":null,
"err":null, "err":null,
"innerInstructions":null,
"logs":[ "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
@ -6077,6 +6094,7 @@ pub mod tests {
"value":{ "value":{
"err":"BlockhashNotFound", "err":"BlockhashNotFound",
"accounts":null, "accounts":null,
"innerInstructions":null,
"logs":[], "logs":[],
"returnData":null, "returnData":null,
"unitsConsumed":0, "unitsConsumed":0,
@ -6103,6 +6121,7 @@ pub mod tests {
"value":{ "value":{
"accounts":null, "accounts":null,
"err":null, "err":null,
"innerInstructions":null,
"logs":[ "logs":[
"Program 11111111111111111111111111111111 invoke [1]", "Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success" "Program 11111111111111111111111111111111 success"
@ -6483,7 +6502,7 @@ pub mod tests {
assert_eq!( assert_eq!(
res, res,
Some( Some(
r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","logs":[],"returnData":null,"unitsConsumed":0}},"id":1}"#.to_string(), r#"{"jsonrpc":"2.0","error":{"code":-32002,"message":"Transaction simulation failed: Blockhash not found","data":{"accounts":null,"err":"BlockhashNotFound","innerInstructions":null,"logs":[],"returnData":null,"unitsConsumed":0}},"id":1}"#.to_string(),
) )
); );

View File

@ -8,7 +8,7 @@ use {
blockstore_processor::{TransactionStatusBatch, TransactionStatusMessage}, blockstore_processor::{TransactionStatusBatch, TransactionStatusMessage},
}, },
solana_transaction_status::{ solana_transaction_status::{
extract_and_fmt_memos, InnerInstruction, InnerInstructions, Reward, TransactionStatusMeta, extract_and_fmt_memos, map_inner_instructions, Reward, TransactionStatusMeta,
}, },
std::{ std::{
sync::{ sync::{
@ -121,21 +121,7 @@ impl TransactionStatusService {
let tx_account_locks = transaction.get_account_locks_unchecked(); let tx_account_locks = transaction.get_account_locks_unchecked();
let inner_instructions = inner_instructions.map(|inner_instructions| { let inner_instructions = inner_instructions.map(|inner_instructions| {
inner_instructions map_inner_instructions(inner_instructions).collect()
.into_iter()
.enumerate()
.map(|(index, instructions)| InnerInstructions {
index: index as u8,
instructions: instructions
.into_iter()
.map(|info| InnerInstruction {
instruction: info.instruction,
stack_height: Some(u32::from(info.stack_height)),
})
.collect(),
})
.filter(|i| !i.instructions.is_empty())
.collect()
}); });
let pre_token_balances = Some(pre_token_balances); let pre_token_balances = Some(pre_token_balances);

View File

@ -150,6 +150,7 @@ use {
hash::{extend_and_hash, hashv, Hash}, hash::{extend_and_hash, hashv, Hash},
incinerator, incinerator,
inflation::Inflation, inflation::Inflation,
inner_instruction::InnerInstructions,
instruction::InstructionError, instruction::InstructionError,
loader_v4::{self, LoaderV4State, LoaderV4Status}, loader_v4::{self, LoaderV4State, LoaderV4Status},
message::{AccountKeys, SanitizedMessage}, message::{AccountKeys, SanitizedMessage},
@ -338,6 +339,7 @@ pub struct TransactionSimulationResult {
pub post_simulation_accounts: Vec<TransactionAccount>, pub post_simulation_accounts: Vec<TransactionAccount>,
pub units_consumed: u64, pub units_consumed: u64,
pub return_data: Option<TransactionReturnData>, pub return_data: Option<TransactionReturnData>,
pub inner_instructions: Option<Vec<InnerInstructions>>,
} }
pub struct TransactionBalancesSet { pub struct TransactionBalancesSet {
pub pre_balances: TransactionBalances, pub pre_balances: TransactionBalances,
@ -4308,23 +4310,25 @@ impl Bank {
/// Run transactions against a frozen bank without committing the results /// Run transactions against a frozen bank without committing the results
pub fn simulate_transaction( pub fn simulate_transaction(
&self, &self,
transaction: SanitizedTransaction, transaction: &SanitizedTransaction,
enable_cpi_recording: bool,
) -> TransactionSimulationResult { ) -> TransactionSimulationResult {
assert!(self.is_frozen(), "simulation bank must be frozen"); assert!(self.is_frozen(), "simulation bank must be frozen");
self.simulate_transaction_unchecked(transaction) self.simulate_transaction_unchecked(transaction, enable_cpi_recording)
} }
/// Run transactions against a bank without committing the results; does not check if the bank /// Run transactions against a bank without committing the results; does not check if the bank
/// is frozen, enabling use in single-Bank test frameworks /// is frozen, enabling use in single-Bank test frameworks
pub fn simulate_transaction_unchecked( pub fn simulate_transaction_unchecked(
&self, &self,
transaction: SanitizedTransaction, transaction: &SanitizedTransaction,
enable_cpi_recording: bool,
) -> TransactionSimulationResult { ) -> TransactionSimulationResult {
let account_keys = transaction.message().account_keys(); let account_keys = transaction.message().account_keys();
let number_of_accounts = account_keys.len(); let number_of_accounts = account_keys.len();
let account_overrides = self.get_account_overrides_for_simulation(&account_keys); let account_overrides = self.get_account_overrides_for_simulation(&account_keys);
let batch = self.prepare_unlocked_batch_from_single_tx(&transaction); let batch = self.prepare_unlocked_batch_from_single_tx(transaction);
let mut timings = ExecuteTimings::default(); let mut timings = ExecuteTimings::default();
let LoadAndExecuteTransactionsOutput { let LoadAndExecuteTransactionsOutput {
@ -4337,7 +4341,7 @@ impl Bank {
// for processing. During forwarding, the transaction could expire if the // for processing. During forwarding, the transaction could expire if the
// delay is not accounted for. // delay is not accounted for.
MAX_PROCESSING_AGE - MAX_TRANSACTION_FORWARDING_DELAY, MAX_PROCESSING_AGE - MAX_TRANSACTION_FORWARDING_DELAY,
false, enable_cpi_recording,
true, true,
true, true,
&mut timings, &mut timings,
@ -4374,11 +4378,13 @@ impl Bank {
let execution_result = execution_results.pop().unwrap(); let execution_result = execution_results.pop().unwrap();
let flattened_result = execution_result.flattened_result(); let flattened_result = execution_result.flattened_result();
let (logs, return_data) = match execution_result { let (logs, return_data, inner_instructions) = match execution_result {
TransactionExecutionResult::Executed { details, .. } => { TransactionExecutionResult::Executed { details, .. } => (
(details.log_messages, details.return_data) details.log_messages,
} details.return_data,
TransactionExecutionResult::NotExecuted(_) => (None, None), details.inner_instructions,
),
TransactionExecutionResult::NotExecuted(_) => (None, None, None),
}; };
let logs = logs.unwrap_or_default(); let logs = logs.unwrap_or_default();
@ -4388,6 +4394,7 @@ impl Bank {
post_simulation_accounts, post_simulation_accounts,
units_consumed, units_consumed,
return_data, return_data,
inner_instructions,
} }
} }

View File

@ -14136,6 +14136,6 @@ fn test_failed_simulation_compute_units() {
bank.freeze(); bank.freeze();
let sanitized = SanitizedTransaction::from_transaction_for_tests(transaction); let sanitized = SanitizedTransaction::from_transaction_for_tests(transaction);
let simulation = bank.simulate_transaction(sanitized); let simulation = bank.simulate_transaction(&sanitized, false);
assert_eq!(TEST_UNITS, simulation.units_consumed); assert_eq!(TEST_UNITS, simulation.units_consumed);
} }

View File

@ -0,0 +1,21 @@
use {
crate::instruction::CompiledInstruction,
serde::{Deserialize, Serialize},
};
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct InnerInstruction {
pub instruction: CompiledInstruction,
/// Invocation stack height of this instruction. Instruction stack height
/// starts at 1 for transaction instructions.
pub stack_height: u8,
}
/// An ordered list of compiled instructions that were invoked during a
/// transaction instruction
pub type InnerInstructions = Vec<InnerInstruction>;
/// A list of compiled instructions that were invoked during each instruction of
/// a transaction
pub type InnerInstructionsList = Vec<InnerInstructions>;

View File

@ -78,6 +78,7 @@ pub mod genesis_config;
pub mod hard_forks; pub mod hard_forks;
pub mod hash; pub mod hash;
pub mod inflation; pub mod inflation;
pub mod inner_instruction;
pub mod log; pub mod log;
pub mod native_loader; pub mod native_loader;
pub mod net; pub mod net;

View File

@ -230,6 +230,27 @@ pub struct InnerInstruction {
pub stack_height: Option<u32>, pub stack_height: Option<u32>,
} }
/// Maps a list of inner instructions from `solana_sdk` into a list of this
/// crate's representation of inner instructions (with instruction indices).
pub fn map_inner_instructions(
inner_instructions: solana_sdk::inner_instruction::InnerInstructionsList,
) -> impl Iterator<Item = InnerInstructions> {
inner_instructions
.into_iter()
.enumerate()
.map(|(index, instructions)| InnerInstructions {
index: index as u8,
instructions: instructions
.into_iter()
.map(|info| InnerInstruction {
stack_height: Some(u32::from(info.stack_height)),
instruction: info.instruction,
})
.collect(),
})
.filter(|i| !i.instructions.is_empty())
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct UiInnerInstructions { pub struct UiInnerInstructions {
@ -240,7 +261,7 @@ pub struct UiInnerInstructions {
} }
impl UiInnerInstructions { impl UiInnerInstructions {
fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self { pub fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self {
Self { Self {
index: inner_instructions.index, index: inner_instructions.index,
instructions: inner_instructions instructions: inner_instructions