From 8b97af634ebcad6833dc2626dcbb00929a96b4b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alexander=20Mei=C3=9Fner?= Date: Tue, 8 Oct 2024 14:10:13 +0000 Subject: [PATCH] Adds ABIv2 de/serialization interface. --- ledger-tool/src/program.rs | 3 +- program-test/src/lib.rs | 1 + programs/bpf_loader/benches/serialization.rs | 18 +- programs/bpf_loader/src/lib.rs | 2 + programs/bpf_loader/src/serialization.rs | 201 +++++++++++++++++-- programs/sbf/benches/bpf_loader.rs | 4 +- sdk/program/src/entrypoint.rs | 94 +++++++++ 7 files changed, 302 insertions(+), 21 deletions(-) diff --git a/ledger-tool/src/program.rs b/ledger-tool/src/program.rs index 463d017b17dbed..33a001f9c30f50 100644 --- a/ledger-tool/src/program.rs +++ b/ledger-tool/src/program.rs @@ -543,7 +543,8 @@ pub fn program(ledger_path: &Path, matches: &ArgMatches<'_>) { .transaction_context .get_current_instruction_context() .unwrap(), - true, // copy_account_data + true, // copy_account_data + false, // is_abi_v2 ) .unwrap(); diff --git a/program-test/src/lib.rs b/program-test/src/lib.rs index 38199b270f1d52..9cd01a7b6f2e1a 100644 --- a/program-test/src/lib.rs +++ b/program-test/src/lib.rs @@ -134,6 +134,7 @@ pub fn invoke_builtin_function( transaction_context, instruction_context, true, // copy_account_data // There is no VM so direct mapping can not be implemented here + false, // is_abi_v2 )?; // Deserialize data back into instruction params diff --git a/programs/bpf_loader/benches/serialization.rs b/programs/bpf_loader/benches/serialization.rs index 5d3c55a165e399..ac3aa0ffaa5857 100644 --- a/programs/bpf_loader/benches/serialization.rs +++ b/programs/bpf_loader/benches/serialization.rs @@ -126,7 +126,8 @@ fn bench_serialize_unaligned(bencher: &mut Bencher) { .get_current_instruction_context() .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, false).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, false, false).unwrap(); }); } @@ -137,7 +138,8 @@ fn bench_serialize_unaligned_copy_account_data(bencher: &mut Bencher) { .get_current_instruction_context() .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, true).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, true, false).unwrap(); }); } @@ -149,7 +151,8 @@ fn bench_serialize_aligned(bencher: &mut Bencher) { .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, false).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, false, false).unwrap(); }); } @@ -161,7 +164,8 @@ fn bench_serialize_aligned_copy_account_data(bencher: &mut Bencher) { .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, true).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, true, false).unwrap(); }); } @@ -172,7 +176,8 @@ fn bench_serialize_unaligned_max_accounts(bencher: &mut Bencher) { .get_current_instruction_context() .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, false).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, false, false).unwrap(); }); } @@ -184,6 +189,7 @@ fn bench_serialize_aligned_max_accounts(bencher: &mut Bencher) { .unwrap(); bencher.iter(|| { - let _ = serialize_parameters(&transaction_context, instruction_context, false).unwrap(); + let _ = + serialize_parameters(&transaction_context, instruction_context, false, false).unwrap(); }); } diff --git a/programs/bpf_loader/src/lib.rs b/programs/bpf_loader/src/lib.rs index 5e81062c504e97..63856bd392f459 100644 --- a/programs/bpf_loader/src/lib.rs +++ b/programs/bpf_loader/src/lib.rs @@ -1367,6 +1367,7 @@ pub fn execute<'a, 'b: 'a>( invoke_context.transaction_context, instruction_context, !direct_mapping, + false, )?; serialize_time.stop(); @@ -1488,6 +1489,7 @@ pub fn execute<'a, 'b: 'a>( .transaction_context .get_current_instruction_context()?, copy_account_data, + false, parameter_bytes, &invoke_context.get_syscall_context()?.accounts_metadata, ) diff --git a/programs/bpf_loader/src/serialization.rs b/programs/bpf_loader/src/serialization.rs index 2cb2f20b162280..46f64d0719f468 100644 --- a/programs/bpf_loader/src/serialization.rs +++ b/programs/bpf_loader/src/serialization.rs @@ -5,12 +5,15 @@ use { solana_program_runtime::invoke_context::SerializedAccountMetadata, solana_rbpf::{ aligned_memory::{AlignedMemory, Pod}, - ebpf::{HOST_ALIGN, MM_INPUT_START}, + ebpf::{HOST_ALIGN, MM_INPUT_START, MM_PROGRAM_START}, memory_region::{MemoryRegion, MemoryState}, }, solana_sdk::{ bpf_loader_deprecated, - entrypoint::{BPF_ALIGN_OF_U128, MAX_PERMITTED_DATA_INCREASE, NON_DUP_MARKER}, + entrypoint::{ + ABIv2InstructionAccount, ABIv2InstructionContext, BPF_ALIGN_OF_U128, + MAX_PERMITTED_DATA_INCREASE, NON_DUP_MARKER, + }, instruction::InstructionError, pubkey::Pubkey, system_instruction::MAX_PERMITTED_DATA_LENGTH, @@ -102,7 +105,8 @@ impl Serializer { } else { self.push_region(true); let vaddr = self.vaddr; - self.push_account_data_region(account)?; + self.push_account_data_region(vaddr, account)?; + self.vaddr += account.get_data().len() as u64; vaddr }; @@ -129,21 +133,19 @@ impl Serializer { Ok(vm_data_addr) } - fn push_account_data_region( + pub fn push_account_data_region( &mut self, + vaddr: u64, account: &mut BorrowedAccount<'_>, ) -> Result<(), InstructionError> { if !account.get_data().is_empty() { let region = match account_data_region_memory_state(account) { - MemoryState::Readable => MemoryRegion::new_readonly(account.get_data(), self.vaddr), - MemoryState::Writable => { - MemoryRegion::new_writable(account.get_data_mut()?, self.vaddr) - } + MemoryState::Readable => MemoryRegion::new_readonly(account.get_data(), vaddr), + MemoryState::Writable => MemoryRegion::new_writable(account.get_data_mut()?, vaddr), MemoryState::Cow(index_in_transaction) => { - MemoryRegion::new_cow(account.get_data(), self.vaddr, index_in_transaction) + MemoryRegion::new_cow(account.get_data(), vaddr, index_in_transaction) } }; - self.vaddr += region.len; self.regions.push(region); } @@ -192,6 +194,7 @@ pub fn serialize_parameters( transaction_context: &TransactionContext, instruction_context: &InstructionContext, copy_account_data: bool, + is_abi_v2: bool, ) -> Result< ( AlignedMemory, @@ -234,7 +237,13 @@ pub fn serialize_parameters( // time it's iterated on. .collect::>(); - if is_loader_deprecated { + if is_abi_v2 { + serialize_parameters_v2( + accounts, + instruction_context.get_instruction_data(), + &program_id, + ) + } else if is_loader_deprecated { serialize_parameters_unaligned( accounts, instruction_context.get_instruction_data(), @@ -255,6 +264,7 @@ pub fn deserialize_parameters( transaction_context: &TransactionContext, instruction_context: &InstructionContext, copy_account_data: bool, + is_abi_v2: bool, buffer: &[u8], accounts_metadata: &[SerializedAccountMetadata], ) -> Result<(), InstructionError> { @@ -263,7 +273,14 @@ pub fn deserialize_parameters( .get_owner() == bpf_loader_deprecated::id(); let account_lengths = accounts_metadata.iter().map(|a| a.original_data_len); - if is_loader_deprecated { + if is_abi_v2 { + deserialize_parameters_v2( + transaction_context, + instruction_context, + buffer, + account_lengths, + ) + } else if is_loader_deprecated { deserialize_parameters_unaligned( transaction_context, instruction_context, @@ -603,6 +620,159 @@ pub fn deserialize_parameters_aligned>( Ok(()) } +fn serialize_parameters_v2( + mut accounts: Vec, + instruction_data: &[u8], + program_id: &Pubkey, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + ), + InstructionError, +> { + let mut accounts_metadata = Vec::with_capacity(accounts.len()); + // Calculate size in order to alloc once + let number_of_unique_instruction_accounts = accounts.iter().fold(0, |accumulator, account| { + if matches!(account, SerializeAccount::Account(_, _)) { + accumulator + 1 + } else { + accumulator + } + }); + let size = size_of::() + + size_of::() * number_of_unique_instruction_accounts + + size_of::() * accounts.len(); // instruction_context.get_number_of_instruction_accounts(); + + // Serialize into the buffer + const INSTRUCTION_DATA_VM_ADDRESS: u64 = MM_INPUT_START + MM_PROGRAM_START; + let mut s = Serializer::new(size, MM_INPUT_START, true, false); + s.write::(0x76494241u32.to_le()); + s.write::(0x00000002u32.to_le()); + s.write::(INSTRUCTION_DATA_VM_ADDRESS.to_le()); + s.write::((instruction_data.len() as u32).to_le()); + s.write::((number_of_unique_instruction_accounts as u16).to_le()); + s.write::((accounts.len() as u16).to_le()); + s.write_all(program_id.as_ref()); + let mut index: IndexOfAccount = 1; + for account in accounts.iter_mut() { + if let SerializeAccount::Account(_, ref mut borrowed_account) = account { + let vm_key_addr = s.write_all(borrowed_account.get_key().as_ref()); + let vm_owner_addr = s.write_all(borrowed_account.get_owner().as_ref()); + let mut flags = 0u64; + if borrowed_account.is_signer() { + flags |= 1 << 8; + } + if borrowed_account.is_writable() { + flags |= 1 << 16; + } + s.write::(flags.to_le()); + let vm_lamports_addr = s.write::(borrowed_account.get_lamports().to_le()); + let vm_data_addr = MM_INPUT_START + MM_PROGRAM_START * index as u64; + s.push_account_data_region(vm_data_addr, borrowed_account)?; + s.write::((borrowed_account.get_data().len() as u64).to_le()); + accounts_metadata.push(SerializedAccountMetadata { + original_data_len: borrowed_account.get_data().len(), + vm_key_addr, + vm_owner_addr, + vm_lamports_addr, + vm_data_addr, + }); + index += 1; + } + } + index = 0; + let mut deduplicated_account_indices = Vec::with_capacity(accounts.len()); + for account in accounts.iter() { + match account { + SerializeAccount::Account(instruction_account_index, _) => { + deduplicated_account_indices.push(instruction_account_index - index); + } + SerializeAccount::Duplicate(position) => { + deduplicated_account_indices.push( + *deduplicated_account_indices + .get(*position as usize) + .unwrap(), + ); + index += 1; + } + } + } + for deduplicated_account_index in deduplicated_account_indices { + s.write::(deduplicated_account_index.to_le()); + } + + let (mem, mut regions) = s.finish(); + regions.push(MemoryRegion::new_readonly( + instruction_data, + INSTRUCTION_DATA_VM_ADDRESS, + )); + Ok((mem, regions, accounts_metadata)) +} + +pub fn deserialize_parameters_v2>( + transaction_context: &TransactionContext, + instruction_context: &InstructionContext, + buffer: &[u8], + account_lengths: I, +) -> Result<(), InstructionError> { + let mut start = size_of::(); + for (instruction_account_index, pre_len) in (0..instruction_context + .get_number_of_instruction_accounts()) + .zip(account_lengths.into_iter()) + { + let duplicate = + instruction_context.is_instruction_account_duplicate(instruction_account_index)?; + if duplicate.is_none() { + let mut borrowed_account = instruction_context + .try_borrow_instruction_account(transaction_context, instruction_account_index)?; + start += size_of::(); // key + let owner = buffer + .get(start..start + size_of::()) + .ok_or(InstructionError::InvalidArgument)?; + start += size_of::(); // owner + start += size_of::(); // flags + let lamports = LittleEndian::read_u64( + buffer + .get(start..) + .ok_or(InstructionError::InvalidArgument)?, + ); + if borrowed_account.get_lamports() != lamports { + borrowed_account.set_lamports(lamports)?; + } + start += size_of::(); // lamports + start += size_of::(); // payload_vm_address + let post_len = LittleEndian::read_u64( + buffer + .get(start..) + .ok_or(InstructionError::InvalidArgument)?, + ) as usize; + start += size_of::(); // length_of_payload + match borrowed_account + .can_data_be_resized(post_len) + .and_then(|_| borrowed_account.can_data_be_changed()) + { + Ok(()) => { + if post_len.saturating_sub(pre_len) > MAX_PERMITTED_DATA_INCREASE + || post_len > MAX_PERMITTED_DATA_LENGTH as usize + { + return Err(InstructionError::InvalidRealloc); + } + borrowed_account.set_data_length(post_len)?; + } + Err(err) if borrowed_account.get_data().len() != post_len => return Err(err), + _ => {} + } + if borrowed_account.get_owner().to_bytes() != owner { + // Change the owner at the end so that we are allowed to change the lamports and data before + borrowed_account.set_owner(owner)?; + } + } + } + Ok(()) +} + pub(crate) fn account_data_region_memory_state(account: &BorrowedAccount<'_>) -> MemoryState { if account.can_data_be_changed().is_ok() { if account.is_shared() { @@ -729,6 +899,7 @@ mod tests { invoke_context.transaction_context, instruction_context, copy_account_data, + false, ); assert_eq!( serialization_result.as_ref().err(), @@ -776,7 +947,7 @@ mod tests { #[test] fn test_serialize_parameters() { - for copy_account_data in [false, true] { + for (copy_account_data, is_abi_v2) in [(false, false), (true, false), (false, true)] { let program_id = solana_sdk::pubkey::new_rand(); let transaction_accounts = vec![ ( @@ -883,6 +1054,7 @@ mod tests { invoke_context.transaction_context, instruction_context, copy_account_data, + is_abi_v2, ) .unwrap(); @@ -942,6 +1114,7 @@ mod tests { invoke_context.transaction_context, instruction_context, copy_account_data, + is_abi_v2, serialized.as_slice(), &accounts_metadata, ) @@ -974,6 +1147,7 @@ mod tests { invoke_context.transaction_context, instruction_context, copy_account_data, + is_abi_v2, ) .unwrap(); let mut serialized_regions = concat_regions(®ions); @@ -1012,6 +1186,7 @@ mod tests { invoke_context.transaction_context, instruction_context, copy_account_data, + is_abi_v2, serialized.as_slice(), &account_lengths, ) diff --git a/programs/sbf/benches/bpf_loader.rs b/programs/sbf/benches/bpf_loader.rs index 7475e9ea2d0f53..5bc7f4f46dfc6b 100644 --- a/programs/sbf/benches/bpf_loader.rs +++ b/programs/sbf/benches/bpf_loader.rs @@ -256,7 +256,8 @@ fn bench_create_vm(bencher: &mut Bencher) { .transaction_context .get_current_instruction_context() .unwrap(), - !direct_mapping, // copy_account_data, + !direct_mapping, // copy_account_data + false, // is_abi_v2 ) .unwrap(); @@ -291,6 +292,7 @@ fn bench_instruction_count_tuner(_bencher: &mut Bencher) { .get_current_instruction_context() .unwrap(), !direct_mapping, // copy_account_data + false, // is_abi_v2 ) .unwrap(); diff --git a/sdk/program/src/entrypoint.rs b/sdk/program/src/entrypoint.rs index f360d5ef2b2ece..2023d16f7a4b42 100644 --- a/sdk/program/src/entrypoint.rs +++ b/sdk/program/src/entrypoint.rs @@ -313,6 +313,100 @@ unsafe impl std::alloc::GlobalAlloc for BumpAllocator { /// `assert_eq(std::mem::align_of::(), 8)` is true for BPF but not for some host machines pub const BPF_ALIGN_OF_U128: usize = 8; +#[repr(C)] +pub struct ABIv2InstructionContext { + magic: u32, + version: u32, + instruction_data_vm_address: u64, + length_of_instruction_data: u32, + number_of_unique_instruction_accounts: u16, + number_of_instruction_accounts: u16, + program_key: Pubkey, +} + +#[repr(C)] +pub struct ABIv2InstructionAccount { + key: Pubkey, + owner: Pubkey, + flags: u64, + lamports: u64, + payload_vm_address: u64, + length_of_payload: u64, +} + +/// Eagerly deserialize the input arguments in ABI v2 +/// +/// # Safety +/// This method is safe when called once, on a buffer serialized by the program runtime. +pub unsafe fn deserialize_abi_v2_eagerly<'a>( + input: *mut u8, +) -> (&'a Pubkey, Vec>, &'a [u8]) { + let header = &*(input as *const ABIv2InstructionContext); + let unique_instruction_accounts = from_raw_parts_mut( + input.add(std::mem::size_of::()) as *mut ABIv2InstructionAccount, + header.number_of_unique_instruction_accounts as usize, + ); + let instruction_account_indirection = from_raw_parts_mut( + (unique_instruction_accounts.as_ptr() as *mut u8).add( + std::mem::size_of::() + .unchecked_mul(header.number_of_unique_instruction_accounts as usize), + ) as *mut u16, + header.number_of_instruction_accounts as usize, + ); + + let mut number_of_unique_instruction_accounts = 0; + let mut accounts = Vec::with_capacity(header.number_of_instruction_accounts as usize); + for index in 0..header.number_of_instruction_accounts { + let deduplicated_index = *instruction_account_indirection + .get(index as usize) + .unwrap_unchecked() as usize; + let aliased_index = instruction_account_indirection + .get_mut(deduplicated_index) + .unwrap_unchecked(); + if deduplicated_index == number_of_unique_instruction_accounts { + let instruction_account = unique_instruction_accounts + .get_mut(deduplicated_index) + .unwrap_unchecked(); + accounts.push(unsafe { + AccountInfo { + key: &*(&instruction_account.key as *const Pubkey), + is_signer: (instruction_account.flags & (1 << 8)) != 0, + is_writable: (instruction_account.flags & (1 << 16)) != 0, + lamports: Rc::new(RefCell::new( + &mut *(&mut instruction_account.lamports as *mut u64), + )), + data: Rc::new(RefCell::new(from_raw_parts_mut( + instruction_account.payload_vm_address as *mut u8, + instruction_account.length_of_payload as usize, + ))), + owner: &*(&instruction_account.owner as *const Pubkey), + executable: false, + rent_epoch: 0, + } + }); + *aliased_index = index; + number_of_unique_instruction_accounts = + number_of_unique_instruction_accounts.unchecked_add(1); + } else { + accounts.push( + accounts + .get(*aliased_index as usize) + .unwrap_unchecked() + .clone(), + ); + } + } + + ( + &header.program_key, + accounts, + from_raw_parts( + header.instruction_data_vm_address as *mut u8, + header.length_of_instruction_data as usize, + ), + ) +} + #[allow(clippy::arithmetic_side_effects)] #[inline(always)] // this reduces CU usage unsafe fn deserialize_instruction_data<'a>(input: *mut u8, mut offset: usize) -> (&'a [u8], usize) {