diff --git a/transaction-view/src/address_table_lookup_meta.rs b/transaction-view/src/address_table_lookup_meta.rs index 28ecc67adc51ed..c065386641c193 100644 --- a/transaction-view/src/address_table_lookup_meta.rs +++ b/transaction-view/src/address_table_lookup_meta.rs @@ -2,11 +2,12 @@ use { crate::{ bytes::{ advance_offset_for_array, advance_offset_for_type, check_remaining, - optimized_read_compressed_u16, read_byte, + optimized_read_compressed_u16, read_byte, read_slice_data, read_type, }, result::Result, }, solana_sdk::{hash::Hash, packet::PACKET_DATA_SIZE, pubkey::Pubkey, signature::Signature}, + solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup, }; // Each ATL has at least a Pubkey, one byte for the number of write indexes, @@ -46,7 +47,7 @@ const MAX_ATLS_PER_PACKET: usize = (PACKET_DATA_SIZE - MIN_SIZED_PACKET_WITH_ATL /// Contains metadata about the address table lookups in a transaction packet. pub struct AddressTableLookupMeta { /// The number of address table lookups in the transaction. - pub(crate) num_address_table_lookup: u8, + pub(crate) num_address_table_lookups: u8, /// The offset to the first address table lookup in the transaction. pub(crate) offset: u16, } @@ -97,12 +98,84 @@ impl AddressTableLookupMeta { } Ok(Self { - num_address_table_lookup: num_address_table_lookups, + num_address_table_lookups, offset: address_table_lookups_offset, }) } } +pub struct AddressTableLookupIterator<'a> { + pub(crate) bytes: &'a [u8], + pub(crate) offset: usize, + pub(crate) num_address_table_lookups: u8, + pub(crate) index: u8, +} + +impl<'a> Iterator for AddressTableLookupIterator<'a> { + type Item = SVMMessageAddressTableLookup<'a>; + + fn next(&mut self) -> Option { + if self.index < self.num_address_table_lookups { + self.index = self.index.wrapping_add(1); + + // Each ATL has 3 pieces: + // 1. Address (Pubkey) + // 2. write indexes ([u8]) + // 3. read indexes ([u8]) + + // Advance offset for address of the lookup table. + const _: () = assert!(core::mem::align_of::() == 1, "Pubkey alignment"); + // SAFETY: + // - The offset is checked to be valid in the slice. + // - The alignment of Pubkey is 1. + // - `Pubkey` is a byte array, it cannot be improperly initialized. + let account_key = unsafe { read_type::(self.bytes, &mut self.offset) }.ok()?; + + // Read the number of write indexes, and then update the offset. + let num_write_accounts = + optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let writable_indexes = + unsafe { read_slice_data::(self.bytes, &mut self.offset, num_write_accounts) } + .ok()?; + + // Read the number of read indexes, and then update the offset. + let num_read_accounts = + optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let readonly_indexes = + unsafe { read_slice_data::(self.bytes, &mut self.offset, num_read_accounts) } + .ok()?; + + Some(SVMMessageAddressTableLookup { + account_key, + writable_indexes, + readonly_indexes, + }) + } else { + None + } + } +} + +impl ExactSizeIterator for AddressTableLookupIterator<'_> { + fn len(&self) -> usize { + usize::from(self.num_address_table_lookups.wrapping_sub(self.index)) + } +} + #[cfg(test)] mod tests { use { @@ -115,7 +188,7 @@ mod tests { let bytes = bincode::serialize(&ShortVec::(vec![])).unwrap(); let mut offset = 0; let meta = AddressTableLookupMeta::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(meta.num_address_table_lookup, 0); + assert_eq!(meta.num_address_table_lookups, 0); assert_eq!(meta.offset, 1); assert_eq!(offset, bytes.len()); } @@ -141,7 +214,7 @@ mod tests { .unwrap(); let mut offset = 0; let meta = AddressTableLookupMeta::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(meta.num_address_table_lookup, 1); + assert_eq!(meta.num_address_table_lookups, 1); assert_eq!(meta.offset, 1); assert_eq!(offset, bytes.len()); } @@ -163,7 +236,7 @@ mod tests { .unwrap(); let mut offset = 0; let meta = AddressTableLookupMeta::try_new(&bytes, &mut offset).unwrap(); - assert_eq!(meta.num_address_table_lookup, 2); + assert_eq!(meta.num_address_table_lookups, 2); assert_eq!(meta.offset, 1); assert_eq!(offset, bytes.len()); } diff --git a/transaction-view/src/bytes.rs b/transaction-view/src/bytes.rs index e66442efbb2a93..9e2724e3cac6de 100644 --- a/transaction-view/src/bytes.rs +++ b/transaction-view/src/bytes.rs @@ -3,11 +3,15 @@ use crate::result::{Result, TransactionParsingError}; /// Check that the buffer has at least `len` bytes remaining starting at /// `offset`. Returns Err if the buffer is too short. /// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// * `num_bytes` - Number of bytes that must be remaining. +/// /// Assumptions: /// - The current offset is not greater than `bytes.len()`. #[inline(always)] -pub fn check_remaining(bytes: &[u8], offset: usize, len: usize) -> Result<()> { - if len > bytes.len().wrapping_sub(offset) { +pub fn check_remaining(bytes: &[u8], offset: usize, num_bytes: usize) -> Result<()> { + if num_bytes > bytes.len().wrapping_sub(offset) { Err(TransactionParsingError) } else { Ok(()) @@ -29,6 +33,9 @@ pub fn read_byte(bytes: &[u8], offset: &mut usize) -> Result { /// If the buffer is too short or the encoding is invalid, return Err. /// `offset` is updated to point to the byte after the compressed u16. /// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// /// Assumptions: /// - The current offset is not greater than `bytes.len()`. #[allow(dead_code)] @@ -61,6 +68,7 @@ pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { } /// Domain-specific optimization for reading a compressed u16. +/// /// The compressed u16's are only used for array-lengths in our transaction /// format. The transaction packet has a maximum size of 1232 bytes. /// This means that the maximum array length within a **valid** transaction is @@ -70,6 +78,9 @@ pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { /// case, and reads a maximum of 2 bytes. /// If the buffer is too short or the encoding is invalid, return Err. /// `offset` is updated to point to the byte after the compressed u16. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. #[inline(always)] pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { let mut result = 0u16; @@ -98,6 +109,10 @@ pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result /// Update the `offset` to point to the byte after an array of length `len` and /// of type `T`. If the buffer is too short, return Err. /// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Curernt offset into `bytes`. +/// * `num_elements` - Number of `T` elements in the array. +/// /// Assumptions: /// 1. The current offset is not greater than `bytes.len()`. /// 2. The size of `T` is small enough such that a usize will not overflow if @@ -106,9 +121,9 @@ pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result pub fn advance_offset_for_array( bytes: &[u8], offset: &mut usize, - len: u16, + num_elements: u16, ) -> Result<()> { - let array_len_bytes = usize::from(len).wrapping_mul(core::mem::size_of::()); + let array_len_bytes = usize::from(num_elements).wrapping_mul(core::mem::size_of::()); check_remaining(bytes, *offset, array_len_bytes)?; *offset = offset.wrapping_add(array_len_bytes); Ok(()) @@ -117,6 +132,9 @@ pub fn advance_offset_for_array( /// Update the `offset` to point t the byte after the `T`. /// If the buffer is too short, return Err. /// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Curernt offset into `bytes`. +/// /// Assumptions: /// 1. The current offset is not greater than `bytes.len()`. /// 2. The size of `T` is small enough such that a usize will not overflow. @@ -128,6 +146,51 @@ pub fn advance_offset_for_type(bytes: &[u8], offset: &mut usize) -> Re Ok(()) } +/// Return a reference to the next slice of `T` in the buffer, checking bounds +/// and advancing the offset. +/// If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Curernt offset into `bytes`. +/// * `num_elements` - Number of `T` elements in the slice. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 3. `bytes + offset` must be properly aligned for `T`. +/// 4. `T` slice must be validly initialized. +/// 5. The size of `T` is small enough such that a usize will not overflow if +/// given the maximum slice size (u16::MAX). +#[inline(always)] +pub unsafe fn read_slice_data<'a, T: Sized>( + bytes: &'a [u8], + offset: &mut usize, + num_elements: u16, +) -> Result<&'a [T]> { + let current_ptr = bytes.as_ptr().wrapping_add(*offset); + advance_offset_for_array::(bytes, offset, num_elements)?; + Ok(unsafe { core::slice::from_raw_parts(current_ptr as *const T, usize::from(num_elements)) }) +} + +/// Return a reference to the next `T` in the buffer, checking bounds and +/// advancing the offset. +/// If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Curernt offset into `bytes`. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 3. `bytes + offset` must be properly aligned for `T`. +/// 4. `T` must be validly initialized. +#[inline(always)] +pub unsafe fn read_type<'a, T: Sized>(bytes: &'a [u8], offset: &mut usize) -> Result<&'a T> { + let current_ptr = bytes.as_ptr().wrapping_add(*offset); + advance_offset_for_type::(bytes, offset)?; + Ok(unsafe { &*(current_ptr as *const T) }) +} + #[cfg(test)] mod tests { use { diff --git a/transaction-view/src/instructions_meta.rs b/transaction-view/src/instructions_meta.rs index 8d68019f932c66..45de23d47c60fd 100644 --- a/transaction-view/src/instructions_meta.rs +++ b/transaction-view/src/instructions_meta.rs @@ -2,6 +2,7 @@ use { crate::{ bytes::{ advance_offset_for_array, check_remaining, optimized_read_compressed_u16, read_byte, + read_slice_data, }, result::Result, }, @@ -82,6 +83,8 @@ impl<'a> Iterator for InstructionsIterator<'a> { fn next(&mut self) -> Option { if self.index < self.num_instructions { + self.index = self.index.wrapping_add(1); + // Each instruction has 3 pieces: // 1. Program ID index (u8) // 2. Accounts indexes ([u8]) @@ -93,27 +96,29 @@ impl<'a> Iterator for InstructionsIterator<'a> { // Read the number of account indexes, and then update the offset // to skip over the account indexes. let num_accounts = optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; - // SAFETY: Only returned after we check that there are enough bytes. - let accounts = unsafe { - core::slice::from_raw_parts( - self.bytes.as_ptr().add(self.offset), - usize::from(num_accounts), - ) - }; - advance_offset_for_array::(self.bytes, &mut self.offset, num_accounts).ok()?; + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let accounts = + unsafe { read_slice_data::(self.bytes, &mut self.offset, num_accounts) } + .ok()?; // Read the length of the data, and then update the offset to skip // over the data. let data_len = optimized_read_compressed_u16(self.bytes, &mut self.offset).ok()?; - // SAFETY: Only returned after we check that there are enough bytes. - let data = unsafe { - core::slice::from_raw_parts( - self.bytes.as_ptr().add(self.offset), - usize::from(data_len), - ) - }; - advance_offset_for_array::(self.bytes, &mut self.offset, data_len).ok()?; - self.index = self.index.wrapping_add(1); + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let data = + unsafe { read_slice_data::(self.bytes, &mut self.offset, data_len) }.ok()?; Some(SVMInstruction { program_id_index, diff --git a/transaction-view/src/transaction_meta.rs b/transaction-view/src/transaction_meta.rs index 38cf52468ac9ee..17d92599a102ee 100644 --- a/transaction-view/src/transaction_meta.rs +++ b/transaction-view/src/transaction_meta.rs @@ -1,6 +1,6 @@ use { crate::{ - address_table_lookup_meta::AddressTableLookupMeta, + address_table_lookup_meta::{AddressTableLookupIterator, AddressTableLookupMeta}, bytes::advance_offset_for_type, instructions_meta::{InstructionsIterator, InstructionsMeta}, message_header_meta::{MessageHeaderMeta, TransactionVersion}, @@ -44,7 +44,7 @@ impl TransactionMeta { let instructions = InstructionsMeta::try_new(bytes, &mut offset)?; let address_table_lookup = match message_header.version { TransactionVersion::Legacy => AddressTableLookupMeta { - num_address_table_lookup: 0, + num_address_table_lookups: 0, offset: 0, }, TransactionVersion::V0 => AddressTableLookupMeta::try_new(bytes, &mut offset)?, @@ -102,7 +102,7 @@ impl TransactionMeta { /// Return the number of address table lookups in the transaction. pub fn num_address_table_lookups(&self) -> u8 { - self.address_table_lookup.num_address_table_lookup + self.address_table_lookup.num_address_table_lookups } } @@ -201,6 +201,22 @@ impl TransactionMeta { index: 0, } } + + /// Return an iterator over the address table lookups in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionMeta` instance. + pub unsafe fn address_table_lookup_iter<'a>( + &self, + bytes: &'a [u8], + ) -> AddressTableLookupIterator<'a> { + AddressTableLookupIterator { + bytes, + offset: usize::from(self.address_table_lookup.offset), + num_address_table_lookups: self.address_table_lookup.num_address_table_lookups, + index: 0, + } + } } #[cfg(test)] @@ -246,7 +262,7 @@ mod tests { tx.message.instructions().len() as u16 ); assert_eq!( - meta.address_table_lookup.num_address_table_lookup, + meta.address_table_lookup.num_address_table_lookups, tx.message .address_table_lookups() .map(|x| x.len() as u8) @@ -305,7 +321,21 @@ mod tests { } } - fn v0_with_lookup() -> VersionedTransaction { + fn multiple_transfers() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[ + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + ], + Some(&payer), + )), + } + } + + fn v0_with_single_lookup() -> VersionedTransaction { let payer = Pubkey::new_unique(); let to = Pubkey::new_unique(); VersionedTransaction { @@ -325,6 +355,36 @@ mod tests { } } + fn v0_with_multiple_lookups() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + let to1 = Pubkey::new_unique(); + let to2 = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::V0( + v0::Message::try_compile( + &payer, + &[ + system_instruction::transfer(&payer, &to1, 1), + system_instruction::transfer(&payer, &to2, 1), + ], + &[ + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![to1], + }, + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![to2], + }, + ], + Hash::default(), + ) + .unwrap(), + ), + } + } + #[test] fn test_minimal_sized_transaction() { verify_transaction_view_meta(&minimally_sized_transaction()); @@ -342,7 +402,7 @@ mod tests { #[test] fn test_v0_with_lookup() { - verify_transaction_view_meta(&v0_with_lookup()); + verify_transaction_view_meta(&v0_with_single_lookup()); } #[test] @@ -452,7 +512,20 @@ mod tests { } #[test] - fn test_instructions_iter() { + fn test_instructions_iter_empty() { + let tx = minimally_sized_transaction(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.instructions_iter(&bytes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_instructions_iter_single() { let tx = simple_transfer(); let bytes = bincode::serialize(&tx).unwrap(); let meta = TransactionMeta::try_new(&bytes).unwrap(); @@ -470,4 +543,87 @@ mod tests { assert!(iter.next().is_none()); } } + + #[test] + fn test_instructions_iter_multiple() { + let tx = multiple_transfers(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.instructions_iter(&bytes); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 3); + assert_eq!(ix.accounts, &[0, 1]); + assert_eq!( + ix.data, + &bincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 3); + assert_eq!(ix.accounts, &[0, 2]); + assert_eq!( + ix.data, + &bincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_empty() { + let tx = simple_transfer(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.address_table_lookup_iter(&bytes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_single() { + let tx = v0_with_single_lookup(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + let atls_actual = tx.message.address_table_lookups().unwrap(); + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.address_table_lookup_iter(&bytes); + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[0].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_multiple() { + let tx = v0_with_multiple_lookups(); + let bytes = bincode::serialize(&tx).unwrap(); + let meta = TransactionMeta::try_new(&bytes).unwrap(); + + let atls_actual = tx.message.address_table_lookups().unwrap(); + // SAFETY: `bytes` is the same slice used to create `meta`. + unsafe { + let mut iter = meta.address_table_lookup_iter(&bytes); + + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[0].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); + + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[1].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[1].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[1].readonly_indexes); + + assert!(iter.next().is_none()); + } + } }