Skip to content

Commit 7040146

Browse files
committed
Include the address of the program that caused TransactionError::InstructionError and the inner instruction index that points to its location in the outer instruction
This will help app developers correlate an error apparent to the program from which it originated in cases where the instruction index alone is insufficient to do so (eg. when the program that caused the error is in an inner instruction / CPI) Addresses: anza-xyz/agave#5152
1 parent 84954b4 commit 7040146

File tree

4 files changed

+120
-10
lines changed

4 files changed

+120
-10
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

transaction-error/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,13 @@ solana-frozen-abi-macro = { workspace = true, optional = true }
1717
solana-instruction = { workspace = true, default-features = false, features = [
1818
"std",
1919
] }
20+
solana-pubkey = { workspace = true }
2021
solana-sanitize = { workspace = true }
2122

23+
[dev-dependencies]
24+
serde_json = { workspace = true }
25+
test-case = { workspace = true }
26+
2227
[features]
2328
frozen-abi = ["dep:solana-frozen-abi", "dep:solana-frozen-abi-macro"]
2429
serde = ["dep:serde", "dep:serde_derive", "solana-instruction/serde"]

transaction-error/src/lib.rs

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@
44
use serde_derive::{Deserialize, Serialize};
55
#[cfg(feature = "frozen-abi")]
66
use solana_frozen_abi_macro::{AbiEnumVisitor, AbiExample};
7-
use {core::fmt, solana_instruction::error::InstructionError, solana_sanitize::SanitizeError};
7+
use {
8+
core::fmt, solana_instruction::error::InstructionError, solana_pubkey::Pubkey,
9+
solana_sanitize::SanitizeError,
10+
};
811

912
pub type TransactionResult<T> = Result<T, TransactionError>;
1013

