Skip to content

Commit e18cf20

Browse files
committed
Add the account index of the program that threw TransactionError::InstructionError
1 parent bc73c93 commit e18cf20

File tree

7 files changed

+522
-243
lines changed

7 files changed

+522
-243
lines changed

cli/src/cli.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1768,7 +1768,7 @@ where
17681768
match result {
17691769
Err(err) => {
17701770
let maybe_tx_err = err.get_transaction_error();
1771-
if let Some(TransactionError::InstructionError(_, ix_error)) = maybe_tx_err {
1771+
if let Some(TransactionError::InstructionError(_, ix_error, _)) = maybe_tx_err {
17721772
if let Some(specific_error) = error_adapter(&ix_error) {
17731773
return Err(specific_error.into());
17741774
}

cli/src/program.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2214,12 +2214,14 @@ fn close(
22142214
if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
22152215
_,
22162216
InstructionError::InvalidInstructionData,
2217+
_,
22172218
)) = err.kind()
22182219
{
22192220
return Err("Closing a buffer account is not supported by the cluster".into());
22202221
} else if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
22212222
_,
22222223
InstructionError::InvalidArgument,
2224+
_,
22232225
)) = err.kind()
22242226
{
22252227
return Err("Closing a program account is not supported by the cluster".into());
@@ -2437,6 +2439,7 @@ fn process_extend_program(
24372439
if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
24382440
_,
24392441
InstructionError::InvalidInstructionData,
2442+
_,
24402443
)) = err.kind()
24412444
{
24422445
return Err("Extending a program is not supported by the cluster".into());
@@ -2532,6 +2535,7 @@ fn process_migrate_program(
25322535
if let ClientErrorKind::TransactionError(TransactionError::InstructionError(
25332536
_,
25342537
InstructionError::InvalidInstructionData,
2538+
_,
25352539
)) = err.kind()
25362540
{
25372541
return Err("Migrating a program is not supported by the cluster".into());

compute-budget-instruction/src/compute_budget_instruction_details.rs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ impl ComputeBudgetInstructionDetails {
111111
return Err(TransactionError::InstructionError(
112112
index,
113113
InstructionError::InvalidInstructionData,
114+
None,
114115
));
115116
}
116117
} else {
@@ -153,8 +154,11 @@ impl ComputeBudgetInstructionDetails {
153154
}
154155

155156
fn process_instruction(&mut self, index: u8, instruction: &SVMInstruction) -> Result<()> {
156-
let invalid_instruction_data_error =
157-
TransactionError::InstructionError(index, InstructionError::InvalidInstructionData);
157+
let invalid_instruction_data_error = TransactionError::InstructionError(
158+
index,
159+
InstructionError::InvalidInstructionData,
160+
Some(instruction.program_id_index),
161+
);
158162
let duplicate_instruction_error = TransactionError::DuplicateInstruction(index);
159163

160164
match try_from_slice_unchecked(instruction.data) {

storage-proto/proto/transaction_by_addr.proto

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ message InstructionError {
7070
uint32 index = 1;
7171
InstructionErrorType error = 2;
7272
CustomError custom = 3;
73+
optional uint32 responsible_program_account_index = 4;
7374
}
7475

7576
message TransactionDetails {

storage-proto/src/convert.rs

Lines changed: 316 additions & 238 deletions
Large diffs are not rendered by default.

svm/src/message_processor.rs

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ use {
33
solana_program_runtime::invoke_context::InvokeContext,
44
solana_svm_transaction::svm_message::SVMMessage,
55
solana_timings::{ExecuteDetailsTimings, ExecuteTimings},
6-
solana_transaction_context::{IndexOfAccount, InstructionAccount},
6+
solana_transaction_context::{IndexOfAccount, InstructionAccount, TransactionContext},
77
solana_transaction_error::TransactionError,
88
};
99

@@ -86,7 +86,33 @@ pub(crate) fn process_message(
8686
.total_us += process_instruction_us;
8787

8888
result.map_err(|err| {
89-
TransactionError::InstructionError(top_level_instruction_index as u8, err)
89+
let transaction_context: &TransactionContext = invoke_context.transaction_context;
90+
let responsible_program_account_index = transaction_context
91+
// By definition the last instruction (outer or inner) in the trace before the trace
92+
// stopped being appended to is the one that encountered an error.
93+
.get_instruction_trace_length()
94+
.checked_sub(1)
95+
.and_then(|index_in_trace| {
96+
transaction_context
97+
.get_instruction_context_at_index_in_trace(index_in_trace)
98+
.ok()
99+
})
100+
// The last program address in the instruction is that of the program being called.
101+
.and_then(|ctx| ctx.get_last_program_key(transaction_context).ok())
102+
// The order of program accounts in the `TransactionContext` has no relation to the
103+
// order of the program accounts in the original message. It's the index in the
104+
// message that we need.
105+
.and_then(|errored_program_pubkey| {
106+
message
107+
.account_keys()
108+
.iter()
109+
.position(|message_pubkey| message_pubkey.eq(errored_program_pubkey))
110+
});
111+
TransactionError::InstructionError(
112+
top_level_instruction_index as u8,
113+
err,
114+
responsible_program_account_index.map(|i| i as u8),
115+
)
90116
})?;
91117
}
92118
Ok(())

svm/src/transaction_processor.rs

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1069,17 +1069,22 @@ mod tests {
10691069
rollback_accounts::RollbackAccounts,
10701070
},
10711071
agave_reserved_account_keys::ReservedAccountKeys,
1072+
assert_matches::assert_matches,
1073+
itertools::Itertools,
1074+
lazy_static::lazy_static,
10721075
solana_account::{create_account_shared_data_for_test, WritableAccount},
10731076
solana_clock::Clock,
10741077
solana_compute_budget_interface::ComputeBudgetInstruction,
10751078
solana_epoch_schedule::EpochSchedule,
10761079
solana_fee_calculator::FeeCalculator,
10771080
solana_fee_structure::FeeDetails,
10781081
solana_hash::Hash,
1082+
solana_instruction::{AccountMeta, Instruction},
10791083
solana_keypair::Keypair,
10801084
solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage},
10811085
solana_nonce as nonce,
10821086
solana_program_runtime::{
1087+
declare_process_instruction,
10831088
execution_budget::{
10841089
SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget,
10851090
},
@@ -1088,12 +1093,14 @@ mod tests {
10881093
solana_rent::Rent,
10891094
solana_rent_collector::{RentCollector, RENT_EXEMPT_RENT_EPOCH},
10901095
solana_rent_debits::RentDebits,
1096+
solana_sdk::instruction::InstructionError,
10911097
solana_sdk_ids::{bpf_loader, system_program, sysvar},
10921098
solana_signature::Signature,
10931099
solana_svm_callback::{AccountState, InvokeContextCallback},
10941100
solana_transaction::{sanitized::SanitizedTransaction, Transaction},
10951101
solana_transaction_context::TransactionContext,
10961102
solana_transaction_error::{TransactionError, TransactionError::DuplicateInstruction},
1103+
std::convert::TryInto,
10971104
test_case::test_case,
10981105
};
10991106

@@ -1292,6 +1299,165 @@ mod tests {
12921299
);
12931300
}
12941301

1302+
#[test_case(0; "Throw in program 0")]
1303+
#[test_case(1; "Throw in program 1")]
1304+
#[test_case(2; "Throw in program 2")]
1305+
#[test_case(3; "Throw in program 3")]
1306+
#[test_case(4; "Throw in program 4")]
1307+
#[test_case(5; "Throw in program 5")]
1308+
#[test_case(6; "Throw in program 6")]
1309+
fn test_instruction_error_carries_responsible_program_account_index(
1310+
index_of_program_that_should_throw_exception: u8,
1311+
) {
1312+
lazy_static! {
1313+
static ref PROGRAM_ADDRESSES: [Pubkey; 7] =
1314+
std::array::from_fn(|_| Pubkey::new_unique());
1315+
}
1316+
// This mock program takes in a list of programs and two bytes of data:
1317+
// (1) the index of the program that should throw an error
1318+
// (2) the index of the program being called
1319+
//
1320+
// The programs get executed - two at each CPI call depth - like this:
1321+
//
1322+
// Program 0
1323+
// -- Program 1
1324+
// -- Program 2
1325+
// ---- Program 3
1326+
// ---- Program 4
1327+
// ------ Program 5
1328+
// ------ Program 6
1329+
declare_process_instruction!(MockBuiltin, 1 /* cu_to_consume */, |invoke_context| {
1330+
let transaction_context = invoke_context.transaction_context.clone();
1331+
let instruction_context = transaction_context.get_current_instruction_context()?;
1332+
1333+
let stack_height: u8 = invoke_context.get_stack_height().try_into().unwrap();
1334+
let index_of_program_that_must_throw_exception =
1335+
instruction_context.get_instruction_data()[0] as usize;
1336+
let current_program_index = instruction_context.get_instruction_data()[1] as usize;
1337+
1338+
// Ensure that the program the instruction data claims to be running is the program that
1339+
// is actually running.
1340+
let actual_program_address = *instruction_context
1341+
.get_last_program_key(&transaction_context)
1342+
.unwrap();
1343+
assert_eq!(
1344+
actual_program_address,
1345+
PROGRAM_ADDRESSES[current_program_index],
1346+
"The address of the program at index {} that the instruction data claims to be running ({}) is not the program that is actually running ({})",
1347+
current_program_index,
1348+
PROGRAM_ADDRESSES[current_program_index],
1349+
actual_program_address,
1350+
);
1351+
1352+
if current_program_index == index_of_program_that_must_throw_exception {
1353+
return Err(InstructionError::Custom(0xdeadbeef));
1354+
}
1355+
1356+
if stack_height < 4 && current_program_index % 2 == 0 {
1357+
// Every odd program does CPIs unless it has reached the maximum CPI stack depth.
1358+
let mut last_result = Ok(());
1359+
for ii in 1..3 {
1360+
let next_program_index = current_program_index.checked_add(ii).unwrap();
1361+
let next_program_address = PROGRAM_ADDRESSES[next_program_index];
1362+
let accounts = (next_program_index..PROGRAM_ADDRESSES.len())
1363+
.map(|index| AccountMeta::new_readonly(PROGRAM_ADDRESSES[index], false))
1364+
.collect_vec();
1365+
last_result = invoke_context.native_invoke(
1366+
Instruction::new_with_bytes(
1367+
next_program_address,
1368+
&[
1369+
index_of_program_that_must_throw_exception as u8,
1370+
next_program_index as u8,
1371+
],
1372+
accounts,
1373+
)
1374+
.into(),
1375+
&[],
1376+
);
1377+
if last_result.is_err() {
1378+
return last_result;
1379+
}
1380+
}
1381+
return last_result;
1382+
}
1383+
1384+
Ok(())
1385+
});
1386+
1387+
// =======================================================
1388+
// BEGIN: Create a transaction that calls the mock program
1389+
// =======================================================
1390+
let base_program_index = 0;
1391+
let accounts = (0..PROGRAM_ADDRESSES.len())
1392+
.map(|index| AccountMeta::new_readonly(PROGRAM_ADDRESSES[index], false))
1393+
.collect_vec();
1394+
let message: Message = Message::new(
1395+
&[Instruction::new_with_bytes(
1396+
PROGRAM_ADDRESSES[base_program_index],
1397+
&[
1398+
index_of_program_that_should_throw_exception,
1399+
base_program_index as u8, /* index of program being called */
1400+
],
1401+
accounts,
1402+
)],
1403+
None,
1404+
);
1405+
let sanitized_message = new_unchecked_sanitized_message(message);
1406+
let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default();
1407+
for index in 0..PROGRAM_ADDRESSES.len() {
1408+
program_cache_for_tx_batch.replenish(
1409+
PROGRAM_ADDRESSES[index],
1410+
Arc::new(ProgramCacheEntry::new_builtin(0, 0, MockBuiltin::vm)),
1411+
);
1412+
}
1413+
let batch_processor = TransactionBatchProcessor::<TestForkGraph>::default();
1414+
let sanitized_transaction = SanitizedTransaction::new_for_tests(
1415+
sanitized_message,
1416+
vec![Signature::new_unique()],
1417+
false,
1418+
);
1419+
let mut mock_program_account = AccountSharedData::new(1, 0, &native_loader::id());
1420+
mock_program_account.set_executable(true);
1421+
let loaded_transaction = LoadedTransaction {
1422+
accounts: (0..PROGRAM_ADDRESSES.len())
1423+
.map(|index| (PROGRAM_ADDRESSES[index], mock_program_account.clone()))
1424+
.collect_vec(),
1425+
program_indices: vec![vec![0]],
1426+
fee_details: FeeDetails::default(),
1427+
rollback_accounts: RollbackAccounts::default(),
1428+
compute_budget: SVMTransactionExecutionBudget::default(),
1429+
rent: 0,
1430+
rent_debits: RentDebits::default(),
1431+
loaded_accounts_data_size: 32,
1432+
};
1433+
// =======================================================
1434+
// END: Create a transaction that calls the mock program
1435+
// =======================================================
1436+
1437+
let result = batch_processor.execute_loaded_transaction(
1438+
&MockBankCallback::default(),
1439+
&sanitized_transaction,
1440+
loaded_transaction,
1441+
&mut ExecuteTimings::default(),
1442+
&mut TransactionErrorMetrics::default(),
1443+
&mut program_cache_for_tx_batch,
1444+
&TransactionProcessingEnvironment::default(),
1445+
&TransactionProcessingConfig::default(),
1446+
);
1447+
1448+
let status = result.execution_details.status;
1449+
assert_matches!(
1450+
status.err(),
1451+
Some(TransactionError::InstructionError(
1452+
0,
1453+
InstructionError::Custom(0xdeadbeef),
1454+
Some(ii),
1455+
)) if ii == index_of_program_that_should_throw_exception,
1456+
"Expected the error to be attributable to the program with account index: \
1457+
{index_of_program_that_should_throw_exception}."
1458+
);
1459+
}
1460+
12951461
#[test]
12961462
fn test_execute_loaded_transaction_recordings() {
12971463
// Setting all the arguments correctly is too burdensome for testing

0 commit comments

Comments
 (0)