Store versioned transactions in the ledger, disabled by default (#19139)
* Add support for versioned transactions, but disable by default * merge conflicts * trent's feedback * bump Cargo.lock * Fix transaction error encoding * Rename legacy_transaction method * cargo clippy * Clean up casts, int arithmetic, and unused methods * Check for duplicates in sanitized message conversion * fix clippy * fix new test * Fix bpf conditional compilation for message module
This commit is contained in:
@ -1,13 +0,0 @@
|
||||
#![feature(test)]
|
||||
|
||||
extern crate test;
|
||||
use solana_sdk::sanitized_transaction::SanitizedTransaction;
|
||||
use test::Bencher;
|
||||
|
||||
#[bench]
|
||||
fn bench_has_duplicates(bencher: &mut Bencher) {
|
||||
bencher.iter(|| {
|
||||
let data = test::black_box([1, 2, 3]);
|
||||
assert!(!SanitizedTransaction::has_duplicates(&data));
|
||||
})
|
||||
}
|
@ -3,9 +3,10 @@
|
||||
extern crate test;
|
||||
use bincode::{deserialize, serialize};
|
||||
use solana_sdk::instruction::{AccountMeta, Instruction};
|
||||
use solana_sdk::message::Message;
|
||||
use solana_sdk::pubkey;
|
||||
use solana_sdk::message::{Message, SanitizedMessage};
|
||||
use solana_sdk::pubkey::{self, Pubkey};
|
||||
use solana_sdk::sysvar::instructions;
|
||||
use std::convert::TryFrom;
|
||||
use test::Bencher;
|
||||
|
||||
fn make_instructions() -> Vec<Instruction> {
|
||||
@ -25,7 +26,9 @@ fn bench_bincode_instruction_serialize(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn bench_manual_instruction_serialize(b: &mut Bencher) {
|
||||
let instructions = make_instructions();
|
||||
let message = Message::new(&instructions, None);
|
||||
let message =
|
||||
SanitizedMessage::try_from(Message::new(&instructions, Some(&Pubkey::new_unique())))
|
||||
.unwrap();
|
||||
b.iter(|| {
|
||||
test::black_box(message.serialize_instructions());
|
||||
});
|
||||
@ -43,7 +46,9 @@ fn bench_bincode_instruction_deserialize(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn bench_manual_instruction_deserialize(b: &mut Bencher) {
|
||||
let instructions = make_instructions();
|
||||
let message = Message::new(&instructions, None);
|
||||
let message =
|
||||
SanitizedMessage::try_from(Message::new(&instructions, Some(&Pubkey::new_unique())))
|
||||
.unwrap();
|
||||
let serialized = message.serialize_instructions();
|
||||
b.iter(|| {
|
||||
for i in 0..instructions.len() {
|
||||
@ -55,7 +60,9 @@ fn bench_manual_instruction_deserialize(b: &mut Bencher) {
|
||||
#[bench]
|
||||
fn bench_manual_instruction_deserialize_single(b: &mut Bencher) {
|
||||
let instructions = make_instructions();
|
||||
let message = Message::new(&instructions, None);
|
||||
let message =
|
||||
SanitizedMessage::try_from(Message::new(&instructions, Some(&Pubkey::new_unique())))
|
||||
.unwrap();
|
||||
let serialized = message.serialize_instructions();
|
||||
b.iter(|| {
|
||||
test::black_box(instructions::load_instruction_at(3, &serialized).unwrap());
|
||||
|
@ -35,6 +35,7 @@ solana-sdk-macro = { path = "../macro", version = "=1.8.0" }
|
||||
thiserror = "1.0"
|
||||
|
||||
[target.'cfg(not(target_arch = "bpf"))'.dependencies]
|
||||
bitflags = "1.3.1"
|
||||
curve25519-dalek = "3.0.0"
|
||||
libsecp256k1 = "0.6.0"
|
||||
rand = "0.7.0"
|
||||
|
@ -369,6 +369,7 @@ impl Message {
|
||||
i < self.header.num_required_signatures as usize
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
pub fn get_account_keys_by_lock_type(&self) -> (Vec<&Pubkey>, Vec<&Pubkey>) {
|
||||
let mut writable_keys = vec![];
|
||||
let mut readonly_keys = vec![];
|
||||
@ -395,6 +396,7 @@ impl Message {
|
||||
// 35..67 - program_id
|
||||
// 67..69 - data len - u16
|
||||
// 69..data_len - data
|
||||
#[deprecated]
|
||||
pub fn serialize_instructions(&self) -> Vec<u8> {
|
||||
// 64 bytes is a reasonable guess, calculating exactly is slower in benchmarks
|
||||
let mut data = Vec::with_capacity(self.instructions.len() * (32 * 2));
|
||||
@ -485,10 +487,25 @@ impl Message {
|
||||
.min(self.header.num_required_signatures as usize);
|
||||
self.account_keys[..last_key].iter().collect()
|
||||
}
|
||||
|
||||
/// Return true if account_keys has any duplicate keys
|
||||
pub fn has_duplicates(&self) -> bool {
|
||||
// Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark
|
||||
// `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is
|
||||
// ~50 times faster than using HashSet for very short slices.
|
||||
for i in 1..self.account_keys.len() {
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
if self.account_keys[i..].contains(&self.account_keys[i - 1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(deprecated)]
|
||||
use super::*;
|
||||
use crate::{hash, instruction::AccountMeta, message::MESSAGE_HEADER_LENGTH};
|
||||
use std::collections::HashSet;
|
||||
|
245
sdk/program/src/message/mapped.rs
Normal file
245
sdk/program/src/message/mapped.rs
Normal file
@ -0,0 +1,245 @@
|
||||
use {
|
||||
crate::{
|
||||
message::{legacy::BUILTIN_PROGRAMS_KEYS, v0},
|
||||
pubkey::Pubkey,
|
||||
sysvar,
|
||||
},
|
||||
std::collections::HashSet,
|
||||
};
|
||||
|
||||
/// Combination of a version #0 message and its mapped addresses
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct MappedMessage {
|
||||
/// Message which loaded a collection of mapped addresses
|
||||
pub message: v0::Message,
|
||||
/// Collection of mapped addresses loaded by this message
|
||||
pub mapped_addresses: MappedAddresses,
|
||||
}
|
||||
|
||||
/// Collection of mapped addresses loaded succinctly by a transaction using
|
||||
/// on-chain address map accounts.
|
||||
#[derive(Clone, Default, Debug, PartialEq, Serialize, Deserialize)]
|
||||
pub struct MappedAddresses {
|
||||
/// List of addresses for writable loaded accounts
|
||||
pub writable: Vec<Pubkey>,
|
||||
/// List of addresses for read-only loaded accounts
|
||||
pub readonly: Vec<Pubkey>,
|
||||
}
|
||||
|
||||
impl MappedMessage {
|
||||
/// Returns an iterator of account key segments. The ordering of segments
|
||||
/// affects how account indexes from compiled instructions are resolved and
|
||||
/// so should not be changed.
|
||||
fn account_keys_segment_iter(&self) -> impl Iterator<Item = &Vec<Pubkey>> {
|
||||
vec![
|
||||
&self.message.account_keys,
|
||||
&self.mapped_addresses.writable,
|
||||
&self.mapped_addresses.readonly,
|
||||
]
|
||||
.into_iter()
|
||||
}
|
||||
|
||||
/// Returns the total length of loaded accounts for this message
|
||||
pub fn account_keys_len(&self) -> usize {
|
||||
let mut len = 0usize;
|
||||
for key_segment in self.account_keys_segment_iter() {
|
||||
len = len.saturating_add(key_segment.len());
|
||||
}
|
||||
len
|
||||
}
|
||||
|
||||
/// Iterator for the addresses of the loaded accounts for this message
|
||||
pub fn account_keys_iter(&self) -> impl Iterator<Item = &Pubkey> {
|
||||
self.account_keys_segment_iter().flatten()
|
||||
}
|
||||
|
||||
/// Returns true if any account keys are duplicates
|
||||
pub fn has_duplicates(&self) -> bool {
|
||||
let mut uniq = HashSet::new();
|
||||
self.account_keys_iter().any(|x| !uniq.insert(x))
|
||||
}
|
||||
|
||||
/// Returns the address of the account at the specified index of the list of
|
||||
/// message account keys constructed from unmapped keys, followed by mapped
|
||||
/// writable addresses, and lastly the list of mapped readonly addresses.
|
||||
pub fn get_account_key(&self, mut index: usize) -> Option<&Pubkey> {
|
||||
for key_segment in self.account_keys_segment_iter() {
|
||||
if index < key_segment.len() {
|
||||
return Some(&key_segment[index]);
|
||||
}
|
||||
index = index.saturating_sub(key_segment.len());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index was requested to be
|
||||
/// writable. This method should not be used directly.
|
||||
fn is_writable_index(&self, key_index: usize) -> bool {
|
||||
let header = &self.message.header;
|
||||
let num_account_keys = self.message.account_keys.len();
|
||||
let num_signed_accounts = usize::from(header.num_required_signatures);
|
||||
if key_index >= num_account_keys {
|
||||
let mapped_addresses_index = key_index.saturating_sub(num_account_keys);
|
||||
mapped_addresses_index < self.mapped_addresses.writable.len()
|
||||
} else if key_index >= num_signed_accounts {
|
||||
let num_unsigned_accounts = num_account_keys.saturating_sub(num_signed_accounts);
|
||||
let num_writable_unsigned_accounts = num_unsigned_accounts
|
||||
.saturating_sub(usize::from(header.num_readonly_unsigned_accounts));
|
||||
let unsigned_account_index = key_index.saturating_sub(num_signed_accounts);
|
||||
unsigned_account_index < num_writable_unsigned_accounts
|
||||
} else {
|
||||
let num_writable_signed_accounts = num_signed_accounts
|
||||
.saturating_sub(usize::from(header.num_readonly_signed_accounts));
|
||||
key_index < num_writable_signed_accounts
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index was loaded as writable
|
||||
pub fn is_writable(&self, key_index: usize) -> bool {
|
||||
if self.is_writable_index(key_index) {
|
||||
if let Some(key) = self.get_account_key(key_index) {
|
||||
return !(sysvar::is_sysvar_id(key) || BUILTIN_PROGRAMS_KEYS.contains(key));
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{message::MessageHeader, system_program, sysvar};
|
||||
use itertools::Itertools;
|
||||
|
||||
fn create_test_mapped_message() -> (MappedMessage, [Pubkey; 6]) {
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let key2 = Pubkey::new_unique();
|
||||
let key3 = Pubkey::new_unique();
|
||||
let key4 = Pubkey::new_unique();
|
||||
let key5 = Pubkey::new_unique();
|
||||
|
||||
let message = MappedMessage {
|
||||
message: v0::Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 2,
|
||||
num_readonly_signed_accounts: 1,
|
||||
num_readonly_unsigned_accounts: 1,
|
||||
},
|
||||
account_keys: vec![key0, key1, key2, key3],
|
||||
..v0::Message::default()
|
||||
},
|
||||
mapped_addresses: MappedAddresses {
|
||||
writable: vec![key4],
|
||||
readonly: vec![key5],
|
||||
},
|
||||
};
|
||||
|
||||
(message, [key0, key1, key2, key3, key4, key5])
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_keys_segment_iter() {
|
||||
let (message, keys) = create_test_mapped_message();
|
||||
|
||||
let expected_segments = vec![
|
||||
vec![keys[0], keys[1], keys[2], keys[3]],
|
||||
vec![keys[4]],
|
||||
vec![keys[5]],
|
||||
];
|
||||
|
||||
let mut iter = message.account_keys_segment_iter();
|
||||
for expected_segment in expected_segments {
|
||||
assert_eq!(iter.next(), Some(&expected_segment));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_keys_len() {
|
||||
let (message, keys) = create_test_mapped_message();
|
||||
|
||||
assert_eq!(message.account_keys_len(), keys.len());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_account_keys_iter() {
|
||||
let (message, keys) = create_test_mapped_message();
|
||||
|
||||
let mut iter = message.account_keys_iter();
|
||||
for expected_key in keys {
|
||||
assert_eq!(iter.next(), Some(&expected_key));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_duplicates() {
|
||||
let message = create_test_mapped_message().0;
|
||||
|
||||
assert!(!message.has_duplicates());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_duplicates_with_dupe_keys() {
|
||||
let create_message_with_dupe_keys = |mut keys: Vec<Pubkey>| MappedMessage {
|
||||
message: v0::Message {
|
||||
account_keys: keys.split_off(2),
|
||||
..v0::Message::default()
|
||||
},
|
||||
mapped_addresses: MappedAddresses {
|
||||
writable: keys.split_off(2),
|
||||
readonly: keys,
|
||||
},
|
||||
};
|
||||
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let key2 = Pubkey::new_unique();
|
||||
let key3 = Pubkey::new_unique();
|
||||
let dupe_key = Pubkey::new_unique();
|
||||
|
||||
let keys = vec![key0, key1, key2, key3, dupe_key, dupe_key];
|
||||
let keys_len = keys.len();
|
||||
for keys in keys.into_iter().permutations(keys_len).unique() {
|
||||
let message = create_message_with_dupe_keys(keys);
|
||||
assert!(message.has_duplicates());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_get_account_key() {
|
||||
let (message, keys) = create_test_mapped_message();
|
||||
|
||||
assert_eq!(message.get_account_key(0), Some(&keys[0]));
|
||||
assert_eq!(message.get_account_key(1), Some(&keys[1]));
|
||||
assert_eq!(message.get_account_key(2), Some(&keys[2]));
|
||||
assert_eq!(message.get_account_key(3), Some(&keys[3]));
|
||||
assert_eq!(message.get_account_key(4), Some(&keys[4]));
|
||||
assert_eq!(message.get_account_key(5), Some(&keys[5]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_writable_index() {
|
||||
let message = create_test_mapped_message().0;
|
||||
|
||||
assert!(message.is_writable_index(0));
|
||||
assert!(!message.is_writable_index(1));
|
||||
assert!(message.is_writable_index(2));
|
||||
assert!(!message.is_writable_index(3));
|
||||
assert!(message.is_writable_index(4));
|
||||
assert!(!message.is_writable_index(5));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_writable() {
|
||||
let mut mapped_msg = create_test_mapped_message().0;
|
||||
|
||||
mapped_msg.message.account_keys[0] = sysvar::clock::id();
|
||||
assert!(mapped_msg.is_writable_index(0));
|
||||
assert!(!mapped_msg.is_writable(0));
|
||||
|
||||
mapped_msg.message.account_keys[0] = system_program::id();
|
||||
assert!(mapped_msg.is_writable_index(0));
|
||||
assert!(!mapped_msg.is_writable(0));
|
||||
}
|
||||
}
|
@ -1,12 +1,26 @@
|
||||
//! A library for generating a message from a sequence of instructions
|
||||
|
||||
mod legacy;
|
||||
mod v0;
|
||||
mod versions;
|
||||
pub mod legacy;
|
||||
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
#[path = ""]
|
||||
mod non_bpf_modules {
|
||||
mod mapped;
|
||||
mod sanitized;
|
||||
pub mod v0;
|
||||
mod versions;
|
||||
|
||||
pub use mapped::*;
|
||||
pub use sanitized::*;
|
||||
pub use versions::*;
|
||||
}
|
||||
|
||||
pub use legacy::Message;
|
||||
pub use versions::MESSAGE_VERSION_PREFIX;
|
||||
|
||||
#[cfg(not(target_arch = "bpf"))]
|
||||
pub use non_bpf_modules::*;
|
||||
|
||||
/// The length of a message header in bytes
|
||||
pub const MESSAGE_HEADER_LENGTH: usize = 3;
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, PartialEq, Eq, Clone, AbiExample)]
|
||||
|
599
sdk/program/src/message/sanitized.rs
Normal file
599
sdk/program/src/message/sanitized.rs
Normal file
@ -0,0 +1,599 @@
|
||||
use {
|
||||
crate::{
|
||||
fee_calculator::FeeCalculator,
|
||||
hash::Hash,
|
||||
instruction::{CompiledInstruction, Instruction},
|
||||
message::{MappedAddresses, MappedMessage, Message, MessageHeader},
|
||||
pubkey::Pubkey,
|
||||
sanitize::{Sanitize, SanitizeError},
|
||||
secp256k1_program,
|
||||
serialize_utils::{append_slice, append_u16, append_u8},
|
||||
},
|
||||
bitflags::bitflags,
|
||||
std::convert::TryFrom,
|
||||
thiserror::Error,
|
||||
};
|
||||
|
||||
/// Sanitized message of a transaction which includes a set of atomic
|
||||
/// instructions to be executed on-chain
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum SanitizedMessage {
|
||||
/// Sanitized legacy message
|
||||
Legacy(Message),
|
||||
/// Sanitized version #0 message with mapped addresses
|
||||
V0(MappedMessage),
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Debug, Error, Eq, Clone)]
|
||||
pub enum SanitizeMessageError {
|
||||
#[error("index out of bounds")]
|
||||
IndexOutOfBounds,
|
||||
#[error("value out of bounds")]
|
||||
ValueOutOfBounds,
|
||||
#[error("invalid value")]
|
||||
InvalidValue,
|
||||
#[error("duplicate account key")]
|
||||
DuplicateAccountKey,
|
||||
}
|
||||
|
||||
impl From<SanitizeError> for SanitizeMessageError {
|
||||
fn from(err: SanitizeError) -> Self {
|
||||
match err {
|
||||
SanitizeError::IndexOutOfBounds => Self::IndexOutOfBounds,
|
||||
SanitizeError::ValueOutOfBounds => Self::ValueOutOfBounds,
|
||||
SanitizeError::InvalidValue => Self::InvalidValue,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<Message> for SanitizedMessage {
|
||||
type Error = SanitizeMessageError;
|
||||
fn try_from(message: Message) -> Result<Self, Self::Error> {
|
||||
message.sanitize()?;
|
||||
|
||||
let sanitized_msg = Self::Legacy(message);
|
||||
if sanitized_msg.has_duplicates() {
|
||||
return Err(SanitizeMessageError::DuplicateAccountKey);
|
||||
}
|
||||
|
||||
Ok(sanitized_msg)
|
||||
}
|
||||
}
|
||||
|
||||
bitflags! {
|
||||
struct InstructionsSysvarAccountMeta: u8 {
|
||||
const NONE = 0b00000000;
|
||||
const IS_SIGNER = 0b00000001;
|
||||
const IS_WRITABLE = 0b00000010;
|
||||
}
|
||||
}
|
||||
|
||||
impl SanitizedMessage {
|
||||
/// Return true if this message contains duplicate account keys
|
||||
pub fn has_duplicates(&self) -> bool {
|
||||
match self {
|
||||
SanitizedMessage::Legacy(message) => message.has_duplicates(),
|
||||
SanitizedMessage::V0(message) => message.has_duplicates(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Message header which identifies the number of signer and writable or
|
||||
/// readonly accounts
|
||||
pub fn header(&self) -> &MessageHeader {
|
||||
match self {
|
||||
Self::Legacy(message) => &message.header,
|
||||
Self::V0(mapped_msg) => &mapped_msg.message.header,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a legacy message if this sanitized message wraps one
|
||||
pub fn legacy_message(&self) -> Option<&Message> {
|
||||
if let Self::Legacy(message) = &self {
|
||||
Some(message)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the fee payer for the transaction
|
||||
pub fn fee_payer(&self) -> &Pubkey {
|
||||
self.get_account_key(0)
|
||||
.expect("sanitized message always has non-program fee payer at index 0")
|
||||
}
|
||||
|
||||
/// The hash of a recent block, used for timing out a transaction
|
||||
pub fn recent_blockhash(&self) -> &Hash {
|
||||
match self {
|
||||
Self::Legacy(message) => &message.recent_blockhash,
|
||||
Self::V0(mapped_msg) => &mapped_msg.message.recent_blockhash,
|
||||
}
|
||||
}
|
||||
|
||||
/// Program instructions that will be executed in sequence and committed in
|
||||
/// one atomic transaction if all succeed.
|
||||
pub fn instructions(&self) -> &[CompiledInstruction] {
|
||||
match self {
|
||||
Self::Legacy(message) => &message.instructions,
|
||||
Self::V0(mapped_msg) => &mapped_msg.message.instructions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Program instructions iterator which includes each instruction's program
|
||||
/// id.
|
||||
pub fn program_instructions_iter(
|
||||
&self,
|
||||
) -> impl Iterator<Item = (&Pubkey, &CompiledInstruction)> {
|
||||
match self {
|
||||
Self::Legacy(message) => message.instructions.iter(),
|
||||
Self::V0(mapped_msg) => mapped_msg.message.instructions.iter(),
|
||||
}
|
||||
.map(move |ix| {
|
||||
(
|
||||
self.get_account_key(usize::from(ix.program_id_index))
|
||||
.expect("program id index is sanitized"),
|
||||
ix,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterator of all account keys referenced in this message, included mapped keys.
|
||||
pub fn account_keys_iter(&self) -> Box<dyn Iterator<Item = &Pubkey> + '_> {
|
||||
match self {
|
||||
Self::Legacy(message) => Box::new(message.account_keys.iter()),
|
||||
Self::V0(mapped_msg) => Box::new(mapped_msg.account_keys_iter()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Length of all account keys referenced in this message, included mapped keys.
|
||||
pub fn account_keys_len(&self) -> usize {
|
||||
match self {
|
||||
Self::Legacy(message) => message.account_keys.len(),
|
||||
Self::V0(mapped_msg) => mapped_msg.account_keys_len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the address of the account at the specified index.
|
||||
pub fn get_account_key(&self, index: usize) -> Option<&Pubkey> {
|
||||
match self {
|
||||
Self::Legacy(message) => message.account_keys.get(index),
|
||||
Self::V0(message) => message.get_account_key(index),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index is an input to some
|
||||
/// program instruction in this message.
|
||||
fn is_key_passed_to_program(&self, key_index: usize) -> bool {
|
||||
if let Ok(key_index) = u8::try_from(key_index) {
|
||||
self.instructions()
|
||||
.iter()
|
||||
.any(|ix| ix.accounts.contains(&key_index))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index is invoked as a
|
||||
/// program in this message.
|
||||
pub fn is_invoked(&self, key_index: usize) -> bool {
|
||||
if let Ok(key_index) = u8::try_from(key_index) {
|
||||
self.instructions()
|
||||
.iter()
|
||||
.any(|ix| ix.program_id_index == key_index)
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index is not invoked as a
|
||||
/// program or, if invoked, is passed to a program.
|
||||
pub fn is_non_loader_key(&self, key_index: usize) -> bool {
|
||||
!self.is_invoked(key_index) || self.is_key_passed_to_program(key_index)
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index is writable by the
|
||||
/// instructions in this message.
|
||||
pub fn is_writable(&self, index: usize) -> bool {
|
||||
match self {
|
||||
Self::Legacy(message) => message.is_writable(index),
|
||||
Self::V0(message) => message.is_writable(index),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if the account at the specified index signed this
|
||||
/// message.
|
||||
pub fn is_signer(&self, index: usize) -> bool {
|
||||
index < usize::from(self.header().num_required_signatures)
|
||||
}
|
||||
|
||||
// First encode the number of instructions:
|
||||
// [0..2 - num_instructions
|
||||
//
|
||||
// Then a table of offsets of where to find them in the data
|
||||
// 3..2 * num_instructions table of instruction offsets
|
||||
//
|
||||
// Each instruction is then encoded as:
|
||||
// 0..2 - num_accounts
|
||||
// 2 - meta_byte -> (bit 0 signer, bit 1 is_writable)
|
||||
// 3..35 - pubkey - 32 bytes
|
||||
// 35..67 - program_id
|
||||
// 67..69 - data len - u16
|
||||
// 69..data_len - data
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
pub fn serialize_instructions(&self) -> Vec<u8> {
|
||||
// 64 bytes is a reasonable guess, calculating exactly is slower in benchmarks
|
||||
let mut data = Vec::with_capacity(self.instructions().len() * (32 * 2));
|
||||
append_u16(&mut data, self.instructions().len() as u16);
|
||||
for _ in 0..self.instructions().len() {
|
||||
append_u16(&mut data, 0);
|
||||
}
|
||||
for (i, (program_id, instruction)) in self.program_instructions_iter().enumerate() {
|
||||
let start_instruction_offset = data.len() as u16;
|
||||
let start = 2 + (2 * i);
|
||||
data[start..start + 2].copy_from_slice(&start_instruction_offset.to_le_bytes());
|
||||
append_u16(&mut data, instruction.accounts.len() as u16);
|
||||
for account_index in &instruction.accounts {
|
||||
let account_index = *account_index as usize;
|
||||
let is_signer = self.is_signer(account_index);
|
||||
let is_writable = self.is_writable(account_index);
|
||||
let mut account_meta = InstructionsSysvarAccountMeta::NONE;
|
||||
if is_signer {
|
||||
account_meta |= InstructionsSysvarAccountMeta::IS_SIGNER;
|
||||
}
|
||||
if is_writable {
|
||||
account_meta |= InstructionsSysvarAccountMeta::IS_WRITABLE;
|
||||
}
|
||||
append_u8(&mut data, account_meta.bits());
|
||||
append_slice(
|
||||
&mut data,
|
||||
self.get_account_key(account_index).unwrap().as_ref(),
|
||||
);
|
||||
}
|
||||
|
||||
append_slice(&mut data, program_id.as_ref());
|
||||
append_u16(&mut data, instruction.data.len() as u16);
|
||||
append_slice(&mut data, &instruction.data);
|
||||
}
|
||||
data
|
||||
}
|
||||
|
||||
/// Return the mapped addresses for this message if it has any.
|
||||
fn mapped_addresses(&self) -> Option<&MappedAddresses> {
|
||||
match &self {
|
||||
SanitizedMessage::V0(message) => Some(&message.mapped_addresses),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the number of readonly accounts loaded by this message.
|
||||
pub fn num_readonly_accounts(&self) -> usize {
|
||||
let mapped_readonly_addresses = self
|
||||
.mapped_addresses()
|
||||
.map(|keys| keys.readonly.len())
|
||||
.unwrap_or_default();
|
||||
mapped_readonly_addresses
|
||||
.saturating_add(usize::from(self.header().num_readonly_signed_accounts))
|
||||
.saturating_add(usize::from(self.header().num_readonly_unsigned_accounts))
|
||||
}
|
||||
|
||||
fn try_position(&self, key: &Pubkey) -> Option<u8> {
|
||||
u8::try_from(self.account_keys_iter().position(|k| k == key)?).ok()
|
||||
}
|
||||
|
||||
/// Try to compile an instruction using the account keys in this message.
|
||||
pub fn try_compile_instruction(&self, ix: &Instruction) -> Option<CompiledInstruction> {
|
||||
let accounts: Vec<_> = ix
|
||||
.accounts
|
||||
.iter()
|
||||
.map(|account_meta| self.try_position(&account_meta.pubkey))
|
||||
.collect::<Option<_>>()?;
|
||||
|
||||
Some(CompiledInstruction {
|
||||
program_id_index: self.try_position(&ix.program_id)?,
|
||||
data: ix.data.clone(),
|
||||
accounts,
|
||||
})
|
||||
}
|
||||
|
||||
/// Calculate the total fees for a transaction given a fee calculator
|
||||
pub fn calculate_fee(&self, fee_calculator: &FeeCalculator) -> u64 {
|
||||
let mut num_secp256k1_signatures: u64 = 0;
|
||||
for (program_id, instruction) in self.program_instructions_iter() {
|
||||
if secp256k1_program::check_id(program_id) {
|
||||
if let Some(num_signatures) = instruction.data.get(0) {
|
||||
num_secp256k1_signatures =
|
||||
num_secp256k1_signatures.saturating_add(u64::from(*num_signatures));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fee_calculator.lamports_per_signature.saturating_mul(
|
||||
u64::from(self.header().num_required_signatures)
|
||||
.saturating_add(num_secp256k1_signatures),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{
|
||||
instruction::{AccountMeta, Instruction},
|
||||
message::v0,
|
||||
secp256k1_program, system_instruction,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_try_from_message() {
|
||||
let dupe_key = Pubkey::new_unique();
|
||||
let legacy_message_with_dupes = Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 1,
|
||||
..MessageHeader::default()
|
||||
},
|
||||
account_keys: vec![dupe_key, dupe_key],
|
||||
..Message::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
SanitizedMessage::try_from(legacy_message_with_dupes).err(),
|
||||
Some(SanitizeMessageError::DuplicateAccountKey),
|
||||
);
|
||||
|
||||
let legacy_message_with_no_signers = Message {
|
||||
account_keys: vec![Pubkey::new_unique()],
|
||||
..Message::default()
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
SanitizedMessage::try_from(legacy_message_with_no_signers).err(),
|
||||
Some(SanitizeMessageError::IndexOutOfBounds),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_non_loader_key() {
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let loader_key = Pubkey::new_unique();
|
||||
let instructions = vec![
|
||||
CompiledInstruction::new(1, &(), vec![0]),
|
||||
CompiledInstruction::new(2, &(), vec![0, 1]),
|
||||
];
|
||||
|
||||
let message = SanitizedMessage::try_from(Message::new_with_compiled_instructions(
|
||||
1,
|
||||
0,
|
||||
2,
|
||||
vec![key0, key1, loader_key],
|
||||
Hash::default(),
|
||||
instructions,
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
assert!(message.is_non_loader_key(0));
|
||||
assert!(message.is_non_loader_key(1));
|
||||
assert!(!message.is_non_loader_key(2));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_num_readonly_accounts() {
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let key2 = Pubkey::new_unique();
|
||||
let key3 = Pubkey::new_unique();
|
||||
let key4 = Pubkey::new_unique();
|
||||
let key5 = Pubkey::new_unique();
|
||||
|
||||
let legacy_message = SanitizedMessage::try_from(Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 2,
|
||||
num_readonly_signed_accounts: 1,
|
||||
num_readonly_unsigned_accounts: 1,
|
||||
},
|
||||
account_keys: vec![key0, key1, key2, key3],
|
||||
..Message::default()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(legacy_message.num_readonly_accounts(), 2);
|
||||
|
||||
let mapped_message = SanitizedMessage::V0(MappedMessage {
|
||||
message: v0::Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 2,
|
||||
num_readonly_signed_accounts: 1,
|
||||
num_readonly_unsigned_accounts: 1,
|
||||
},
|
||||
account_keys: vec![key0, key1, key2, key3],
|
||||
..v0::Message::default()
|
||||
},
|
||||
mapped_addresses: MappedAddresses {
|
||||
writable: vec![key4],
|
||||
readonly: vec![key5],
|
||||
},
|
||||
});
|
||||
|
||||
assert_eq!(mapped_message.num_readonly_accounts(), 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(deprecated)]
|
||||
fn test_serialize_instructions() {
|
||||
let program_id0 = Pubkey::new_unique();
|
||||
let program_id1 = Pubkey::new_unique();
|
||||
let id0 = Pubkey::new_unique();
|
||||
let id1 = Pubkey::new_unique();
|
||||
let id2 = Pubkey::new_unique();
|
||||
let id3 = Pubkey::new_unique();
|
||||
let instructions = vec![
|
||||
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id0, false)]),
|
||||
Instruction::new_with_bincode(program_id0, &0, vec![AccountMeta::new(id1, true)]),
|
||||
Instruction::new_with_bincode(
|
||||
program_id1,
|
||||
&0,
|
||||
vec![AccountMeta::new_readonly(id2, false)],
|
||||
),
|
||||
Instruction::new_with_bincode(
|
||||
program_id1,
|
||||
&0,
|
||||
vec![AccountMeta::new_readonly(id3, true)],
|
||||
),
|
||||
];
|
||||
|
||||
let message = Message::new(&instructions, Some(&id1));
|
||||
let sanitized_message = SanitizedMessage::try_from(message.clone()).unwrap();
|
||||
let serialized = sanitized_message.serialize_instructions();
|
||||
|
||||
// assert that SanitizedMessage::serialize_instructions has the same behavior as the
|
||||
// deprecated Message::serialize_instructions method
|
||||
assert_eq!(serialized, message.serialize_instructions());
|
||||
|
||||
// assert that Message::deserialize_instruction is compatible with SanitizedMessage::serialize_instructions
|
||||
for (i, instruction) in instructions.iter().enumerate() {
|
||||
assert_eq!(
|
||||
Message::deserialize_instruction(i, &serialized).unwrap(),
|
||||
*instruction
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_fee() {
|
||||
// Default: no fee.
|
||||
let message =
|
||||
SanitizedMessage::try_from(Message::new(&[], Some(&Pubkey::new_unique()))).unwrap();
|
||||
assert_eq!(message.calculate_fee(&FeeCalculator::default()), 0);
|
||||
|
||||
// One signature, a fee.
|
||||
assert_eq!(message.calculate_fee(&FeeCalculator::new(1)), 1);
|
||||
|
||||
// Two signatures, double the fee.
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let ix0 = system_instruction::transfer(&key0, &key1, 1);
|
||||
let ix1 = system_instruction::transfer(&key1, &key0, 1);
|
||||
let message = SanitizedMessage::try_from(Message::new(&[ix0, ix1], Some(&key0))).unwrap();
|
||||
assert_eq!(message.calculate_fee(&FeeCalculator::new(2)), 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_try_compile_instruction() {
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let key2 = Pubkey::new_unique();
|
||||
let program_id = Pubkey::new_unique();
|
||||
|
||||
let valid_instruction = Instruction {
|
||||
program_id,
|
||||
accounts: vec![
|
||||
AccountMeta::new_readonly(key0, false),
|
||||
AccountMeta::new_readonly(key1, false),
|
||||
AccountMeta::new_readonly(key2, false),
|
||||
],
|
||||
data: vec![],
|
||||
};
|
||||
|
||||
let invalid_program_id_instruction = Instruction {
|
||||
program_id: Pubkey::new_unique(),
|
||||
accounts: vec![
|
||||
AccountMeta::new_readonly(key0, false),
|
||||
AccountMeta::new_readonly(key1, false),
|
||||
AccountMeta::new_readonly(key2, false),
|
||||
],
|
||||
data: vec![],
|
||||
};
|
||||
|
||||
let invalid_account_key_instruction = Instruction {
|
||||
program_id: Pubkey::new_unique(),
|
||||
accounts: vec![
|
||||
AccountMeta::new_readonly(key0, false),
|
||||
AccountMeta::new_readonly(key1, false),
|
||||
AccountMeta::new_readonly(Pubkey::new_unique(), false),
|
||||
],
|
||||
data: vec![],
|
||||
};
|
||||
|
||||
let legacy_message = SanitizedMessage::try_from(Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 1,
|
||||
num_readonly_signed_accounts: 0,
|
||||
num_readonly_unsigned_accounts: 0,
|
||||
},
|
||||
account_keys: vec![key0, key1, key2, program_id],
|
||||
..Message::default()
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
let mapped_message = SanitizedMessage::V0(MappedMessage {
|
||||
message: v0::Message {
|
||||
header: MessageHeader {
|
||||
num_required_signatures: 1,
|
||||
num_readonly_signed_accounts: 0,
|
||||
num_readonly_unsigned_accounts: 0,
|
||||
},
|
||||
account_keys: vec![key0, key1],
|
||||
..v0::Message::default()
|
||||
},
|
||||
mapped_addresses: MappedAddresses {
|
||||
writable: vec![key2],
|
||||
readonly: vec![program_id],
|
||||
},
|
||||
});
|
||||
|
||||
for message in vec![legacy_message, mapped_message] {
|
||||
assert_eq!(
|
||||
message.try_compile_instruction(&valid_instruction),
|
||||
Some(CompiledInstruction {
|
||||
program_id_index: 3,
|
||||
accounts: vec![0, 1, 2],
|
||||
data: vec![],
|
||||
})
|
||||
);
|
||||
|
||||
assert!(message
|
||||
.try_compile_instruction(&invalid_program_id_instruction)
|
||||
.is_none());
|
||||
assert!(message
|
||||
.try_compile_instruction(&invalid_account_key_instruction)
|
||||
.is_none());
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calculate_fee_secp256k1() {
|
||||
let key0 = Pubkey::new_unique();
|
||||
let key1 = Pubkey::new_unique();
|
||||
let ix0 = system_instruction::transfer(&key0, &key1, 1);
|
||||
|
||||
let mut secp_instruction1 = Instruction {
|
||||
program_id: secp256k1_program::id(),
|
||||
accounts: vec![],
|
||||
data: vec![],
|
||||
};
|
||||
let mut secp_instruction2 = Instruction {
|
||||
program_id: secp256k1_program::id(),
|
||||
accounts: vec![],
|
||||
data: vec![1],
|
||||
};
|
||||
|
||||
let message = SanitizedMessage::try_from(Message::new(
|
||||
&[
|
||||
ix0.clone(),
|
||||
secp_instruction1.clone(),
|
||||
secp_instruction2.clone(),
|
||||
],
|
||||
Some(&key0),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(message.calculate_fee(&FeeCalculator::new(1)), 2);
|
||||
|
||||
secp_instruction1.data = vec![0];
|
||||
secp_instruction2.data = vec![10];
|
||||
let message = SanitizedMessage::try_from(Message::new(
|
||||
&[ix0, secp_instruction1, secp_instruction2],
|
||||
Some(&key0),
|
||||
))
|
||||
.unwrap();
|
||||
assert_eq!(message.calculate_fee(&FeeCalculator::new(1)), 11);
|
||||
}
|
||||
}
|
@ -1,9 +1,7 @@
|
||||
#![allow(clippy::integer_arithmetic)]
|
||||
|
||||
use crate::{
|
||||
hash::Hash,
|
||||
instruction::CompiledInstruction,
|
||||
message::MessageHeader,
|
||||
message::{MessageHeader, MESSAGE_VERSION_PREFIX},
|
||||
pubkey::Pubkey,
|
||||
sanitize::{Sanitize, SanitizeError},
|
||||
short_vec,
|
||||
@ -62,8 +60,8 @@ impl Sanitize for Message {
|
||||
fn sanitize(&self) -> Result<(), SanitizeError> {
|
||||
// signing area and read-only non-signing area should not
|
||||
// overlap
|
||||
if self.header.num_required_signatures as usize
|
||||
+ self.header.num_readonly_unsigned_accounts as usize
|
||||
if usize::from(self.header.num_required_signatures)
|
||||
.saturating_add(usize::from(self.header.num_readonly_unsigned_accounts))
|
||||
> self.account_keys.len()
|
||||
{
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
@ -76,7 +74,7 @@ impl Sanitize for Message {
|
||||
|
||||
// there cannot be more address maps than read-only unsigned accounts.
|
||||
let num_address_map_indexes = self.address_map_indexes.len();
|
||||
if num_address_map_indexes > self.header.num_readonly_unsigned_accounts as usize {
|
||||
if num_address_map_indexes > usize::from(self.header.num_readonly_unsigned_accounts) {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
|
||||
@ -102,7 +100,7 @@ impl Sanitize for Message {
|
||||
}
|
||||
|
||||
for ci in &self.instructions {
|
||||
if ci.program_id_index as usize >= num_loaded_accounts {
|
||||
if usize::from(ci.program_id_index) >= num_loaded_accounts {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
// A program cannot be a payer.
|
||||
@ -110,7 +108,7 @@ impl Sanitize for Message {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
for ai in &ci.accounts {
|
||||
if *ai as usize >= num_loaded_accounts {
|
||||
if usize::from(*ai) >= num_loaded_accounts {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
}
|
||||
@ -120,9 +118,17 @@ impl Sanitize for Message {
|
||||
}
|
||||
}
|
||||
|
||||
impl Message {
|
||||
/// Serialize this message with a version #0 prefix using bincode encoding.
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
bincode::serialize(&(MESSAGE_VERSION_PREFIX, self)).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::message::VersionedMessage;
|
||||
|
||||
fn simple_message() -> Message {
|
||||
Message {
|
||||
@ -381,4 +387,11 @@ mod tests {
|
||||
.sanitize()
|
||||
.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_serialize() {
|
||||
let message = simple_message();
|
||||
let versioned_msg = VersionedMessage::V0(message.clone());
|
||||
assert_eq!(message.serialize(), versioned_msg.serialize());
|
||||
}
|
||||
}
|
||||
|
@ -26,13 +26,77 @@ pub const MESSAGE_VERSION_PREFIX: u8 = 0x80;
|
||||
/// which message version is serialized starting from version `0`. If the first
|
||||
/// is bit is not set, all bytes are used to encode the legacy `Message`
|
||||
/// format.
|
||||
#[frozen_abi(digest = "C4MZ7qztFJHUp1bVcuh7Gn43PQExadzEGyEb8UMn9unz")]
|
||||
#[frozen_abi(digest = "qKNCqQpsBZYQxS9P3hVcFr8hAF4VnqV6ZBdC6KoUvHJ")]
|
||||
#[derive(Debug, PartialEq, Eq, Clone, AbiEnumVisitor, AbiExample)]
|
||||
pub enum VersionedMessage {
|
||||
Legacy(Message),
|
||||
V0(v0::Message),
|
||||
}
|
||||
|
||||
impl VersionedMessage {
|
||||
pub fn header(&self) -> &MessageHeader {
|
||||
match self {
|
||||
Self::Legacy(message) => &message.header,
|
||||
Self::V0(message) => &message.header,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmapped_keys(self) -> Vec<Pubkey> {
|
||||
match self {
|
||||
Self::Legacy(message) => message.account_keys,
|
||||
Self::V0(message) => message.account_keys,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmapped_keys_iter(&self) -> impl Iterator<Item = &Pubkey> {
|
||||
match self {
|
||||
Self::Legacy(message) => message.account_keys.iter(),
|
||||
Self::V0(message) => message.account_keys.iter(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn unmapped_keys_len(&self) -> usize {
|
||||
match self {
|
||||
Self::Legacy(message) => message.account_keys.len(),
|
||||
Self::V0(message) => message.account_keys.len(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recent_blockhash(&self) -> &Hash {
|
||||
match self {
|
||||
Self::Legacy(message) => &message.recent_blockhash,
|
||||
Self::V0(message) => &message.recent_blockhash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_recent_blockhash(&mut self, recent_blockhash: Hash) {
|
||||
match self {
|
||||
Self::Legacy(message) => message.recent_blockhash = recent_blockhash,
|
||||
Self::V0(message) => message.recent_blockhash = recent_blockhash,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
bincode::serialize(self).unwrap()
|
||||
}
|
||||
|
||||
/// Compute the blake3 hash of this transaction's message
|
||||
pub fn hash(&self) -> Hash {
|
||||
let message_bytes = self.serialize();
|
||||
Self::hash_raw_message(&message_bytes)
|
||||
}
|
||||
|
||||
/// Compute the blake3 hash of a raw transaction message
|
||||
pub fn hash_raw_message(message_bytes: &[u8]) -> Hash {
|
||||
use blake3::traits::digest::Digest;
|
||||
use std::convert::TryFrom;
|
||||
let mut hasher = blake3::Hasher::new();
|
||||
hasher.update(b"solana-tx-message-v1");
|
||||
hasher.update(message_bytes);
|
||||
Hash(<[u8; crate::hash::HASH_BYTES]>::try_from(hasher.finalize().as_slice()).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VersionedMessage {
|
||||
fn default() -> Self {
|
||||
Self::Legacy(Message::default())
|
||||
|
@ -1,10 +1,12 @@
|
||||
#![cfg(feature = "full")]
|
||||
|
||||
use crate::transaction::{Transaction, TransactionError};
|
||||
use borsh::{BorshDeserialize, BorshSchema, BorshSerialize};
|
||||
use solana_sdk::{
|
||||
borsh::try_from_slice_unchecked,
|
||||
instruction::{Instruction, InstructionError},
|
||||
use {
|
||||
crate::{
|
||||
borsh::try_from_slice_unchecked,
|
||||
instruction::{Instruction, InstructionError},
|
||||
transaction::{SanitizedTransaction, TransactionError},
|
||||
},
|
||||
borsh::{BorshDeserialize, BorshSchema, BorshSerialize},
|
||||
};
|
||||
|
||||
crate::declare_id!("ComputeBudget111111111111111111111111111111");
|
||||
@ -96,11 +98,14 @@ impl ComputeBudget {
|
||||
heap_size: None,
|
||||
}
|
||||
}
|
||||
pub fn process_transaction(&mut self, tx: &Transaction) -> Result<(), TransactionError> {
|
||||
pub fn process_transaction(
|
||||
&mut self,
|
||||
tx: &SanitizedTransaction,
|
||||
) -> Result<(), TransactionError> {
|
||||
let error = TransactionError::InstructionError(0, InstructionError::InvalidInstructionData);
|
||||
// Compute budget instruction must be in 1st or 2nd instruction (avoid nonce marker)
|
||||
for instruction in tx.message().instructions.iter().take(2) {
|
||||
if check_id(instruction.program_id(&tx.message().account_keys)) {
|
||||
for (program_id, instruction) in tx.message().program_instructions_iter().take(2) {
|
||||
if check_id(program_id) {
|
||||
let ComputeBudgetInstruction::RequestUnits(units) =
|
||||
try_from_slice_unchecked::<ComputeBudgetInstruction>(&instruction.data)
|
||||
.map_err(|_| error.clone())?;
|
||||
@ -117,22 +122,30 @@ impl ComputeBudget {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{hash::Hash, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer};
|
||||
use crate::{
|
||||
hash::Hash, message::Message, pubkey::Pubkey, signature::Keypair, signer::Signer,
|
||||
transaction::Transaction,
|
||||
};
|
||||
use std::convert::TryInto;
|
||||
|
||||
fn sanitize_tx(tx: Transaction) -> SanitizedTransaction {
|
||||
tx.try_into().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_process_transaction() {
|
||||
let payer_keypair = Keypair::new();
|
||||
let mut compute_budget = ComputeBudget::default();
|
||||
|
||||
let tx = Transaction::new(
|
||||
let tx = sanitize_tx(Transaction::new(
|
||||
&[&payer_keypair],
|
||||
Message::new(&[], Some(&payer_keypair.pubkey())),
|
||||
Hash::default(),
|
||||
);
|
||||
));
|
||||
compute_budget.process_transaction(&tx).unwrap();
|
||||
assert_eq!(compute_budget, ComputeBudget::default());
|
||||
|
||||
let tx = Transaction::new(
|
||||
let tx = sanitize_tx(Transaction::new(
|
||||
&[&payer_keypair],
|
||||
Message::new(
|
||||
&[
|
||||
@ -142,7 +155,7 @@ mod tests {
|
||||
Some(&payer_keypair.pubkey()),
|
||||
),
|
||||
Hash::default(),
|
||||
);
|
||||
));
|
||||
compute_budget.process_transaction(&tx).unwrap();
|
||||
assert_eq!(
|
||||
compute_budget,
|
||||
@ -152,7 +165,7 @@ mod tests {
|
||||
}
|
||||
);
|
||||
|
||||
let tx = Transaction::new(
|
||||
let tx = sanitize_tx(Transaction::new(
|
||||
&[&payer_keypair],
|
||||
Message::new(
|
||||
&[
|
||||
@ -162,7 +175,7 @@ mod tests {
|
||||
Some(&payer_keypair.pubkey()),
|
||||
),
|
||||
Hash::default(),
|
||||
);
|
||||
));
|
||||
let result = compute_budget.process_transaction(&tx);
|
||||
assert_eq!(
|
||||
result,
|
||||
@ -172,7 +185,7 @@ mod tests {
|
||||
))
|
||||
);
|
||||
|
||||
let tx = Transaction::new(
|
||||
let tx = sanitize_tx(Transaction::new(
|
||||
&[&payer_keypair],
|
||||
Message::new(
|
||||
&[
|
||||
@ -182,7 +195,7 @@ mod tests {
|
||||
Some(&payer_keypair.pubkey()),
|
||||
),
|
||||
Hash::default(),
|
||||
);
|
||||
));
|
||||
compute_budget.process_transaction(&tx).unwrap();
|
||||
assert_eq!(
|
||||
compute_budget,
|
||||
|
@ -195,6 +195,10 @@ pub mod mem_overlap_fix {
|
||||
solana_sdk::declare_id!("vXDCFK7gphrEmyf5VnKgLmqbdJ4UxD2eZH1qbdouYKF");
|
||||
}
|
||||
|
||||
pub mod versioned_tx_message_enabled {
|
||||
solana_sdk::declare_id!("3KZZ6Ks1885aGBQ45fwRcPXVBCtzUvxhUTkwKMR41Tca");
|
||||
}
|
||||
|
||||
lazy_static! {
|
||||
/// Map of feature identifiers to user-visible description
|
||||
pub static ref FEATURE_NAMES: HashMap<Pubkey, &'static str> = [
|
||||
@ -238,6 +242,7 @@ lazy_static! {
|
||||
(stake_merge_with_unmatched_credits_observed::id(), "allow merging active stakes with unmatched credits_observed #18985"),
|
||||
(gate_large_block::id(), "validator checks block cost against max limit in realtime, reject if exceeds."),
|
||||
(mem_overlap_fix::id(), "Memory overlap fix"),
|
||||
(versioned_tx_message_enabled::id(), "enable versioned transaction message processing"),
|
||||
/*************** ADD NEW FEATURES HERE ***************/
|
||||
]
|
||||
.iter()
|
||||
|
@ -40,7 +40,6 @@ pub mod program_utils;
|
||||
pub mod pubkey;
|
||||
pub mod recent_blockhashes_account;
|
||||
pub mod rpc_port;
|
||||
pub mod sanitized_transaction;
|
||||
pub mod secp256k1_instruction;
|
||||
pub mod shred_version;
|
||||
pub mod signature;
|
||||
|
@ -1,87 +0,0 @@
|
||||
#![cfg(feature = "full")]
|
||||
|
||||
use crate::{
|
||||
hash::Hash,
|
||||
sanitize::Sanitize,
|
||||
transaction::{Result, Transaction, TransactionError},
|
||||
};
|
||||
use std::{borrow::Cow, convert::TryFrom, ops::Deref};
|
||||
|
||||
/// Sanitized transaction and the hash of its message
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SanitizedTransaction<'a> {
|
||||
transaction: Cow<'a, Transaction>,
|
||||
pub message_hash: Hash,
|
||||
}
|
||||
|
||||
impl<'a> SanitizedTransaction<'a> {
|
||||
pub fn try_create(transaction: Cow<'a, Transaction>, message_hash: Hash) -> Result<Self> {
|
||||
transaction.sanitize()?;
|
||||
if Self::has_duplicates(&transaction.message.account_keys) {
|
||||
return Err(TransactionError::AccountLoadedTwice);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
transaction,
|
||||
message_hash,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return true if the slice has any duplicate elements
|
||||
pub fn has_duplicates<T: PartialEq>(xs: &[T]) -> bool {
|
||||
// Note: This is an O(n^2) algorithm, but requires no heap allocations. The benchmark
|
||||
// `bench_has_duplicates` in benches/message_processor.rs shows that this implementation is
|
||||
// ~50 times faster than using HashSet for very short slices.
|
||||
for i in 1..xs.len() {
|
||||
#[allow(clippy::integer_arithmetic)]
|
||||
if xs[i..].contains(&xs[i - 1]) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for SanitizedTransaction<'_> {
|
||||
type Target = Transaction;
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.transaction
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<Transaction> for SanitizedTransaction<'_> {
|
||||
type Error = TransactionError;
|
||||
fn try_from(transaction: Transaction) -> Result<Self> {
|
||||
let message_hash = transaction.message().hash();
|
||||
Self::try_create(Cow::Owned(transaction), message_hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> TryFrom<&'a Transaction> for SanitizedTransaction<'a> {
|
||||
type Error = TransactionError;
|
||||
fn try_from(transaction: &'a Transaction) -> Result<Self> {
|
||||
let message_hash = transaction.message().hash();
|
||||
Self::try_create(Cow::Borrowed(transaction), message_hash)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait SanitizedTransactionSlice<'a> {
|
||||
fn as_transactions_iter(&'a self) -> Box<dyn Iterator<Item = &'a Transaction> + '_>;
|
||||
}
|
||||
|
||||
impl<'a> SanitizedTransactionSlice<'a> for [SanitizedTransaction<'a>] {
|
||||
fn as_transactions_iter(&'a self) -> Box<dyn Iterator<Item = &'a Transaction> + '_> {
|
||||
Box::new(self.iter().map(Deref::deref))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_has_duplicates() {
|
||||
assert!(!SanitizedTransaction::has_duplicates(&[1, 2]));
|
||||
assert!(SanitizedTransaction::has_duplicates(&[1, 2, 1]));
|
||||
}
|
||||
}
|
@ -2,23 +2,31 @@
|
||||
|
||||
#![cfg(feature = "full")]
|
||||
|
||||
use crate::sanitize::{Sanitize, SanitizeError};
|
||||
use crate::secp256k1_instruction::verify_eth_addresses;
|
||||
use crate::{
|
||||
hash::Hash,
|
||||
instruction::{CompiledInstruction, Instruction, InstructionError},
|
||||
message::Message,
|
||||
nonce::NONCED_TX_MARKER_IX_INDEX,
|
||||
program_utils::limited_deserialize,
|
||||
pubkey::Pubkey,
|
||||
short_vec,
|
||||
signature::{Signature, SignerError},
|
||||
signers::Signers,
|
||||
system_instruction::SystemInstruction,
|
||||
system_program,
|
||||
use {
|
||||
crate::{
|
||||
hash::Hash,
|
||||
instruction::{CompiledInstruction, Instruction, InstructionError},
|
||||
message::{Message, SanitizeMessageError},
|
||||
nonce::NONCED_TX_MARKER_IX_INDEX,
|
||||
program_utils::limited_deserialize,
|
||||
pubkey::Pubkey,
|
||||
sanitize::{Sanitize, SanitizeError},
|
||||
secp256k1_instruction::verify_eth_addresses,
|
||||
short_vec,
|
||||
signature::{Signature, SignerError},
|
||||
signers::Signers,
|
||||
},
|
||||
serde::Serialize,
|
||||
solana_program::{system_instruction::SystemInstruction, system_program},
|
||||
std::result,
|
||||
thiserror::Error,
|
||||
};
|
||||
use std::result;
|
||||
use thiserror::Error;
|
||||
|
||||
mod sanitized;
|
||||
mod versioned;
|
||||
|
||||
pub use sanitized::*;
|
||||
pub use versioned::*;
|
||||
|
||||
/// Reasons a transaction might be rejected.
|
||||
#[derive(
|
||||
@ -104,6 +112,10 @@ pub enum TransactionError {
|
||||
"Transaction could not fit into current block without exceeding the Max Block Cost Limit"
|
||||
)]
|
||||
WouldExceedMaxBlockCostLimit,
|
||||
|
||||
/// Transaction version is unsupported
|
||||
#[error("Transaction version is unsupported")]
|
||||
UnsupportedVersion,
|
||||
}
|
||||
|
||||
pub type Result<T> = result::Result<T, TransactionError>;
|
||||
@ -114,6 +126,17 @@ impl From<SanitizeError> for TransactionError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SanitizeMessageError> for TransactionError {
|
||||
fn from(err: SanitizeMessageError) -> Self {
|
||||
match err {
|
||||
SanitizeMessageError::IndexOutOfBounds
|
||||
| SanitizeMessageError::ValueOutOfBounds
|
||||
| SanitizeMessageError::InvalidValue => Self::SanitizeFailure,
|
||||
SanitizeMessageError::DuplicateAccountKey => Self::AccountLoadedTwice,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An atomic transaction
|
||||
#[frozen_abi(digest = "FZtncnS1Xk8ghHfKiXE5oGiUbw2wJhmfXQuNgQR3K6Mc")]
|
||||
#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)]
|
||||
@ -230,10 +253,12 @@ impl Transaction {
|
||||
.and_then(|instruction| instruction.accounts.get(accounts_index))
|
||||
.map(|&account_keys_index| account_keys_index as usize)
|
||||
}
|
||||
|
||||
pub fn key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> {
|
||||
self.key_index(instruction_index, accounts_index)
|
||||
.and_then(|account_keys_index| self.message.account_keys.get(account_keys_index))
|
||||
}
|
||||
|
||||
pub fn signer_key(&self, instruction_index: usize, accounts_index: usize) -> Option<&Pubkey> {
|
||||
match self.key_index(instruction_index, accounts_index) {
|
||||
None => None,
|
||||
@ -484,6 +509,7 @@ pub fn uses_durable_nonce(tx: &Transaction) -> Option<&CompiledInstruction> {
|
||||
)
|
||||
}
|
||||
|
||||
#[deprecated]
|
||||
pub fn get_nonce_pubkey_from_instruction<'a>(
|
||||
ix: &CompiledInstruction,
|
||||
tx: &'a Transaction,
|
||||
@ -496,6 +522,8 @@ pub fn get_nonce_pubkey_from_instruction<'a>(
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(deprecated)]
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
hash::hash,
|
||||
@ -553,6 +581,7 @@ mod tests {
|
||||
assert_eq!(*get_program_id(&tx, 0), prog1);
|
||||
assert_eq!(*get_program_id(&tx, 1), prog2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_refs_invalid_program_id() {
|
||||
let key = Keypair::new();
|
226
sdk/src/transaction/sanitized.rs
Normal file
226
sdk/src/transaction/sanitized.rs
Normal file
@ -0,0 +1,226 @@
|
||||
#![cfg(feature = "full")]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
hash::Hash,
|
||||
message::{v0, MappedAddresses, MappedMessage, SanitizedMessage, VersionedMessage},
|
||||
nonce::NONCED_TX_MARKER_IX_INDEX,
|
||||
program_utils::limited_deserialize,
|
||||
pubkey::Pubkey,
|
||||
sanitize::Sanitize,
|
||||
secp256k1_instruction::verify_eth_addresses,
|
||||
secp256k1_program,
|
||||
signature::Signature,
|
||||
transaction::{Result, Transaction, TransactionError, VersionedTransaction},
|
||||
},
|
||||
solana_program::{system_instruction::SystemInstruction, system_program},
|
||||
std::convert::TryFrom,
|
||||
};
|
||||
|
||||
/// Sanitized transaction and the hash of its message
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SanitizedTransaction {
|
||||
message: SanitizedMessage,
|
||||
message_hash: Hash,
|
||||
signatures: Vec<Signature>,
|
||||
}
|
||||
|
||||
/// Set of accounts that must be locked for safe transaction processing
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct TransactionAccountLocks<'a> {
|
||||
/// List of readonly account key locks
|
||||
pub readonly: Vec<&'a Pubkey>,
|
||||
/// List of writable account key locks
|
||||
pub writable: Vec<&'a Pubkey>,
|
||||
}
|
||||
|
||||
impl TryFrom<Transaction> for SanitizedTransaction {
|
||||
type Error = TransactionError;
|
||||
fn try_from(tx: Transaction) -> Result<Self> {
|
||||
tx.sanitize()?;
|
||||
|
||||
if tx.message.has_duplicates() {
|
||||
return Err(TransactionError::AccountLoadedTwice);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
message_hash: tx.message.hash(),
|
||||
message: SanitizedMessage::Legacy(tx.message),
|
||||
signatures: tx.signatures,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SanitizedTransaction {
|
||||
/// Create a sanitized transaction from an unsanitized transaction.
|
||||
/// If the input transaction uses address maps, attempt to map the
|
||||
/// transaction keys to full addresses.
|
||||
pub fn try_create(
|
||||
tx: VersionedTransaction,
|
||||
message_hash: Hash,
|
||||
address_mapper: impl Fn(&v0::Message) -> Result<MappedAddresses>,
|
||||
) -> Result<Self> {
|
||||
tx.sanitize()?;
|
||||
|
||||
let signatures = tx.signatures;
|
||||
let message = match tx.message {
|
||||
VersionedMessage::Legacy(message) => SanitizedMessage::Legacy(message),
|
||||
VersionedMessage::V0(message) => SanitizedMessage::V0(MappedMessage {
|
||||
mapped_addresses: address_mapper(&message)?,
|
||||
message,
|
||||
}),
|
||||
};
|
||||
|
||||
if message.has_duplicates() {
|
||||
return Err(TransactionError::AccountLoadedTwice);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
message,
|
||||
message_hash,
|
||||
signatures,
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the first signature for this transaction.
|
||||
///
|
||||
/// Notes:
|
||||
///
|
||||
/// Sanitized transactions must have at least one signature because the
|
||||
/// number of signatures must be greater than or equal to the message header
|
||||
/// value `num_required_signatures` which must be greater than 0 itself.
|
||||
pub fn signature(&self) -> &Signature {
|
||||
&self.signatures[0]
|
||||
}
|
||||
|
||||
/// Return the list of signatures for this transaction
|
||||
pub fn signatures(&self) -> &[Signature] {
|
||||
&self.signatures
|
||||
}
|
||||
|
||||
/// Return the signed message
|
||||
pub fn message(&self) -> &SanitizedMessage {
|
||||
&self.message
|
||||
}
|
||||
|
||||
/// Return the hash of the signed message
|
||||
pub fn message_hash(&self) -> &Hash {
|
||||
&self.message_hash
|
||||
}
|
||||
|
||||
/// Convert this sanitized transaction into a versioned transaction for
|
||||
/// recording in the ledger.
|
||||
pub fn to_versioned_transaction(&self) -> VersionedTransaction {
|
||||
let signatures = self.signatures.clone();
|
||||
match &self.message {
|
||||
SanitizedMessage::V0(mapped_msg) => VersionedTransaction {
|
||||
signatures,
|
||||
message: VersionedMessage::V0(mapped_msg.message.clone()),
|
||||
},
|
||||
SanitizedMessage::Legacy(message) => VersionedTransaction {
|
||||
signatures,
|
||||
message: VersionedMessage::Legacy(message.clone()),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the list of accounts that must be locked during processing this transaction.
|
||||
pub fn get_account_locks(&self) -> TransactionAccountLocks {
|
||||
let message = &self.message;
|
||||
let num_readonly_accounts = message.num_readonly_accounts();
|
||||
let num_writable_accounts = message
|
||||
.account_keys_len()
|
||||
.saturating_sub(num_readonly_accounts);
|
||||
|
||||
let mut account_locks = TransactionAccountLocks {
|
||||
writable: Vec::with_capacity(num_writable_accounts),
|
||||
readonly: Vec::with_capacity(num_readonly_accounts),
|
||||
};
|
||||
|
||||
for (i, key) in message.account_keys_iter().enumerate() {
|
||||
if message.is_writable(i) {
|
||||
account_locks.writable.push(key);
|
||||
} else {
|
||||
account_locks.readonly.push(key);
|
||||
}
|
||||
}
|
||||
|
||||
account_locks
|
||||
}
|
||||
|
||||
/// If the transaction uses a durable nonce, return the pubkey of the nonce account
|
||||
pub fn get_durable_nonce(&self) -> Option<&Pubkey> {
|
||||
self.message
|
||||
.instructions()
|
||||
.get(NONCED_TX_MARKER_IX_INDEX as usize)
|
||||
.filter(
|
||||
|ix| match self.message.get_account_key(ix.program_id_index as usize) {
|
||||
Some(program_id) => system_program::check_id(program_id),
|
||||
_ => false,
|
||||
},
|
||||
)
|
||||
.filter(|ix| {
|
||||
matches!(
|
||||
limited_deserialize(&ix.data),
|
||||
Ok(SystemInstruction::AdvanceNonceAccount)
|
||||
)
|
||||
})
|
||||
.and_then(|ix| {
|
||||
ix.accounts.get(0).and_then(|idx| {
|
||||
let idx = *idx as usize;
|
||||
self.message.get_account_key(idx)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
/// Return the serialized message data to sign.
|
||||
fn message_data(&self) -> Vec<u8> {
|
||||
match &self.message {
|
||||
SanitizedMessage::Legacy(message) => message.serialize(),
|
||||
SanitizedMessage::V0(mapped_msg) => mapped_msg.message.serialize(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the length of signatures matches the value in the message header
|
||||
pub fn verify_signatures_len(&self) -> bool {
|
||||
self.signatures.len() == self.message.header().num_required_signatures as usize
|
||||
}
|
||||
|
||||
/// Verify the transaction signatures
|
||||
pub fn verify(&self) -> Result<()> {
|
||||
let message_bytes = self.message_data();
|
||||
if self
|
||||
.signatures
|
||||
.iter()
|
||||
.zip(self.message.account_keys_iter())
|
||||
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
|
||||
.any(|verified| !verified)
|
||||
{
|
||||
Err(TransactionError::SignatureFailure)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the encoded secp256k1 signatures in this transaction
|
||||
pub fn verify_precompiles(&self, libsecp256k1_0_5_upgrade_enabled: bool) -> Result<()> {
|
||||
for (program_id, instruction) in self.message.program_instructions_iter() {
|
||||
if secp256k1_program::check_id(program_id) {
|
||||
let instruction_datas: Vec<_> = self
|
||||
.message
|
||||
.instructions()
|
||||
.iter()
|
||||
.map(|instruction| instruction.data.as_ref())
|
||||
.collect();
|
||||
let data = &instruction.data;
|
||||
let e = verify_eth_addresses(
|
||||
data,
|
||||
&instruction_datas,
|
||||
libsecp256k1_0_5_upgrade_enabled,
|
||||
);
|
||||
e.map_err(|_| TransactionError::InvalidAccountIndex)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
84
sdk/src/transaction/versioned.rs
Normal file
84
sdk/src/transaction/versioned.rs
Normal file
@ -0,0 +1,84 @@
|
||||
//! Defines a transaction which supports multiple versions of messages.
|
||||
|
||||
#![cfg(feature = "full")]
|
||||
|
||||
use {
|
||||
crate::{
|
||||
hash::Hash,
|
||||
message::VersionedMessage,
|
||||
sanitize::{Sanitize, SanitizeError},
|
||||
short_vec,
|
||||
signature::Signature,
|
||||
transaction::{Result, Transaction, TransactionError},
|
||||
},
|
||||
serde::Serialize,
|
||||
};
|
||||
|
||||
// NOTE: Serialization-related changes must be paired with the direct read at sigverify.
|
||||
/// An atomic transaction
|
||||
#[derive(Debug, PartialEq, Default, Eq, Clone, Serialize, Deserialize, AbiExample)]
|
||||
pub struct VersionedTransaction {
|
||||
/// List of signatures
|
||||
#[serde(with = "short_vec")]
|
||||
pub signatures: Vec<Signature>,
|
||||
/// Message to sign.
|
||||
pub message: VersionedMessage,
|
||||
}
|
||||
|
||||
impl Sanitize for VersionedTransaction {
|
||||
fn sanitize(&self) -> std::result::Result<(), SanitizeError> {
|
||||
self.message.sanitize()?;
|
||||
|
||||
// Once the "verify_tx_signatures_len" feature is enabled, this may be
|
||||
// updated to an equality check.
|
||||
if usize::from(self.message.header().num_required_signatures) > self.signatures.len() {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
|
||||
// Signatures are verified before message keys are mapped so all signers
|
||||
// must correspond to unmapped keys.
|
||||
if self.signatures.len() > self.message.unmapped_keys_len() {
|
||||
return Err(SanitizeError::IndexOutOfBounds);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Transaction> for VersionedTransaction {
|
||||
fn from(transaction: Transaction) -> Self {
|
||||
Self {
|
||||
signatures: transaction.signatures,
|
||||
message: VersionedMessage::Legacy(transaction.message),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl VersionedTransaction {
|
||||
/// Returns a legacy transaction if the transaction message is legacy.
|
||||
pub fn into_legacy_transaction(self) -> Option<Transaction> {
|
||||
match self.message {
|
||||
VersionedMessage::Legacy(message) => Some(Transaction {
|
||||
signatures: self.signatures,
|
||||
message,
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Verify the transaction and hash its message
|
||||
pub fn verify_and_hash_message(&self) -> Result<Hash> {
|
||||
let message_bytes = self.message.serialize();
|
||||
if self
|
||||
.signatures
|
||||
.iter()
|
||||
.zip(self.message.unmapped_keys_iter())
|
||||
.map(|(signature, pubkey)| signature.verify(pubkey.as_ref(), &message_bytes))
|
||||
.any(|verified| !verified)
|
||||
{
|
||||
Err(TransactionError::SignatureFailure)
|
||||
} else {
|
||||
Ok(VersionedMessage::hash_raw_message(&message_bytes))
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user