@@ -42,9 +45,23 @@ pub enum TransactionError {
4245
/// the `recent_blockhash` has been discarded.
4346
BlockhashNotFound,
4447

45-
/// An error occurred while processing an instruction. The first element of the tuple
46-
/// indicates the instruction index in which the error occurred.
47-
InstructionError(u8, InstructionError),
48+
/// An error occurred while processing an instruction.
49+
InstructionError {
50+
err: InstructionError,
51+
/// The index of the inner instruction in which the error was thrown, starting from zero.
52+
/// This value will be `None` when the error was thrown from the outer instruction's
53+
/// program, and also for all errors stored using a version of this enum variant prior to
54+
/// `solana-transaction-error` 3.0.0.
55+
inner_instruction_index: Option<u8>,
56+
/// The index of the outer instruction in which the error was thrown, starting from zero. Do
57+
/// not infer the responsible program from the instruction at this index; the error might
58+
/// have been thrown from one of its inner instructions. See `responsible_program_address`.
59+
outer_instruction_index: u8,
60+
/// The address of the program that threw the error. Use this to decode the error (eg. to
61+
/// look up a custom error code in the program's IDL). This value will be `None` for errors
62+
/// stored using a version of this enum variant prior to `solana-transaction-error` 3.0.0.
63+
responsible_program_address: Option<Pubkey>,
64+
},
4865

4966
/// Loader call chain is too deep
5067
CallChainTooDeep,
@@ -163,7 +180,17 @@ impl fmt::Display for TransactionError {
163180
=> f.write_str("This transaction has already been processed"),
164181
Self::BlockhashNotFound
165182
=> f.write_str("Blockhash not found"),
166-
Self::InstructionError(idx, err) => write!(f, "Error processing Instruction {idx}: {err}"),
183+
Self::InstructionError {
184+
err,
185+
outer_instruction_index,
186+
..
187+
}
188+
// NOTE: We intentionally do not augment the error message in the event that the error
189+
// carries the address of the responsible program or the index of the inner
190+
// instruction. While it would add value to the log, to do so at this point would also
191+
// break any log parser that presumes a stable log format
192+
// (eg. https://tinyurl.com/3uuczr68).
193+
=> write!(f, "Error processing Instruction {outer_instruction_index}: {err}"),
167194
Self::CallChainTooDeep
168195
=> f.write_str("Loader call chain is too deep"),
169196
Self::MissingSignatureForFee
@@ -415,3 +442,78 @@ impl TransportError {
415442

416443
#[cfg(not(target_os = "solana"))]
417444
pub type TransportResult<T> = std::result::Result<T, TransportError>;
445+
446+
#[cfg(feature = "serde")]
447+
#[cfg(test)]
448+
mod tests {
449+
use {
450+
crate::TransactionError,
451+
serde_json::{from_value, json, to_value},
452+
solana_instruction::error::InstructionError,
453+
solana_pubkey::Pubkey,
454+
test_case::test_case,
455+
};
456+
457+
#[cfg(feature = "serde")]
458+
#[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), None; "From top-level instruction")]
459+
#[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), Some(41); "From inner instruction")]
460+
#[test_case(InstructionError::Custom(42), 1, None, None; "Legacy instruction without inner/program")]
461+
fn test_serialize_instruction_error(
462+
err: InstructionError,
463+
outer_instruction_index: u8,
464+
responsible_program_address: Option<Pubkey>,
465+
inner_instruction_index: Option<u8>,
466+
) {
467+
let json = to_value(TransactionError::InstructionError {
468+
err: err.clone(),
469+
inner_instruction_index,
470+
outer_instruction_index,
471+
responsible_program_address,
472+
})
473+
.unwrap();
474+
475+
assert_eq!(
476+
json,
477+
json!({
478+
"InstructionError": {
479+
"err": err,
480+
"inner_instruction_index": inner_instruction_index,
481+
"outer_instruction_index": outer_instruction_index,
482+
"responsible_program_address": responsible_program_address,
483+
},
484+
}),
485+
)
486+
}
487+
488+
#[cfg(feature = "serde")]
489+
#[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), None; "From top-level instruction")]
490+
#[test_case(InstructionError::Custom(42), 1, Some(Pubkey::new_unique()), Some(41); "From inner instruction")]
491+
#[test_case(InstructionError::Custom(42), 1, None, None; "Legacy instruction without inner/program")]
492+
#[test_case(InstructionError::Custom(42), 1, None, Some(41); "Mixed instruction with inner index but no program address")]
493+
fn test_deserialize_instruction_error(
494+
err: InstructionError,
495+
outer_instruction_index: u8,
496+
responsible_program_address: Option<Pubkey>,
497+
inner_instruction_index: Option<u8>,
498+
) {
499+
let decoded_err: TransactionError = from_value(json!({
500+
"InstructionError": {
501+
"err": err,
502+
"inner_instruction_index": inner_instruction_index,
503+
"outer_instruction_index": outer_instruction_index,
504+
"responsible_program_address": responsible_program_address,
505+
},
506+
}))
507+
.unwrap();
508+
509+
assert_eq!(
510+
decoded_err,
511+
TransactionError::InstructionError {
512+
err,
513+
inner_instruction_index,
514+
outer_instruction_index,
515+
responsible_program_address,
516+
},
517+
)
518+
}
519+
}

transaction/src/sanitized.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -298,11 +298,11 @@ impl SanitizedTransaction {
298298
self.message().instructions(),
299299
feature_set,
300300
)
301-
.map_err(|err| {
302-
TransactionError::InstructionError(
303-
index as u8,
304-
solana_instruction::error::InstructionError::Custom(err as u32),
305-
)
301+
.map_err(|err| TransactionError::InstructionError {
302+
err: solana_instruction::error::InstructionError::Custom(err as u32),
303+
inner_instruction_index: None,
304+
outer_instruction_index: index as u8,
305+
responsible_program_address: Some(*program_id),
306306
})?;
307307
}
308308
Ok(())

0 commit comments

Comments
 (0)