Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 61 additions & 14 deletions src/chain_parsers/visualsign-solana/src/core/accounts/decode.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ pub fn decode_accounts(message: &Message) -> Result<Vec<SolanaAccountInfo>, Visu
let is_signer = i < message.header.num_required_signatures as usize;
let is_writable = if i < message.header.num_required_signatures as usize {
// For signers: readonly ones come at the end of the signer range
let readonly_signer_start = message.header.num_required_signatures as usize
- message.header.num_readonly_signed_accounts as usize;
let readonly_signer_start = (message.header.num_required_signatures as usize)
.saturating_sub(message.header.num_readonly_signed_accounts as usize);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice, I always forget about saturating_sub

i < readonly_signer_start
} else {
// For non-signers: readonly ones come at the end of the non-signer range
let non_signer_index = i - message.header.num_required_signatures as usize;
let total_non_signers =
message.account_keys.len() - message.header.num_required_signatures as usize;
let writable_non_signers =
total_non_signers - message.header.num_readonly_unsigned_accounts as usize;
let non_signer_index =
i.saturating_sub(message.header.num_required_signatures as usize);
let total_non_signers = message
.account_keys
.len()
.saturating_sub(message.header.num_required_signatures as usize);
let writable_non_signers = total_non_signers
.saturating_sub(message.header.num_readonly_unsigned_accounts as usize);
non_signer_index < writable_non_signers
Comment thread
shahan-khatchadourian-anchorage marked this conversation as resolved.
};

Expand Down Expand Up @@ -94,16 +97,19 @@ pub fn decode_v0_accounts(
let is_signer = i < v0_message.header.num_required_signatures as usize;
let is_writable = if i < v0_message.header.num_required_signatures as usize {
// For signers: readonly ones come at the end of the signer range
let readonly_signer_start = v0_message.header.num_required_signatures as usize
- v0_message.header.num_readonly_signed_accounts as usize;
let readonly_signer_start = (v0_message.header.num_required_signatures as usize)
.saturating_sub(v0_message.header.num_readonly_signed_accounts as usize);
i < readonly_signer_start
} else {
// For non-signers: readonly ones come at the end of the non-signer range
let non_signer_index = i - v0_message.header.num_required_signatures as usize;
let total_non_signers = v0_message.account_keys.len()
- v0_message.header.num_required_signatures as usize;
let writable_non_signers =
total_non_signers - v0_message.header.num_readonly_unsigned_accounts as usize;
let non_signer_index =
i.saturating_sub(v0_message.header.num_required_signatures as usize);
let total_non_signers = v0_message
.account_keys
.len()
.saturating_sub(v0_message.header.num_required_signatures as usize);
let writable_non_signers = total_non_signers
.saturating_sub(v0_message.header.num_readonly_unsigned_accounts as usize);
Comment thread
shahan-khatchadourian-anchorage marked this conversation as resolved.
non_signer_index < writable_non_signers
};

Expand Down Expand Up @@ -803,4 +809,45 @@ mod tests {
_ => panic!("Expected PreviewLayout field"),
}
}

/// Malformed legacy message: header counts exceed account keys length.
/// Must not panic (saturating_sub prevents underflow).
#[test]
fn test_decode_accounts_inconsistent_header_no_panic() {
let account1 = Pubkey::new_unique();

// num_required_signatures (5) > account_keys.len() (1),
// num_readonly_signed_accounts (3) > num_required_signatures would be too,
// num_readonly_unsigned_accounts (2) > total non-signers (0).
let message = create_test_message(5, 3, 2, vec![account1]);

// Must not panic — the saturating_sub ensures graceful degradation.
let accounts = decode_accounts(&message).unwrap();
assert_eq!(accounts.len(), 1);
}

/// Malformed V0 message: header counts exceed account keys length.
/// Must not panic (saturating_sub prevents underflow).
#[test]
fn test_decode_v0_accounts_inconsistent_header_no_panic() {
use solana_sdk::message::{MessageHeader, v0::Message as V0Message};

let account1 = Pubkey::new_unique();

let v0_message = V0Message {
header: MessageHeader {
num_required_signatures: 10,
num_readonly_signed_accounts: 8,
num_readonly_unsigned_accounts: 5,
},
account_keys: vec![account1],
recent_blockhash: Hash::new_unique(),
instructions: vec![],
address_table_lookups: vec![],
};

// Must not panic — the saturating_sub ensures graceful degradation.
let accounts = decode_v0_accounts(&v0_message).unwrap();
assert_eq!(accounts.len(), 1);
}
}
42 changes: 31 additions & 11 deletions src/chain_parsers/visualsign-solana/src/core/instructions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,23 +25,43 @@ pub fn decode_instructions(
let message = &transaction.message;
let account_keys = &message.account_keys;

// Convert compiled instructions to full instructions
if account_keys.is_empty() {
return Err(VisualSignError::ParseError(
TransactionParseError::DecodeError(
"Legacy transaction has no account keys".to_string(),
),
));
}

// Convert compiled instructions to full instructions. Instructions with an
// out-of-bounds program_id_index are skipped entirely, while individual
// out-of-bounds account indices are silently omitted (same approach as v0 transaction handling).
let instructions: Vec<Instruction> = message
.instructions
.iter()
.map(|ci| Instruction {
program_id: account_keys[ci.program_id_index as usize],
accounts: ci
.filter_map(|ci| {
if (ci.program_id_index as usize) >= account_keys.len() {
return None;
}
let accounts: Vec<solana_sdk::instruction::AccountMeta> = ci
.accounts
.iter()
.map(|&i| {
solana_sdk::instruction::AccountMeta::new_readonly(
account_keys[i as usize],
false,
)
.filter_map(|&i| {
if (i as usize) < account_keys.len() {
Some(solana_sdk::instruction::AccountMeta::new_readonly(
account_keys[i as usize],
false,
))
} else {
None
}
})
.collect(),
data: ci.data.clone(),
.collect();
Some(Instruction {
program_id: account_keys[ci.program_id_index as usize],
accounts,
data: ci.data.clone(),
})
})
.collect();

Expand Down
Loading