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 {
crate::{
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
pub fn inner_instructions_list_from_instruction_trace(
transaction_context: &TransactionContext,

View File

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

View File

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

View File

@ -54,7 +54,7 @@ use {
transaction::VersionedTransaction,
},
solana_transaction_status::{
ConfirmedTransactionWithStatusMeta, InnerInstructions, TransactionStatusMeta,
map_inner_instructions, ConfirmedTransactionWithStatusMeta, TransactionStatusMeta,
TransactionWithStatusMeta, VersionedTransactionWithStatusMeta,
},
std::collections::HashMap,
@ -212,21 +212,7 @@ fn execute_transactions(
);
let inner_instructions = inner_instructions.map(|inner_instructions| {
inner_instructions
.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()
map_inner_instructions(inner_instructions).collect()
});
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 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());

View File

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

View File

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

View File

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

View File

@ -87,10 +87,10 @@ use {
solana_storage_bigtable::Error as StorageError,
solana_streamer::socket::SocketAddrSpace,
solana_transaction_status::{
BlockEncodingOptions, ConfirmedBlock, ConfirmedTransactionStatusWithSignature,
ConfirmedTransactionWithStatusMeta, EncodedConfirmedTransactionWithStatusMeta, Reward,
RewardType, TransactionBinaryEncoding, TransactionConfirmationStatus, TransactionStatus,
UiConfirmedBlock, UiTransactionEncoding,
map_inner_instructions, BlockEncodingOptions, ConfirmedBlock,
ConfirmedTransactionStatusWithSignature, ConfirmedTransactionWithStatusMeta,
EncodedConfirmedTransactionWithStatusMeta, Reward, RewardType, TransactionBinaryEncoding,
TransactionConfirmationStatus, TransactionStatus, UiConfirmedBlock, UiTransactionEncoding,
},
solana_vote_program::vote_state::{VoteState, MAX_LOCKOUT_HISTORY},
spl_token_2022::{
@ -3266,6 +3266,7 @@ pub mod rpc_full {
use {
super::*,
solana_sdk::message::{SanitizedVersionedMessage, VersionedMessage},
solana_transaction_status::UiInnerInstructions,
};
#[rpc]
pub trait Full {
@ -3676,7 +3677,8 @@ pub mod rpc_full {
post_simulation_accounts: _,
units_consumed,
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 {
TransactionError::BlockhashNotFound => {
@ -3694,6 +3696,7 @@ pub mod rpc_full {
accounts: None,
units_consumed: Some(units_consumed),
return_data: return_data.map(|return_data| return_data.into()),
inner_instructions: None,
},
}
.into());
@ -3724,6 +3727,7 @@ pub mod rpc_full {
encoding,
accounts: config_accounts,
min_context_slot,
inner_instructions: enable_cpi_recording,
} = config.unwrap_or_default();
let tx_encoding = encoding.unwrap_or(UiTransactionEncoding::Base58);
let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
@ -3753,7 +3757,6 @@ pub mod rpc_full {
if sig_verify {
verify_transaction(&transaction, &bank.feature_set)?;
}
let number_of_accounts = transaction.message().account_keys().len();
let TransactionSimulationResult {
result,
@ -3761,7 +3764,11 @@ pub mod rpc_full {
post_simulation_accounts,
units_consumed,
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_encoding = config_accounts
@ -3804,6 +3811,12 @@ pub mod rpc_full {
None
};
let inner_instructions = inner_instructions.map(|info| {
map_inner_instructions(info)
.map(|converted| UiInnerInstructions::parse(converted, &account_keys))
.collect()
});
Ok(new_response(
bank,
RpcSimulateTransactionResult {
@ -3812,6 +3825,7 @@ pub mod rpc_full {
accounts,
units_consumed: Some(units_consumed),
return_data: return_data.map(|return_data| return_data.into()),
inner_instructions,
},
))
}
@ -5913,6 +5927,7 @@ pub mod tests {
}
],
"err":null,
"innerInstructions": null,
"logs":[
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
@ -5997,6 +6012,7 @@ pub mod tests {
"value":{
"accounts":null,
"err":null,
"innerInstructions":null,
"logs":[
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
@ -6025,6 +6041,7 @@ pub mod tests {
"value":{
"accounts":null,
"err":null,
"innerInstructions":null,
"logs":[
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
@ -6077,6 +6094,7 @@ pub mod tests {
"value":{
"err":"BlockhashNotFound",
"accounts":null,
"innerInstructions":null,
"logs":[],
"returnData":null,
"unitsConsumed":0,
@ -6103,6 +6121,7 @@ pub mod tests {
"value":{
"accounts":null,
"err":null,
"innerInstructions":null,
"logs":[
"Program 11111111111111111111111111111111 invoke [1]",
"Program 11111111111111111111111111111111 success"
@ -6483,7 +6502,7 @@ pub mod tests {
assert_eq!(
res,
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},
},
solana_transaction_status::{
extract_and_fmt_memos, InnerInstruction, InnerInstructions, Reward, TransactionStatusMeta,
extract_and_fmt_memos, map_inner_instructions, Reward, TransactionStatusMeta,
},
std::{
sync::{
@ -121,21 +121,7 @@ impl TransactionStatusService {
let tx_account_locks = transaction.get_account_locks_unchecked();
let inner_instructions = inner_instructions.map(|inner_instructions| {
inner_instructions
.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()
map_inner_instructions(inner_instructions).collect()
});
let pre_token_balances = Some(pre_token_balances);

View File

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

View File

@ -14136,6 +14136,6 @@ fn test_failed_simulation_compute_units() {
bank.freeze();
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);
}

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 hash;
pub mod inflation;
pub mod inner_instruction;
pub mod log;
pub mod native_loader;
pub mod net;

View File

@ -230,6 +230,27 @@ pub struct InnerInstruction {
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)]
#[serde(rename_all = "camelCase")]
pub struct UiInnerInstructions {
@ -240,7 +261,7 @@ pub struct UiInnerInstructions {
}
impl UiInnerInstructions {
fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self {
pub fn parse(inner_instructions: InnerInstructions, account_keys: &AccountKeys) -> Self {
Self {
index: inner_instructions.index,
instructions: inner_instructions