diff --git a/Cargo.toml b/Cargo.toml index e722048..119c732 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["solana/account"] +members = ["solana/account", "solana/transaction-context"] resolver = "3" [workspace.package] @@ -15,6 +15,7 @@ version = "0.1.0" magic-root-interface = { path = "programs/magic-root-interface" } magic-root-program = { path = "programs/magic-root-program" } solana-account = { path = "solana/account" } +solana-transaction-context = { path = "solana/transaction-context" } ahash = "0.8.12" arc-swap = "1.9.1" @@ -33,18 +34,29 @@ tracing-subscriber = { version = "0.3.23", features = ["env-filter", "fmt"] } wincode = "0.5.1" zstd = { version = "0.13.3", default-features = false } -agave-feature-set = { version = "3.1.14", features = ["agave-unstable-api"] } -agave-transaction-view = { version = "3.1.13", features = ["agave-unstable-api"] } +agave-feature-set = "4.1.1" +agave-precompiles = "4.1.1" +agave-syscalls = { package = "solana-syscalls", version = "4.1.1", default-features = false } +agave-transaction-view = "4.1.1" solana-account-info = "3.1.1" solana-clock = "3.1.0" solana-compute-budget-instruction = "4.1.1" solana-cpi = "3.1.0" solana-hash = "4.3.0" +solana-instruction = "3.4.0" solana-instruction-error = "2.3.0" +solana-instructions-sysvar = "4.0.0" +solana-program-entrypoint = "3.1.1" solana-program-error = "3.0.1" solana-pubkey = "4.2.0" +solana-rent = "4.0.0-rc.1" +solana-sbpf = "0.13.1" solana-sdk-ids = "3.1.0" +solana-sha256-hasher = "3.1.0" +solana-short-vec = "3.2.1" +solana-signature = "3.4.0" solana-sysvar = "4.0.0" +solana-system-interface = "3.2.0" solana-transaction-error = "3.2.0" diff --git a/solana/transaction-context/Cargo.toml b/solana/transaction-context/Cargo.toml new file mode 100644 index 0000000..d0c0a3d --- /dev/null +++ b/solana/transaction-context/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "solana-transaction-context" + +authors = { workspace = true } +description = "Solana data shared between program runtime and built-in programs as well as SBF programs." +documentation = "https://docs.rs/solana-transaction-context" +edition = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +repository = { workspace = true } +version = { workspace = true } + +[package.metadata.docs.rs] +all-features = true +rustdoc-args = ["--cfg=docsrs"] +targets = ["x86_64-unknown-linux-gnu"] + +[features] +# No-op stub retained only so external (patched-in) crates that reference +# `solana-transaction-context/agave-unstable-api` still resolve; the lib is no +# longer gated on it. +agave-unstable-api = [] +bincode = ["dep:bincode", "serde", "solana-account/bincode"] +dev-context-only-utils = ["bincode", "dep:qualifier_attr", "solana-account/dev-context-only-utils"] +serde = ["serde/derive", "solana-pubkey/serde"] + +[dependencies] +solana-account = { workspace = true } +solana-instruction = { workspace = true, features = ["std"] } +solana-instructions-sysvar = { workspace = true } +solana-pubkey = { workspace = true } + +[target.'cfg(not(any(target_arch = "sbf", target_arch = "bpf")))'.dependencies] +bincode = { workspace = true, optional = true } +qualifier_attr = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-signature = { workspace = true, optional = true } + +[dev-dependencies] +solana-account-info = { workspace = true } +solana-program-entrypoint = { workspace = true } +solana-system-interface = { workspace = true } +solana-transaction-context = { path = ".", features = ["dev-context-only-utils"] } +static_assertions = "1.1.0" + +[lints.rust] +unexpected_cfgs = "allow" diff --git a/solana/transaction-context/README.md b/solana/transaction-context/README.md new file mode 100644 index 0000000..2007530 --- /dev/null +++ b/solana/transaction-context/README.md @@ -0,0 +1,20 @@ +# `solana-transaction-context` + +A fork of Agave's `solana-transaction-context` (`anza-xyz/agave`), reshaped for +the engine and patched in workspace-wide through `[patch.crates-io]`. + +These are the low-level structures a single transaction executes over: +`TransactionContext`, `InstructionContext`, the `TransactionAccounts` store with +its `AccountRef`/`AccountRefMut` views, `ExecutionRecord`, and the account and +transaction limits. + +The detail that drives the design: accounts live in `UnsafeCell` behind explicit +borrow counters rather than ordinary Rust borrows. That keeps account borrows +*local to the context*, while still letting the VM's access handlers remap an +account's data underneath an active borrow when a program writes through a mapped +region — something the normal borrow checker can't express. The counters are what +make that safe at runtime. + +The engine's intentional divergences from upstream are documented for the whole +fork in [`../README.md`](../README.md) — see "Transaction Context", "VM Account +Mapping", and "Access-Violation Growth". diff --git a/solana/transaction-context/src/instruction.rs b/solana/transaction-context/src/instruction.rs new file mode 100644 index 0000000..35a4d3c --- /dev/null +++ b/solana/transaction-context/src/instruction.rs @@ -0,0 +1,275 @@ +use { + crate::{ + IndexOfAccount, + instruction_accounts::{BorrowedInstructionAccount, InstructionAccount}, + transaction::TransactionContext, + vm_addresses::{ + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS, GUEST_INSTRUCTION_DATA_BASE_ADDRESS, + GUEST_REGION_SIZE, + }, + vm_slice::VmSlice, + }, + solana_account::ReadableAccount, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + std::collections::HashSet, +}; + +/// Instruction shared between runtime and programs. +#[repr(C)] +#[derive(Debug)] +pub struct InstructionFrame { + /// Reserved field for alignment and potential future usage. + pub reserved: u16, + pub program_account_index_in_tx: u16, + pub nesting_level: u16, + /// This is the index of the parent instruction if this is a CPI and u16::MAX if this is a + /// top-level instruction + pub index_of_caller_instruction: u16, + pub instruction_accounts: VmSlice, + pub instruction_data: VmSlice, +} + +impl Default for InstructionFrame { + fn default() -> Self { + InstructionFrame { + nesting_level: 0, + program_account_index_in_tx: 0, + index_of_caller_instruction: u16::MAX, + // Using u64::MAX as the default pointer value, since it shall never be accessible. + instruction_accounts: VmSlice::new(0, 0), + instruction_data: VmSlice::new(0, 0), + reserved: 0, + } + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl InstructionFrame { + pub fn configure_vm_slices( + &mut self, + instruction_index: u64, + instruction_accounts_len: usize, + instruction_data_len: u64, + ) { + let common_offset = GUEST_REGION_SIZE.saturating_mul(instruction_index); + + // Instruction data slice + self.instruction_data = VmSlice::new( + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(common_offset), + instruction_data_len, + ); + + // Instruction accounts slice + self.instruction_accounts = VmSlice::new( + GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS.saturating_add(common_offset), + instruction_accounts_len as u64, + ); + } +} + +/// View interface to read instructions. +#[derive(Debug)] +pub struct InstructionContext<'a, 'ix_data> { + pub(crate) transaction_context: &'a TransactionContext<'ix_data>, + // The rest of the fields are redundant shortcuts + pub(crate) index_in_trace: usize, + pub(crate) nesting_level: usize, + pub(crate) index_of_caller_instruction: usize, + pub(crate) program_account_index_in_tx: IndexOfAccount, + pub(crate) instruction_accounts: &'a [InstructionAccount], + pub(crate) dedup_map: &'a [u16], + pub(crate) instruction_data: &'ix_data [u8], +} + +impl<'a> InstructionContext<'a, '_> { + /// How many Instructions were on the trace before this one was pushed + pub fn get_index_in_trace(&self) -> usize { + self.index_in_trace + } + + /// Returns the index of the instruction that called into this one. + pub fn get_index_of_caller(&self) -> usize { + self.index_of_caller_instruction + } + + /// How many Instructions were on the stack after this one was pushed + /// + /// That is the number of nested parent Instructions plus one (itself). + pub fn get_stack_height(&self) -> usize { + self.nesting_level.saturating_add(1) + } + + /// Number of accounts in this Instruction (without program accounts) + pub fn get_number_of_instruction_accounts(&self) -> IndexOfAccount { + self.instruction_accounts.len() as IndexOfAccount + } + + /// Assert that enough accounts were supplied to this Instruction + pub fn check_number_of_instruction_accounts( + &self, + expected_at_least: IndexOfAccount, + ) -> Result<(), InstructionError> { + if self.get_number_of_instruction_accounts() < expected_at_least { + Err(InstructionError::MissingAccount) + } else { + Ok(()) + } + } + + /// Data parameter for the programs `process_instruction` handler + pub fn get_instruction_data(&self) -> &[u8] { + self.instruction_data + } + + /// Translates the given instruction wide program_account_index into a transaction wide index + pub fn get_index_of_program_account_in_transaction( + &self, + ) -> Result { + if self.program_account_index_in_tx == u16::MAX { + Err(InstructionError::MissingAccount) + } else { + Ok(self.program_account_index_in_tx) + } + } + + /// Translates the given instruction wide instruction_account_index into a transaction wide index + pub fn get_index_of_instruction_account_in_transaction( + &self, + instruction_account_index: IndexOfAccount, + ) -> Result { + Ok(self + .instruction_accounts + .get(instruction_account_index as usize) + .ok_or(InstructionError::MissingAccount)? + .index_in_transaction as IndexOfAccount) + } + + /// Get the index of account in instruction from the index in transaction + pub fn get_index_of_account_in_instruction( + &self, + index_in_transaction: IndexOfAccount, + ) -> Result { + self.dedup_map + .get(index_in_transaction as usize) + .and_then(|idx| { + if *idx as usize >= self.instruction_accounts.len() { + None + } else { + Some(*idx as IndexOfAccount) + } + }) + .ok_or(InstructionError::MissingAccount) + } + + /// Returns `Some(instruction_account_index)` if this is a duplicate + /// and `None` if it is the first account with this key + pub fn is_instruction_account_duplicate( + &self, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + let index_in_transaction = + self.get_index_of_instruction_account_in_transaction(instruction_account_index)?; + let first_instruction_account_index = + self.get_index_of_account_in_instruction(index_in_transaction)?; + + Ok( + if first_instruction_account_index == instruction_account_index { + None + } else { + Some(first_instruction_account_index) + }, + ) + } + + /// Gets the key of the last program account of this Instruction + pub fn get_program_key(&self) -> Result<&'a Pubkey, InstructionError> { + self.get_index_of_program_account_in_transaction() + .and_then(|index_in_transaction| { + self.transaction_context.get_key_of_account_at_index(index_in_transaction) + }) + } + + /// Get the owner of the program account of this instruction + pub fn get_program_owner(&self) -> Result { + self.get_index_of_program_account_in_transaction() + .and_then(|index_in_transaction| { + self.transaction_context.accounts.try_borrow(index_in_transaction) + }) + .map(|acc| *acc.owner()) + } + + /// Gets an instruction account of this Instruction + pub fn try_borrow_instruction_account( + &self, + index_in_instruction: IndexOfAccount, + ) -> Result, InstructionError> { + let instruction_account = *self + .instruction_accounts + .get(index_in_instruction as usize) + .ok_or(InstructionError::MissingAccount)?; + + let account = self + .transaction_context + .accounts + .try_borrow_mut(instruction_account.index_in_transaction)?; + + Ok(BorrowedInstructionAccount { + transaction_context: self.transaction_context, + instruction_account, + account, + index_in_transaction_of_instruction_program: self.program_account_index_in_tx, + }) + } + + /// Returns whether an instruction account is a signer + pub fn is_instruction_account_signer( + &self, + instruction_account_index: IndexOfAccount, + ) -> Result { + Ok(self + .instruction_accounts + .get(instruction_account_index as usize) + .ok_or(InstructionError::MissingAccount)? + .is_signer()) + } + + /// Returns whether an instruction account is writable + pub fn is_instruction_account_writable( + &self, + instruction_account_index: IndexOfAccount, + ) -> Result { + Ok(self + .instruction_accounts + .get(instruction_account_index as usize) + .ok_or(InstructionError::MissingAccount)? + .is_writable()) + } + + /// Calculates the set of all keys of signer instruction accounts in this Instruction + pub fn get_signers(&self) -> Result, InstructionError> { + let mut result = HashSet::new(); + for instruction_account in self.instruction_accounts.iter() { + if instruction_account.is_signer() { + result.insert( + *self + .transaction_context + .get_key_of_account_at_index(instruction_account.index_in_transaction)?, + ); + } + } + Ok(result) + } + + pub fn instruction_accounts(&self) -> &[InstructionAccount] { + self.instruction_accounts + } + + pub fn get_key_of_instruction_account( + &self, + index_in_instruction: IndexOfAccount, + ) -> Result<&'a Pubkey, InstructionError> { + self.get_index_of_instruction_account_in_transaction(index_in_instruction) + .and_then(|idx| self.transaction_context.get_key_of_account_at_index(idx)) + } +} diff --git a/solana/transaction-context/src/instruction_accounts.rs b/solana/transaction-context/src/instruction_accounts.rs new file mode 100644 index 0000000..fb0168d --- /dev/null +++ b/solana/transaction-context/src/instruction_accounts.rs @@ -0,0 +1,384 @@ +use { + crate::{ + IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION, transaction::TransactionContext, + transaction_accounts::AccountRefMut, + }, + solana_account::{CoWAccount, ReadableAccount, WritableAccount}, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, +}; + +/// Contains account meta data which varies between instruction. +/// +/// It also contains indices to other structures for faster lookup. +/// +/// This data structure is supposed to be shared with programs in ABIv2, so do not modify it +/// without consulting SIMD-0177. +#[repr(C)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] +pub struct InstructionAccount { + /// Points to the account and its key in the `TransactionContext` + pub index_in_transaction: IndexOfAccount, + /// Is this account supposed to sign + is_signer: u8, + /// Is this account allowed to become writable + is_writable: u8, +} + +impl InstructionAccount { + pub fn new( + index_in_transaction: IndexOfAccount, + is_signer: bool, + is_writable: bool, + ) -> InstructionAccount { + InstructionAccount { + index_in_transaction, + is_signer: is_signer as u8, + is_writable: is_writable as u8, + } + } + + pub fn is_signer(&self) -> bool { + self.is_signer != 0 + } + + pub fn is_writable(&self) -> bool { + self.is_writable != 0 + } + + pub fn set_is_signer(&mut self, value: bool) { + self.is_signer = value as u8; + } + + pub fn set_is_writable(&mut self, value: bool) { + self.is_writable = value as u8; + } +} + +/// Shared account borrowed from the TransactionContext and an InstructionContext. +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +#[derive(Debug)] +pub struct BorrowedInstructionAccount<'a, 'ix_data> { + pub(crate) transaction_context: &'a TransactionContext<'ix_data>, + pub(crate) account: AccountRefMut<'a>, + pub(crate) instruction_account: InstructionAccount, + pub(crate) index_in_transaction_of_instruction_program: IndexOfAccount, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl BorrowedInstructionAccount<'_, '_> { + /// Returns the index of this account (transaction wide) + #[inline] + pub fn get_index_in_transaction(&self) -> IndexOfAccount { + self.instruction_account.index_in_transaction + } + + /// Returns the public key of this account (transaction wide) + #[inline] + pub fn get_key(&self) -> &Pubkey { + self.transaction_context + .get_key_of_account_at_index(self.instruction_account.index_in_transaction) + .unwrap() + } + + /// Returns the owner of this account (transaction wide) + #[inline] + pub fn get_owner(&self) -> &Pubkey { + self.account.owner() + } + + /// Assignes the owner of this account (transaction wide) + pub fn set_owner(&mut self, pubkey: &[u8]) -> Result<(), InstructionError> { + // Only the owner can assign a new owner + if !self.is_owned_by_current_program() { + return Err(InstructionError::ModifiedProgramId); + } + // and only if the account is writable + if !self.is_writable() { + return Err(InstructionError::ModifiedProgramId); + } + // and only if the data is zero-initialized or empty + if !is_zeroed(self.get_data()) { + return Err(InstructionError::ModifiedProgramId); + } + // don't touch the account if the owner does not change + if self.get_owner().to_bytes() == pubkey { + return Ok(()); + } + self.touch()?; + self.account.copy_into_owner_from_slice(pubkey); + Ok(()) + } + + /// Returns the number of lamports of this account (transaction wide) + #[inline] + pub fn get_lamports(&self) -> u64 { + self.account.lamports() + } + + /// Overwrites the number of lamports of this account (transaction wide) + pub fn set_lamports(&mut self, lamports: u64) -> Result<(), InstructionError> { + // An account not owned by the program cannot have its balance decrease + if !self.is_owned_by_current_program() && lamports < self.get_lamports() { + return Err(InstructionError::ExternalAccountLamportSpend); + } + // The balance of read-only may not change + if !self.is_writable() { + return Err(InstructionError::ReadonlyLamportChange); + } + // don't touch the account if the lamports do not change + let old_lamports = self.get_lamports(); + if old_lamports == lamports { + return Ok(()); + } + + let lamports_balance = (lamports as i128).saturating_sub(old_lamports as i128); + self.transaction_context.accounts.add_lamports_delta(lamports_balance)?; + + self.touch()?; + self.account.set_lamports(lamports); + Ok(()) + } + + /// Adds lamports to this account (transaction wide) + pub fn checked_add_lamports(&mut self, lamports: u64) -> Result<(), InstructionError> { + self.set_lamports( + self.get_lamports() + .checked_add(lamports) + .ok_or(InstructionError::ArithmeticOverflow)?, + ) + } + + /// Subtracts lamports from this account (transaction wide) + pub fn checked_sub_lamports(&mut self, lamports: u64) -> Result<(), InstructionError> { + self.set_lamports( + self.get_lamports() + .checked_sub(lamports) + .ok_or(InstructionError::ArithmeticOverflow)?, + ) + } + + /// Returns a read-only slice of the account data (transaction wide) + #[inline] + pub fn get_data(&self) -> &[u8] { + self.account.data() + } + + /// Returns a writable slice of the account data (transaction wide) + pub fn get_data_mut(&mut self) -> Result<&mut [u8], InstructionError> { + self.can_data_be_changed()?; + self.touch()?; + self.make_data_mut(); + Ok(self.account.data_as_mut_slice()) + } + + /// Overwrites the account data and size (transaction wide). + /// + /// Call this when you have a slice of data you do not own and want to + /// replace the account data with it. + pub fn set_data_from_slice(&mut self, data: &[u8]) -> Result<(), InstructionError> { + self.can_data_be_resized(data.len())?; + self.touch()?; + self.update_accounts_resize_delta(data.len())?; + // Note that we intentionally don't call self.make_data_mut() here. make_data_mut() will + // allocate + memcpy the current data if self.account is shared. We don't need the memcpy + // here tho because account.set_data_from_slice(data) is going to replace the content + // anyway. + self.account.set_data_from_slice(data); + + Ok(()) + } + + /// Resizes the account data (transaction wide) + /// + /// Fills it with zeros at the end if is extended or truncates at the end otherwise. + pub fn set_data_length(&mut self, new_length: usize) -> Result<(), InstructionError> { + self.can_data_be_resized(new_length)?; + // don't touch the account if the length does not change + if self.get_data().len() == new_length { + return Ok(()); + } + self.touch()?; + self.update_accounts_resize_delta(new_length)?; + self.account.resize(new_length, 0); + Ok(()) + } + + /// Appends all elements in a slice to the account + pub fn extend_from_slice(&mut self, data: &[u8]) -> Result<(), InstructionError> { + let new_len = self.get_data().len().saturating_add(data.len()); + self.can_data_be_resized(new_len)?; + + if data.is_empty() { + return Ok(()); + } + + self.touch()?; + self.update_accounts_resize_delta(new_len)?; + // Even if extend_from_slice never reduces capacity, still realloc using + // make_data_mut() if necessary so that we grow the account of the full + // max realloc length in one go, avoiding smaller reallocations. + self.make_data_mut(); + self.account.extend_from_slice(data); + Ok(()) + } + + /// Returns whether account data must be mapped through the CoW handler. + /// + /// Owned shared buffers and borrowed account images both need first-write + /// translation before the VM can mutate them. + pub fn is_shared(&self) -> bool { + self.account.is_shared() || matches!(self.account.cow(), CoWAccount::Borrowed(_)) + } + + fn make_data_mut(&mut self) { + // if the account is still shared, it means this is the first time we're + // about to write into it. Make the account mutable by copying it in a + // buffer with MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION capacity so that if the + // transaction reallocs, we don't have to copy the whole account data a + // second time to fullfill the realloc. + if self.account.is_shared() { + self.account.reserve(MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION); + } + } + + /// Deserializes the account data into a state + #[cfg(feature = "bincode")] + pub fn get_state(&self) -> Result { + bincode::deserialize(self.account.data()).map_err(|_| InstructionError::InvalidAccountData) + } + + /// Serializes a state into the account data + #[cfg(feature = "bincode")] + pub fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { + let data = self.get_data_mut()?; + let serialized_size = + bincode::serialized_size(state).map_err(|_| InstructionError::GenericError)?; + if serialized_size > data.len() as u64 { + return Err(InstructionError::AccountDataTooSmall); + } + bincode::serialize_into(&mut *data, state).map_err(|_| InstructionError::GenericError)?; + Ok(()) + } + + // Returns whether or the lamports currently in the account is sufficient for rent exemption should the + // data be resized to the given size + pub fn is_rent_exempt_at_data_length(&self, data_length: usize) -> bool { + self.transaction_context.rent.is_exempt(self.get_lamports(), data_length) + } + + /// Returns whether this account is executable (transaction wide) + #[inline] + #[deprecated(since = "2.1.0", note = "Use `get_owner` instead")] + pub fn is_executable(&self) -> bool { + #[allow(deprecated)] + self.account.executable() + } + + /// Configures whether this account is executable (transaction wide) + pub fn set_executable(&mut self, is_executable: bool) -> Result<(), InstructionError> { + // To become executable an account must be rent exempt + if !self + .transaction_context + .rent + .is_exempt(self.get_lamports(), self.get_data().len()) + { + return Err(InstructionError::ExecutableAccountNotRentExempt); + } + // Only the owner can set the executable flag + if !self.is_owned_by_current_program() { + return Err(InstructionError::ExecutableModified); + } + // and only if the account is writable + if !self.is_writable() { + return Err(InstructionError::ExecutableModified); + } + // don't touch the account if the executable flag does not change + #[allow(deprecated)] + if self.is_executable() == is_executable { + return Ok(()); + } + self.touch()?; + self.account.set_executable(is_executable); + Ok(()) + } + + /// Returns the rent epoch of this account (transaction wide) + #[inline] + pub fn get_rent_epoch(&self) -> u64 { + self.account.rent_epoch() + } + + /// Returns whether this account is a signer (instruction wide) + pub fn is_signer(&self) -> bool { + self.instruction_account.is_signer() + } + + /// Returns whether this account is writable (instruction wide) + pub fn is_writable(&self) -> bool { + self.instruction_account.is_writable() + } + + /// Returns true if the owner of this account is the current `InstructionContext`s last program (instruction wide) + pub fn is_owned_by_current_program(&self) -> bool { + self.transaction_context + .get_key_of_account_at_index(self.index_in_transaction_of_instruction_program) + .map(|program_key| program_key == self.get_owner()) + .unwrap_or_default() + } + + /// Returns an error if the account data can not be mutated by the current program + pub fn can_data_be_changed(&self) -> Result<(), InstructionError> { + // and only if the account is writable + if !self.is_writable() { + return Err(InstructionError::ReadonlyDataModified); + } + // and only if we are the owner + if !self.is_owned_by_current_program() { + return Err(InstructionError::ExternalAccountDataModified); + } + Ok(()) + } + + /// Returns an error if the account data can not be resized to the given length + pub fn can_data_be_resized(&self, new_len: usize) -> Result<(), InstructionError> { + let old_len = self.get_data().len(); + if new_len != old_len { + use solana_account::AccountMode; + if !self.is_owned_by_current_program() { + // Only the owner can change the length of the data + return Err(InstructionError::AccountDataSizeChanged); + } else if self.account.is(AccountMode::Ephemeral) { + // Ephemeral accounts can only be resized with special builtin instruction + return Err(InstructionError::InvalidRealloc); + } + } + self.transaction_context.accounts.can_data_be_resized(old_len, new_len)?; + self.can_data_be_changed() + } + + fn touch(&self) -> Result<(), InstructionError> { + self.transaction_context + .accounts + .touch(self.instruction_account.index_in_transaction) + } + + fn update_accounts_resize_delta(&mut self, new_len: usize) -> Result<(), InstructionError> { + self.transaction_context + .accounts + .update_accounts_resize_delta(self.get_data().len(), new_len) + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +fn is_zeroed(buf: &[u8]) -> bool { + const ZEROS_LEN: usize = 1024; + const ZEROS: [u8; ZEROS_LEN] = [0; ZEROS_LEN]; + let mut chunks = buf.chunks_exact(ZEROS_LEN); + + #[allow(clippy::indexing_slicing)] + { + chunks.all(|chunk| chunk == &ZEROS[..]) + && chunks.remainder() == &ZEROS[..chunks.remainder().len()] + } +} diff --git a/solana/transaction-context/src/lib.rs b/solana/transaction-context/src/lib.rs new file mode 100644 index 0000000..9801113 --- /dev/null +++ b/solana/transaction-context/src/lib.rs @@ -0,0 +1,50 @@ +#![allow(clippy::disallowed_methods)] +#![deny(clippy::indexing_slicing)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![doc = include_str!("../README.md")] + +pub mod instruction; +pub mod instruction_accounts; +pub mod transaction_accounts; +mod vm_addresses; +pub mod vm_slice; + +pub mod transaction; + +pub const MAX_ACCOUNTS_PER_TRANSACTION: usize = 256; +// This is one less than MAX_ACCOUNTS_PER_TRANSACTION because +// one index is used as NON_DUP_MARKER in ABI v0 and v1. +pub const MAX_ACCOUNTS_PER_INSTRUCTION: usize = 255; +pub const MAX_INSTRUCTION_DATA_LEN: usize = 10 * 1024; +pub const MAX_ACCOUNT_DATA_LEN: u64 = 10 * 1024 * 1024; +// Note: Direct account-region mapping lets programs grow accounts through the +// AccessViolationHandler, which might grow an account up to +// MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION at once. +pub const MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION: i64 = MAX_ACCOUNT_DATA_LEN as i64 * 2; +pub const MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION: usize = 10 * 1_024; +// Maximum cross-program invocation and instructions per transaction +pub const MAX_INSTRUCTION_TRACE_LENGTH: usize = 64; + +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_ACCOUNTS_PER_INSTRUCTION, + solana_program_entrypoint::NON_DUP_MARKER as usize, +); +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_ACCOUNT_DATA_LEN, + solana_system_interface::MAX_PERMITTED_DATA_LENGTH, +); +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION, + solana_system_interface::MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION, +); +#[cfg(test)] +static_assertions::const_assert_eq!( + MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION, + solana_account_info::MAX_PERMITTED_DATA_INCREASE, +); + +/// Index of an account inside of the transaction or an instruction. +pub type IndexOfAccount = u16; diff --git a/solana/transaction-context/src/transaction.rs b/solana/transaction-context/src/transaction.rs new file mode 100644 index 0000000..87954ce --- /dev/null +++ b/solana/transaction-context/src/transaction.rs @@ -0,0 +1,1158 @@ +use { + crate::{ + IndexOfAccount, MAX_ACCOUNT_DATA_LEN, MAX_ACCOUNTS_PER_TRANSACTION, + instruction::{InstructionContext, InstructionFrame}, + instruction_accounts::InstructionAccount, + transaction_accounts::{KeyedAccountSharedData, TransactionAccounts}, + vm_addresses::{ + GUEST_INSTRUCTION_DATA_BASE_ADDRESS, GUEST_REGION_SIZE, RETURN_DATA_SCRATCHPAD, + }, + vm_slice::VmSlice, + }, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_instruction::error::InstructionError, + solana_instructions_sysvar as instructions, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sbpf::memory_region::{AccessType, AccessViolationHandler, MemoryRegion}, + std::{borrow::Cow, cell::Cell, rc::Rc}, +}; + +/// Used only in fn `take_instruction_trace` for deconstructing TransactionContext +pub type InstructionTrace<'ix_data> = ( + Vec, + Vec>, + Vec>, +); + +/// This data structure is shared with programs in ABIv2, providing information about the +/// transaction metadata. +/// +/// Modifications without a feature gate and proper versioning might break programs. +#[repr(C)] +#[derive(Debug)] +struct TransactionFrame { + /// Pubkey of the last program to write to the return data scratchpad + return_data_pubkey: Pubkey, + return_data_scratchpad: VmSlice, + /// Scratchpad for programs to write CPI instruction data + cpi_scratchpad: VmSlice, + /// Index of current executing instruction + current_executing_instruction: u16, + /// Number of instructions in the instruction trace (including top level and CPIs) + total_number_of_instructions_in_trace: u16, + /// Number of CPIs in the instruction trace + number_of_cpis_in_trace: u16, + /// Number of transaction accounts + number_of_transaction_accounts: u16, +} + +/// Loaded transaction shared between runtime and programs. +/// +/// This context is valid for the entire duration of a transaction being processed. +#[derive(Debug)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionContext<'ix_data> { + pub(crate) accounts: Rc, + instruction_stack_capacity: usize, + instruction_trace_capacity: usize, + instruction_stack: Vec, + instruction_trace: Vec, + transaction_frame: TransactionFrame, + return_data_bytes: Vec, + next_top_level_instruction_index: usize, + #[cfg(not(target_os = "solana"))] + pub(crate) rent: Rent, + /// This is an account deduplication map that maps index_in_transaction to index_in_instruction + /// Usage: dedup_map[index_in_transaction] = index_in_instruction + /// Each entry in `deduplication_maps` represents the deduplication map for each instruction. + deduplication_maps: Vec>, + /// Each entry in `instruction_accounts` represents the array of accounts for each instruction. + instruction_accounts: Vec>, + /// Each entry in `instruction_data` represents the data for instruction at the corresponding + /// index. + instruction_data: Vec>, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl<'ix_data> TransactionContext<'ix_data> { + /// Constructs a new TransactionContext + pub fn new( + transaction_accounts: Vec, + rent: Rent, + instruction_stack_capacity: usize, + instruction_trace_capacity: usize, + number_of_top_level_instructions: usize, + ) -> Self { + let transaction_frame = TransactionFrame { + return_data_pubkey: Pubkey::default(), + return_data_scratchpad: VmSlice::new(RETURN_DATA_SCRATCHPAD, 0), + cpi_scratchpad: VmSlice::new(0, 0), + current_executing_instruction: 0, + total_number_of_instructions_in_trace: number_of_top_level_instructions as u16, + number_of_cpis_in_trace: 0, + number_of_transaction_accounts: transaction_accounts.len() as u16, + }; + + Self { + accounts: Rc::new(TransactionAccounts::new(transaction_accounts)), + instruction_stack_capacity, + instruction_trace_capacity, + instruction_stack: Vec::with_capacity(instruction_stack_capacity), + instruction_trace: vec![InstructionFrame::default()], + return_data_bytes: Vec::new(), + transaction_frame, + next_top_level_instruction_index: 0, + rent, + instruction_accounts: Vec::with_capacity(instruction_trace_capacity), + deduplication_maps: Vec::with_capacity(instruction_trace_capacity), + instruction_data: Vec::with_capacity(instruction_trace_capacity), + } + } + + /// Used in mock_process_instruction + pub fn deconstruct_without_keys(self) -> Result, InstructionError> { + if !self.instruction_stack.is_empty() { + return Err(InstructionError::CallDepth); + } + + let accounts = Rc::try_unwrap(self.accounts) + .expect("transaction_context.accounts has unexpected outstanding refs") + .deconstruct_into_account_shared_data(); + + Ok(accounts) + } + + pub fn accounts(&self) -> &Rc { + &self.accounts + } + + /// Returns the total number of accounts loaded in this Transaction + pub fn get_number_of_accounts(&self) -> IndexOfAccount { + self.accounts.len() as IndexOfAccount + } + + /// Searches for an account by its key + pub fn get_key_of_account_at_index( + &self, + index_in_transaction: IndexOfAccount, + ) -> Result<&Pubkey, InstructionError> { + self.accounts + .account_key(index_in_transaction) + .ok_or(InstructionError::MissingAccount) + } + + /// Searches for an account by its key + pub fn find_index_of_account(&self, pubkey: &Pubkey) -> Option { + self.accounts + .account_keys_iter() + .position(|key| key == pubkey) + .map(|index| index as IndexOfAccount) + } + + /// Gets the max length of the instruction trace + pub fn get_instruction_trace_capacity(&self) -> usize { + self.instruction_trace_capacity + } + + /// Returns the instruction trace length. + /// + /// Not counting the last empty instruction which is always pre-reserved for the next instruction. + pub fn get_instruction_trace_length(&self) -> usize { + self.instruction_trace.len().saturating_sub(1) + } + + /// Gets a view on an instruction by its index in the trace + pub fn get_instruction_context_at_index_in_trace( + &self, + index_in_trace: usize, + ) -> Result, InstructionError> { + let instruction = + self.instruction_trace.get(index_in_trace).ok_or(InstructionError::CallDepth)?; + + // These commands will return a default empty slice if we are retrieving an instruction + // that hasn't been configured yet. + let instruction_accounts = self + .instruction_accounts + .get(index_in_trace) + .map(|item| item.as_ref()) + .unwrap_or_default(); + let dedup_map = self + .deduplication_maps + .get(index_in_trace) + .map(|item| item.as_ref()) + .unwrap_or_default(); + let instruction_data = self + .instruction_data + .get(index_in_trace) + .map(|item| item.as_ref()) + .unwrap_or_default(); + Ok(InstructionContext { + transaction_context: self, + index_in_trace, + nesting_level: instruction.nesting_level as usize, + program_account_index_in_tx: instruction.program_account_index_in_tx as IndexOfAccount, + instruction_accounts, + dedup_map, + instruction_data, + index_of_caller_instruction: instruction.index_of_caller_instruction as usize, + }) + } + + /// Gets a view on the instruction by its nesting level in the stack + pub fn get_instruction_context_at_nesting_level( + &self, + nesting_level: usize, + ) -> Result, InstructionError> { + let index_in_trace = + *self.instruction_stack.get(nesting_level).ok_or(InstructionError::CallDepth)?; + let instruction_context = self.get_instruction_context_at_index_in_trace(index_in_trace)?; + debug_assert_eq!(instruction_context.nesting_level, nesting_level); + Ok(instruction_context) + } + + /// Gets the max height of the instruction stack + pub fn get_instruction_stack_capacity(&self) -> usize { + self.instruction_stack_capacity + } + + /// Gets instruction stack height, top-level instructions are height + /// `solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT` + pub fn get_instruction_stack_height(&self) -> usize { + self.instruction_stack.len() + } + + /// Returns the index in the instruction trace of the current executing instruction + pub fn get_current_instruction_index(&self) -> Result { + self.instruction_stack.last().copied().ok_or(InstructionError::CallDepth) + } + + /// Returns a view on the current instruction + pub fn get_current_instruction_context( + &self, + ) -> Result, InstructionError> { + let index_in_trace = self.get_current_instruction_index()?; + self.get_instruction_context_at_index_in_trace(index_in_trace) + } + + /// Returns a view on the next instruction. This function assumes it has already been + /// configured with the correct values in `prepare_next_instruction` or + /// `prepare_next_top_level_instruction` + pub fn get_next_instruction_context( + &self, + ) -> Result, InstructionError> { + let index_in_trace = + self.instruction_trace.len().checked_sub(1).ok_or(InstructionError::CallDepth)?; + self.get_instruction_context_at_index_in_trace(index_in_trace) + } + + /// Configures an instruction at a specific index in trace. + pub fn configure_instruction_at_index( + &mut self, + instruction_index: usize, + program_index: IndexOfAccount, + instruction_accounts: Vec, + deduplication_map: Vec, + instruction_data: Cow<'ix_data, [u8]>, + caller_index: Option, + ) -> Result<(), InstructionError> { + debug_assert_eq!(deduplication_map.len(), MAX_ACCOUNTS_PER_TRANSACTION); + + let instruction = self + .instruction_trace + .get_mut(instruction_index) + .ok_or(InstructionError::MaxInstructionTraceLengthExceeded)?; + + // If we have a parent index, then we are dealing with a CPI. + if let Some(caller_index) = caller_index { + self.transaction_frame.total_number_of_instructions_in_trace = + self.transaction_frame.total_number_of_instructions_in_trace.saturating_add(1); + instruction.index_of_caller_instruction = caller_index; + } + + self.transaction_frame.cpi_scratchpad = VmSlice::new( + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul( + self.transaction_frame.total_number_of_instructions_in_trace as u64, + )), + 0, + ); + + instruction.program_account_index_in_tx = program_index; + instruction.configure_vm_slices( + instruction_index as u64, + instruction_accounts.len(), + instruction_data.len() as u64, + ); + self.deduplication_maps.push(deduplication_map.into_boxed_slice()); + self.instruction_accounts.push(instruction_accounts.into_boxed_slice()); + self.instruction_data.push(instruction_data); + Ok(()) + } + + /// For tests only + fn deduplicate_accounts_for_tests(instruction_accounts: &[InstructionAccount]) -> Vec { + let mut dedup_map = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + for (idx, account) in instruction_accounts.iter().enumerate() { + let index_in_instruction = + dedup_map.get_mut(account.index_in_transaction as usize).unwrap(); + if *index_in_instruction == u16::MAX { + *index_in_instruction = idx as u16; + } + } + dedup_map + } + + /// A version of `configure_top_level_instruction` to help creating the deduplication map in tests + pub fn configure_top_level_instruction_for_tests( + &mut self, + program_index: IndexOfAccount, + instruction_accounts: Vec, + instruction_data: Vec, + ) -> Result<(), InstructionError> { + debug_assert!(instruction_accounts.len() <= u16::MAX as usize); + let dedup_map = Self::deduplicate_accounts_for_tests(&instruction_accounts); + + self.configure_instruction_at_index( + self.get_instruction_trace_length(), + program_index, + instruction_accounts, + dedup_map, + Cow::Owned(instruction_data), + None, + )?; + Ok(()) + } + + /// A helper function to facilitate creating a CPI in tests + pub fn configure_next_cpi_for_tests( + &mut self, + program_index: IndexOfAccount, + instruction_accounts: Vec, + instruction_data: Vec, + ) -> Result<(), InstructionError> { + debug_assert!(instruction_accounts.len() <= u16::MAX as usize); + let dedup_map = Self::deduplicate_accounts_for_tests(&instruction_accounts); + let caller_index = self.get_current_instruction_index()?; + let cpi_index = self.get_instruction_trace_length(); + self.configure_instruction_at_index( + cpi_index, + program_index, + instruction_accounts, + dedup_map, + Cow::Owned(instruction_data), + Some(caller_index as u16), + )?; + Ok(()) + } + + /// Pushes the next instruction + pub fn push(&mut self) -> Result<(), InstructionError> { + let nesting_level = self.get_instruction_stack_height(); + if !self.instruction_stack.is_empty() && self.accounts.get_lamports_delta() != 0 { + return Err(InstructionError::UnbalancedInstruction); + } + { + let instruction = + self.instruction_trace.last_mut().ok_or(InstructionError::CallDepth)?; + instruction.nesting_level = nesting_level as u16; + } + let index_in_trace = self.get_instruction_trace_length(); + if index_in_trace >= self.instruction_trace_capacity { + return Err(InstructionError::MaxInstructionTraceLengthExceeded); + } + + let current_top_level_instruction = if self.instruction_stack.is_empty() { + let index = self.next_top_level_instruction_index; + self.next_top_level_instruction_index = + self.next_top_level_instruction_index.saturating_add(1); + index + } else { + self.transaction_frame.number_of_cpis_in_trace = + self.transaction_frame.number_of_cpis_in_trace.saturating_add(1); + self.next_top_level_instruction_index.saturating_sub(1) + }; + + self.instruction_trace.push(InstructionFrame::default()); + if nesting_level >= self.instruction_stack_capacity { + return Err(InstructionError::CallDepth); + } + self.transaction_frame.current_executing_instruction = index_in_trace as u16; + self.instruction_stack.push(index_in_trace); + if let Some(index_in_transaction) = self.find_index_of_account(&instructions::id()) { + let mut mut_account_ref = self.accounts.try_borrow_mut(index_in_transaction)?; + if mut_account_ref.owner() != &solana_sdk_ids::sysvar::id() { + return Err(InstructionError::InvalidAccountOwner); + } + instructions::store_current_index_checked( + mut_account_ref.data_as_mut_slice(), + current_top_level_instruction as u16, + )?; + } + Ok(()) + } + + /// Pops the current instruction + pub fn pop(&mut self) -> Result<(), InstructionError> { + if self.instruction_stack.is_empty() { + return Err(InstructionError::CallDepth); + } + // Verify (before we pop) that the total sum of all lamports in this instruction did not change + let detected_an_unbalanced_instruction = + self.get_current_instruction_context().and_then(|instruction_context| { + // Verify all executable accounts have no outstanding refs + self.accounts + .try_borrow_mut( + instruction_context.get_index_of_program_account_in_transaction()?, + ) + .map_err(|err| { + if err == InstructionError::AccountBorrowFailed { + InstructionError::AccountBorrowOutstanding + } else { + err + } + })?; + Ok(self.accounts.get_lamports_delta() != 0) + }); + // Always pop, even if we `detected_an_unbalanced_instruction` + self.instruction_stack.pop(); + if let Some(instr_idx) = self.instruction_stack.last() { + self.transaction_frame.current_executing_instruction = *instr_idx as u16; + } + if detected_an_unbalanced_instruction? { + Err(InstructionError::UnbalancedInstruction) + } else { + Ok(()) + } + } + + /// Gets the return data of the current instruction or any above + pub fn get_return_data(&self) -> (&Pubkey, &[u8]) { + ( + &self.transaction_frame.return_data_pubkey, + &self.return_data_bytes, + ) + } + + /// Set the return data of the current instruction + pub fn set_return_data( + &mut self, + program_id: Pubkey, + data: Vec, + ) -> Result<(), InstructionError> { + self.transaction_frame.return_data_pubkey = program_id; + // SAFETY: `return_data_scratchpad` is backed by `self.return_data_bytes` + // and `return_data_bytes` is being reset to `data` + // in the next statement. + unsafe { + self.transaction_frame.return_data_scratchpad.set_len(data.len() as u64); + } + self.return_data_bytes = data; + Ok(()) + } + + /// Returns a new account data write access handler + pub fn access_violation_handler(&self) -> AccessViolationHandler { + let accounts = Rc::clone(&self.accounts); + Box::new( + move |region: &mut MemoryRegion, + address_space_reserved_for_account: u64, + access_type: AccessType, + vm_addr: u64, + len: u64| { + use solana_account::AccountMode; + + if access_type == AccessType::Load { + return; + } + let Some(index_in_transaction) = region.access_violation_handler_payload else { + // This region is not a writable account. + return; + }; + let requested_length = + vm_addr.saturating_add(len).saturating_sub(region.vm_addr) as usize; + if requested_length > address_space_reserved_for_account as usize { + // Requested access goes further than the account region. + return; + } + + // The four calls below can't really fail. If they fail because of a bug, + // whatever is writing will trigger an EbpfError::AccessViolation like + // if the region was readonly, and the transaction will fail gracefully. + let Ok(mut account) = accounts.try_borrow_mut(index_in_transaction) else { + debug_assert!(false); + return; + }; + if accounts.touch(index_in_transaction).is_err() { + debug_assert!(false); + return; + } + + if requested_length > region.len as usize { + let old_len = account.data().len(); + let new_len = requested_length; + if new_len > MAX_ACCOUNT_DATA_LEN as usize + || accounts.can_data_be_resized(old_len, new_len).is_err() + || account.is(AccountMode::Ephemeral) + { + return; + } + if accounts.update_accounts_resize_delta(old_len, new_len).is_err() { + return; + } + account.resize(new_len, 0); + } + + let data = account.data_as_mut_slice(); + region.host_addr = data.as_mut_ptr() as u64; + region.len = data.len() as u64; + region.writable = true; + }, + ) + } + + /// Take ownership of the instruction trace + pub fn take_instruction_trace(&mut self) -> InstructionTrace<'_> { + // The last frame is a placeholder for the next instruction to be executed, so it + // is empty. + self.instruction_trace.pop(); + ( + std::mem::take(&mut self.instruction_trace), + std::mem::take(&mut self.instruction_accounts), + std::mem::take(&mut self.instruction_data), + ) + } + + /// An active instruction is either one that has already finished execution or that is + /// under execution (e.g. all nested CPIs are active). + /// For ABIv2 only. + pub fn number_of_active_instructions_in_trace(&self) -> usize { + self.next_top_level_instruction_index + .saturating_add(self.transaction_frame.number_of_cpis_in_trace as usize) + } + + /// Return next top level instruction to execute + pub fn next_top_level_instruction_index(&self) -> usize { + self.next_top_level_instruction_index + } + + /// Return number of CPIs in instruction trace + pub fn number_of_cpis_in_trace(&self) -> usize { + self.transaction_frame.number_of_cpis_in_trace as usize + } +} + +/// Return data at the end of a transaction +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +#[cfg_attr(feature = "serde", derive(serde::Deserialize, serde::Serialize))] +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct TransactionReturnData { + pub program_id: Pubkey, + pub data: Vec, +} + +/// Everything that needs to be recorded from a TransactionContext after execution +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct ExecutionRecord { + pub accounts: Vec, + pub return_data: TransactionReturnData, + pub touched_account_count: u64, + pub accounts_resize_delta: i64, +} + +/// Used by the bank in the runtime to write back the processed accounts and recorded instructions +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl From> for ExecutionRecord { + fn from(context: TransactionContext) -> Self { + let (accounts, touched_flags, resize_delta) = Rc::try_unwrap(context.accounts) + .expect("transaction_context.accounts has unexpected outstanding refs") + .take(); + let touched_account_count = touched_flags.iter().fold(0usize, |accumulator, was_touched| { + accumulator.saturating_add(was_touched.get() as usize) + }) as u64; + + let return_data = TransactionReturnData { + program_id: context.transaction_frame.return_data_pubkey, + data: context.return_data_bytes, + }; + + Self { + accounts, + return_data, + touched_account_count, + accounts_resize_delta: Cell::into_inner(resize_delta), + } + } +} + +#[cfg(all(test, not(target_arch = "sbf"), not(target_arch = "bpf")))] +mod tests { + use super::*; + + #[test] + fn test_instructions_sysvar_store_index_checked() { + let build_transaction_context = |account: AccountSharedData| { + TransactionContext::new( + vec![ + (Pubkey::new_unique(), AccountSharedData::default()), + (instructions::id(), account), + ], + Rent::default(), + /* max_instruction_stack_depth */ 2, + /* max_instruction_trace_length */ 2, + /* number_of_top_level_instructions */ 1, + ) + }; + + let correct_space = 2; + let rent_exempt_lamports = Rent::default().minimum_balance(correct_space); + + // First try it with the wrong owner. + let account = + AccountSharedData::new(rent_exempt_lamports, correct_space, &Pubkey::new_unique()); + assert_eq!( + build_transaction_context(account).push(), + Err(InstructionError::InvalidAccountOwner), + ); + + // Now with the wrong data length. + let account = + AccountSharedData::new(rent_exempt_lamports, 0, &solana_sdk_ids::sysvar::id()); + assert_eq!( + build_transaction_context(account).push(), + Err(InstructionError::AccountDataTooSmall), + ); + + // Finally provide the correct account setup. + let account = AccountSharedData::new( + rent_exempt_lamports, + correct_space, + &solana_sdk_ids::sysvar::id(), + ); + assert_eq!(build_transaction_context(account).push(), Ok(()),); + } + + #[test] + fn test_invalid_native_loader_index() { + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )], + Rent::default(), + 20, + 20, + 1, + ); + + transaction_context + .configure_top_level_instruction_for_tests( + u16::MAX, + vec![InstructionAccount::new(0, false, false)], + vec![], + ) + .unwrap(); + let instruction_context = transaction_context.get_next_instruction_context().unwrap(); + + let result = instruction_context.get_index_of_program_account_in_transaction(); + assert_eq!(result, Err(InstructionError::MissingAccount)); + + let result = instruction_context.get_program_key(); + assert_eq!(result, Err(InstructionError::MissingAccount)); + + let result = instruction_context.get_program_owner(); + assert_eq!(result.err(), Some(InstructionError::MissingAccount)); + } + + #[test] + fn test_instruction_shared_items() { + let transaction_accounts = vec![(Pubkey::new_unique(), AccountSharedData::default()); 10]; + let mut transaction_context = + TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 3); + + let instruction_accounts_1 = + vec![InstructionAccount::new(0, false, true), InstructionAccount::new(3, true, false)]; + transaction_context + .configure_top_level_instruction_for_tests( + 1, + instruction_accounts_1.clone(), + vec![1, 2, 3, 4], + ) + .unwrap(); + transaction_context.push().unwrap(); + + let instruction_accounts_2 = vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(3, true, false), + InstructionAccount::new(5, false, false), + ]; + transaction_context + .configure_top_level_instruction_for_tests( + 1, + instruction_accounts_2.clone(), + vec![5, 6, 7, 8, 9], + ) + .unwrap(); + transaction_context.push().unwrap(); + + let instruction_accounts_3 = vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(3, true, false), + InstructionAccount::new(5, false, false), + InstructionAccount::new(3, false, false), + InstructionAccount::new(10, false, false), + ]; + transaction_context + .configure_top_level_instruction_for_tests( + 1, + instruction_accounts_3.clone(), + vec![10, 11], + ) + .unwrap(); + transaction_context.push().unwrap(); + + let first_ix_context = + transaction_context.get_instruction_context_at_index_in_trace(0).unwrap(); + assert_eq!( + instruction_accounts_1.as_slice(), + first_ix_context.instruction_accounts + ); + assert_eq!( + *first_ix_context.instruction_data, + **transaction_context.instruction_data.first().unwrap() + ); + for (idx_in_ix, acc) in instruction_accounts_1.iter().enumerate() { + assert_eq!( + *first_ix_context.dedup_map.get(acc.index_in_transaction as usize).unwrap(), + idx_in_ix as u16 + ); + } + + let second_ix_context = + transaction_context.get_instruction_context_at_index_in_trace(1).unwrap(); + assert_eq!( + instruction_accounts_2.as_slice(), + second_ix_context.instruction_accounts + ); + assert_eq!( + *second_ix_context.instruction_data, + **transaction_context.instruction_data.get(1).unwrap() + ); + for (idx_in_ix, acc) in instruction_accounts_2.iter().enumerate() { + assert_eq!( + *second_ix_context.dedup_map.get(acc.index_in_transaction as usize).unwrap(), + idx_in_ix as u16 + ); + } + + let third_ix_context = + transaction_context.get_instruction_context_at_index_in_trace(2).unwrap(); + assert_eq!( + instruction_accounts_3.as_slice(), + third_ix_context.instruction_accounts + ); + assert_eq!( + *third_ix_context.instruction_data, + **transaction_context.instruction_data.get(2).unwrap() + ); + for (idx_in_ix, acc) in instruction_accounts_3.iter().enumerate() { + if idx_in_ix == 3 { + assert_eq!( + *third_ix_context.dedup_map.get(acc.index_in_transaction as usize).unwrap(), + 1 + ); + } else { + assert_eq!( + *third_ix_context.dedup_map.get(acc.index_in_transaction as usize).unwrap(), + idx_in_ix as u16 + ); + } + } + } + + #[test] + fn test_number_of_instructions() { + let transaction_accounts = vec![(Pubkey::new_unique(), AccountSharedData::default()); 3]; + let mut transaction_context = + TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 2); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 0 + ); + + // Instruction #0 + transaction_context + .configure_instruction_at_index( + 0, + 0, + vec![InstructionAccount::new(1, false, false)], + vec![0; MAX_ACCOUNTS_PER_TRANSACTION], + Vec::new().into(), + None, + ) + .unwrap(); + + // Executing instruction #0 + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 0 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 2 + ); + + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 0 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(2)) + ); + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.len(), + 0, + ); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 1 + ); + + // Instruction #0 does a CPI. + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(2, false, true)], + Vec::new(), + ) + .unwrap(); + + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 1, + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 3 + ); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 1 + ); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 2 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(3)) + ); + + // A nested CPI + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(2, false, true)], + Vec::new(), + ) + .unwrap(); + + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 2 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 4 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(4)) + ); + + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 2 + ); + + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 3 + ); + // Return from nested CPI + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 3 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 4 + ); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 2, + ); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 1 + ); + + // A second nested CPI + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(2, false, true)], + Vec::new(), + ) + .unwrap(); + + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 3 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 5 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 3 + ); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 4 + ); + + // Return from second nested CPI + transaction_context.pop().unwrap(); + + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 1 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 5 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); + + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 3 + ); + + // Return from first CPI + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 4 + ); + + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 0 + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 5 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(5)) + ); + + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 3, + ); + + // Let's go to Instruction #1 (top level) + transaction_context.pop().unwrap(); + + // Instruction #1 + transaction_context + .configure_top_level_instruction_for_tests( + 0, + vec![InstructionAccount::new(1, false, false)], + Vec::new(), + ) + .unwrap(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 4, + ); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 3 + ); + + // Instruction #1 will do a CPI. + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(2, false, true)], + Vec::new(), + ) + .unwrap(); + + transaction_context.push().unwrap(); + + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 5, + ); + + assert_eq!( + transaction_context.transaction_frame.total_number_of_instructions_in_trace, + 6 + ); + + assert_eq!( + transaction_context.transaction_frame.cpi_scratchpad.ptr(), + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add(GUEST_REGION_SIZE.saturating_mul(6)) + ); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 4 + ); + assert_eq!( + transaction_context.number_of_active_instructions_in_trace(), + 6 + ); + + // Return from CPI + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.transaction_frame.number_of_cpis_in_trace, + 4 + ); + assert_eq!( + transaction_context.transaction_frame.current_executing_instruction, + 4, + ); + + transaction_context.pop().unwrap(); + } + + #[test] + fn test_get_current_instruction_index() { + let transaction_accounts = vec![(Pubkey::new_unique(), AccountSharedData::default()); 3]; + let mut transaction_context = + TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 2); + + // First top level instruction + transaction_context + .configure_instruction_at_index( + 0, + 1, + vec![ + InstructionAccount::new(0, false, false), + InstructionAccount::new(1, false, false), + ], + vec![u16::MAX; 256], + Cow::Owned(Vec::new()), + None, + ) + .unwrap(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 0 + ); + transaction_context.pop().unwrap(); + + // Second top-level instruction + transaction_context + .configure_instruction_at_index( + 1, + 1, + vec![ + InstructionAccount::new(0, false, false), + InstructionAccount::new(1, false, true), + ], + vec![u16::MAX; 256], + Cow::Owned(Vec::new()), + None, + ) + .unwrap(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 1 + ); + + // Simulating a CPI + transaction_context + .configure_next_cpi_for_tests( + 1, + vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(1, false, false), + ], + Vec::new(), + ) + .unwrap(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 2 + ); + + // Yet another CPI + transaction_context + .configure_next_cpi_for_tests( + 1, + vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(1, false, false), + ], + Vec::new(), + ) + .unwrap(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 3 + ); + + // CPI return + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 2 + ); + + // CPI return 2 + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.get_current_instruction_index().unwrap(), + 1 + ); + } +} diff --git a/solana/transaction-context/src/transaction_accounts.rs b/solana/transaction-context/src/transaction_accounts.rs new file mode 100644 index 0000000..00b2f18 --- /dev/null +++ b/solana/transaction-context/src/transaction_accounts.rs @@ -0,0 +1,532 @@ +use { + crate::{IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION, MAX_ACCOUNT_DATA_LEN}, + solana_account::AccountSharedData, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + std::{ + cell::{Cell, UnsafeCell}, + ops::{Deref, DerefMut}, + }, +}; + +#[derive(Debug, PartialEq)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionAccountView<'a> { + account: &'a AccountSharedData, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl Deref for TransactionAccountView<'_> { + type Target = AccountSharedData; + fn deref(&self) -> &Self::Target { + self.account + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl PartialEq for TransactionAccountView<'_> { + fn eq(&self, other: &AccountSharedData) -> bool { + self.account == other + } +} + +#[derive(Debug)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionAccountViewMut<'a> { + account: &'a mut AccountSharedData, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl TransactionAccountViewMut<'_> { + pub(crate) fn reserve(&mut self, additional: usize) { + self.account.cow_mut().reserve(additional); + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl Deref for TransactionAccountViewMut<'_> { + type Target = AccountSharedData; + fn deref(&self) -> &Self::Target { + self.account + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl DerefMut for TransactionAccountViewMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + self.account + } +} + +// +/// An account key and the matching account +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub type KeyedAccountSharedData = (Pubkey, AccountSharedData); +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub(crate) type DeconstructedTransactionAccounts = + (Vec, Box<[Cell]>, Cell); + +#[derive(Debug)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionAccounts { + accounts: Box<[UnsafeCell]>, + borrow_counters: Box<[BorrowCounter]>, + touched_flags: Box<[Cell]>, + resize_delta: Cell, + lamports_delta: Cell, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl TransactionAccounts { + pub(crate) fn new(accounts: Vec) -> TransactionAccounts { + let touched_flags = vec![Cell::new(false); accounts.len()].into_boxed_slice(); + let borrow_counters = vec![BorrowCounter::default(); accounts.len()].into_boxed_slice(); + let accounts = + accounts.into_iter().map(UnsafeCell::new).collect::>().into_boxed_slice(); + + TransactionAccounts { + accounts, + borrow_counters, + touched_flags, + resize_delta: Cell::new(0), + lamports_delta: Cell::new(0), + } + } + + pub(crate) fn len(&self) -> usize { + self.accounts.len() + } + + pub fn touch(&self, index: IndexOfAccount) -> Result<(), InstructionError> { + self.touched_flags + .get(index as usize) + .ok_or(InstructionError::MissingAccount)? + .set(true); + Ok(()) + } + + pub(crate) fn update_accounts_resize_delta( + &self, + old_len: usize, + new_len: usize, + ) -> Result<(), InstructionError> { + let accounts_resize_delta = self.resize_delta.get(); + self.resize_delta.set( + accounts_resize_delta.saturating_add((new_len as i64).saturating_sub(old_len as i64)), + ); + Ok(()) + } + + pub(crate) fn can_data_be_resized( + &self, + old_len: usize, + new_len: usize, + ) -> Result<(), InstructionError> { + // The new length can not exceed the maximum permitted length + if new_len > MAX_ACCOUNT_DATA_LEN as usize { + return Err(InstructionError::InvalidRealloc); + } + // The resize can not exceed the per-transaction maximum + let length_delta = (new_len as i64).saturating_sub(old_len as i64); + if self.resize_delta.get().saturating_add(length_delta) + > MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION + { + return Err(InstructionError::MaxAccountsDataAllocationsExceeded); + } + Ok(()) + } + + pub fn try_borrow_mut( + &self, + index: IndexOfAccount, + ) -> Result, InstructionError> { + let borrow_counter = self + .borrow_counters + .get(index as usize) + .ok_or(InstructionError::MissingAccount)?; + borrow_counter.try_borrow_mut()?; + + // SAFETY: The borrow counter guarantees this is the only mutable borrow of this account. + // The unwrap is safe because accounts.len() == borrow_counters.len(), so the missing + // account error should have been returned above. + let keyed_account = unsafe { &mut *self.accounts.get(index as usize).unwrap().get() }; + + let account = TransactionAccountViewMut { account: &mut keyed_account.1 }; + + Ok(AccountRefMut { account, borrow_counter }) + } + + pub fn try_borrow(&self, index: IndexOfAccount) -> Result, InstructionError> { + let borrow_counter = self + .borrow_counters + .get(index as usize) + .ok_or(InstructionError::MissingAccount)?; + borrow_counter.try_borrow()?; + + // SAFETY: The borrow counter guarantees there are no mutable borrow of this account. + // The unwrap is safe because accounts.len() == borrow_counters.len(), so the missing + // account error should have been returned above. + let keyed_account = unsafe { &*self.accounts.get(index as usize).unwrap().get() }; + + let account = TransactionAccountView { account: &keyed_account.1 }; + + Ok(AccountRef { account, borrow_counter }) + } + + pub(crate) fn add_lamports_delta(&self, balance: i128) -> Result<(), InstructionError> { + let delta = self.lamports_delta.get(); + self.lamports_delta + .set(delta.checked_add(balance).ok_or(InstructionError::ArithmeticOverflow)?); + Ok(()) + } + + pub(crate) fn get_lamports_delta(&self) -> i128 { + self.lamports_delta.get() + } + + fn deconstruct_into_keyed_account_shared_data(&mut self) -> Vec { + std::mem::take(&mut self.accounts) + .into_iter() + .map(UnsafeCell::into_inner) + .collect() + } + + pub(crate) fn deconstruct_into_account_shared_data(&mut self) -> Vec { + std::mem::take(&mut self.accounts) + .into_iter() + .map(|cell| cell.into_inner().1) + .collect() + } + + pub(crate) fn take(mut self) -> DeconstructedTransactionAccounts { + let shared_data = self.deconstruct_into_keyed_account_shared_data(); + (shared_data, self.touched_flags, self.resize_delta) + } + + pub fn resize_delta(&self) -> i64 { + self.resize_delta.get() + } + + pub(crate) fn account_key(&self, index: IndexOfAccount) -> Option<&Pubkey> { + // SAFETY: We never modify an account key, so returning a reference to it is safe. + unsafe { self.accounts.get(index as usize).map(|acc| &(*acc.get()).0) } + } + + pub(crate) fn account_keys_iter(&self) -> impl Iterator { + // SAFETY: We never modify account keys, so returning an immutable reference to them is safe. + unsafe { self.accounts.iter().map(|item| &(*item.get()).0) } + } +} + +#[derive(Default, Debug, Clone)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +struct BorrowCounter { + counter: Cell, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl BorrowCounter { + #[inline] + fn is_writing(&self) -> bool { + self.counter.get() < 0 + } + + #[inline] + fn is_reading(&self) -> bool { + self.counter.get() > 0 + } + + #[inline] + fn try_borrow(&self) -> Result<(), InstructionError> { + if self.is_writing() { + return Err(InstructionError::AccountBorrowFailed); + } + + if let Some(counter) = self.counter.get().checked_add(1) { + self.counter.set(counter); + return Ok(()); + } + + Err(InstructionError::AccountBorrowFailed) + } + + #[inline] + fn try_borrow_mut(&self) -> Result<(), InstructionError> { + if self.is_writing() || self.is_reading() { + return Err(InstructionError::AccountBorrowFailed); + } + + self.counter.set(self.counter.get().saturating_sub(1)); + + Ok(()) + } + + #[inline] + fn release_borrow(&self) { + self.counter.set(self.counter.get().saturating_sub(1)); + } + + #[inline] + fn release_borrow_mut(&self) { + self.counter.set(self.counter.get().saturating_add(1)); + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct AccountRef<'a> { + account: TransactionAccountView<'a>, + borrow_counter: &'a BorrowCounter, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl Drop for AccountRef<'_> { + fn drop(&mut self) { + self.borrow_counter.release_borrow(); + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl<'a> Deref for AccountRef<'a> { + type Target = TransactionAccountView<'a>; + fn deref(&self) -> &Self::Target { + &self.account + } +} + +#[derive(Debug)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct AccountRefMut<'a> { + account: TransactionAccountViewMut<'a>, + borrow_counter: &'a BorrowCounter, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl Drop for AccountRefMut<'_> { + fn drop(&mut self) { + self.borrow_counter.release_borrow_mut(); + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl<'a> Deref for AccountRefMut<'a> { + type Target = TransactionAccountViewMut<'a>; + fn deref(&self) -> &Self::Target { + &self.account + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl DerefMut for AccountRefMut<'_> { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.account + } +} + +#[cfg(all(test, not(target_arch = "sbf"), not(target_arch = "bpf")))] +mod tests { + use { + crate::transaction_accounts::TransactionAccounts, + solana_account::{ + AccountBuilder, AccountFieldPatch, AccountMode, AccountSharedData, DirtyMarkers, + ReadableAccount, StateFlags, WritableAccount, + }, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + }; + + #[test] + fn test_missing_account() { + let accounts = vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ]; + + let tx_accounts = TransactionAccounts::new(accounts); + + let res = tx_accounts.try_borrow(3); + assert_eq!(res.err(), Some(InstructionError::MissingAccount)); + + let res = tx_accounts.try_borrow_mut(3); + assert_eq!(res.err(), Some(InstructionError::MissingAccount)); + } + + #[test] + fn test_invalid_borrow() { + let accounts = vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ]; + + let tx_accounts = TransactionAccounts::new(accounts); + + // Two immutable borrows are valid + { + let acc_1 = tx_accounts.try_borrow(0); + assert!(acc_1.is_ok()); + + let acc_2 = tx_accounts.try_borrow(1); + assert!(acc_2.is_ok()); + + let acc_1_new = tx_accounts.try_borrow(0); + assert!(acc_1_new.is_ok()); + + assert_eq!(acc_1.unwrap().account, acc_1_new.unwrap().account); + } + + // Two mutable borrows are invalid + { + let acc_1 = tx_accounts.try_borrow_mut(0); + assert!(acc_1.is_ok()); + + let acc_2 = tx_accounts.try_borrow_mut(1); + assert!(acc_2.is_ok()); + + let acc_1_new = tx_accounts.try_borrow_mut(0); + assert_eq!(acc_1_new.err(), Some(InstructionError::AccountBorrowFailed)); + } + + // Mutable after immutable must fail + { + let acc_1 = tx_accounts.try_borrow(0); + assert!(acc_1.is_ok()); + + let acc_2 = tx_accounts.try_borrow(1); + assert!(acc_2.is_ok()); + + let acc_1_new = tx_accounts.try_borrow_mut(0); + assert_eq!(acc_1_new.err(), Some(InstructionError::AccountBorrowFailed)); + } + + // Immutable after mutable must fail + { + let acc_1 = tx_accounts.try_borrow_mut(0); + assert!(acc_1.is_ok()); + + let acc_2 = tx_accounts.try_borrow_mut(1); + assert!(acc_2.is_ok()); + + let acc_1_new = tx_accounts.try_borrow(0); + assert_eq!(acc_1_new.err(), Some(InstructionError::AccountBorrowFailed)); + } + + // Different scopes are good + { + let acc_1 = tx_accounts.try_borrow_mut(0); + assert!(acc_1.is_ok()); + } + + { + let acc_1 = tx_accounts.try_borrow_mut(0); + assert!(acc_1.is_ok()); + } + } + + #[test] + fn too_many_borrows() { + let accounts = vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ( + Pubkey::new_unique(), + AccountSharedData::new(2, 1, &Pubkey::new_unique()), + ), + ]; + + let tx_accounts = TransactionAccounts::new(accounts); + let mut borrows = Vec::new(); + for i in 0..129 { + let acc = tx_accounts.try_borrow(1); + if i < 127 { + assert!(acc.is_ok()); + borrows.push(acc.unwrap()); + } else { + assert_eq!(acc.err(), Some(InstructionError::AccountBorrowFailed)); + } + } + } + + #[test] + fn preserves_account_shared_data_on_deconstruct() { + let key = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let mut account = AccountBuilder::default() + .lamports(23) + .data(vec![1, 2, 3]) + .owner(owner) + .mode(AccountMode::Delegated) + .slot(41) + .executable(true) + .build::(); + + AccountFieldPatch::Mode(AccountMode::Ephemeral).apply(&mut account); + AccountFieldPatch::Slot(42).apply(&mut account); + AccountFieldPatch::Flag { + flag: StateFlags::COMPRESSED, + val: true, + } + .apply(&mut account); + AccountFieldPatch::DataAt { offset: 0, data: vec![4, 5] }.apply(&mut account); + + let expected_markers = account.markers().bits(); + let mut tx_accounts = TransactionAccounts::new(vec![(key, account)]); + let mut accounts = tx_accounts.deconstruct_into_account_shared_data(); + let account = accounts.pop().unwrap(); + + assert!(accounts.is_empty()); + assert!(account.is(AccountMode::Ephemeral)); + assert_eq!(account.slot(), 42); + assert!(account.executable()); + assert!(account.flags().contains(StateFlags::COMPRESSED)); + assert_eq!(account.data(), &[4, 5, 3]); + assert_eq!(account.markers().bits(), expected_markers); + } + + #[test] + fn mutable_view_updates_account_shared_data() { + let key = Pubkey::new_unique(); + let owner = Pubkey::new_unique(); + let new_owner = Pubkey::new_unique(); + let tx_accounts = + TransactionAccounts::new(vec![(key, AccountSharedData::new(7, 2, &owner))]); + + { + let mut account = tx_accounts.try_borrow_mut(0).unwrap(); + account.set_lamports(11); + account.set_owner(new_owner); + account.set_executable(true); + account.resize(4, 9); + assert_eq!(account.data(), &[0, 0, 9, 9]); + account.set_data_from_slice(&[1, 2, 3]); + account.extend_from_slice(&[4, 5]); + account.data_as_mut_slice()[0] = 8; + } + + let mut tx_accounts = tx_accounts; + let mut accounts = tx_accounts.deconstruct_into_account_shared_data(); + let account = accounts.pop().unwrap(); + + assert!(accounts.is_empty()); + assert_eq!(account.lamports(), 11); + assert_eq!(account.owner(), &new_owner); + assert!(account.executable()); + assert_eq!(account.data(), &[8, 2, 3, 4, 5]); + assert!(account.markers().contains(DirtyMarkers::LAMPORTS)); + assert!(account.markers().contains(DirtyMarkers::OWNER)); + assert!(account.markers().contains(DirtyMarkers::FLAGS)); + assert!(account.markers().contains(DirtyMarkers::DATA)); + } +} diff --git a/solana/transaction-context/src/vm_addresses.rs b/solana/transaction-context/src/vm_addresses.rs new file mode 100644 index 0000000..c2bb03f --- /dev/null +++ b/solana/transaction-context/src/vm_addresses.rs @@ -0,0 +1,4 @@ +pub(crate) const GUEST_REGION_SIZE: u64 = 1 << 32; +pub(crate) const RETURN_DATA_SCRATCHPAD: u64 = 7 * GUEST_REGION_SIZE; +pub(crate) const GUEST_INSTRUCTION_DATA_BASE_ADDRESS: u64 = 264 * GUEST_REGION_SIZE; +pub(crate) const GUEST_INSTRUCTION_ACCOUNT_BASE_ADDRESS: u64 = 328 * GUEST_REGION_SIZE; diff --git a/solana/transaction-context/src/vm_slice.rs b/solana/transaction-context/src/vm_slice.rs new file mode 100644 index 0000000..a98566c --- /dev/null +++ b/solana/transaction-context/src/vm_slice.rs @@ -0,0 +1,54 @@ +// The VmSlice class is used for cases when you need a slice that is stored in the BPF +// interpreter's virtual address space. Because this source code can be compiled with +// addresses of different bit depths, we cannot assume that the 64-bit BPF interpreter's +// pointer sizes can be mapped to physical pointer sizes. In particular, if you need a +// slice-of-slices in the virtual space, the inner slices will be different sizes in a +// 32-bit app build than in the 64-bit virtual space. Therefore instead of a slice-of-slices, +// you should implement a slice-of-VmSlices, which can then use VmSlice::translate() to +// map to the physical address. +// This class must consist only of 16 bytes: a u64 ptr and a u64 len, to match the 64-bit +// implementation of a slice in Rust. The PhantomData entry takes up 0 bytes. + +use std::marker::PhantomData; + +#[repr(C)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub struct VmSlice { + ptr: u64, + len: u64, + resource_type: PhantomData, +} + +impl VmSlice { + pub fn new(ptr: u64, len: u64) -> Self { + VmSlice { + ptr, + len, + resource_type: PhantomData, + } + } + + pub fn ptr(&self) -> u64 { + self.ptr + } + + pub fn len(&self) -> u64 { + self.len + } + + pub fn is_empty(&self) -> bool { + self.len == 0 + } + + pub fn end(&self) -> u64 { + self.ptr().saturating_add(self.len().saturating_mul(size_of::() as u64)) + } + + /// # Safety + /// Set a new length for the mapped area. + /// This function is not safe to use if not coupled with the respective change in + /// the underlying vector. + pub unsafe fn set_len(&mut self, new_len: u64) { + self.len = new_len; + } +}