diff --git a/solana/account/Cargo.toml b/solana/account/Cargo.toml new file mode 100644 index 0000000..4e0c0cd --- /dev/null +++ b/solana/account/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "solana-account" +description = "Solana Account type" +documentation = "https://docs.rs/solana-account" +version = "4.3.1" +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + +[features] +bincode = ["dep:bincode", "dep:solana-sysvar", "serde"] +wincode = ["dep:wincode", "solana-pubkey/wincode"] +dev-context-only-utils = ["bincode", "dep:qualifier_attr"] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-pubkey/frozen-abi", +] +serde = ["dep:serde", "dep:serde_bytes", "dep:serde_derive", "solana-pubkey/serde"] + +[dependencies] +bincode = { workspace = true, optional = true } +qualifier_attr = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +serde_bytes = { workspace = true, optional = true } +serde_derive = { workspace = true, optional = true } +solana-account-info = { workspace = true } +solana-clock = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = ["frozen-abi"] } +solana-frozen-abi-macro = { workspace = true, optional = true } +solana-instruction-error = { workspace = true } +solana-pubkey = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-sysvar = { workspace = true, features = ["bincode"], optional = true } +thiserror = { workspace = true } +wincode = { workspace = true, features = ["alloc"], optional = true } + +[dev-dependencies] +solana-account = { path = ".", features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["std"] } diff --git a/solana/account/src/lib.rs b/solana/account/src/lib.rs new file mode 100644 index 0000000..4c0ea4c --- /dev/null +++ b/solana/account/src/lib.rs @@ -0,0 +1,1111 @@ +#![cfg_attr(feature = "frozen-abi", feature(min_specialization))] +#![cfg_attr(docsrs, feature(doc_cfg))] +//! The Solana [`Account`] type. + +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; +#[cfg(feature = "serde")] +use serde::ser::{Serialize, Serializer}; +#[cfg(feature = "frozen-abi")] +use solana_frozen_abi_macro::{frozen_abi, AbiExample, StableAbi, StableAbiSample}; +#[cfg(feature = "bincode")] +use solana_sysvar::SysvarSerialize; +use { + solana_account_info::{debug_account_data::*, AccountInfo}, + solana_clock::{Epoch, INITIAL_RENT_EPOCH}, + solana_instruction_error::LamportsError, + solana_pubkey::Pubkey, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, + std::{cell::RefCell, fmt, mem::MaybeUninit, ops::Deref, ptr, rc::Rc, sync::Arc}, +}; +#[cfg(feature = "bincode")] +pub mod state_traits; + +/// An Account with data that is stored on chain +#[repr(C)] +#[cfg_attr( + feature = "frozen-abi", + derive(AbiExample, StableAbi, StableAbiSample), + frozen_abi( + api_digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME", + abi_digest = "G4phLpfhujMpk4wS1WswCe4HqnQjCBPWjrXjvDZ6iUw8" + ) +)] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize), + serde(rename_all = "camelCase") +)] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +#[derive(PartialEq, Eq, Clone, Default)] +pub struct Account { + /// lamports in the account + pub lamports: u64, + /// data held in this account + #[cfg_attr(feature = "serde", serde(with = "serde_bytes"))] + #[cfg_attr( + feature = "frozen-abi", + stable_abi_sample( + with = "(0..rng.random_range(0..=1000)).map(|_| rng.random()).collect()" + ) + )] + pub data: Vec, + /// the program that owns this account. If executable, the program that loads this account. + pub owner: Pubkey, + /// this account's data contains a loaded program (and is now read-only) + pub executable: bool, + /// the epoch at which this account will next owe rent + pub rent_epoch: Epoch, +} + +// mod because we need 'Account' below to have the name 'Account' to match expected serialization +#[cfg(feature = "serde")] +mod account_serialize { + #[cfg(feature = "frozen-abi")] + use solana_frozen_abi_macro::{frozen_abi, AbiExample}; + use { + crate::ReadableAccount, + serde::{ser::Serializer, Serialize}, + solana_clock::Epoch, + solana_pubkey::Pubkey, + }; + #[repr(C)] + #[cfg_attr( + feature = "frozen-abi", + derive(AbiExample), + frozen_abi(digest = "62EqVoynUFvuui7DVfqWCvZP7bxKGJGioeSBnWrdjRME") + )] + #[derive(serde_derive::Serialize)] + #[serde(rename_all = "camelCase")] + struct Account<'a> { + lamports: u64, + #[serde(with = "serde_bytes")] + // a slice so we don't have to make a copy just to serialize this + data: &'a [u8], + owner: &'a Pubkey, + executable: bool, + rent_epoch: Epoch, + } + + /// allows us to implement serialize on AccountSharedData that is equivalent to Account::serialize without making a copy of the Vec + pub fn serialize_account( + account: &impl ReadableAccount, + serializer: S, + ) -> Result + where + S: Serializer, + { + let temp = Account { + lamports: account.lamports(), + data: account.data(), + owner: account.owner(), + executable: account.executable(), + rent_epoch: account.rent_epoch(), + }; + temp.serialize(serializer) + } +} + +#[cfg(feature = "serde")] +impl Serialize for Account { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + crate::account_serialize::serialize_account(self, serializer) + } +} + +#[cfg(feature = "serde")] +impl Serialize for AccountSharedData { + fn serialize(&self, serializer: S) -> Result + where + S: Serializer, + { + crate::account_serialize::serialize_account(self, serializer) + } +} + +/// An Account with data that is stored on chain +/// This will be the in-memory representation of the 'Account' struct data. +/// The existing 'Account' structure cannot easily change due to downstream projects. +#[cfg_attr(feature = "frozen-abi", derive(AbiExample, StableAbi, StableAbiSample))] +#[cfg_attr( + feature = "serde", + derive(serde_derive::Deserialize), + serde(from = "Account") +)] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +#[derive(PartialEq, Eq, Clone, Default)] +pub struct AccountSharedData { + /// lamports in the account + lamports: u64, + /// data held in this account + data: Arc>, + /// the program that owns this account. If executable, the program that loads this account. + owner: Pubkey, + /// this account's data contains a loaded program (and is now read-only) + executable: bool, + /// the epoch at which this account will next owe rent + rent_epoch: Epoch, +} + +/// Compares two ReadableAccounts +/// +/// Returns true if accounts are essentially equivalent as in all fields are equivalent. +pub fn accounts_equal(me: &T, other: &U) -> bool { + me.lamports() == other.lamports() + && me.executable() == other.executable() + && me.rent_epoch() == other.rent_epoch() + && me.owner() == other.owner() + && me.data() == other.data() +} + +impl From for Account { + fn from(mut other: AccountSharedData) -> Self { + let account_data = Arc::make_mut(&mut other.data); + Self { + lamports: other.lamports, + data: std::mem::take(account_data), + owner: other.owner, + executable: other.executable, + rent_epoch: other.rent_epoch, + } + } +} + +impl From for AccountSharedData { + fn from(other: Account) -> Self { + Self { + lamports: other.lamports, + data: Arc::new(other.data), + owner: other.owner, + executable: other.executable, + rent_epoch: other.rent_epoch, + } + } +} + +pub trait WritableAccount: ReadableAccount { + fn set_lamports(&mut self, lamports: u64); + fn checked_add_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports() + .checked_add(lamports) + .ok_or(LamportsError::ArithmeticOverflow)?, + ); + Ok(()) + } + fn checked_sub_lamports(&mut self, lamports: u64) -> Result<(), LamportsError> { + self.set_lamports( + self.lamports() + .checked_sub(lamports) + .ok_or(LamportsError::ArithmeticUnderflow)?, + ); + Ok(()) + } + fn saturating_add_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_add(lamports)) + } + fn saturating_sub_lamports(&mut self, lamports: u64) { + self.set_lamports(self.lamports().saturating_sub(lamports)) + } + fn data_as_mut_slice(&mut self) -> &mut [u8]; + fn set_owner(&mut self, owner: Pubkey); + fn copy_into_owner_from_slice(&mut self, source: &[u8]); + fn set_executable(&mut self, executable: bool); + fn set_rent_epoch(&mut self, epoch: Epoch); +} + +pub trait ReadableAccount: Sized { + fn lamports(&self) -> u64; + fn data(&self) -> &[u8]; + fn owner(&self) -> &Pubkey; + fn executable(&self) -> bool; + fn rent_epoch(&self) -> Epoch; +} + +impl ReadableAccount for T +where + T: Deref, + T::Target: ReadableAccount, +{ + fn lamports(&self) -> u64 { + self.deref().lamports() + } + fn data(&self) -> &[u8] { + self.deref().data() + } + fn owner(&self) -> &Pubkey { + self.deref().owner() + } + fn executable(&self) -> bool { + self.deref().executable() + } + fn rent_epoch(&self) -> Epoch { + self.deref().rent_epoch() + } +} + +impl ReadableAccount for Account { + fn lamports(&self) -> u64 { + self.lamports + } + fn data(&self) -> &[u8] { + &self.data + } + fn owner(&self) -> &Pubkey { + &self.owner + } + fn executable(&self) -> bool { + self.executable + } + fn rent_epoch(&self) -> Epoch { + self.rent_epoch + } +} + +impl WritableAccount for Account { + fn set_lamports(&mut self, lamports: u64) { + self.lamports = lamports; + } + fn data_as_mut_slice(&mut self) -> &mut [u8] { + &mut self.data + } + fn set_owner(&mut self, owner: Pubkey) { + self.owner = owner; + } + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.owner.as_mut().copy_from_slice(source); + } + fn set_executable(&mut self, executable: bool) { + self.executable = executable; + } + fn set_rent_epoch(&mut self, epoch: Epoch) { + self.rent_epoch = epoch; + } +} + +impl WritableAccount for AccountSharedData { + fn set_lamports(&mut self, lamports: u64) { + self.lamports = lamports; + } + fn data_as_mut_slice(&mut self) -> &mut [u8] { + &mut self.data_mut()[..] + } + fn set_owner(&mut self, owner: Pubkey) { + self.owner = owner; + } + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.owner.as_mut().copy_from_slice(source); + } + fn set_executable(&mut self, executable: bool) { + self.executable = executable; + } + fn set_rent_epoch(&mut self, epoch: Epoch) { + self.rent_epoch = epoch; + } +} + +impl ReadableAccount for AccountSharedData { + fn lamports(&self) -> u64 { + self.lamports + } + fn data(&self) -> &[u8] { + &self.data + } + fn owner(&self) -> &Pubkey { + &self.owner + } + fn executable(&self) -> bool { + self.executable + } + fn rent_epoch(&self) -> Epoch { + self.rent_epoch + } +} + +fn debug_fmt(item: &T, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut f = f.debug_struct("Account"); + + f.field("lamports", &item.lamports()) + .field("data.len", &item.data().len()) + .field("owner", &item.owner()) + .field("executable", &item.executable()) + .field("rent_epoch", &item.rent_epoch()); + debug_account_data(item.data(), &mut f); + + f.finish() +} + +impl fmt::Debug for Account { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f) + } +} + +impl fmt::Debug for AccountSharedData { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + debug_fmt(self, f) + } +} + +#[cfg(feature = "bincode")] +fn shared_deserialize_data( + account: &U, +) -> Result { + bincode::deserialize(account.data()) +} + +#[cfg(feature = "bincode")] +fn shared_serialize_data( + account: &mut U, + state: &T, +) -> Result<(), bincode::Error> { + if bincode::serialized_size(state)? > account.data().len() as u64 { + return Err(Box::new(bincode::ErrorKind::SizeLimit)); + } + bincode::serialize_into(account.data_as_mut_slice(), state) +} + +impl Account { + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + Account { + lamports, + data: vec![0; space], + owner: *owner, + executable: false, + rent_epoch: Epoch::default(), + } + } + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(Account::new(lamports, space, owner))) + } + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Account { + lamports, + data, + owner: *owner, + executable: false, + rent_epoch: Epoch::default(), + }) + } + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Account::new_data(lamports, state, owner).map(RefCell::new) + } + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = Account::new(lamports, space, owner); + shared_serialize_data(&mut account, state)?; + Ok(account) + } + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + Account::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + Account { + lamports, + data: vec![0; space], + owner: *owner, + executable: false, + rent_epoch, + } + } + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + shared_deserialize_data(self) + } + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + shared_serialize_data(self, state) + } +} + +impl AccountSharedData { + pub fn is_shared(&self) -> bool { + Arc::strong_count(&self.data) > 1 + } + + pub fn reserve(&mut self, additional: usize) { + if let Some(data) = Arc::get_mut(&mut self.data) { + data.reserve(additional) + } else { + let mut data = Vec::with_capacity(self.data.len().saturating_add(additional)); + data.extend_from_slice(&self.data); + self.data = Arc::new(data); + } + } + + pub fn capacity(&self) -> usize { + self.data.capacity() + } + + pub fn data_clone(&self) -> Arc> { + Arc::clone(&self.data) + } + + fn data_mut(&mut self) -> &mut Vec { + Arc::make_mut(&mut self.data) + } + + pub fn resize(&mut self, new_len: usize, value: u8) { + self.data_mut().resize(new_len, value) + } + + pub fn extend_from_slice(&mut self, data: &[u8]) { + self.data_mut().extend_from_slice(data) + } + + pub fn set_data_from_slice(&mut self, new_data: &[u8]) { + // If the buffer isn't shared, we're going to memcpy in place. + let Some(data) = Arc::get_mut(&mut self.data) else { + // If the buffer is shared, the cheapest thing to do is to clone the + // incoming slice and replace the buffer. + return self.set_data(new_data.to_vec()); + }; + + let new_len = new_data.len(); + + // Reserve additional capacity if needed. Here we make the assumption + // that growing the current buffer is cheaper than doing a whole new + // allocation to make `new_data` owned. + // + // This assumption holds true during CPI, especially when the account + // size doesn't change but the account is only changed in place. And + // it's also true when the account is grown by a small margin (the + // realloc limit is quite low), in which case the allocator can just + // update the allocation metadata without moving. + // + // Shrinking and copying in place is always faster than making + // `new_data` owned, since shrinking boils down to updating the Vec's + // length. + + data.reserve(new_len.saturating_sub(data.len())); + + // Safety: + // We just reserved enough capacity. We set data::len to 0 to avoid + // possible UB on panic (dropping uninitialized elements), do the copy, + // finally set the new length once everything is initialized. + #[allow(clippy::uninit_vec)] + // this is a false positive, the lint doesn't currently special case set_len(0) + unsafe { + data.set_len(0); + ptr::copy_nonoverlapping(new_data.as_ptr(), data.as_mut_ptr(), new_len); + data.set_len(new_len); + }; + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + fn set_data(&mut self, data: Vec) { + self.data = Arc::new(data); + } + + pub fn spare_data_capacity_mut(&mut self) -> &mut [MaybeUninit] { + self.data_mut().spare_capacity_mut() + } + + pub fn new(lamports: u64, space: usize, owner: &Pubkey) -> Self { + AccountSharedData { + lamports, + data: Arc::new(vec![0u8; space]), + owner: *owner, + executable: false, + rent_epoch: Epoch::default(), + } + } + pub fn new_ref(lamports: u64, space: usize, owner: &Pubkey) -> Rc> { + Rc::new(RefCell::new(AccountSharedData::new(lamports, space, owner))) + } + #[cfg(feature = "bincode")] + pub fn new_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result { + let data = bincode::serialize(state)?; + Ok(Self::create_from_existing_shared_data( + lamports, + Arc::new(data), + *owner, + false, + Epoch::default(), + )) + } + #[cfg(feature = "bincode")] + pub fn new_ref_data( + lamports: u64, + state: &T, + owner: &Pubkey, + ) -> Result, bincode::Error> { + AccountSharedData::new_data(lamports, state, owner).map(RefCell::new) + } + #[cfg(feature = "bincode")] + pub fn new_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result { + let mut account = AccountSharedData::new(lamports, space, owner); + shared_serialize_data(&mut account, state)?; + Ok(account) + } + #[cfg(feature = "bincode")] + pub fn new_ref_data_with_space( + lamports: u64, + state: &T, + space: usize, + owner: &Pubkey, + ) -> Result, bincode::Error> { + AccountSharedData::new_data_with_space(lamports, state, space, owner).map(RefCell::new) + } + pub fn new_rent_epoch(lamports: u64, space: usize, owner: &Pubkey, rent_epoch: Epoch) -> Self { + AccountSharedData { + lamports, + data: Arc::new(vec![0; space]), + owner: *owner, + executable: false, + rent_epoch, + } + } + #[cfg(feature = "bincode")] + pub fn deserialize_data(&self) -> Result { + shared_deserialize_data(self) + } + #[cfg(feature = "bincode")] + pub fn serialize_data(&mut self, state: &T) -> Result<(), bincode::Error> { + shared_serialize_data(self, state) + } + + pub fn create_from_existing_shared_data( + lamports: u64, + data: Arc>, + owner: Pubkey, + executable: bool, + rent_epoch: Epoch, + ) -> AccountSharedData { + AccountSharedData { + lamports, + data, + owner, + executable, + rent_epoch, + } + } +} + +pub type InheritableAccountFields = (u64, Epoch); +pub const DUMMY_INHERITABLE_ACCOUNT_FIELDS: InheritableAccountFields = (1, INITIAL_RENT_EPOCH); + +#[cfg(feature = "bincode")] +pub fn create_account_with_fields( + sysvar: &S, + (lamports, rent_epoch): InheritableAccountFields, +) -> Account { + let data_len = S::size_of().max(bincode::serialized_size(sysvar).unwrap() as usize); + let mut account = Account::new(lamports, data_len, &solana_sdk_ids::sysvar::id()); + to_account::(sysvar, &mut account).unwrap(); + account.rent_epoch = rent_epoch; + account +} + +#[cfg(feature = "bincode")] +pub fn create_account_for_test(sysvar: &S) -> Account { + create_account_with_fields(sysvar, DUMMY_INHERITABLE_ACCOUNT_FIELDS) +} + +#[cfg(feature = "bincode")] +/// Create an `Account` from a `Sysvar`. +pub fn create_account_shared_data_with_fields( + sysvar: &S, + fields: InheritableAccountFields, +) -> AccountSharedData { + AccountSharedData::from(create_account_with_fields(sysvar, fields)) +} + +#[cfg(feature = "bincode")] +pub fn create_account_shared_data_for_test(sysvar: &S) -> AccountSharedData { + AccountSharedData::from(create_account_with_fields( + sysvar, + DUMMY_INHERITABLE_ACCOUNT_FIELDS, + )) +} + +#[cfg(feature = "bincode")] +/// Create a `Sysvar` from an `Account`'s data. +pub fn from_account(account: &T) -> Option { + bincode::deserialize(account.data()).ok() +} + +#[cfg(feature = "bincode")] +/// Serialize a `Sysvar` into an `Account`'s data. +pub fn to_account( + sysvar: &S, + account: &mut T, +) -> Option<()> { + bincode::serialize_into(account.data_as_mut_slice(), sysvar).ok() +} + +/// Return the information required to construct an `AccountInfo`. Used by the +/// `AccountInfo` conversion implementations. +impl solana_account_info::Account for Account { + fn get(&mut self) -> (&mut u64, &mut [u8], &Pubkey, bool) { + ( + &mut self.lamports, + &mut self.data, + &self.owner, + self.executable, + ) + } +} + +/// Create `AccountInfo`s +pub fn create_is_signer_account_infos<'a>( + accounts: &'a mut [(&'a Pubkey, bool, &'a mut Account)], +) -> Vec> { + accounts + .iter_mut() + .map(|(key, is_signer, account)| { + AccountInfo::new( + key, + *is_signer, + false, + &mut account.lamports, + &mut account.data, + &account.owner, + account.executable, + ) + }) + .collect() +} + +/// Replacement for the executable flag: An account being owned by one of these contains a program. +#[deprecated(since = "4.3.0", note = "no longer available as a constant")] +pub const PROGRAM_OWNERS: &[Pubkey] = &[ + bpf_loader_upgradeable::id(), + bpf_loader::id(), + bpf_loader_deprecated::id(), + loader_v4::id(), +]; + +#[cfg(test)] +pub mod tests { + use super::*; + + fn make_two_accounts(key: &Pubkey) -> (Account, AccountSharedData) { + let mut account1 = Account::new(1, 2, key); + account1.executable = true; + account1.rent_epoch = 4; + let mut account2 = AccountSharedData::new(1, 2, key); + account2.executable = true; + account2.rent_epoch = 4; + assert!(accounts_equal(&account1, &account2)); + (account1, account2) + } + + #[test] + fn test_account_data_copy_as_slice() { + let key = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let (mut account1, mut account2) = make_two_accounts(&key); + account1.copy_into_owner_from_slice(key2.as_ref()); + account2.copy_into_owner_from_slice(key2.as_ref()); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.owner(), &key2); + } + + #[test] + fn test_account_set_data_from_slice() { + let key = Pubkey::new_unique(); + let (_, mut account) = make_two_accounts(&key); + assert_eq!(account.data(), &vec![0, 0]); + account.set_data_from_slice(&[1, 2]); + assert_eq!(account.data(), &vec![1, 2]); + account.set_data_from_slice(&[1, 2, 3]); + assert_eq!(account.data(), &vec![1, 2, 3]); + account.set_data_from_slice(&[4, 5, 6]); + assert_eq!(account.data(), &vec![4, 5, 6]); + account.set_data_from_slice(&[4, 5, 6, 0]); + assert_eq!(account.data(), &vec![4, 5, 6, 0]); + account.set_data_from_slice(&[]); + assert_eq!(account.data().len(), 0); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &vec![44]); + account.set_data_from_slice(&[44]); + assert_eq!(account.data(), &vec![44]); + } + + #[test] + fn test_account_data_set_data() { + let key = Pubkey::new_unique(); + let (_, mut account) = make_two_accounts(&key); + assert_eq!(account.data(), &vec![0, 0]); + account.set_data(vec![1, 2]); + assert_eq!(account.data(), &vec![1, 2]); + account.set_data(vec![]); + assert_eq!(account.data().len(), 0); + } + + #[test] + #[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: Io(Kind(UnexpectedEof))" + )] + fn test_account_deserialize() { + let key = Pubkey::new_unique(); + let (account1, _account2) = make_two_accounts(&key); + account1.deserialize_data::().unwrap(); + } + + #[test] + #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: SizeLimit")] + fn test_account_serialize() { + let key = Pubkey::new_unique(); + let (mut account1, _account2) = make_two_accounts(&key); + account1.serialize_data(&"hello world").unwrap(); + } + + #[test] + #[should_panic( + expected = "called `Result::unwrap()` on an `Err` value: Io(Kind(UnexpectedEof))" + )] + fn test_account_shared_data_deserialize() { + let key = Pubkey::new_unique(); + let (_account1, account2) = make_two_accounts(&key); + account2.deserialize_data::().unwrap(); + } + + #[test] + #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: SizeLimit")] + fn test_account_shared_data_serialize() { + let key = Pubkey::new_unique(); + let (_account1, mut account2) = make_two_accounts(&key); + account2.serialize_data(&"hello world").unwrap(); + } + + #[test] + fn test_account_shared_data() { + let key = Pubkey::new_unique(); + let (account1, account2) = make_two_accounts(&key); + assert!(accounts_equal(&account1, &account2)); + let account = account1; + assert_eq!(account.lamports, 1); + assert_eq!(account.lamports(), 1); + assert_eq!(account.data.len(), 2); + assert_eq!(account.data().len(), 2); + assert_eq!(account.owner, key); + assert_eq!(account.owner(), &key); + assert!(account.executable); + assert!(account.executable()); + assert_eq!(account.rent_epoch, 4); + assert_eq!(account.rent_epoch(), 4); + let account = account2; + assert_eq!(account.lamports, 1); + assert_eq!(account.lamports(), 1); + assert_eq!(account.data.len(), 2); + assert_eq!(account.data().len(), 2); + assert_eq!(account.owner, key); + assert_eq!(account.owner(), &key); + assert!(account.executable); + assert!(account.executable()); + assert_eq!(account.rent_epoch, 4); + assert_eq!(account.rent_epoch(), 4); + } + + // test clone and from for both types against expected + fn test_equal( + should_be_equal: bool, + account1: &Account, + account2: &AccountSharedData, + account_expected: &Account, + ) { + assert_eq!(should_be_equal, accounts_equal(account1, account2)); + if should_be_equal { + assert!(accounts_equal(account_expected, account2)); + } + assert_eq!( + accounts_equal(account_expected, account1), + accounts_equal(account_expected, &account1.clone()) + ); + assert_eq!( + accounts_equal(account_expected, account2), + accounts_equal(account_expected, &account2.clone()) + ); + assert_eq!( + accounts_equal(account_expected, account1), + accounts_equal(account_expected, &AccountSharedData::from(account1.clone())) + ); + assert_eq!( + accounts_equal(account_expected, account2), + accounts_equal(account_expected, &Account::from(account2.clone())) + ); + } + + #[test] + fn test_account_add_sub_lamports() { + let key = Pubkey::new_unique(); + let (mut account1, mut account2) = make_two_accounts(&key); + assert!(accounts_equal(&account1, &account2)); + account1.checked_add_lamports(1).unwrap(); + account2.checked_add_lamports(1).unwrap(); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 2); + account1.checked_sub_lamports(2).unwrap(); + account2.checked_sub_lamports(2).unwrap(); + assert!(accounts_equal(&account1, &account2)); + assert_eq!(account1.lamports(), 0); + } + + #[test] + #[should_panic(expected = "Overflow")] + fn test_account_checked_add_lamports_overflow() { + let key = Pubkey::new_unique(); + let (mut account1, _account2) = make_two_accounts(&key); + account1.checked_add_lamports(u64::MAX).unwrap(); + } + + #[test] + #[should_panic(expected = "Underflow")] + fn test_account_checked_sub_lamports_underflow() { + let key = Pubkey::new_unique(); + let (mut account1, _account2) = make_two_accounts(&key); + account1.checked_sub_lamports(u64::MAX).unwrap(); + } + + #[test] + #[should_panic(expected = "Overflow")] + fn test_account_checked_add_lamports_overflow2() { + let key = Pubkey::new_unique(); + let (_account1, mut account2) = make_two_accounts(&key); + account2.checked_add_lamports(u64::MAX).unwrap(); + } + + #[test] + #[should_panic(expected = "Underflow")] + fn test_account_checked_sub_lamports_underflow2() { + let key = Pubkey::new_unique(); + let (_account1, mut account2) = make_two_accounts(&key); + account2.checked_sub_lamports(u64::MAX).unwrap(); + } + + #[test] + fn test_account_saturating_add_lamports() { + let key = Pubkey::new_unique(); + let (mut account, _) = make_two_accounts(&key); + + let remaining = 22; + account.set_lamports(u64::MAX - remaining); + account.saturating_add_lamports(remaining * 2); + assert_eq!(account.lamports(), u64::MAX); + } + + #[test] + fn test_account_saturating_sub_lamports() { + let key = Pubkey::new_unique(); + let (mut account, _) = make_two_accounts(&key); + + let remaining = 33; + account.set_lamports(remaining); + account.saturating_sub_lamports(remaining * 2); + assert_eq!(account.lamports(), 0); + } + + #[test] + fn test_account_shared_data_all_fields() { + let key = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let key3 = Pubkey::new_unique(); + let (mut account1, mut account2) = make_two_accounts(&key); + assert!(accounts_equal(&account1, &account2)); + + let mut account_expected = account1.clone(); + assert!(accounts_equal(&account1, &account_expected)); + assert!(accounts_equal(&account1, &account2.clone())); // test the clone here + + for field_index in 0..5 { + for pass in 0..4 { + if field_index == 0 { + if pass == 0 { + account1.checked_add_lamports(1).unwrap(); + } else if pass == 1 { + account_expected.checked_add_lamports(1).unwrap(); + account2.set_lamports(account2.lamports + 1); + } else if pass == 2 { + account1.set_lamports(account1.lamports + 1); + } else if pass == 3 { + account_expected.checked_add_lamports(1).unwrap(); + account2.checked_add_lamports(1).unwrap(); + } + } else if field_index == 1 { + if pass == 0 { + account1.data[0] += 1; + } else if pass == 1 { + account_expected.data[0] += 1; + account2.data_as_mut_slice()[0] = account2.data[0] + 1; + } else if pass == 2 { + account1.data_as_mut_slice()[0] = account1.data[0] + 1; + } else if pass == 3 { + account_expected.data[0] += 1; + account2.data_as_mut_slice()[0] += 1; + } + } else if field_index == 2 { + if pass == 0 { + account1.owner = key2; + } else if pass == 1 { + account_expected.owner = key2; + account2.set_owner(key2); + } else if pass == 2 { + account1.set_owner(key3); + } else if pass == 3 { + account_expected.owner = key3; + account2.owner = key3; + } + } else if field_index == 3 { + if pass == 0 { + account1.executable = !account1.executable; + } else if pass == 1 { + account_expected.executable = !account_expected.executable; + account2.set_executable(!account2.executable); + } else if pass == 2 { + account1.set_executable(!account1.executable); + } else if pass == 3 { + account_expected.executable = !account_expected.executable; + account2.executable = !account2.executable; + } + } else if field_index == 4 { + if pass == 0 { + account1.rent_epoch += 1; + } else if pass == 1 { + account_expected.rent_epoch += 1; + account2.set_rent_epoch(account2.rent_epoch + 1); + } else if pass == 2 { + account1.set_rent_epoch(account1.rent_epoch + 1); + } else if pass == 3 { + account_expected.rent_epoch += 1; + account2.rent_epoch += 1; + } + } + + let should_be_equal = pass == 1 || pass == 3; + test_equal(should_be_equal, &account1, &account2, &account_expected); + + // test new_ref + if should_be_equal { + assert!(accounts_equal( + &Account::new_ref( + account_expected.lamports(), + account_expected.data().len(), + account_expected.owner() + ) + .borrow(), + &AccountSharedData::new_ref( + account_expected.lamports(), + account_expected.data().len(), + account_expected.owner() + ) + .borrow() + )); + + { + // test new_data + let account1_with_data = Account::new_data( + account_expected.lamports(), + &account_expected.data()[0], + account_expected.owner(), + ) + .unwrap(); + let account2_with_data = AccountSharedData::new_data( + account_expected.lamports(), + &account_expected.data()[0], + account_expected.owner(), + ) + .unwrap(); + + assert!(accounts_equal(&account1_with_data, &account2_with_data)); + assert_eq!( + account1_with_data.deserialize_data::().unwrap(), + account2_with_data.deserialize_data::().unwrap() + ); + } + + // test new_data_with_space + assert!(accounts_equal( + &Account::new_data_with_space( + account_expected.lamports(), + &account_expected.data()[0], + 1, + account_expected.owner() + ) + .unwrap(), + &AccountSharedData::new_data_with_space( + account_expected.lamports(), + &account_expected.data()[0], + 1, + account_expected.owner() + ) + .unwrap() + )); + + // test new_ref_data + assert!(accounts_equal( + &Account::new_ref_data( + account_expected.lamports(), + &account_expected.data()[0], + account_expected.owner() + ) + .unwrap() + .borrow(), + &AccountSharedData::new_ref_data( + account_expected.lamports(), + &account_expected.data()[0], + account_expected.owner() + ) + .unwrap() + .borrow() + )); + + //new_ref_data_with_space + assert!(accounts_equal( + &Account::new_ref_data_with_space( + account_expected.lamports(), + &account_expected.data()[0], + 1, + account_expected.owner() + ) + .unwrap() + .borrow(), + &AccountSharedData::new_ref_data_with_space( + account_expected.lamports(), + &account_expected.data()[0], + 1, + account_expected.owner() + ) + .unwrap() + .borrow() + )); + } + } + } + } +} diff --git a/solana/account/src/state_traits.rs b/solana/account/src/state_traits.rs new file mode 100644 index 0000000..a7852a9 --- /dev/null +++ b/solana/account/src/state_traits.rs @@ -0,0 +1,83 @@ +//! Useful extras for `Account` state. + +use { + crate::{Account, AccountSharedData}, + bincode::ErrorKind, + solana_instruction_error::InstructionError, + std::cell::Ref, +}; + +/// Convenience trait to covert bincode errors to instruction errors. +pub trait StateMut { + fn state(&self) -> Result; + fn set_state(&mut self, state: &T) -> Result<(), InstructionError>; +} +pub trait State { + fn state(&self) -> Result; + fn set_state(&self, state: &T) -> Result<(), InstructionError>; +} + +impl StateMut for Account +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + self.deserialize_data() + .map_err(|_| InstructionError::InvalidAccountData) + } + fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { + self.serialize_data(state).map_err(|err| match *err { + ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) + } +} + +impl StateMut for AccountSharedData +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + self.deserialize_data() + .map_err(|_| InstructionError::InvalidAccountData) + } + fn set_state(&mut self, state: &T) -> Result<(), InstructionError> { + self.serialize_data(state).map_err(|err| match *err { + ErrorKind::SizeLimit => InstructionError::AccountDataTooSmall, + _ => InstructionError::GenericError, + }) + } +} + +impl StateMut for Ref<'_, AccountSharedData> +where + T: serde::Serialize + serde::de::DeserializeOwned, +{ + fn state(&self) -> Result { + self.deserialize_data() + .map_err(|_| InstructionError::InvalidAccountData) + } + fn set_state(&mut self, _state: &T) -> Result<(), InstructionError> { + panic!("illegal"); + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_pubkey::Pubkey}; + + #[test] + fn test_account_state() { + let state = 42u64; + + assert!(AccountSharedData::default().set_state(&state).is_err()); + let res = AccountSharedData::default().state() as Result; + assert!(res.is_err()); + + let mut account = AccountSharedData::new(0, std::mem::size_of::(), &Pubkey::default()); + + assert!(account.set_state(&state).is_ok()); + let stored_state: u64 = account.state().unwrap(); + assert_eq!(stored_state, state); + } +} diff --git a/solana/program-runtime/Cargo.toml b/solana/program-runtime/Cargo.toml new file mode 100644 index 0000000..a2670bc --- /dev/null +++ b/solana/program-runtime/Cargo.toml @@ -0,0 +1,94 @@ +[package] +name = "solana-program-runtime" +description = "Solana program runtime" +documentation = "https://docs.rs/solana-program-runtime" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["lib"] +name = "solana_program_runtime" + +[features] +agave-unstable-api = [] +conf-stack-frame-size = ["solana-sbpf/conf-stack-frame-size"] +dev-context-only-utils = ["dep:solana-message"] +dummy-for-ci-check = ["metrics"] +# Compatibility stub: this fork does not derive or consume frozen ABI metadata. +frozen-abi = [] +metrics = [] +sbpf-debugger = ["solana-sbpf/debugger"] +shuttle-test = ["solana-sbpf/shuttle-test", "solana-svm-type-overrides/shuttle-test"] +svm-internal = ["dep:qualifier_attr"] + +[dependencies] +base64 = { workspace = true } +bincode = { workspace = true } +cfg-if = { workspace = true } +itertools = { workspace = true } +percentage = { workspace = true } +qualifier_attr = { workspace = true, optional = true } +serde = { workspace = true } +solana-account = { workspace = true, features = ["bincode"] } +solana-account-info = { workspace = true } +solana-clock = { workspace = true } +solana-epoch-rewards = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-fee-structure = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-hash = { workspace = true } +solana-instruction = { workspace = true } +solana-last-restart-slot = { workspace = true } +solana-loader-v3-interface = { workspace = true } +solana-message = { workspace = true, optional = true } +solana-program-entrypoint = { workspace = true } +solana-pubkey = { workspace = true } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true, features = ["jit"] } +solana-sdk-ids = { workspace = true } +solana-slot-hashes = { workspace = true } +solana-stable-layout = { workspace = true } +solana-stake-interface = { workspace = true, features = ["bincode", "sysvar"] } +solana-svm-callback = { workspace = true } +solana-svm-feature-set = { workspace = true } +solana-svm-log-collector = { workspace = true } +solana-svm-measure = { workspace = true } +solana-svm-timings = { workspace = true } +solana-svm-transaction = { workspace = true } +solana-svm-type-overrides = { workspace = true } +solana-system-interface = { workspace = true } +solana-sysvar = { workspace = true, features = ["bincode"] } +solana-sysvar-id = { workspace = true } +solana-transaction-context = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +solana-account-info = { workspace = true } +solana-instruction = { workspace = true, features = ["bincode"] } +solana-keypair = { workspace = true } +solana-program-runtime = { path = ".", features = ["dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } +solana-signer = { workspace = true } +solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } +solana-transaction-context = { path = "../transaction-context", features = [ + "agave-unstable-api", + "bincode", + "dev-context-only-utils", +] } +test-case = { workspace = true } + +[lints] +workspace = true diff --git a/solana/program-runtime/src/cpi.rs b/solana/program-runtime/src/cpi.rs new file mode 100644 index 0000000..98a9631 --- /dev/null +++ b/solana/program-runtime/src/cpi.rs @@ -0,0 +1,2529 @@ +//! Cross-Program Invocation (CPI) error types + +use { + crate::{ + invoke_context::InvokeContext, + memory::{translate_slice, translate_type, translate_type_mut_for_cpi, translate_vm_slice}, + memory_context::SerializedAccountMetadata, + serialization::{create_memory_region_of_account, modify_memory_region_of_account}, + }, + solana_account_info::AccountInfo, + solana_instruction::{AccountMeta, Instruction, error::InstructionError}, + solana_loader_v3_interface::instruction as bpf_loader_upgradeable, + solana_program_entrypoint::MAX_PERMITTED_DATA_INCREASE, + solana_pubkey::{MAX_SEEDS, Pubkey, PubkeyError}, + solana_sbpf::{ebpf, memory_region::MemoryMapping}, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, native_loader}, + solana_stable_layout::stable_instruction::StableInstruction, + solana_svm_log_collector::ic_msg, + solana_svm_timings::ExecuteTimings, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_INSTRUCTION, MAX_INSTRUCTION_DATA_LEN, + instruction_accounts::BorrowedInstructionAccount, vm_slice::VmSlice, + }, + std::mem, + thiserror::Error, +}; + +/// CPI-specific error types +#[derive(Debug, Error, PartialEq, Eq)] +pub enum CpiError { + #[error("Invalid pointer")] + InvalidPointer, + #[error("Too many signers")] + TooManySigners, + #[error("Could not create program address with signer seeds: {0}")] + BadSeeds(PubkeyError), + #[error("InvalidLength")] + InvalidLength, + #[error("Invoked an instruction with too many accounts ({num_accounts} > {max_accounts})")] + MaxInstructionAccountsExceeded { + num_accounts: u64, + max_accounts: u64, + }, + #[error("Invoked an instruction with data that is too large ({data_len} > {max_data_len})")] + MaxInstructionDataLenExceeded { data_len: u64, max_data_len: u64 }, + #[error( + "Invoked an instruction with too many account info's ({num_account_infos} > \ + {max_account_infos})" + )] + MaxInstructionAccountInfosExceeded { + num_account_infos: u64, + max_account_infos: u64, + }, + #[error("Program {0} not supported by inner instructions")] + ProgramNotSupported(Pubkey), +} + +type Error = Box; + +const SUCCESS: u64 = 0; +/// Maximum signers +const MAX_SIGNERS: usize = 16; +///SIMD-0339 based calculation of AccountInfo translation byte size. Fixed size of **80 bytes** for each AccountInfo broken down as: +/// - 32 bytes for account address +/// - 32 bytes for owner address +/// - 8 bytes for lamport balance +/// - 8 bytes for data length +const ACCOUNT_INFO_BYTE_SIZE: usize = 80; + +/// Rust representation of C's SolInstruction +#[derive(Debug)] +#[repr(C)] +struct SolInstruction { + pub program_id_addr: u64, + pub accounts_addr: u64, + pub accounts_len: u64, + pub data_addr: u64, + pub data_len: u64, +} + +/// Rust representation of C's SolAccountMeta +#[derive(Debug)] +#[repr(C)] +struct SolAccountMeta { + pub pubkey_addr: u64, + pub is_writable: bool, + pub is_signer: bool, +} + +/// Rust representation of C's SolAccountInfo +#[derive(Debug)] +#[repr(C)] +struct SolAccountInfo { + pub key_addr: u64, + pub lamports_addr: u64, + pub data_len: u64, + pub data_addr: u64, + pub owner_addr: u64, + pub rent_epoch: u64, + pub is_signer: bool, + pub is_writable: bool, + pub executable: bool, +} + +/// Rust representation of C's SolSignerSeed +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedC { + pub addr: u64, + pub len: u64, +} + +/// Rust representation of C's SolSignerSeeds +#[derive(Debug)] +#[repr(C)] +struct SolSignerSeedsC { + pub addr: u64, + pub len: u64, +} + +/// Maximum number of account info structs that can be used in a single CPI invocation +const MAX_CPI_ACCOUNT_INFOS: usize = 255; + +/// Check that an account info pointer field points to the expected address +fn check_account_info_pointer( + invoke_context: &InvokeContext, + vm_addr: u64, + expected_vm_addr: u64, + field: &str, +) -> Result<(), Error> { + if vm_addr != expected_vm_addr { + ic_msg!( + invoke_context, + "Invalid account info pointer `{}': {:#x} != {:#x}", + field, + vm_addr, + expected_vm_addr + ); + return Err(Box::new(CpiError::InvalidPointer)); + } + Ok(()) +} + +/// Check that an instruction's account and data lengths are within limits +fn check_instruction_size(num_accounts: usize, data_len: usize) -> Result<(), Error> { + if num_accounts > MAX_ACCOUNTS_PER_INSTRUCTION { + return Err(Box::new(CpiError::MaxInstructionAccountsExceeded { + num_accounts: num_accounts as u64, + max_accounts: MAX_ACCOUNTS_PER_INSTRUCTION as u64, + })); + } + if data_len > MAX_INSTRUCTION_DATA_LEN { + return Err(Box::new(CpiError::MaxInstructionDataLenExceeded { + data_len: data_len as u64, + max_data_len: MAX_INSTRUCTION_DATA_LEN as u64, + })); + } + Ok(()) +} + +/// Check that the number of account infos is within the CPI limit +fn check_account_infos(num_account_infos: usize) -> Result<(), Error> { + let num_account_infos = num_account_infos as u64; + let max_account_infos = MAX_CPI_ACCOUNT_INFOS as u64; + if num_account_infos > max_account_infos { + return Err(Box::new(CpiError::MaxInstructionAccountInfosExceeded { + num_account_infos, + max_account_infos, + })); + } + Ok(()) +} + +/// Check whether a program is authorized for CPI +fn check_authorized_program( + program_id: &Pubkey, + instruction_data: &[u8], + invoke_context: &InvokeContext, +) -> Result<(), Error> { + if native_loader::check_id(program_id) + || bpf_loader::check_id(program_id) + || bpf_loader_deprecated::check_id(program_id) + || (solana_sdk_ids::bpf_loader_upgradeable::check_id(program_id) + && !(bpf_loader_upgradeable::is_upgrade_instruction(instruction_data) + || bpf_loader_upgradeable::is_set_authority_instruction(instruction_data) + || (invoke_context + .get_feature_set() + .enable_bpf_loader_set_authority_checked_ix + && bpf_loader_upgradeable::is_set_authority_checked_instruction( + instruction_data, + )) + || bpf_loader_upgradeable::is_close_instruction(instruction_data))) + || invoke_context.is_precompile(program_id) + { + return Err(Box::new(CpiError::ProgramNotSupported(*program_id))); + } + Ok(()) +} + +/// Host side representation of AccountInfo or SolAccountInfo passed to the CPI syscall. +/// +/// At the start of a CPI, this can be different from the data stored in the +/// corresponding BorrowedAccount, and needs to be synched. +#[derive(Debug)] +pub struct CallerAccount<'a> { + pub lamports: &'a mut u64, + pub owner: &'a mut Pubkey, + // The original data length of the account at the start of the current + // instruction. We use this to determine whether an account was shrunk or + // grown before or after CPI, and to derive the vm address of the realloc + // region. + pub original_data_len: usize, + // This points to the data section for this account, as serialized and + // mapped inside the vm (see serialize_parameters() in + // BpfExecutor::execute). + // + // This is only set when account_data_direct_mapping is off. + pub serialized_data: &'a mut [u8], + // Given the corresponding input AccountInfo::data, vm_data_addr points to + // the pointer field and ref_to_len_in_vm points to the length field. + pub vm_data_addr: u64, + pub ref_to_len_in_vm: &'a mut u64, +} + +impl<'a> CallerAccount<'a> { + pub fn get_serialized_data( + memory_mapping: &solana_sbpf::memory_region::MemoryMapping, + check_aligned: bool, + vm_addr: u64, + original_data_len: usize, + len: usize, + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) -> Result<&'a mut [u8], Error> { + use crate::memory::translate_slice_mut_for_cpi; + + if syscall_parameter_address_restrictions { + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = if is_caller_loader_deprecated { + original_data_len + } else { + original_data_len.saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + if len > address_space_reserved_for_account { + return Err(InstructionError::InvalidRealloc.into()); + } + } + if virtual_address_space_adjustments && account_data_direct_mapping { + Ok(&mut []) + } else if virtual_address_space_adjustments { + // Workaround the memory permissions (as these are from the PoV of being inside the VM) + let serialization_ptr = translate_slice_mut_for_cpi::( + memory_mapping, + solana_sbpf::ebpf::MM_INPUT_START, + 1, + false, // Don't care since it is byte aligned + )? + .as_mut_ptr(); + unsafe { + Ok(std::slice::from_raw_parts_mut( + serialization_ptr + .add(vm_addr.saturating_sub(solana_sbpf::ebpf::MM_INPUT_START) as usize), + len, + )) + } + } else { + translate_slice_mut_for_cpi::( + memory_mapping, + vm_addr, + len as u64, + false, // Don't care since it is byte aligned + ) + } + } + + // Create a CallerAccount given an AccountInfo. + pub fn from_account_info( + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + _vm_addr: u64, + account_info: &solana_account_info::AccountInfo, + account_metadata: &crate::memory_context::SerializedAccountMetadata, + ) -> Result, Error> { + use crate::memory::{translate_type, translate_type_mut_for_cpi}; + + let syscall_parameter_address_restrictions = invoke_context + .get_feature_set() + .syscall_parameter_address_restrictions; + let virtual_address_space_adjustments = invoke_context + .get_feature_set() + .virtual_address_space_adjustments; + let account_data_direct_mapping = + invoke_context.get_feature_set().account_data_direct_mapping; + + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + account_info.key as *const _ as u64, + account_metadata.vm_key_addr, + "key", + )?; + check_account_info_pointer( + invoke_context, + account_info.owner as *const _ as u64, + account_metadata.vm_owner_addr, + "owner", + )?; + } + + // account_info points to host memory. The addresses used internally are + // in vm space so they need to be translated. + let lamports = { + // Double translate lamports out of RefCell + let ptr = translate_type::( + memory_mapping, + account_info.lamports.as_ptr() as u64, + check_aligned, + )?; + if syscall_parameter_address_restrictions { + if account_info.lamports.as_ptr() as u64 >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + + check_account_info_pointer( + invoke_context, + *ptr, + account_metadata.vm_lamports_addr, + "lamports", + )?; + } + translate_type_mut_for_cpi::(memory_mapping, *ptr, check_aligned)? + }; + + let owner = translate_type_mut_for_cpi::( + memory_mapping, + account_info.owner as *const _ as u64, + check_aligned, + )?; + + let (serialized_data, vm_data_addr, ref_to_len_in_vm) = { + if syscall_parameter_address_restrictions + && account_info.data.as_ptr() as u64 >= solana_sbpf::ebpf::MM_INPUT_START + { + return Err(Box::new(CpiError::InvalidPointer)); + } + + // Double translate data out of RefCell + let data = *translate_type::<&[u8]>( + memory_mapping, + account_info.data.as_ptr() as *const _ as u64, + check_aligned, + )?; + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + data.as_ptr() as u64, + account_metadata.vm_data_addr, + "data", + )?; + } else { + // Moved to translate_accounts_common() via feature gate. + invoke_context.compute_meter.consume_checked( + (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + + let vm_len_addr = (account_info.data.as_ptr() as *const u64 as u64) + .saturating_add(size_of::() as u64); + if syscall_parameter_address_restrictions { + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if vm_len_addr >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + } + let ref_to_len_in_vm = + translate_type_mut_for_cpi::(memory_mapping, vm_len_addr, false)?; + let vm_data_addr = data.as_ptr() as u64; + let serialized_data = CallerAccount::get_serialized_data( + memory_mapping, + check_aligned, + vm_data_addr, + account_metadata.original_data_len, + if syscall_parameter_address_restrictions { + *ref_to_len_in_vm as usize + } else { + data.len() + }, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + (serialized_data, vm_data_addr, ref_to_len_in_vm) + }; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: account_metadata.original_data_len, + serialized_data, + vm_data_addr, + ref_to_len_in_vm, + }) + } + + // Create a CallerAccount given a SolAccountInfo. + fn from_sol_account_info( + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + vm_addr: u64, + account_info: &SolAccountInfo, + account_metadata: &crate::memory_context::SerializedAccountMetadata, + ) -> Result, Error> { + use crate::memory::translate_type_mut_for_cpi; + + let syscall_parameter_address_restrictions = invoke_context + .get_feature_set() + .syscall_parameter_address_restrictions; + let virtual_address_space_adjustments = invoke_context + .get_feature_set() + .virtual_address_space_adjustments; + let account_data_direct_mapping = + invoke_context.get_feature_set().account_data_direct_mapping; + + if syscall_parameter_address_restrictions { + check_account_info_pointer( + invoke_context, + account_info.key_addr, + account_metadata.vm_key_addr, + "key", + )?; + + check_account_info_pointer( + invoke_context, + account_info.owner_addr, + account_metadata.vm_owner_addr, + "owner", + )?; + + check_account_info_pointer( + invoke_context, + account_info.lamports_addr, + account_metadata.vm_lamports_addr, + "lamports", + )?; + + check_account_info_pointer( + invoke_context, + account_info.data_addr, + account_metadata.vm_data_addr, + "data", + )?; + } + + // account_info points to host memory. The addresses used internally are + // in vm space so they need to be translated. + let lamports = translate_type_mut_for_cpi::( + memory_mapping, + account_info.lamports_addr, + check_aligned, + )?; + let owner = translate_type_mut_for_cpi::( + memory_mapping, + account_info.owner_addr, + check_aligned, + )?; + + if !syscall_parameter_address_restrictions { + // Moved to translate_accounts_common() via feature gate. + invoke_context.compute_meter.consume_checked( + account_info + .data_len + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX), + )?; + } + + // we already have the host addr we want: &mut account_info.data_len. + // The account info might be read only in the vm though, so we translate + // to ensure we can write. This is tested by programs/sbf/rust/ro_modify + // which puts SolAccountInfo in rodata. + let vm_len_addr = vm_addr + .saturating_add(&account_info.data_len as *const u64 as u64) + .saturating_sub(account_info as *const _ as *const u64 as u64); + if syscall_parameter_address_restrictions { + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if vm_len_addr >= solana_sbpf::ebpf::MM_INPUT_START { + return Err(Box::new(CpiError::InvalidPointer)); + } + } + let ref_to_len_in_vm = + translate_type_mut_for_cpi::(memory_mapping, vm_len_addr, false)?; + let serialized_data = CallerAccount::get_serialized_data( + memory_mapping, + check_aligned, + account_info.data_addr, + account_metadata.original_data_len, + if syscall_parameter_address_restrictions { + *ref_to_len_in_vm as usize + } else { + account_info.data_len as usize + }, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + + Ok(CallerAccount { + lamports, + owner, + original_data_len: account_metadata.original_data_len, + serialized_data, + vm_data_addr: account_info.data_addr, + ref_to_len_in_vm, + }) + } +} + +/// Implemented by language specific data structure translators +pub trait SyscallInvokeSigned { + fn translate_instruction( + addr: u64, + invoke_context: &InvokeContext, + ) -> Result; + fn translate_accounts<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, + ) -> Result>, Error>; + fn translate_signers( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, + ) -> Result, Error>; +} + +pub fn translate_instruction_rust( + addr: u64, + invoke_context: &InvokeContext, +) -> Result { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let ix = translate_type::(memory_mapping, addr, check_aligned)?; + let account_metas = translate_slice::>( + memory_mapping, + ix.accounts.as_vaddr(), + ix.accounts.len(), + check_aligned, + )?; + let data = translate_slice::( + memory_mapping, + ix.data.as_vaddr(), + ix.data.len(), + check_aligned, + )?; + + check_instruction_size(account_metas.len(), data.len())?; + + let mut total_cu_translation_cost: u64 = (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + // Each account meta is 34 bytes (32 for pubkey, 1 for is_signer, 1 for is_writable) + let account_meta_translation_cost = + (account_metas.len().saturating_mul(size_of::()) as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + total_cu_translation_cost = + total_cu_translation_cost.saturating_add(account_meta_translation_cost); + + invoke_context + .compute_meter + .consume_checked(total_cu_translation_cost)?; + + let mut accounts = Vec::with_capacity(account_metas.len()); + for account_meta in account_metas { + // Before using `account_meta` directly, verify that `is_signer` and `is_writable` + // contain valid boolean values to prevent UB. + let account_meta = unsafe { + let ptr = account_meta.as_ptr(); + if (&raw const (*ptr).is_signer).cast::().read_volatile() > 1 + || (&raw const (*ptr).is_writable).cast::().read_volatile() > 1 + { + return Err(Box::new(InstructionError::InvalidArgument)); + } + // SAFETY: VM memory is initialized, and we have validated that the boolean fields + // contain valid data. + account_meta.assume_init_ref() + }; + + accounts.push(account_meta.clone()); + } + + Ok(Instruction { + accounts, + data: data.to_vec(), + program_id: ix.program_id, + }) +} + +pub fn translate_accounts_rust<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, +) -> Result>, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + translate_account_infos( + account_infos_addr, + account_infos_len, + |account_info: &AccountInfo| account_info.key as *const _ as u64, + invoke_context, + memory_mapping, + check_aligned, + |account_infos, account_info_keys| { + translate_accounts_common( + &account_info_keys, + account_infos, + account_infos_addr, + invoke_context, + memory_mapping, + check_aligned, + CallerAccount::from_account_info, + ) + }, + )? +} + +pub fn translate_signers_rust( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, +) -> Result, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let mut signers = Vec::new(); + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::>>( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + check_aligned, + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(Box::new(CpiError::TooManySigners)); + } + for signer_seeds in signers_seeds.iter() { + let untranslated_seeds = translate_slice::>( + memory_mapping, + signer_seeds.ptr(), + signer_seeds.len(), + check_aligned, + )?; + if untranslated_seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded)); + } + let seeds = untranslated_seeds + .iter() + .map(|untranslated_seed| { + translate_vm_slice(untranslated_seed, memory_mapping, check_aligned) + }) + .collect::, Error>>()?; + let signer = + Pubkey::create_program_address(&seeds, program_id).map_err(CpiError::BadSeeds)?; + signers.push(signer); + } + Ok(signers) + } else { + Ok(vec![]) + } +} + +pub fn translate_instruction_c( + addr: u64, + invoke_context: &InvokeContext, +) -> Result { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + let ix_c = translate_type::(memory_mapping, addr, check_aligned)?; + + let program_id = translate_type::(memory_mapping, ix_c.program_id_addr, check_aligned)?; + let account_metas = translate_slice::>( + memory_mapping, + ix_c.accounts_addr, + ix_c.accounts_len, + check_aligned, + )?; + let data = translate_slice::(memory_mapping, ix_c.data_addr, ix_c.data_len, check_aligned)?; + + check_instruction_size(ix_c.accounts_len as usize, data.len())?; + + let mut total_cu_translation_cost: u64 = (data.len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + // Each account meta is 34 bytes (32 for pubkey, 1 for is_signer, 1 for is_writable) + let account_meta_translation_cost = (ix_c + .accounts_len + .saturating_mul(size_of::() as u64)) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + + total_cu_translation_cost = + total_cu_translation_cost.saturating_add(account_meta_translation_cost); + + invoke_context + .compute_meter + .consume_checked(total_cu_translation_cost)?; + + let mut accounts = Vec::with_capacity(ix_c.accounts_len as usize); + for account_meta in account_metas { + // Before using `account_meta` directly, verify that `is_signer` and `is_writable` + // contain valid boolean values to prevent UB. + let account_meta = unsafe { + let ptr = account_meta.as_ptr(); + if (&raw const (*ptr).is_signer).cast::().read_volatile() > 1 + || (&raw const (*ptr).is_writable).cast::().read_volatile() > 1 + { + return Err(Box::new(InstructionError::InvalidArgument)); + } + // SAFETY: VM memory is initialized, and we have validated that the boolean fields + // contain valid data. + account_meta.assume_init_ref() + }; + let pubkey = + translate_type::(memory_mapping, account_meta.pubkey_addr, check_aligned)?; + accounts.push(AccountMeta { + pubkey: *pubkey, + is_signer: account_meta.is_signer, + is_writable: account_meta.is_writable, + }); + } + + Ok(Instruction { + accounts, + data: data.to_vec(), + program_id: *program_id, + }) +} + +pub fn translate_accounts_c<'a>( + account_infos_addr: u64, + account_infos_len: u64, + invoke_context: &InvokeContext, +) -> Result>, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + translate_account_infos( + account_infos_addr, + account_infos_len, + |account_info: &SolAccountInfo| account_info.key_addr, + invoke_context, + memory_mapping, + check_aligned, + |account_infos, account_info_keys| { + translate_accounts_common( + &account_info_keys, + account_infos, + account_infos_addr, + invoke_context, + memory_mapping, + check_aligned, + CallerAccount::from_sol_account_info, + ) + }, + )? +} + +pub fn translate_signers_c( + program_id: &Pubkey, + signers_seeds_addr: u64, + signers_seeds_len: u64, + invoke_context: &InvokeContext, +) -> Result, Error> { + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + if signers_seeds_len > 0 { + let signers_seeds = translate_slice::( + memory_mapping, + signers_seeds_addr, + signers_seeds_len, + check_aligned, + )?; + if signers_seeds.len() > MAX_SIGNERS { + return Err(Box::new(CpiError::TooManySigners)); + } + Ok(signers_seeds + .iter() + .map(|signer_seeds| { + let seeds = translate_slice::( + memory_mapping, + signer_seeds.addr, + signer_seeds.len, + check_aligned, + )?; + if seeds.len() > MAX_SEEDS { + return Err(Box::new(InstructionError::MaxSeedLengthExceeded) as Error); + } + let seeds_bytes = seeds + .iter() + .map(|seed| { + translate_slice::(memory_mapping, seed.addr, seed.len, check_aligned) + }) + .collect::, Error>>()?; + Pubkey::create_program_address(&seeds_bytes, program_id) + .map_err(|err| Box::new(CpiError::BadSeeds(err)) as Error) + }) + .collect::, Error>>()?) + } else { + Ok(vec![]) + } +} + +/// Call process instruction, common to both Rust and C +pub fn cpi_common( + invoke_context: &mut InvokeContext, + instruction_addr: u64, + account_infos_addr: u64, + account_infos_len: u64, + signers_seeds_addr: u64, + signers_seeds_len: u64, +) -> Result { + // CPI entry. + // + // Translate the inputs to the syscall and synchronize the caller's account + // changes so the callee can see them. + let amount = invoke_context.get_execution_cost().invoke_units; + invoke_context.compute_meter.consume_checked(amount)?; + let syscall_parameter_address_restrictions = invoke_context + .get_feature_set() + .syscall_parameter_address_restrictions; + let virtual_address_space_adjustments = invoke_context + .get_feature_set() + .virtual_address_space_adjustments; + let account_data_direct_mapping = invoke_context.get_feature_set().account_data_direct_mapping; + let check_aligned = invoke_context.get_check_aligned(); + + let instruction = S::translate_instruction(instruction_addr, invoke_context)?; + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context()?; + let caller_program_id = instruction_context.get_program_key()?; + let signers = S::translate_signers( + caller_program_id, + signers_seeds_addr, + signers_seeds_len, + invoke_context, + )?; + check_authorized_program(&instruction.program_id, &instruction.data, invoke_context)?; + invoke_context.prepare_next_cpi_instruction(instruction, &signers)?; + + let mut accounts = + S::translate_accounts(account_infos_addr, account_infos_len, invoke_context)?; + + if syscall_parameter_address_restrictions { + // before initiating CPI, the caller may have modified the + // account (caller_account). We need to update the corresponding + // BorrowedAccount (callee_account) so the callee can see the + // changes. + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + for translated_account in accounts.iter_mut() { + let callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + // update_callee_account() is moved from translate_accounts_common() + let update_caller = update_callee_account( + memory_mapping, + check_aligned, + &translated_account.caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + translated_account.update_caller_account_region = + translated_account.update_caller_account_info || update_caller; + } + } + + // Process the callee instruction + let mut compute_units_consumed = 0; + invoke_context + .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default())?; + + // re-bind to please the borrow checker + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + + // CPI exit. + // + // Synchronize the callee's account changes so the caller can see them. + for translated_account in accounts.iter_mut() { + let mut callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + if translated_account.update_caller_account_info { + update_caller_account( + invoke_context, + check_aligned, + &mut translated_account.caller_account, + &mut callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + } + } + + if virtual_address_space_adjustments { + let memory_mapping = invoke_context.memory_contexts.memory_mapping_mut()?; + for translated_account in accounts.iter() { + let mut callee_account = instruction_context + .try_borrow_instruction_account(translated_account.index_in_caller)?; + if translated_account.update_caller_account_region { + unsafe { + // SAFETY: lifetime is valid by construction: we're resetting the caller memory + // region back to the account that was here before the CPI call, meaning that + // the memory region was guaranteed to be live for sufficient duration upon + // call of this function. + update_caller_account_region( + memory_mapping, + check_aligned, + &translated_account.caller_account, + &mut callee_account, + account_data_direct_mapping, + )?; + } + } + } + } + + Ok(SUCCESS) +} + +/// Account data and metadata that has been translated from caller space. +pub struct TranslatedAccount<'a> { + pub index_in_caller: IndexOfAccount, + pub caller_account: CallerAccount<'a>, + pub update_caller_account_region: bool, + pub update_caller_account_info: bool, +} + +fn translate_account_infos( + account_infos_addr: u64, + account_infos_len: u64, + key_addr: impl Fn(&T) -> u64, + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + cb: impl FnOnce(&[T], Vec<&Pubkey>) -> R, +) -> Result { + let syscall_parameter_address_restrictions = invoke_context + .get_feature_set() + .syscall_parameter_address_restrictions; + + // In the same vein as the other check_account_info_pointer() checks, we don't lock + // this pointer to a specific address but we don't want it to be inside accounts, or + // callees might be able to write to the pointed memory. + if syscall_parameter_address_restrictions + && account_infos_addr + .saturating_add(account_infos_len.saturating_mul(std::mem::size_of::() as u64)) + >= ebpf::MM_INPUT_START + { + return Err(CpiError::InvalidPointer.into()); + } + + let account_infos = translate_slice::( + memory_mapping, + account_infos_addr, + account_infos_len, + check_aligned, + )?; + check_account_infos(account_infos.len())?; + + let account_infos_bytes = account_infos.len().saturating_mul(ACCOUNT_INFO_BYTE_SIZE); + + let amount = (account_infos_bytes as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + invoke_context.compute_meter.consume_checked(amount)?; + + let mut account_info_keys = Vec::with_capacity(account_infos_len as usize); + #[expect(clippy::needless_range_loop)] + for account_index in 0..account_infos_len as usize { + #[expect(clippy::indexing_slicing)] + let account_info = &account_infos[account_index]; + account_info_keys.push(translate_type::( + memory_mapping, + key_addr(account_info), + check_aligned, + )?); + } + Ok(cb(account_infos, account_info_keys)) +} + +// Finish translating accounts and build TranslatedAccount from CallerAccount. +fn translate_accounts_common<'a, T, F>( + account_info_keys: &[&Pubkey], + account_infos: &[T], + account_infos_addr: u64, + invoke_context: &InvokeContext, + memory_mapping: &MemoryMapping, + check_aligned: bool, + do_translate: F, +) -> Result>, Error> +where + F: Fn( + &InvokeContext, + &MemoryMapping, + bool, + u64, + &T, + &SerializedAccountMetadata, + ) -> Result, Error>, +{ + let transaction_context = &invoke_context.transaction_context; + let next_instruction_context = transaction_context.get_next_instruction_context()?; + let next_instruction_accounts = next_instruction_context.instruction_accounts(); + let instruction_context = transaction_context.get_current_instruction_context()?; + let mut accounts = Vec::with_capacity(next_instruction_accounts.len()); + + // unwrapping here is fine: we're in a syscall and the method below fails + // only outside syscalls + let accounts_metadata = &invoke_context + .memory_contexts + .memory_context_abi_v1() + .unwrap() + .accounts_metadata; + + let syscall_parameter_address_restrictions = invoke_context + .get_feature_set() + .syscall_parameter_address_restrictions; + let virtual_address_space_adjustments = invoke_context + .get_feature_set() + .virtual_address_space_adjustments; + let account_data_direct_mapping = invoke_context.get_feature_set().account_data_direct_mapping; + + for (instruction_account_index, instruction_account) in + next_instruction_accounts.iter().enumerate() + { + if next_instruction_context + .is_instruction_account_duplicate(instruction_account_index as IndexOfAccount)? + .is_some() + { + continue; // Skip duplicate account + } + + let index_in_caller = instruction_context + .get_index_of_account_in_instruction(instruction_account.index_in_transaction)?; + let callee_account = instruction_context.try_borrow_instruction_account(index_in_caller)?; + let account_key = invoke_context + .transaction_context + .get_key_of_account_at_index(instruction_account.index_in_transaction)?; + + #[expect(deprecated)] + if callee_account.is_executable() { + // Use the known account + let amount = (callee_account.get_data().len() as u64) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + invoke_context.compute_meter.consume_checked(amount)?; + } else if let Some(caller_account_index) = + account_info_keys.iter().position(|key| *key == account_key) + { + let serialized_metadata = + accounts_metadata + .get(index_in_caller as usize) + .ok_or_else(|| { + ic_msg!( + invoke_context, + "Internal error: index mismatch for account {}", + account_key + ); + Box::new(InstructionError::MissingAccount) as Error + })?; + + // build the CallerAccount corresponding to this account. + if caller_account_index >= account_infos.len() { + return Err(Box::new(CpiError::InvalidLength)); + } + #[expect(clippy::indexing_slicing)] + let caller_account = + do_translate( + invoke_context, + memory_mapping, + check_aligned, + account_infos_addr.saturating_add( + caller_account_index.saturating_mul(mem::size_of::()) as u64, + ), + &account_infos[caller_account_index], + serialized_metadata, + )?; + + if syscall_parameter_address_restrictions { + // Moved from do_translate() via feature gate. + let amount = (*caller_account.ref_to_len_in_vm) + .checked_div(invoke_context.get_execution_cost().cpi_bytes_per_unit) + .unwrap_or(u64::MAX); + invoke_context.compute_meter.consume_checked(amount)?; + } + let update_caller = if syscall_parameter_address_restrictions { + // update_callee_account() is moved to cpi_common() + true + } else { + // before initiating CPI, the caller may have modified the + // account (caller_account). We need to update the corresponding + // BorrowedAccount (callee_account) so the callee can see the + // changes. + update_callee_account( + memory_mapping, + check_aligned, + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )? + }; + + accounts.push(TranslatedAccount { + index_in_caller, + caller_account, + update_caller_account_region: instruction_account.is_writable() || update_caller, + update_caller_account_info: instruction_account.is_writable(), + }); + } else { + ic_msg!( + invoke_context, + "Instruction references an unknown account {}", + account_key + ); + return Err(Box::new(InstructionError::MissingAccount)); + } + } + + Ok(accounts) +} + +// Update the given account before executing CPI. +// +// caller_account and callee_account describe the same account. At CPI entry +// caller_account might include changes the caller has made to the account +// before executing CPI. +// +// This method updates callee_account so the CPI callee can see the caller's +// changes. +// +// When true is returned, the caller account must be updated after CPI. This +// is only set for virtual_address_space_adjustments when the pointer may have changed. +fn update_callee_account( + memory_mapping: &MemoryMapping, + check_aligned: bool, + caller_account: &CallerAccount, + mut callee_account: BorrowedInstructionAccount<'_, '_>, + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, +) -> Result { + let mut must_update_caller = false; + + if callee_account.get_lamports() != *caller_account.lamports { + callee_account.set_lamports(*caller_account.lamports)?; + } + + if virtual_address_space_adjustments { + let prev_len = callee_account.get_data().len(); + let post_len = *caller_account.ref_to_len_in_vm as usize; + if prev_len != post_len { + if !account_data_direct_mapping && post_len < prev_len { + // If the account has been shrunk, we're going to zero the unused memory + // *that was previously used*. + let serialized_data = CallerAccount::get_serialized_data( + memory_mapping, + check_aligned, + caller_account.vm_data_addr, + caller_account.original_data_len, + prev_len, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + serialized_data + .get_mut(post_len..) + .ok_or_else(|| Box::new(InstructionError::AccountDataTooSmall) as Error)? + .fill(0); + } + callee_account.set_data_length(post_len)?; + // pointer to data may have changed, so caller must be updated + must_update_caller = true; + } + if !account_data_direct_mapping && callee_account.can_data_be_changed().is_ok() { + callee_account.set_data_from_slice(caller_account.serialized_data)?; + } + } else { + // The redundant check helps to avoid the expensive data comparison if we can + match callee_account.can_data_be_resized(caller_account.serialized_data.len()) { + Ok(()) => callee_account.set_data_from_slice(caller_account.serialized_data)?, + Err(err) if callee_account.get_data() != caller_account.serialized_data => { + return Err(Box::new(err)); + } + _ => {} + } + } + + // Change the owner at the end so that we are allowed to change the lamports and data before + if callee_account.get_owner() != caller_account.owner { + callee_account.set_owner(caller_account.owner.as_ref())?; + // caller gave ownership and thus write access away, so caller must be updated + must_update_caller = true; + } + + Ok(must_update_caller) +} + +/// # Safety +/// +/// The the account data pointed to by `callee_account` must outlive the uses of the +/// [`MemoryMapping`]. +unsafe fn update_caller_account_region( + memory_mapping: &mut MemoryMapping, + check_aligned: bool, + caller_account: &CallerAccount, + callee_account: &mut BorrowedInstructionAccount<'_, '_>, + account_data_direct_mapping: bool, +) -> Result<(), Error> { + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = if is_caller_loader_deprecated { + caller_account.original_data_len + } else { + caller_account + .original_data_len + .saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + + if address_space_reserved_for_account > 0 { + // We can trust vm_data_addr to point to the correct region because we + // enforce that in CallerAccount::from_(sol_)account_info. + let (region_index, region) = memory_mapping + .find_region(caller_account.vm_data_addr) + .ok_or_else(|| Box::new(InstructionError::MissingAccount) as Error)?; + // vm_data_addr must always point to the beginning of the region + debug_assert_eq!(region.vm_addr, caller_account.vm_data_addr); + let mut new_region; + if !account_data_direct_mapping { + new_region = region.clone(); + modify_memory_region_of_account(callee_account, &mut new_region); + } else { + new_region = create_memory_region_of_account(callee_account, region.vm_addr)?; + } + unsafe { + // SAFETY: the lifetime invariants are delegated to the callers of this function. Both + // `modify_memory_region_of_account` and `create_memory_region_of_account` create memory + // regions pointing to valid buffers by the virtue of the region being produced out of + // an intermediate slice, which itself must be wholly valid. + memory_mapping.replace_region(region_index, new_region)?; + } + } + + Ok(()) +} + +// Update the given account after executing CPI. +// +// caller_account and callee_account describe to the same account. At CPI exit +// callee_account might include changes the callee has made to the account +// after executing. +// +// This method updates caller_account so the CPI caller can see the callee's +// changes. +// +// Safety: Once `syscall_parameter_address_restrictions` is enabled all fields of [CallerAccount] used +// in this function should never point inside the address space reserved for +// accounts (regardless of the current size of an account). +fn update_caller_account( + invoke_context: &InvokeContext, + check_aligned: bool, + caller_account: &mut CallerAccount<'_>, + callee_account: &mut BorrowedInstructionAccount<'_, '_>, + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, +) -> Result<(), Error> { + *caller_account.lamports = callee_account.get_lamports(); + *caller_account.owner = *callee_account.get_owner(); + + let prev_len = *caller_account.ref_to_len_in_vm as usize; + let post_len = callee_account.get_data().len(); + let is_caller_loader_deprecated = !check_aligned; + let address_space_reserved_for_account = + if syscall_parameter_address_restrictions && is_caller_loader_deprecated { + caller_account.original_data_len + } else { + caller_account + .original_data_len + .saturating_add(MAX_PERMITTED_DATA_INCREASE) + }; + + if post_len > address_space_reserved_for_account + && (syscall_parameter_address_restrictions || prev_len != post_len) + { + let max_increase = + address_space_reserved_for_account.saturating_sub(caller_account.original_data_len); + ic_msg!( + invoke_context, + "Account data size realloc limited to {max_increase} in inner instructions", + ); + return Err(Box::new(InstructionError::InvalidRealloc)); + } + + let memory_mapping = invoke_context.memory_contexts.memory_mapping()?; + if prev_len != post_len { + // when virtual_address_space_adjustments is enabled we don't cache the serialized data in + // caller_account.serialized_data. See CallerAccount::from_account_info. + if !(virtual_address_space_adjustments && account_data_direct_mapping) { + // If the account has been shrunk, we're going to zero the unused memory + // *that was previously used*. + if post_len < prev_len { + caller_account + .serialized_data + .get_mut(post_len..) + .ok_or_else(|| Box::new(InstructionError::AccountDataTooSmall) as Error)? + .fill(0); + } + // Set the length of caller_account.serialized_data to post_len. + caller_account.serialized_data = CallerAccount::get_serialized_data( + memory_mapping, + check_aligned, + caller_account.vm_data_addr, + caller_account.original_data_len, + post_len, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + )?; + } + // this is the len field in the AccountInfo::data slice + *caller_account.ref_to_len_in_vm = post_len as u64; + + // this is the len field in the serialized parameters + let serialized_len_ptr = translate_type_mut_for_cpi::( + memory_mapping, + caller_account + .vm_data_addr + .saturating_sub(std::mem::size_of::() as u64), + check_aligned, + )?; + *serialized_len_ptr = post_len as u64; + } + + if !(virtual_address_space_adjustments && account_data_direct_mapping) { + // Propagate changes in the callee up to the caller. + let to_slice = &mut caller_account.serialized_data; + let from_slice = callee_account + .get_data() + .get(0..post_len) + .ok_or(CpiError::InvalidLength)?; + if to_slice.len() != from_slice.len() { + return Err(Box::new(InstructionError::AccountDataTooSmall)); + } + to_slice.copy_from_slice(from_slice); + } + + Ok(()) +} + +#[allow(clippy::indexing_slicing)] +#[allow(clippy::arithmetic_side_effects)] +#[cfg(test)] +mod tests { + use { + super::*, + crate::{ + invoke_context::BpfAllocator, + memory::translate_type, + memory_context::{MemoryContext, SerializedAccountMetadata}, + with_mock_invoke_context_with_feature_set, + }, + assert_matches::assert_matches, + solana_account::{Account, AccountSharedData, ReadableAccount}, + solana_account_info::AccountInfo, + solana_sbpf::{ + ebpf::MM_INPUT_START, memory_region::MemoryRegion, program::SBPFVersion, vm::Config, + }, + solana_sdk_ids::{bpf_loader, system_program}, + solana_svm_feature_set::SVMFeatureSet, + solana_transaction_context::{ + IndexOfAccount, instruction_accounts::InstructionAccount, + transaction_accounts::KeyedAccountSharedData, + }, + std::{ + cell::{Cell, RefCell}, + mem, ptr, + rc::Rc, + slice, + }, + test_case::case, + }; + + macro_rules! mock_invoke_context { + ($invoke_context:ident, + $transaction_context:ident, + $instruction_data:expr, + $transaction_accounts:expr, + $program_account:expr, + $instruction_accounts:expr) => { + let instruction_data = $instruction_data; + let instruction_accounts = $instruction_accounts + .iter() + .map(|index_in_transaction| { + InstructionAccount::new( + *index_in_transaction as IndexOfAccount, + false, + $transaction_accounts[*index_in_transaction as usize].2, + ) + }) + .collect::>(); + let transaction_accounts = $transaction_accounts + .into_iter() + .map(|a| (a.0, a.1)) + .collect::>(); + let mut feature_set = SVMFeatureSet::all_enabled(); + feature_set.syscall_parameter_address_restrictions = false; + feature_set.virtual_address_space_adjustments = false; + feature_set.account_data_direct_mapping = false; + let feature_set = &feature_set; + with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + feature_set, + transaction_accounts + ); + $invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + $program_account, + instruction_accounts, + instruction_data.to_vec(), + ) + .unwrap(); + $invoke_context.push().unwrap(); + }; + } + + macro_rules! borrow_instruction_account { + ($borrowed_account:ident, $invoke_context:expr, $index:expr) => { + let instruction_context = $invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + let $borrowed_account = instruction_context + .try_borrow_instruction_account($index) + .unwrap(); + }; + } + + fn is_zeroed(data: &[u8]) -> bool { + data.iter().all(|b| *b == 0) + } + + struct MockCallerAccount { + lamports: u64, + owner: Pubkey, + vm_addr: u64, + data: Vec, + len: u64, + regions: Vec, + virtual_address_space_adjustments: bool, + } + + impl MockCallerAccount { + fn new( + lamports: u64, + owner: Pubkey, + data: &[u8], + virtual_address_space_adjustments: bool, + ) -> MockCallerAccount { + let vm_addr = MM_INPUT_START; + let mut region_addr = vm_addr; + let region_len = mem::size_of::() + + if virtual_address_space_adjustments { + 0 + } else { + data.len() + MAX_PERMITTED_DATA_INCREASE + }; + let mut d = vec![0; region_len]; + let mut regions = vec![]; + + // always write the [len] part even when virtual_address_space_adjustments + unsafe { ptr::write_unaligned::(d.as_mut_ptr().cast(), data.len() as u64) }; + + // write the account data when not virtual_address_space_adjustments + if !virtual_address_space_adjustments { + d[mem::size_of::()..][..data.len()].copy_from_slice(data); + } + + // create a region for [len][data+realloc if !virtual_address_space_adjustments] + regions.push(MemoryRegion::new(&raw mut d[..region_len], vm_addr)); + region_addr += region_len as u64; + + if virtual_address_space_adjustments { + // create a region for the directly mapped data + regions.push(MemoryRegion::new(&raw const data[..], region_addr)); + region_addr += data.len() as u64; + + // create a region for the realloc padding + regions.push(MemoryRegion::new( + &raw mut d[mem::size_of::()..], + region_addr, + )); + } else { + // caller_account.serialized_data must have the actual data length + d.truncate(mem::size_of::() + data.len()); + } + + MockCallerAccount { + lamports, + owner, + vm_addr, + data: d, + len: data.len() as u64, + regions, + virtual_address_space_adjustments, + } + } + + fn data_slice<'a>(&self) -> &'a [u8] { + // lifetime crimes + unsafe { + slice::from_raw_parts( + self.data[mem::size_of::()..].as_ptr(), + self.data.capacity() - mem::size_of::(), + ) + } + } + + fn caller_account(&mut self) -> CallerAccount<'_> { + let data = if self.virtual_address_space_adjustments { + &mut [] + } else { + &mut self.data[mem::size_of::()..] + }; + CallerAccount { + lamports: &mut self.lamports, + owner: &mut self.owner, + original_data_len: self.len as usize, + serialized_data: data, + vm_data_addr: self.vm_addr + mem::size_of::() as u64, + ref_to_len_in_vm: &mut self.len, + } + } + } + + struct MockAccountInfo<'a> { + key: Pubkey, + is_signer: bool, + is_writable: bool, + lamports: u64, + data: &'a [u8], + owner: Pubkey, + executable: bool, + _unused: u64, + } + + impl MockAccountInfo<'_> { + fn new(key: Pubkey, account: &AccountSharedData) -> MockAccountInfo<'_> { + MockAccountInfo { + key, + is_signer: false, + is_writable: false, + lamports: account.lamports(), + data: account.data(), + owner: *account.owner(), + executable: account.executable(), + _unused: account.rent_epoch(), + } + } + + fn into_region(self, vm_addr: u64) -> (Vec, MemoryRegion, SerializedAccountMetadata) { + let size = mem::size_of::() + + mem::size_of::() * 2 + + mem::size_of::>>() + + mem::size_of::() + + mem::size_of::>>() + + self.data.len(); + let mut data = vec![0; size]; + + let vm_addr = vm_addr as usize; + let key_addr = vm_addr + mem::size_of::(); + let lamports_cell_addr = key_addr + mem::size_of::(); + let lamports_addr = lamports_cell_addr + mem::size_of::>>(); + let owner_addr = lamports_addr + mem::size_of::(); + let data_cell_addr = owner_addr + mem::size_of::(); + let data_addr = data_cell_addr + mem::size_of::>>(); + + #[allow(deprecated)] + #[allow(clippy::used_underscore_binding)] + let info = AccountInfo { + key: unsafe { (key_addr as *const Pubkey).as_ref() }.unwrap(), + is_signer: self.is_signer, + is_writable: self.is_writable, + lamports: unsafe { + Rc::from_raw((lamports_cell_addr + RcBox::<&mut u64>::VALUE_OFFSET) as *const _) + }, + data: unsafe { + Rc::from_raw((data_cell_addr + RcBox::<&mut [u8]>::VALUE_OFFSET) as *const _) + }, + owner: unsafe { (owner_addr as *const Pubkey).as_ref() }.unwrap(), + executable: self.executable, + _unused: self._unused, + }; + + unsafe { + ptr::write_unaligned(data.as_mut_ptr().cast(), info); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + key_addr - vm_addr) as *mut _, + self.key, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + lamports_cell_addr - vm_addr) as *mut _, + RcBox::new(RefCell::new((lamports_addr as *mut u64).as_mut().unwrap())), + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + lamports_addr - vm_addr) as *mut _, + self.lamports, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + owner_addr - vm_addr) as *mut _, + self.owner, + ); + ptr::write_unaligned( + (data.as_mut_ptr() as usize + data_cell_addr - vm_addr) as *mut _, + RcBox::new(RefCell::new(slice::from_raw_parts_mut( + data_addr as *mut u8, + self.data.len(), + ))), + ); + data[data_addr - vm_addr..].copy_from_slice(self.data); + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + ( + data, + region, + SerializedAccountMetadata { + vm_addr: vm_addr as u64, + original_data_len: self.data.len(), + vm_key_addr: key_addr as u64, + vm_lamports_addr: lamports_addr as u64, + vm_owner_addr: owner_addr as u64, + vm_data_addr: data_addr as u64, + }, + ) + } + } + + struct MockInstruction { + program_id: Pubkey, + accounts: Vec, + data: Vec, + } + + impl MockInstruction { + fn into_region(self, vm_addr: u64) -> (Vec, MemoryRegion) { + let accounts_len = mem::size_of::() * self.accounts.len(); + + let size = mem::size_of::() + accounts_len + self.data.len(); + + let mut data = vec![0; size]; + + let vm_addr = vm_addr as usize; + let accounts_addr = vm_addr + mem::size_of::(); + let data_addr = accounts_addr + accounts_len; + + let ins = Instruction { + program_id: self.program_id, + accounts: unsafe { + Vec::from_raw_parts( + accounts_addr as *mut _, + self.accounts.len(), + self.accounts.len(), + ) + }, + data: unsafe { + Vec::from_raw_parts(data_addr as *mut _, self.data.len(), self.data.len()) + }, + }; + let ins = StableInstruction::from(ins); + + unsafe { + ptr::write_unaligned(data.as_mut_ptr().cast(), ins); + data[accounts_addr - vm_addr..][..accounts_len].copy_from_slice( + slice::from_raw_parts(self.accounts.as_ptr().cast(), accounts_len), + ); + data[data_addr - vm_addr..].copy_from_slice(&self.data); + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + (data, region) + } + } + + #[repr(C)] + struct RcBox { + strong: Cell, + weak: Cell, + value: T, + } + + impl RcBox { + const VALUE_OFFSET: usize = mem::size_of::>() * 2; + fn new(value: T) -> RcBox { + RcBox { + strong: Cell::new(0), + weak: Cell::new(0), + value, + } + } + } + + type TestTransactionAccount = (Pubkey, AccountSharedData, bool); + + fn transaction_with_one_writable_instruction_account( + data: Vec, + ) -> Vec { + let program_id = Pubkey::new_unique(); + let account = AccountSharedData::from(Account { + lamports: 1, + data, + owner: program_id, + executable: false, + rent_epoch: 100, + }); + vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + false, + ), + (Pubkey::new_unique(), account, true), + ] + } + + fn transaction_with_one_readonly_instruction_account( + data: Vec, + ) -> Vec { + let program_id = Pubkey::new_unique(); + let account_owner = Pubkey::new_unique(); + let account = AccountSharedData::from(Account { + lamports: 1, + data, + owner: account_owner, + executable: false, + rent_epoch: 100, + }); + vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + false, + ), + (Pubkey::new_unique(), account, true), + ] + } + + fn mock_signers(signers: &[&[u8]], vm_addr: u64) -> (Vec, MemoryRegion) { + let vm_addr = vm_addr as usize; + + // calculate size + let fat_ptr_size_of_slice = mem::size_of::<&[()]>(); // pointer size + length size + let singers_length = signers.len(); + let sum_signers_data_length: usize = signers.iter().map(|s| s.len()).sum(); + + // init data vec + let total_size = fat_ptr_size_of_slice + + singers_length * fat_ptr_size_of_slice + + sum_signers_data_length; + let mut data = vec![0; total_size]; + + // data is composed by 3 parts + // A. + // [ singers address, singers length, ..., + // B. | + // signer1 address, signer1 length, signer2 address ..., + // ^ p1 ---> + // C. | + // signer1 data, signer2 data, ... ] + // ^ p2 ---> + + // A. + data[..fat_ptr_size_of_slice / 2] + .clone_from_slice(&(fat_ptr_size_of_slice + vm_addr).to_le_bytes()); + data[fat_ptr_size_of_slice / 2..fat_ptr_size_of_slice] + .clone_from_slice(&(singers_length).to_le_bytes()); + + // B. + C. + let (mut p1, mut p2) = ( + fat_ptr_size_of_slice, + fat_ptr_size_of_slice + singers_length * fat_ptr_size_of_slice, + ); + for signer in signers.iter() { + let signer_length = signer.len(); + + // B. + data[p1..p1 + fat_ptr_size_of_slice / 2] + .clone_from_slice(&(p2 + vm_addr).to_le_bytes()); + data[p1 + fat_ptr_size_of_slice / 2..p1 + fat_ptr_size_of_slice] + .clone_from_slice(&(signer_length).to_le_bytes()); + p1 += fat_ptr_size_of_slice; + + // C. + data[p2..p2 + signer_length].clone_from_slice(signer); + p2 += signer_length; + } + + let region = MemoryRegion::new(&raw mut data[..], vm_addr as u64); + (data, region) + } + + #[test] + fn test_translate_instruction() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let program_id = Pubkey::new_unique(); + let accounts = vec![AccountMeta { + pubkey: Pubkey::new_unique(), + is_signer: true, + is_writable: false, + }]; + let data = b"ins data".to_vec(); + let vm_addr = MM_INPUT_START; + let (_mem, region) = MockInstruction { + program_id, + accounts: accounts.clone(), + data: data.clone(), + } + .into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3).unwrap() }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + + let ins = translate_instruction_rust(vm_addr, &invoke_context).unwrap(); + assert_eq!(ins.program_id, program_id); + assert_eq!(ins.accounts, accounts); + assert_eq!(ins.data, data); + } + + #[test] + fn test_translate_signers() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let program_id = Pubkey::new_unique(); + let (derived_key, bump_seed) = Pubkey::find_program_address(&[b"foo"], &program_id); + + let vm_addr = MM_INPUT_START; + let (_mem, region) = mock_signers(&[b"foo", &[bump_seed]], vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3).unwrap() }; + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(0), + Vec::new(), + mapping, + )) + .unwrap(); + + let signers = translate_signers_rust(&program_id, vm_addr, 1, &invoke_context).unwrap(); + assert_eq!(signers[0], derived_key); + } + + #[test] + fn test_translate_accounts_rust() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + let key = transaction_accounts[1].0; + let original_data_len = account.data().len(); + + let vm_addr = MM_INPUT_START; + let (_mem, region, account_metadata) = + MockAccountInfo::new(key, &account).into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3).unwrap() }; + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1, 1] + ); + + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(solana_program_entrypoint::HEAP_LENGTH as u64), + vec![account_metadata], + memory_mapping, + )) + .unwrap(); + + invoke_context + .transaction_context + .configure_next_cpi_for_tests( + 0, + vec![ + InstructionAccount::new(1, false, true), + InstructionAccount::new(1, false, true), + ], + vec![], + ) + .unwrap(); + + let accounts = translate_accounts_rust(vm_addr, 1, &invoke_context).unwrap(); + assert_eq!(accounts.len(), 1); + let caller_account = &accounts[0].caller_account; + assert_eq!(caller_account.serialized_data, account.data()); + assert_eq!(caller_account.original_data_len, original_data_len); + } + + #[test] + fn test_get_serialized_data() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![], &config, SBPFVersion::V3).unwrap() }; + + assert_matches!( + CallerAccount::get_serialized_data( + &memory_mapping, + true, // check_aligned + MM_INPUT_START, + account.data().len(), + account.data().len().saturating_add(MAX_PERMITTED_DATA_INCREASE).saturating_add(1), + true, // syscall_parameter_address_restrictions + true, // virtual_address_space_adjustments + false, // account_data_direct_mapping + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::InvalidRealloc + ); + } + + #[test] + fn test_caller_account_from_account_info() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foo".to_vec()); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let key = Pubkey::new_unique(); + let vm_addr = MM_INPUT_START; + let (_mem, region, account_metadata) = + MockAccountInfo::new(key, &account).into_region(vm_addr); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = + unsafe { MemoryMapping::new(vec![region], &config, SBPFVersion::V3).unwrap() }; + + let account_info = translate_type::(&memory_mapping, vm_addr, false).unwrap(); + + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + let check_aligned = invoke_context.get_check_aligned(); + let memory_mapping = invoke_context.memory_contexts.memory_mapping().unwrap(); + let caller_account = CallerAccount::from_account_info( + &invoke_context, + memory_mapping, + check_aligned, + vm_addr, + account_info, + &account_metadata, + ) + .unwrap(); + assert_eq!(*caller_account.lamports, account.lamports()); + assert_eq!(caller_account.owner, account.owner()); + assert_eq!(caller_account.original_data_len, account.data().len()); + assert_eq!( + *caller_account.ref_to_len_in_vm as usize, + account.data().len() + ); + assert_eq!(caller_account.serialized_data, account.data()); + } + + #[case(false, false, false)] + #[case(true, false, false)] + #[case(true, true, false)] + #[case(true, true, true)] + fn test_update_caller_account_lamports_owner( + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) { + let transaction_accounts = transaction_with_one_writable_instruction_account(vec![]); + let account = transaction_accounts[1].1.clone(); + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data(), false); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.split_off(0), + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + + let mut caller_account = mock_caller_account.caller_account(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + let mut callee_account = instruction_context + .try_borrow_instruction_account(0) + .unwrap(); + callee_account.set_lamports(42).unwrap(); + callee_account + .set_owner(Pubkey::new_unique().as_ref()) + .unwrap(); + + update_caller_account( + &invoke_context, + true, // check_aligned + &mut caller_account, + &mut callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .unwrap(); + + assert_eq!(*caller_account.lamports, 42); + assert_eq!(caller_account.owner, callee_account.get_owner()); + } + + #[test] + fn test_update_caller_account_data() { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + let original_data_len = account.data().len(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(account.lamports(), *account.owner(), account.data(), false); + + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.clone(), + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + invoke_context + .memory_contexts + .mock_set_mapping_abi_v1(memory_mapping); + + let data_slice = mock_caller_account.data_slice(); + let len_ptr = unsafe { + data_slice + .as_ptr() + .offset(-(mem::size_of::() as isize)) + }; + let serialized_len = || unsafe { *len_ptr.cast::() as usize }; + let mut caller_account = mock_caller_account.caller_account(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + let mut callee_account = instruction_context + .try_borrow_instruction_account(0) + .unwrap(); + + for (new_value, expected_realloc_size) in [ + (b"foo".to_vec(), MAX_PERMITTED_DATA_INCREASE + 3), + (b"foobaz".to_vec(), MAX_PERMITTED_DATA_INCREASE), + (b"foobazbad".to_vec(), MAX_PERMITTED_DATA_INCREASE - 3), + ] { + assert_eq!(caller_account.serialized_data, callee_account.get_data()); + callee_account.set_data_from_slice(&new_value).unwrap(); + + update_caller_account( + &invoke_context, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + false, // virtual_address_space_adjustments + false, // account_data_direct_mapping + ) + .unwrap(); + + let data_len = callee_account.get_data().len(); + assert_eq!(data_len, *caller_account.ref_to_len_in_vm as usize); + assert_eq!(data_len, serialized_len()); + assert_eq!(data_len, caller_account.serialized_data.len()); + assert_eq!( + callee_account.get_data(), + &caller_account.serialized_data[..data_len] + ); + assert_eq!(data_slice[data_len..].len(), expected_realloc_size); + assert!(is_zeroed(&data_slice[data_len..])); + } + + callee_account + .set_data_length(original_data_len + MAX_PERMITTED_DATA_INCREASE) + .unwrap(); + update_caller_account( + &invoke_context, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + false, // virtual_address_space_adjustments + false, // account_data_direct_mapping + ) + .unwrap(); + let data_len = callee_account.get_data().len(); + assert_eq!(data_slice[data_len..].len(), 0); + assert!(is_zeroed(&data_slice[data_len..])); + + callee_account + .set_data_length(original_data_len + MAX_PERMITTED_DATA_INCREASE + 1) + .unwrap(); + assert_matches!( + update_caller_account( + &invoke_context, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + false, // virtual_address_space_adjustments + false, // account_data_direct_mapping + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::InvalidRealloc + ); + + // close the account + callee_account.set_data_length(0).unwrap(); + callee_account + .set_owner(system_program::id().as_ref()) + .unwrap(); + update_caller_account( + &invoke_context, + true, // check_aligned + &mut caller_account, + &mut callee_account, + false, // syscall_parameter_address_restrictions + false, // virtual_address_space_adjustments + false, // account_data_direct_mapping + ) + .unwrap(); + let data_len = callee_account.get_data().len(); + assert_eq!(data_len, 0); + } + + #[case(false, false, false)] + #[case(true, false, false)] + #[case(true, true, false)] + #[case(true, true, true)] + fn test_update_callee_account_lamports_owner( + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) { + let transaction_accounts = transaction_with_one_writable_instruction_account(vec![]); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data(), false); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.clone(), + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + let caller_account = mock_caller_account.caller_account(); + + borrow_instruction_account!(callee_account, invoke_context, 0); + + *caller_account.lamports = 42; + *caller_account.owner = Pubkey::new_unique(); + + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .unwrap(); + + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_lamports(), 42); + assert_eq!(caller_account.owner, callee_account.get_owner()); + } + + #[case(false, false, false)] + #[case(true, false, false)] + #[case(true, true, false)] + #[case(true, true, true)] + fn test_update_callee_account_data_writable( + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) { + let transaction_accounts = + transaction_with_one_writable_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data(), false); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.clone(), + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + let mut caller_account = mock_caller_account.caller_account(); + borrow_instruction_account!(callee_account, invoke_context, 0); + + // Data is not copied in update_callee_account() with virtual_address_space_adjustments + caller_account.serialized_data[0] = b'b'; + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + false, // syscall_parameter_address_restrictions, + false, // virtual_address_space_adjustments, + false, // account_data_direct_mapping + ) + .unwrap(); + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_data(), b"boobar"); + + // growing resize + let mut data = b"foobarbaz".to_vec(); + *caller_account.ref_to_len_in_vm = data.len() as u64; + caller_account.serialized_data = &mut data; + assert_eq!( + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .unwrap(), + virtual_address_space_adjustments, + ); + + // truncating resize + let mut data = b"baz".to_vec(); + *caller_account.ref_to_len_in_vm = data.len() as u64; + caller_account.serialized_data = &mut data; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!( + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .unwrap(), + virtual_address_space_adjustments, + ); + + // close the account + let mut data = Vec::new(); + caller_account.serialized_data = &mut data; + *caller_account.ref_to_len_in_vm = 0; + let mut owner = system_program::id(); + caller_account.owner = &mut owner; + borrow_instruction_account!(callee_account, invoke_context, 0); + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .unwrap(); + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_eq!(callee_account.get_data(), b""); + } + + #[case(false, false, false)] + #[case(true, false, false)] + #[case(true, true, false)] + #[case(true, true, true)] + fn test_update_callee_account_data_readonly( + syscall_parameter_address_restrictions: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) { + let transaction_accounts = + transaction_with_one_readonly_instruction_account(b"foobar".to_vec()); + let account = transaction_accounts[1].1.clone(); + + mock_invoke_context!( + invoke_context, + transaction_context, + b"instruction data", + transaction_accounts, + 0, + &[1] + ); + + let mut mock_caller_account = + MockCallerAccount::new(1234, *account.owner(), account.data(), false); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let memory_mapping = unsafe { + MemoryMapping::new( + mock_caller_account.regions.clone(), + &config, + SBPFVersion::V3, + ) + .unwrap() + }; + let mut caller_account = mock_caller_account.caller_account(); + borrow_instruction_account!(callee_account, invoke_context, 0); + + // Data is not copied in update_callee_account() with virtual_address_space_adjustments + caller_account.serialized_data[0] = b'b'; + assert_matches!( + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + false, // syscall_parameter_address_restrictions, + false, // virtual_address_space_adjustments, + false, // account_data_direct_mapping + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::ExternalAccountDataModified + ); + + // growing resize + let mut data = b"foobarbaz".to_vec(); + *caller_account.ref_to_len_in_vm = data.len() as u64; + caller_account.serialized_data = &mut data; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_matches!( + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + ); + + // truncating resize + let mut data = b"baz".to_vec(); + *caller_account.ref_to_len_in_vm = data.len() as u64; + caller_account.serialized_data = &mut data; + borrow_instruction_account!(callee_account, invoke_context, 0); + assert_matches!( + update_callee_account( + &memory_mapping, + true, // check_aligned + &caller_account, + callee_account, + syscall_parameter_address_restrictions, + virtual_address_space_adjustments, + account_data_direct_mapping, + ), + Err(error) if error.downcast_ref::().unwrap() == &InstructionError::AccountDataSizeChanged + ); + } +} diff --git a/solana/program-runtime/src/deploy.rs b/solana/program-runtime/src/deploy.rs new file mode 100644 index 0000000..e1853a8 --- /dev/null +++ b/solana/program-runtime/src/deploy.rs @@ -0,0 +1,166 @@ +//! Program deployment functionality. + +#[cfg(feature = "metrics")] +use {crate::program_metrics::LoadProgramMetrics, solana_svm_measure::measure::Measure}; +use { + crate::{ + invoke_context::InvokeContext, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironment}, + program_cache_entry::{DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry}, + }, + solana_clock::Slot, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + solana_sbpf::{ + elf::{ElfError, Executable}, + program::{BuiltinProgram, SBPFVersion}, + verifier::RequisiteVerifier, + }, + solana_svm_log_collector::{LogCollector, ic_logger_msg}, + solana_svm_type_overrides::sync::Arc, + std::{cell::RefCell, rc::Rc}, +}; + +fn morph_into_deployment_environment( + from: ProgramRuntimeEnvironment, + disable_sbpf_v0_v1_v2_deployment: bool, +) -> Result>, ElfError> { + let mut config = (*from).get_config().clone(); + config.reject_broken_elfs = true; + if disable_sbpf_v0_v1_v2_deployment { + config.enabled_sbpf_versions = SBPFVersion::V3..=*config.enabled_sbpf_versions.end(); + } + + let mut result = BuiltinProgram::new_loader(config); + + for (_key, (name, value)) in (*from).get_function_registry().iter() { + // Deployment of programs with sol_alloc_free is disabled. So do not register the syscall. + if name != *b"sol_alloc_free_" { + result.register_function(unsafe { std::str::from_utf8_unchecked(name) }, value)?; + } + } + + Ok(result) +} + +/// Directly deploy a program using a provided invoke context. +/// This function should only be invoked from the runtime, since it does not +/// provide any account loads or checks. +#[allow(clippy::too_many_arguments)] +pub fn deploy_program( + log_collector: Option>>, + #[cfg(feature = "metrics")] load_program_metrics: &mut LoadProgramMetrics, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + program_runtime_environment: ProgramRuntimeEnvironment, + disable_sbpf_v0_v1_v2_deployment: bool, + program_id: &Pubkey, + loader_key: &Pubkey, + account_size: usize, + programdata: &[u8], + deployment_slot: Slot, +) -> Result<(), InstructionError> { + #[cfg(feature = "metrics")] + let mut register_syscalls_time = Measure::start("register_syscalls_time"); + let deployment_program_runtime_environment = morph_into_deployment_environment( + ProgramRuntimeEnvironment::clone(&program_runtime_environment), + disable_sbpf_v0_v1_v2_deployment, + ) + .map_err(|e| { + ic_logger_msg!(log_collector, "Failed to register syscalls: {}", e); + InstructionError::ProgramEnvironmentSetupFailure + })?; + #[cfg(feature = "metrics")] + { + register_syscalls_time.stop(); + load_program_metrics.register_syscalls_us = register_syscalls_time.as_us(); + } + // Verify using stricter deployment_program_runtime_environment + #[cfg(feature = "metrics")] + let mut load_elf_time = Measure::start("load_elf_time"); + let executable = Executable::::load( + programdata, + Arc::new(deployment_program_runtime_environment), + ) + .map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + #[cfg(feature = "metrics")] + { + load_elf_time.stop(); + load_program_metrics.load_elf_us = load_elf_time.as_us(); + } + #[cfg(feature = "metrics")] + let mut verify_code_time = Measure::start("verify_code_time"); + executable.verify::().map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + #[cfg(feature = "metrics")] + { + verify_code_time.stop(); + load_program_metrics.verify_code_us = verify_code_time.as_us(); + } + // Reload but with program_runtime_environment + let executor = unsafe { + // SAFETY: The executable has been verified just above. + ProgramCacheEntry::reload( + loader_key, + program_runtime_environment, + deployment_slot, + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + programdata, + account_size, + #[cfg(feature = "metrics")] + load_program_metrics, + ) + } + .map_err(|err| { + ic_logger_msg!(log_collector, "{}", err); + InstructionError::InvalidAccountData + })?; + if let Some(old_entry) = program_cache_for_tx_batch.find(program_id) { + executor.stats.merge_from(&old_entry.stats); + } + #[cfg(feature = "metrics")] + { + load_program_metrics.program_id = program_id.to_string(); + } + program_cache_for_tx_batch.store_modified_entry(*program_id, Arc::new(executor)); + Ok(()) +} + +#[macro_export] +macro_rules! deploy_program { + ($invoke_context:expr, + $program_id:expr, + $loader_key:expr, + $account_size:expr, + $programdata:expr, + $deployment_slot:expr, + $disable_sbpf_v0_v1_v2_deployment:expr $(,)?) => { + assert_eq!( + $deployment_slot, + $invoke_context.program_cache_for_tx_batch.slot() + ); + #[cfg(feature = "metrics")] + let mut load_program_metrics = $crate::program_metrics::LoadProgramMetrics::default(); + $crate::deploy::deploy_program( + $invoke_context.get_log_collector(), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + $invoke_context.program_cache_for_tx_batch, + $invoke_context + .get_program_runtime_environment_for_deployment() + .clone(), + $disable_sbpf_v0_v1_v2_deployment, + $program_id, + $loader_key, + $account_size, + $programdata, + $deployment_slot, + )?; + #[cfg(feature = "metrics")] + load_program_metrics.submit_datapoint(&mut $invoke_context.timings); + }; +} diff --git a/solana/program-runtime/src/execution_budget.rs b/solana/program-runtime/src/execution_budget.rs new file mode 100644 index 0000000..df3d543 --- /dev/null +++ b/solana/program-runtime/src/execution_budget.rs @@ -0,0 +1,320 @@ +use { + solana_fee_structure::FeeDetails, solana_program_entrypoint::HEAP_LENGTH, + solana_transaction_context::MAX_INSTRUCTION_TRACE_LENGTH, std::num::NonZeroU32, +}; + +/// Max instruction stack depth. This is the maximum nesting of instructions that can happen during +/// a transaction. +pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5; +/// Max instruction stack depth with SIMD-0268 enabled. Allows 8 nested CPIs. +pub const MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268: usize = 9; + +fn get_max_instruction_stack_depth(simd_0268_active: bool) -> usize { + if simd_0268_active { + MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 + } else { + MAX_INSTRUCTION_STACK_DEPTH + } +} + +//Default CPI invocation cost +pub const DEFAULT_INVOCATION_COST: u64 = 946; + +/// Max call depth. This is the maximum nesting of SBF to SBF call that can happen within a program. +pub const MAX_CALL_DEPTH: usize = 64; + +pub const MAX_COMPUTE_UNIT_LIMIT: u32 = 1_400_000; + +/// Roughly 0.5us/page, where page is 32K; given roughly 15CU/us, the +/// default heap page cost = 0.5 * 15 ~= 8CU/page +pub const DEFAULT_HEAP_COST: u64 = 8; +pub const DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT: u32 = 200_000; +// SIMD-170 defines max CUs to be allocated for any builtin program instructions, that +// have not been migrated to sBPF programs. +pub const MAX_BUILTIN_ALLOCATION_COMPUTE_UNIT_LIMIT: u32 = 3_000; +pub const MAX_HEAP_FRAME_BYTES: u32 = 256 * 1024; +pub const MIN_HEAP_FRAME_BYTES: u32 = HEAP_LENGTH as u32; + +/// The total accounts data a transaction can load is limited to 64MiB to not break +/// anyone in Mainnet-beta today. It can be set by set_loaded_accounts_data_size_limit instruction +pub const MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES: NonZeroU32 = + NonZeroU32::new(64 * 1024 * 1024).unwrap(); + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionBudget { + /// Number of compute units that a transaction or individual instruction is + /// allowed to consume. Compute units are consumed by program execution, + /// resources they use, etc... + pub compute_unit_limit: u64, + /// Maximum program instruction invocation stack depth. Invocation stack + /// depth starts at 1 for transaction instructions and the stack depth is + /// incremented each time a program invokes an instruction and decremented + /// when a program returns. + pub max_instruction_stack_depth: usize, + /// Maximum cross-program invocation and instructions per transaction + pub max_instruction_trace_length: usize, + /// Maximum number of slices hashed per syscall + pub sha256_max_slices: u64, + /// Maximum SBF to BPF call depth + pub max_call_depth: usize, + /// Size of a stack frame in bytes, must match the size specified in the LLVM SBF backend + pub stack_frame_size: usize, + /// program heap region size, default: solana_program_entrypoint::HEAP_LENGTH + pub heap_size: u32, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for SVMTransactionExecutionBudget { + fn default() -> Self { + Self::new_with_defaults(/* simd_0268_active */ false) + } +} + +impl SVMTransactionExecutionBudget { + pub fn new_with_defaults(simd_0268_active: bool) -> Self { + SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(MAX_COMPUTE_UNIT_LIMIT), + max_instruction_stack_depth: get_max_instruction_stack_depth(simd_0268_active), + max_instruction_trace_length: MAX_INSTRUCTION_TRACE_LENGTH, + sha256_max_slices: 20_000, + max_call_depth: MAX_CALL_DEPTH, + stack_frame_size: solana_sbpf::vm::get_stack_frame_size(), + heap_size: u32::try_from(solana_program_entrypoint::HEAP_LENGTH).unwrap(), + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionCost { + /// Number of compute units consumed by a log_u64 call + pub log_64_units: u64, + /// Number of compute units consumed by a create_program_address call + pub create_program_address_units: u64, + /// Number of compute units consumed by an invoke call (not including the cost incurred by + /// the called program) + pub invoke_units: u64, + /// Base number of compute units consumed to call SHA256 + pub sha256_base_cost: u64, + /// Incremental number of units consumed by SHA256 (based on bytes) + pub sha256_byte_cost: u64, + /// Number of compute units consumed by logging a `Pubkey` + pub log_pubkey_units: u64, + /// Number of account data bytes per compute unit charged during a cross-program invocation + pub cpi_bytes_per_unit: u64, + /// Base number of compute units consumed to get a sysvar + pub sysvar_base_cost: u64, + /// Number of compute units consumed to call secp256k1_recover + pub secp256k1_recover_cost: u64, + /// Number of compute units consumed to do a syscall without any work + pub syscall_base_cost: u64, + /// Number of compute units consumed to validate a curve25519 edwards point + pub curve25519_edwards_validate_point_cost: u64, + /// Number of compute units consumed to add two curve25519 edwards points + pub curve25519_edwards_add_cost: u64, + /// Number of compute units consumed to subtract two curve25519 edwards points + pub curve25519_edwards_subtract_cost: u64, + /// Number of compute units consumed to multiply a curve25519 edwards point + pub curve25519_edwards_multiply_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of edwards points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_edwards_msm_base_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of edwards points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_edwards_msm_incremental_cost: u64, + /// Number of compute units consumed to validate a curve25519 ristretto point + pub curve25519_ristretto_validate_point_cost: u64, + /// Number of compute units consumed to add two curve25519 ristretto points + pub curve25519_ristretto_add_cost: u64, + /// Number of compute units consumed to subtract two curve25519 ristretto points + pub curve25519_ristretto_subtract_cost: u64, + /// Number of compute units consumed to multiply a curve25519 ristretto point + pub curve25519_ristretto_multiply_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of ristretto points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_ristretto_msm_base_cost: u64, + /// Number of compute units consumed for a multiscalar multiplication (msm) of ristretto points. + /// The total cost is calculated as `msm_base_cost + (length - 1) * msm_incremental_cost`. + pub curve25519_ristretto_msm_incremental_cost: u64, + /// Number of compute units per additional 32k heap above the default (~.5 + /// us per 32k at 15 units/us rounded up) + pub heap_cost: u64, + /// Memory operation syscall base cost + pub mem_op_base_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_addition + pub alt_bn128_g1_addition_cost: u64, + /// Number of compute units consumed to call alt_bn128_g2_addition + pub alt_bn128_g2_addition_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_multiplication. + pub alt_bn128_g1_multiplication_cost: u64, + /// Number of compute units consumed to call alt_bn128_g2_multiplication. + pub alt_bn128_g2_multiplication_cost: u64, + /// Total cost will be alt_bn128_pairing_one_pair_cost_first + /// + alt_bn128_pairing_one_pair_cost_other * (num_elems - 1) + pub alt_bn128_pairing_one_pair_cost_first: u64, + pub alt_bn128_pairing_one_pair_cost_other: u64, + /// Big integer modular exponentiation base cost + pub big_modular_exponentiation_base_cost: u64, + /// Big integer moduler exponentiation cost divisor + /// The modular exponentiation cost is computed as + /// `input_length`/`big_modular_exponentiation_cost_divisor` + `big_modular_exponentiation_base_cost` + pub big_modular_exponentiation_cost_divisor: u64, + /// Coefficient `a` of the quadratic function which determines the number + /// of compute units consumed to call poseidon syscall for a given number + /// of inputs. + pub poseidon_cost_coefficient_a: u64, + /// Coefficient `c` of the quadratic function which determines the number + /// of compute units consumed to call poseidon syscall for a given number + /// of inputs. + pub poseidon_cost_coefficient_c: u64, + /// Number of compute units consumed for accessing the remaining compute units. + pub get_remaining_compute_units_cost: u64, + /// Number of compute units consumed to call alt_bn128_g1_compress. + pub alt_bn128_g1_compress: u64, + /// Number of compute units consumed to call alt_bn128_g1_decompress. + pub alt_bn128_g1_decompress: u64, + /// Number of compute units consumed to call alt_bn128_g2_compress. + pub alt_bn128_g2_compress: u64, + /// Number of compute units consumed to call alt_bn128_g2_decompress. + pub alt_bn128_g2_decompress: u64, + /// Number of compute units consumed to add two bls12_381 g1 points. + pub bls12_381_g1_add_cost: u64, + /// Number of compute units consumed to add two bls12_381 g2 points. + pub bls12_381_g2_add_cost: u64, + /// Number of compute units consumed to subtract two bls12_381 g1 points. + pub bls12_381_g1_subtract_cost: u64, + /// Number of compute units consumed to subtract two bls12_381 g2 points. + pub bls12_381_g2_subtract_cost: u64, + /// Number of compute units consumed to multiply a bls12_381 g1 point. + pub bls12_381_g1_multiply_cost: u64, + /// Number of compute units consumed to multiply a bls12_381 g2 point. + pub bls12_381_g2_multiply_cost: u64, + /// Number of compute units consumed to decompress a bls12_381 g1 point. + pub bls12_381_g1_decompress_cost: u64, + /// Number of compute units consumed to decompress a bls12_381 g2 point. + pub bls12_381_g2_decompress_cost: u64, + /// Number of compute units consumed to validate a bls12_381 g1 point. + pub bls12_381_g1_validate_cost: u64, + /// Number of compute units consumed to validate a bls12_381 g2 point. + pub bls12_381_g2_validate_cost: u64, + /// Base number of compute units consumed to perform a bls12_381 pairing. + pub bls12_381_one_pair_cost: u64, + /// Incremental number of compute units consumed per pair in a bls12_381 pairing. + pub bls12_381_additional_pair_cost: u64, +} + +impl Default for SVMTransactionExecutionCost { + fn default() -> Self { + SVMTransactionExecutionCost { + log_64_units: 100, + create_program_address_units: 1500, + invoke_units: DEFAULT_INVOCATION_COST, + sha256_base_cost: 85, + sha256_byte_cost: 1, + log_pubkey_units: 100, + cpi_bytes_per_unit: 250, // ~50MB at 200,000 units + sysvar_base_cost: 100, + secp256k1_recover_cost: 25_000, + syscall_base_cost: 100, + curve25519_edwards_validate_point_cost: 159, + curve25519_edwards_add_cost: 473, + curve25519_edwards_subtract_cost: 475, + curve25519_edwards_multiply_cost: 2_177, + curve25519_edwards_msm_base_cost: 2_273, + curve25519_edwards_msm_incremental_cost: 758, + curve25519_ristretto_validate_point_cost: 169, + curve25519_ristretto_add_cost: 521, + curve25519_ristretto_subtract_cost: 519, + curve25519_ristretto_multiply_cost: 2_208, + curve25519_ristretto_msm_base_cost: 2303, + curve25519_ristretto_msm_incremental_cost: 788, + heap_cost: DEFAULT_HEAP_COST, + mem_op_base_cost: 10, + alt_bn128_g1_addition_cost: 334, + alt_bn128_g2_addition_cost: 535, + alt_bn128_g1_multiplication_cost: 3_840, + alt_bn128_g2_multiplication_cost: 15_670, + alt_bn128_pairing_one_pair_cost_first: 36_364, + alt_bn128_pairing_one_pair_cost_other: 12_121, + big_modular_exponentiation_base_cost: 190, + big_modular_exponentiation_cost_divisor: 2, + poseidon_cost_coefficient_a: 61, + poseidon_cost_coefficient_c: 542, + get_remaining_compute_units_cost: 100, + alt_bn128_g1_compress: 30, + alt_bn128_g1_decompress: 398, + alt_bn128_g2_compress: 86, + alt_bn128_g2_decompress: 13610, + bls12_381_g1_add_cost: 128, + bls12_381_g2_add_cost: 203, + bls12_381_g1_subtract_cost: 129, + bls12_381_g2_subtract_cost: 204, + bls12_381_g1_multiply_cost: 4_627, + bls12_381_g2_multiply_cost: 8_255, + bls12_381_g1_decompress_cost: 2_100, + bls12_381_g2_decompress_cost: 3_050, + bls12_381_g1_validate_cost: 1_565, + bls12_381_g2_validate_cost: 1_968, + bls12_381_one_pair_cost: 25_445, + bls12_381_additional_pair_cost: 13_023, + } + } +} + +impl SVMTransactionExecutionCost { + /// Returns cost of the Poseidon hash function for the given number of + /// inputs is determined by the following quadratic function: + /// + /// 61*n^2 + 542 + /// + /// Which approximates the results of benchmarks of light-posiedon + /// library[0]. These results assume 1 CU per 33 ns. Examples: + /// + /// * 1 input + /// * light-poseidon benchmark: `18,303 / 33 ≈ 555` + /// * function: `61*1^2 + 542 = 603` + /// * 2 inputs + /// * light-poseidon benchmark: `25,866 / 33 ≈ 784` + /// * function: `61*2^2 + 542 = 786` + /// * 3 inputs + /// * light-poseidon benchmark: `37,549 / 33 ≈ 1,138` + /// * function; `61*3^2 + 542 = 1091` + /// + /// [0] https://github.com/Lightprotocol/light-poseidon#performance + pub fn poseidon_cost(&self, nr_inputs: u64) -> Option { + let squared_inputs = nr_inputs.checked_pow(2)?; + let mul_result = self + .poseidon_cost_coefficient_a + .checked_mul(squared_inputs)?; + let final_result = mul_result.checked_add(self.poseidon_cost_coefficient_c)?; + + Some(final_result) + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct SVMTransactionExecutionAndFeeBudgetLimits { + pub budget: SVMTransactionExecutionBudget, + pub loaded_accounts_data_size_limit: u32, + pub fee_details: FeeDetails, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for SVMTransactionExecutionAndFeeBudgetLimits { + fn default() -> Self { + Self { + budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size_limit: MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + fee_details: FeeDetails::default(), + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +impl SVMTransactionExecutionAndFeeBudgetLimits { + pub fn with_fee(fee_details: FeeDetails) -> Self { + Self { + fee_details, + ..SVMTransactionExecutionAndFeeBudgetLimits::default() + } + } +} diff --git a/solana/program-runtime/src/invoke_context.rs b/solana/program-runtime/src/invoke_context.rs new file mode 100644 index 0000000..c81225c --- /dev/null +++ b/solana/program-runtime/src/invoke_context.rs @@ -0,0 +1,1990 @@ +#[cfg(feature = "dev-context-only-utils")] +use { + crate::program_cache_entry::ProgramCacheEntry, + solana_account::{AccountSharedData, WritableAccount, create_account_shared_data_for_test}, + solana_epoch_schedule::EpochSchedule, + solana_instruction::AccountMeta, + solana_message::{LegacyMessage, Message, SanitizedMessage}, + solana_sdk_ids::sysvar, + solana_transaction_context::transaction_accounts::KeyedAccountSharedData, + std::collections::{HashMap, HashSet}, +}; +use { + crate::{ + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + loaded_programs::{ + ProgramCacheForTxBatch, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, + }, + memory_context::{MemoryContext, MemoryContexts}, + program_cache_entry::ProgramCacheEntryType, + stable_log, + sysvar_cache::SysvarCache, + }, + solana_hash::Hash, + solana_instruction::{Instruction, error::InstructionError}, + solana_pubkey::Pubkey, + solana_sbpf::{ + ebpf::MM_HEAP_START, + elf::{ElfError, Executable as GenericExecutable}, + error::{EbpfError, ProgramResult}, + memory_region::MemoryMapping, + program::{BuiltinProgram, SBPFVersion}, + vm::{Config, ContextObject, EbpfVm}, + }, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + }, + solana_svm_callback::InvokeContextCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_log_collector::{LogCollector, ic_msg}, + solana_svm_measure::measure::Measure, + solana_svm_timings::{ExecuteDetailsTimings, ExecuteTimings}, + solana_svm_transaction::svm_message::SVMMessage, + solana_svm_type_overrides::sync::Arc, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_TRANSACTION, instruction::InstructionContext, + instruction_accounts::InstructionAccount, transaction::TransactionContext, + }, + std::{ + alloc::Layout, + borrow::Cow, + cell::{Cell, RefCell}, + fmt::{self, Debug}, + ptr, + rc::Rc, + time::Duration, + }, +}; + +pub type BuiltinFunctionRegisterer = + fn(&mut BuiltinProgram>, &str) -> Result<(), ElfError>; +pub type Executable = GenericExecutable>; +pub type RegisterTrace<'a> = &'a [[u64; 12]]; + +/// Adapter so we can unify the interfaces of built-in programs and syscalls +#[macro_export] +macro_rules! declare_process_instruction { + ($process_instruction:ident, $cu_to_consume:expr, |$invoke_context:ident| $inner:tt) => { + $crate::solana_sbpf::declare_builtin_function!( + $process_instruction, + fn rust( + invoke_context: &mut $crate::invoke_context::InvokeContext<'_, '_>, + _arg0: u64, + _arg1: u64, + _arg2: u64, + _arg3: u64, + _arg4: u64, + ) -> Result> { + fn process_instruction_inner( + $invoke_context: &mut $crate::invoke_context::InvokeContext, + ) -> std::result::Result<(), $crate::__private::InstructionError> + $inner + + let consumption_result = if $cu_to_consume > 0 + { + invoke_context.compute_meter.consume_checked($cu_to_consume) + } else { + Ok(()) + }; + consumption_result + .and_then(|_| { + process_instruction_inner(invoke_context) + .map(|_| 0) + .map_err(|err| Box::new(err) as Box) + }) + .into() + } + ); + }; +} + +impl ContextObject for InvokeContext<'_, '_> { + fn consume(&mut self, amount: u64) { + // 1 to 1 instruction to compute unit mapping + // ignore overflow, Ebpf will bail if exceeded + let compute_meter = self.compute_meter.0.get(); + self.compute_meter + .0 + .set(compute_meter.saturating_sub(amount)); + } + + fn get_remaining(&self) -> u64 { + self.compute_meter.0.get() + } + + fn active_mapping_ptr(&mut self) -> ptr::NonNull { + let memory = self + .memory_contexts + .memory_mapping_mut() + .expect("The memory context must have been set for the current instruction"); + ptr::NonNull::from_mut(memory) + } +} + +#[derive(Clone, PartialEq, Eq, Debug)] +pub struct AllocErr; +impl fmt::Display for AllocErr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + f.write_str("Error: Memory allocation failed") + } +} + +pub struct BpfAllocator { + len: u64, + pos: u64, +} + +impl BpfAllocator { + pub fn new(len: u64) -> Self { + Self { len, pos: 0 } + } + + pub fn alloc(&mut self, layout: Layout) -> Result { + let bytes_to_align = (self.pos as *const u8).align_offset(layout.align()) as u64; + if self + .pos + .saturating_add(bytes_to_align) + .saturating_add(layout.size() as u64) + <= self.len + { + self.pos = self.pos.saturating_add(bytes_to_align); + let addr = MM_HEAP_START.saturating_add(self.pos); + self.pos = self.pos.saturating_add(layout.size() as u64); + Ok(addr) + } else { + Err(AllocErr) + } + } +} + +pub struct EnvironmentConfig<'a> { + pub blockhash: Hash, + pub blockhash_lamports_per_signature: u64, + alpenglow_migration_succeeded: bool, + epoch_stake_callback: &'a dyn InvokeContextCallback, + feature_set: &'a SVMFeatureSet, + program_runtime_environments: &'a ProgramRuntimeEnvironments, + sysvar_cache: &'a SysvarCache, +} +impl<'a> EnvironmentConfig<'a> { + pub fn new( + blockhash: Hash, + blockhash_lamports_per_signature: u64, + alpenglow_migration_succeeded: bool, + epoch_stake_callback: &'a dyn InvokeContextCallback, + feature_set: &'a SVMFeatureSet, + program_runtime_environments: &'a ProgramRuntimeEnvironments, + sysvar_cache: &'a SysvarCache, + ) -> Self { + Self { + blockhash, + blockhash_lamports_per_signature, + alpenglow_migration_succeeded, + epoch_stake_callback, + feature_set, + program_runtime_environments, + sysvar_cache, + } + } + + /// Get cached sysvars + pub fn sysvar_cache(&self) -> &SysvarCache { + self.sysvar_cache + } +} + +pub struct ComputeMeter(Cell); + +impl ComputeMeter { + /// Consume compute units + pub fn consume_checked(&self, amount: u64) -> Result<(), Box> { + let compute_meter = self.0.get(); + let exceeded = compute_meter < amount; + self.0.set(compute_meter.saturating_sub(amount)); + if exceeded { + return Err(Box::new(InstructionError::ComputationalBudgetExceeded)); + } + Ok(()) + } + + /// Set compute units + /// + /// Only use for tests and benchmarks + #[cfg(feature = "dev-context-only-utils")] + pub fn mock_set_remaining(&self, remaining: u64) { + self.0.set(remaining); + } +} + +/// Main pipeline from runtime to program execution. +pub struct InvokeContext<'a, 'ix_data> { + /// Information about the currently executing transaction. + pub transaction_context: &'a mut TransactionContext<'ix_data>, + /// The local program cache for the transaction batch. + pub program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch, + /// Runtime configurations used to provision the invocation environment. + pub environment_config: EnvironmentConfig<'a>, + /// The compute budget for the current invocation. + compute_budget: SVMTransactionExecutionBudget, + /// The compute cost for the current invocation. + execution_cost: SVMTransactionExecutionCost, + /// Instruction compute meter, for tracking compute units consumed against + /// the designated compute budget during program execution. + pub compute_meter: ComputeMeter, + log_collector: Option>>, + /// Time spent so far executing nested program calls. + pub total_nested_exec_time: Duration, + pub timings: ExecuteDetailsTimings, + pub memory_contexts: MemoryContexts, + /// Pairs of index in TX instruction trace and VM register trace + register_traces: Vec<(usize, Vec<[u64; 12]>)>, + /// Debug port to use for this executing transaction. + #[cfg(feature = "sbpf-debugger")] + pub debug_port: Option, +} + +impl<'a, 'ix_data> InvokeContext<'a, 'ix_data> { + pub fn new( + transaction_context: &'a mut TransactionContext<'ix_data>, + program_cache_for_tx_batch: &'a mut ProgramCacheForTxBatch, + environment_config: EnvironmentConfig<'a>, + log_collector: Option>>, + compute_budget: SVMTransactionExecutionBudget, + execution_cost: SVMTransactionExecutionCost, + ) -> Self { + Self { + transaction_context, + program_cache_for_tx_batch, + environment_config, + log_collector, + compute_budget, + execution_cost, + compute_meter: ComputeMeter(Cell::new(compute_budget.compute_unit_limit)), + total_nested_exec_time: Duration::ZERO, + timings: ExecuteDetailsTimings::default(), + memory_contexts: MemoryContexts::new(), + register_traces: Vec::new(), + #[cfg(feature = "sbpf-debugger")] + debug_port: None, + } + } + + /// Push a stack frame onto the invocation stack + pub fn push(&mut self) -> Result<(), InstructionError> { + let instruction_context = self.transaction_context.get_next_instruction_context()?; + let program_id = instruction_context + .get_program_key() + .map_err(|_| InstructionError::UnsupportedProgramId)?; + if self.transaction_context.get_instruction_stack_height() != 0 { + let contains = + (0..self.transaction_context.get_instruction_stack_height()).any(|level| { + self.transaction_context + .get_instruction_context_at_nesting_level(level) + .and_then(|instruction_context| instruction_context.get_program_key()) + .map(|program_key| program_key == program_id) + .unwrap_or(false) + }); + let is_last = self + .transaction_context + .get_current_instruction_context() + .and_then(|instruction_context| instruction_context.get_program_key()) + .map(|program_key| program_key == program_id) + .unwrap_or(false); + if contains && !is_last { + // Reentrancy not allowed unless caller is calling itself + return Err(InstructionError::ReentrancyNotAllowed); + } + } + + self.memory_contexts.push_placeholder(); + self.transaction_context.push() + } + + /// Pop a stack frame from the invocation stack + pub fn pop(&mut self) -> Result<(), InstructionError> { + self.memory_contexts.pop(); + self.transaction_context.pop() + } + + /// Current height of the invocation stack, top level instructions are height + /// `solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT` + pub fn get_stack_height(&self) -> usize { + self.transaction_context.get_instruction_stack_height() + } + + /// Entrypoint for a cross-program invocation from a builtin program. + /// + /// Takes signer seeds and derives PDAs internally via + /// `create_program_address`, mirroring the SBF CPI path. This makes + /// it structurally impossible for a builtin to vouch for a non-PDA + /// address (e.g. a user wallet) as a signer. + pub fn native_invoke_signed( + &mut self, + instruction: Instruction, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), InstructionError> { + let caller_program_id = *self + .transaction_context + .get_current_instruction_context()? + .get_program_key()?; + // The conversion from `PubkeyError` to `InstructionError` through + // num-traits is incorrect, but it's the existing behavior. + let signers = signer_seeds + .iter() + .map(|seeds| Pubkey::create_program_address(seeds, &caller_program_id)) + .collect::, solana_pubkey::PubkeyError>>() + .map_err(|e| e as u64)?; + self.prepare_next_cpi_instruction(instruction, &signers)?; + let mut compute_units_consumed = 0; + self.process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default())?; + Ok(()) + } + + /// Helper to prepare for process_instruction() when the instruction is not a top level one, + /// and depends on `AccountMeta`s + pub fn prepare_next_cpi_instruction( + &mut self, + instruction: Instruction, + signers: &[Pubkey], + ) -> Result<(), InstructionError> { + // We reference accounts by an u8 index, so we have a total of 256 accounts. + let mut transaction_callee_map: Vec = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + let mut instruction_accounts: Vec = + Vec::with_capacity(instruction.accounts.len()); + + // This code block is necessary to restrict the scope of the immutable borrow of + // transaction context (the `instruction_context` variable). At the end of this + // function, we must borrow it again as mutable. + let program_account_index = { + let instruction_context = self.transaction_context.get_current_instruction_context()?; + + for account_meta in instruction.accounts.iter() { + let index_in_transaction = self + .transaction_context + .find_index_of_account(&account_meta.pubkey) + .ok_or_else(|| { + ic_msg!( + self, + "Instruction references an unknown account {}", + account_meta.pubkey, + ); + InstructionError::MissingAccount + })?; + + debug_assert!((index_in_transaction as usize) < transaction_callee_map.len()); + let index_in_callee = transaction_callee_map + .get_mut(index_in_transaction as usize) + .unwrap(); + + if (*index_in_callee as usize) < instruction_accounts.len() { + let cloned_account = { + let instruction_account = instruction_accounts + .get_mut(*index_in_callee as usize) + .ok_or(InstructionError::MissingAccount)?; + instruction_account.set_is_signer( + instruction_account.is_signer() || account_meta.is_signer, + ); + instruction_account.set_is_writable( + instruction_account.is_writable() || account_meta.is_writable, + ); + *instruction_account + }; + instruction_accounts.push(cloned_account); + } else { + *index_in_callee = instruction_accounts.len() as u16; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction, + account_meta.is_signer, + account_meta.is_writable, + )); + } + } + + for current_index in 0..instruction_accounts.len() { + let instruction_account = instruction_accounts.get(current_index).unwrap(); + let index_in_callee = *transaction_callee_map + .get(instruction_account.index_in_transaction as usize) + .unwrap() as usize; + + if current_index != index_in_callee { + let (is_signer, is_writable) = { + let reference_account = instruction_accounts + .get(index_in_callee) + .ok_or(InstructionError::MissingAccount)?; + ( + reference_account.is_signer(), + reference_account.is_writable(), + ) + }; + + let current_account = instruction_accounts.get_mut(current_index).unwrap(); + current_account.set_is_signer(current_account.is_signer() || is_signer); + current_account.set_is_writable(current_account.is_writable() || is_writable); + // This account is repeated, so there is no need to check for permissions + continue; + } + + let index_in_caller = instruction_context.get_index_of_account_in_instruction( + instruction_account.index_in_transaction, + )?; + + // This unwrap is safe because instruction.accounts.len() == instruction_accounts.len() + let account_key = &instruction.accounts.get(current_index).unwrap().pubkey; + // get_index_of_account_in_instruction has already checked if the index is valid. + let caller_instruction_account = instruction_context + .instruction_accounts() + .get(index_in_caller as usize) + .unwrap(); + + // Readonly in caller cannot become writable in callee + if instruction_account.is_writable() && !caller_instruction_account.is_writable() { + ic_msg!(self, "{}'s writable privilege escalated", account_key,); + return Err(InstructionError::PrivilegeEscalation); + } + + // To be signed in the callee, + // it must be either signed in the caller or by the program + if instruction_account.is_signer() + && !(caller_instruction_account.is_signer() || signers.contains(account_key)) + { + ic_msg!(self, "{}'s signer privilege escalated", account_key,); + return Err(InstructionError::PrivilegeEscalation); + } + } + + // Find and validate executables / program accounts + let callee_program_id = &instruction.program_id; + let program_account_index_in_transaction = self + .transaction_context + .find_index_of_account(callee_program_id); + let program_account_index_in_instruction = program_account_index_in_transaction + .map(|index| instruction_context.get_index_of_account_in_instruction(index)); + + // We first check if the account exists in the transaction, and then see if it is part + // of the instruction. + if program_account_index_in_instruction.is_none() + || program_account_index_in_instruction.unwrap().is_err() + { + ic_msg!(self, "Unknown program {}", callee_program_id); + return Err(InstructionError::MissingAccount); + } + + // SAFETY: This unwrap is safe, because we checked the index in instruction in the + // previous if-condition. + program_account_index_in_transaction.unwrap() + }; + + // This ? operator should not error out because `fn get_current_instruction_index` is also called + // in `get_current_instruction_context` + let caller_index = self.transaction_context.get_current_instruction_index()?; + self.transaction_context.configure_instruction_at_index( + self.transaction_context.get_instruction_trace_length(), + program_account_index, + instruction_accounts, + transaction_callee_map, + Cow::Owned(instruction.data), + Some(caller_index as u16), + )?; + Ok(()) + } + + /// Prepare the instruction trace with all the top level instructions + pub fn prepare_top_level_instructions( + &mut self, + message: &'ix_data impl SVMMessage, + ) -> Result<(), (u8, InstructionError)> { + for (top_level_instruction_index, (_, instruction)) in + message.program_instructions_iter().enumerate() + { + let mut transaction_callee_map: Vec = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + + let mut instruction_accounts: Vec = + Vec::with_capacity(instruction.accounts.len()); + for index_in_transaction in instruction.accounts.iter() { + let index_in_callee = transaction_callee_map + .get_mut(*index_in_transaction as usize) + .expect("Invalid index in transaction"); + + if (*index_in_callee as usize) > instruction_accounts.len() { + *index_in_callee = instruction_accounts.len() as u16; + } + + let index_in_transaction = *index_in_transaction as usize; + instruction_accounts.push(InstructionAccount::new( + index_in_transaction as IndexOfAccount, + message.is_signer(index_in_transaction), + message.is_writable(index_in_transaction), + )); + } + + self.transaction_context + .configure_instruction_at_index( + top_level_instruction_index, + instruction.program_id_index as u16, + instruction_accounts, + transaction_callee_map, + Cow::Borrowed(instruction.data), + None, + ) + .map_err(|err| (top_level_instruction_index as u8, err))?; + } + Ok(()) + } + + /// Processes an instruction and returns how many compute units were used + pub fn process_instruction( + &mut self, + compute_units_consumed: &mut u64, + timings: &mut ExecuteTimings, + ) -> Result<(), InstructionError> { + *compute_units_consumed = 0; + self.push()?; + self.process_executable_chain(compute_units_consumed, timings) + // MUST pop if and only if `push` succeeded, independent of `result`. + // Thus, the `.and()` instead of an `.and_then()`. + .and(self.pop()) + } + + /// Processes a precompile instruction + pub fn process_precompile( + &mut self, + program_id: &Pubkey, + instruction_data: &[u8], + message_instruction_datas_iter: impl Iterator, + ) -> Result<(), InstructionError> { + self.push()?; + let instruction_datas: Vec<_> = message_instruction_datas_iter.collect(); + self.environment_config + .epoch_stake_callback + .process_precompile(program_id, instruction_data, instruction_datas) + .map_err(InstructionError::from) + .and(self.pop()) + } + + /// Calls the instruction's program entrypoint method + fn process_executable_chain( + &mut self, + compute_units_consumed: &mut u64, + timings: &mut ExecuteTimings, + ) -> Result<(), InstructionError> { + let instruction_context = self.transaction_context.get_current_instruction_context()?; + let process_executable_chain_time = Measure::start("process_executable_chain_time"); + + let builtin_id = { + let owner_id = instruction_context.get_program_owner()?; + if native_loader::check_id(&owner_id) { + *instruction_context.get_program_key()? + } else if bpf_loader_deprecated::check_id(&owner_id) + || bpf_loader::check_id(&owner_id) + || bpf_loader_upgradeable::check_id(&owner_id) + || loader_v4::check_id(&owner_id) + { + owner_id + } else { + return Err(InstructionError::UnsupportedProgramId); + } + }; + + // The Murmur3 hash value (used by RBPF) of the string "entrypoint" + const ENTRYPOINT_KEY: u32 = 0x71E3CF81; + let entry = self + .program_cache_for_tx_batch + .find(&builtin_id) + .ok_or(InstructionError::UnsupportedProgramId)?; + let function = match &entry.program { + ProgramCacheEntryType::Builtin(program) => program + .get_function_registry() + .lookup_by_key(ENTRYPOINT_KEY) + .map(|(_name, (function, _codegen))| function), + _ => None, + } + .ok_or(InstructionError::UnsupportedProgramId)?; + + let program_id = *instruction_context.get_program_key()?; + self.transaction_context + .set_return_data(program_id, Vec::new())?; + let logger = self.get_log_collector(); + stable_log::program_invoke(&logger, &program_id, self.get_stack_height()); + let pre_remaining_units = self.get_remaining(); + // For now, only built-ins are invoked from here, so the VM and its Config are irrelevant. + self.memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(0), + Vec::new(), + // SAFETY: + // This path invokes a builtin program, so this mapping is never used. + unsafe { + MemoryMapping::new(Vec::new(), &Config::default(), SBPFVersion::Reserved) + .unwrap() + }, + ))?; + let mut vm = EbpfVm::new( + Arc::clone( + &**self + .environment_config + .program_runtime_environments + .get_env_for_execution(), + ), + SBPFVersion::V0, + // Removes lifetime tracking + unsafe { std::mem::transmute::<&mut InvokeContext, &mut InvokeContext>(self) }, + 0, + ); + vm.invoke_function(function); + let result = match vm.program_result { + ProgramResult::Ok(_) => { + stable_log::program_success(&logger, &program_id); + Ok(()) + } + ProgramResult::Err(ref err) => { + if let EbpfError::SyscallError(syscall_error) = err { + if let Some(instruction_err) = syscall_error.downcast_ref::() + { + stable_log::program_failure(&logger, &program_id, instruction_err); + Err(instruction_err.clone()) + } else { + stable_log::program_failure(&logger, &program_id, syscall_error); + Err(InstructionError::ProgramFailedToComplete) + } + } else { + stable_log::program_failure(&logger, &program_id, err); + Err(InstructionError::ProgramFailedToComplete) + } + } + }; + let post_remaining_units = self.get_remaining(); + *compute_units_consumed = pre_remaining_units.saturating_sub(post_remaining_units); + + if builtin_id == program_id && result.is_ok() && *compute_units_consumed == 0 { + return Err(InstructionError::BuiltinProgramsMustConsumeComputeUnits); + } + + timings + .execute_accessories + .process_instructions + .process_executable_chain_us += process_executable_chain_time.end_as_us(); + result + } + + /// Get this invocation's LogCollector + pub fn get_log_collector(&self) -> Option>> { + self.log_collector.clone() + } + + #[cfg(feature = "dev-context-only-utils")] + pub fn set_alpenglow_migration_succeeded_for_tests(&mut self, succeeded: bool) { + self.environment_config.alpenglow_migration_succeeded = succeeded; + } + + /// Get this invocation's compute budget + pub fn get_compute_budget(&self) -> &SVMTransactionExecutionBudget { + &self.compute_budget + } + + /// Get this invocation's compute budget + pub fn get_execution_cost(&self) -> &SVMTransactionExecutionCost { + &self.execution_cost + } + + /// Get the current feature set. + pub fn get_feature_set(&self) -> &SVMFeatureSet { + self.environment_config.feature_set + } + + pub fn get_program_runtime_environment_for_deployment(&self) -> &ProgramRuntimeEnvironment { + self.environment_config + .program_runtime_environments + .get_env_for_deployment() + } + + pub fn is_deprecate_legacy_vote_ixs_active(&self) -> bool { + self.environment_config + .feature_set + .deprecate_legacy_vote_ixs + } + + pub fn is_alpenglow_migration_succeeded(&self) -> bool { + self.environment_config.alpenglow_migration_succeeded + } + + /// Get cached epoch total stake. + pub fn get_epoch_stake(&self) -> u64 { + self.environment_config + .epoch_stake_callback + .get_epoch_stake() + } + + /// Get cached stake for the epoch vote account. + pub fn get_epoch_stake_for_vote_account(&self, pubkey: &'a Pubkey) -> u64 { + self.environment_config + .epoch_stake_callback + .get_epoch_stake_for_vote_account(pubkey) + } + + pub fn is_precompile(&self, pubkey: &Pubkey) -> bool { + self.environment_config + .epoch_stake_callback + .is_precompile(pubkey) + } + + // Should alignment be enforced during user pointer translation + pub fn get_check_aligned(&self) -> bool { + self.transaction_context + .get_current_instruction_context() + .and_then(|instruction_context| { + let owner_id = instruction_context.get_program_owner(); + debug_assert!(owner_id.is_ok()); + owner_id + }) + .map(|owner_key| owner_key != bpf_loader_deprecated::id()) + .unwrap_or(true) + } + + /// Insert a VM register trace + pub fn insert_register_trace(&mut self, register_trace: Vec<[u64; 12]>) { + if register_trace.is_empty() { + return; + } + let Ok(instruction_context) = self.transaction_context.get_current_instruction_context() + else { + return; + }; + self.register_traces + .push((instruction_context.get_index_in_trace(), register_trace)); + } + + /// Iterates over all VM register traces (including CPI) + pub fn iterate_vm_traces( + &self, + callback: &dyn Fn(InstructionContext, &Executable, RegisterTrace), + ) { + for (index_in_trace, register_trace) in &self.register_traces { + let Ok(instruction_context) = self + .transaction_context + .get_instruction_context_at_index_in_trace(*index_in_trace) + else { + continue; + }; + let Ok(program_id) = instruction_context.get_program_key() else { + continue; + }; + let Some(entry) = self.program_cache_for_tx_batch.find(program_id) else { + continue; + }; + let ProgramCacheEntryType::Loaded(ref executable) = entry.program else { + continue; + }; + callback(instruction_context, executable, register_trace.as_slice()); + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +#[macro_export] +macro_rules! with_mock_invoke_context_with_feature_set { + ( + $invoke_context:ident, + $transaction_context:ident, + $feature_set:ident, + $top_level_instructions:literal, + $transaction_accounts:expr, + $all_accounts:expr $(,)? + ) => { + use { + solana_svm_callback::InvokeContextCallback, + solana_svm_log_collector::LogCollector, + $crate::{ + __private::{Hash, ReadableAccount, Rent, TransactionContext}, + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + sysvar_cache::SysvarCache, + }, + }; + + struct MockInvokeContextCallback {} + impl InvokeContextCallback for MockInvokeContextCallback {} + + let compute_budget = SVMTransactionExecutionBudget::new_with_defaults( + $feature_set.raise_cpi_nesting_limit_to_8, + ); + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + for (key, account) in $all_accounts.iter() { + if key == pubkey { + callback(account.data()); + } + } + }); + let mut $transaction_context = TransactionContext::new( + $transaction_accounts, + Rent::default(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + $top_level_instructions, + ); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockInvokeContextCallback {}, + $feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + let mut $invoke_context = InvokeContext::new( + &mut $transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + Some(LogCollector::new_ref()), + compute_budget, + SVMTransactionExecutionCost::default(), + ); + }; + ( + $invoke_context:ident, + $transaction_context:ident, + $feature_set:ident, + $top_level_instructions:literal, + $transaction_accounts:expr $(,)? + ) => { + let transaction_accounts: Vec<(solana_pubkey::Pubkey, solana_account::AccountSharedData)> = + $transaction_accounts; + $crate::with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + $feature_set, + $top_level_instructions, + transaction_accounts, + &transaction_accounts + ); + }; + ( + $invoke_context:ident, + $transaction_context:ident, + $feature_set:ident, + $transaction_accounts:expr $(,)? + ) => { + $crate::with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + $feature_set, + 1, + $transaction_accounts + ); + }; +} + +#[cfg(feature = "dev-context-only-utils")] +#[macro_export] +macro_rules! with_mock_invoke_context { + ( + $invoke_context:ident, + $transaction_context:ident, + $top_level_instructions:literal, + $transaction_accounts:expr $(,)? + ) => { + let feature_set = &solana_svm_feature_set::SVMFeatureSet::default(); + $crate::with_mock_invoke_context_with_feature_set!( + $invoke_context, + $transaction_context, + feature_set, + $top_level_instructions, + $transaction_accounts + ) + }; + ( + $invoke_context:ident, + $transaction_context:ident, + $transaction_accounts:expr $(,)? + ) => { + with_mock_invoke_context!( + $invoke_context, + $transaction_context, + 1, + $transaction_accounts + ); + }; +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn mock_compile_message( + instruction: &Instruction, + accounts: &[(Pubkey, A)], + program_id: &Pubkey, + loader_key: &Pubkey, +) -> Option<(SanitizedMessage, Vec<(Pubkey, AccountSharedData)>)> +where + AccountSharedData: From, + A: Clone, +{ + let message = Message::new(std::slice::from_ref(instruction), None); + let transaction_accounts: Vec<_> = message + .account_keys + .iter() + .map(|key| { + let account = accounts + .iter() + .find(|(k, _)| k == key) + .map(|(_, a)| AccountSharedData::from(a.clone())) + .unwrap_or_else(|| { + if key == program_id { + let mut account = AccountSharedData::new(0, 0, loader_key); + account.set_executable(true); + account + } else { + AccountSharedData::default() + } + }); + (*key, account) + }) + .collect(); + + let sanitized_message = SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + + Some((sanitized_message, transaction_accounts)) +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn mock_process_instruction_with_feature_set< + F: FnMut(&mut InvokeContext), + G: FnMut(&mut InvokeContext), +>( + program_id: &Pubkey, + instruction_data: &[u8], + mut accounts: Vec, + instruction_account_metas: Vec, + expected_result: Result<(), InstructionError>, + builtin: BuiltinFunctionRegisterer, + mut pre_adjustments: F, + mut post_adjustments: G, + feature_set: &SVMFeatureSet, +) -> Vec { + let original_len = accounts.len(); + if !accounts + .iter() + .any(|(key, _)| *key == sysvar::epoch_schedule::id()) + { + accounts.push(( + sysvar::epoch_schedule::id(), + create_account_shared_data_for_test(&EpochSchedule::default()), + )); + } + + let instruction = + Instruction::new_with_bytes(*program_id, instruction_data, instruction_account_metas); + let (sanitized_message, transaction_accounts) = + mock_compile_message(&instruction, &accounts, program_id, &native_loader::id()).unwrap(); + + let program_owner = accounts + .iter() + .find(|(key, _)| key == program_id) + .map(|(_, acct)| *acct.owner()) + .unwrap_or_else(native_loader::id); + let is_builtin = native_loader::check_id(&program_owner); + + with_mock_invoke_context_with_feature_set!( + invoke_context, + transaction_context, + feature_set, + 1, + transaction_accounts, + &accounts + ); + + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + if is_builtin { + *program_id + } else { + program_owner + }, + Arc::new(ProgramCacheEntry::new_builtin(0, 0, builtin)), + ); + program_cache_for_tx_batch.set_slot_for_tests( + invoke_context + .environment_config + .sysvar_cache() + .get_clock() + .map(|clock| clock.slot) + .unwrap_or(1), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + pre_adjustments(&mut invoke_context); + + invoke_context + .prepare_top_level_instructions(&sanitized_message) + .unwrap(); + + let result = invoke_context.process_instruction(&mut 0, &mut ExecuteTimings::default()); + assert_eq!(result, expected_result); + post_adjustments(&mut invoke_context); + + let txn_result_keys: Vec<_> = (0..transaction_context.get_number_of_accounts()) + .map(|i| *transaction_context.get_key_of_account_at_index(i).unwrap()) + .collect(); + let txn_result_accounts = transaction_context.deconstruct_without_keys().unwrap(); + let txn_result_map = txn_result_keys + .into_iter() + .zip(txn_result_accounts) + .collect::>(); + + accounts + .into_iter() + .take(original_len) + .map(|(key, original)| txn_result_map.get(&key).cloned().unwrap_or(original)) + .collect() +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn mock_process_instruction( + program_id: &Pubkey, + instruction_data: &[u8], + accounts: Vec, + instruction_account_metas: Vec, + expected_result: Result<(), InstructionError>, + builtin: BuiltinFunctionRegisterer, + pre_adjustments: F, + post_adjustments: G, +) -> Vec { + mock_process_instruction_with_feature_set( + program_id, + instruction_data, + accounts, + instruction_account_metas, + expected_result, + builtin, + pre_adjustments, + post_adjustments, + &SVMFeatureSet::all_enabled(), + ) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::execution_budget::{ + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_INSTRUCTION_STACK_DEPTH, + MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268, + }, + serde::{Deserialize, Serialize}, + solana_account::Account, + solana_keypair::Keypair, + solana_rent::Rent, + solana_sbpf::program::BuiltinFunctionDefinition, + solana_sdk_ids::system_program, + solana_signer::Signer, + solana_svm_feature_set::SVMFeatureSet, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::MAX_ACCOUNTS_PER_INSTRUCTION, + test_case::test_case, + }; + + #[derive(Debug, Serialize, Deserialize)] + enum MockInstruction { + NoopSuccess, + NoopFail, + ModifyOwned, + ModifyNotOwned, + ModifyReadonly, + UnbalancedPush, + UnbalancedPop, + ConsumeComputeUnits { + compute_units_to_consume: u64, + desired_result: Result<(), InstructionError>, + }, + Resize { + new_len: u64, + }, + } + + const MOCK_BUILTIN_COMPUTE_UNIT_COST: u64 = 1; + + declare_process_instruction!( + MockBuiltin, + MOCK_BUILTIN_COMPUTE_UNIT_COST, + |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + let program_id = instruction_context.get_program_key()?; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new(instruction_account_index, false, false) + }) + .collect::>(); + assert_eq!( + program_id, + instruction_context + .try_borrow_instruction_account(0)? + .get_owner() + ); + assert_ne!( + instruction_context + .try_borrow_instruction_account(1)? + .get_owner(), + instruction_context.get_key_of_instruction_account(0)? + ); + + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockInstruction::NoopSuccess => (), + MockInstruction::NoopFail => return Err(InstructionError::GenericError), + MockInstruction::ModifyOwned => instruction_context + .try_borrow_instruction_account(0)? + .set_data_from_slice(&[1])?, + MockInstruction::ModifyNotOwned => instruction_context + .try_borrow_instruction_account(1)? + .set_data_from_slice(&[1])?, + MockInstruction::ModifyReadonly => instruction_context + .try_borrow_instruction_account(2)? + .set_data_from_slice(&[1])?, + MockInstruction::UnbalancedPush => { + instruction_context + .try_borrow_instruction_account(0)? + .checked_add_lamports(1)?; + let program_id = *transaction_context.get_key_of_account_at_index(3)?; + let metas = vec![ + AccountMeta::new_readonly( + *transaction_context.get_key_of_account_at_index(0)?, + false, + ), + AccountMeta::new_readonly( + *transaction_context.get_key_of_account_at_index(1)?, + false, + ), + ]; + let inner_instruction = Instruction::new_with_bincode( + program_id, + &MockInstruction::NoopSuccess, + metas, + ); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 3, + instruction_accounts, + vec![], + ) + .unwrap(); + let result = invoke_context.push(); + assert_eq!(result, Err(InstructionError::UnbalancedInstruction)); + result?; + invoke_context + .native_invoke_signed(inner_instruction, &[]) + .and(invoke_context.pop())?; + } + MockInstruction::UnbalancedPop => instruction_context + .try_borrow_instruction_account(0)? + .checked_add_lamports(1)?, + MockInstruction::ConsumeComputeUnits { + compute_units_to_consume, + desired_result, + } => { + invoke_context + .compute_meter + .consume_checked(compute_units_to_consume) + .map_err(|_| InstructionError::ComputationalBudgetExceeded)?; + return desired_result; + } + MockInstruction::Resize { new_len } => instruction_context + .try_borrow_instruction_account(0)? + .set_data_from_slice(&vec![0; new_len as usize])?, + } + } else { + return Err(InstructionError::InvalidInstructionData); + } + Ok(()) + } + ); + + #[test_case(false; "SIMD-0268 disabled")] + #[test_case(true; "SIMD-0268 enabled")] + fn test_instruction_stack_height(simd_0268_active: bool) { + let feature_set = &SVMFeatureSet { + raise_cpi_nesting_limit_to_8: simd_0268_active, + ..SVMFeatureSet::all_enabled() + }; + let max_depth = SVMTransactionExecutionBudget::new_with_defaults(simd_0268_active) + .max_instruction_stack_depth; + assert_eq!( + max_depth, + if simd_0268_active { + MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268 + } else { + MAX_INSTRUCTION_STACK_DEPTH + }, + ); + + // Set up max_depth + 1 accounts (one extra to trigger the failing push) + // and a matching program account for each. + let mut invoke_stack = vec![]; + let mut transaction_accounts = vec![]; + let mut instruction_accounts = vec![]; + for index in 0..max_depth.saturating_add(1) { + let program_id = solana_pubkey::new_rand(); + invoke_stack.push(program_id); + transaction_accounts.push(( + solana_pubkey::new_rand(), + AccountSharedData::new(1, 1, &program_id), + )); + instruction_accounts.push(InstructionAccount::new( + index as IndexOfAccount, + false, + true, + )); + } + + // Append program accounts after the regular accounts so that + // `first_program_account + depth` indexes the right program. + let first_program_account = transaction_accounts.len(); + for (index, program_id) in invoke_stack.iter().enumerate() { + transaction_accounts.push(( + *program_id, + AccountSharedData::new(1, 1, &solana_pubkey::Pubkey::default()), + )); + instruction_accounts.push(InstructionAccount::new( + index as IndexOfAccount, + false, + false, + )); + } + with_mock_invoke_context_with_feature_set!( + invoke_context, + transaction_context, + feature_set, + transaction_accounts, + ); + + // Each push must succeed and the stack height must track. + for depth in 0..max_depth { + assert_eq!(invoke_context.get_stack_height(), depth); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + (first_program_account.saturating_add(depth)) as IndexOfAccount, + instruction_accounts.clone(), + vec![], + ) + .unwrap(); + assert!( + invoke_context.push().is_ok(), + "push at depth {depth} should succeed (max_depth={max_depth})", + ); + } + + // At exactly max_depth, one more push must fail with CallDepth. + assert_eq!(invoke_context.get_stack_height(), max_depth); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + (first_program_account.saturating_add(max_depth)) as IndexOfAccount, + instruction_accounts.clone(), + vec![], + ) + .unwrap(); + assert_eq!(invoke_context.push(), Err(InstructionError::CallDepth),); + + // Stack height must not have changed after the rejected push. + assert_eq!(invoke_context.get_stack_height(), max_depth); + } + + #[test] + fn test_max_instruction_trace_length_top_level() { + const MAX_INSTRUCTIONS: usize = 8; + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )], + Rent::default(), + 1, + MAX_INSTRUCTIONS, + MAX_INSTRUCTIONS, + ); + for _ in 0..MAX_INSTRUCTIONS { + transaction_context.push().unwrap(); + transaction_context + .configure_top_level_instruction_for_tests( + 0, + vec![InstructionAccount::new(0, false, false)], + vec![], + ) + .unwrap(); + transaction_context.pop().unwrap(); + } + assert_eq!( + transaction_context.push(), + Err(InstructionError::MaxInstructionTraceLengthExceeded) + ); + } + + #[test] + fn test_max_instruction_trace_length_cpi() { + // Hitting the limit with CPIs + const MAX_INSTRUCTIONS: usize = 8; + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )], + Rent::default(), + 256, + MAX_INSTRUCTIONS, + 2, + ); + + transaction_context + .configure_instruction_at_index( + 0, + 0, + vec![InstructionAccount::new(0, false, false)], + vec![u16::MAX; 256], + Cow::Owned(Vec::new()), + None, + ) + .unwrap(); + + transaction_context + .configure_instruction_at_index( + 1, + 0, + vec![InstructionAccount::new(0, false, false)], + vec![u16::MAX; 256], + Cow::Owned(Vec::new()), + None, + ) + .unwrap(); + + for _ in 0..MAX_INSTRUCTIONS { + transaction_context.push().unwrap(); + transaction_context + .configure_next_cpi_for_tests( + 0, + vec![InstructionAccount::new(0, false, false)], + Vec::new(), + ) + .unwrap(); + } + + assert_eq!( + transaction_context.push(), + Err(InstructionError::MaxInstructionTraceLengthExceeded) + ); + } + + #[test_case(MockInstruction::NoopSuccess, Ok(()); "NoopSuccess")] + #[test_case(MockInstruction::NoopFail, Err(InstructionError::GenericError); "NoopFail")] + #[test_case(MockInstruction::ModifyOwned, Ok(()); "ModifyOwned")] + #[test_case(MockInstruction::ModifyNotOwned, Err(InstructionError::ExternalAccountDataModified); "ModifyNotOwned")] + #[test_case(MockInstruction::ModifyReadonly, Err(InstructionError::ReadonlyDataModified); "ModifyReadonly")] + #[test_case(MockInstruction::UnbalancedPush, Err(InstructionError::UnbalancedInstruction); "UnbalancedPush")] + #[test_case(MockInstruction::UnbalancedPop, Err(InstructionError::UnbalancedInstruction); "UnbalancedPop")] + fn test_process_instruction_account_modifications( + instruction: MockInstruction, + expected_result: Result<(), InstructionError>, + ) { + let callee_program_id = solana_pubkey::new_rand(); + let owned_account = AccountSharedData::new(42, 1, &callee_program_id); + let not_owned_account = AccountSharedData::new(84, 1, &solana_pubkey::new_rand()); + let readonly_account = AccountSharedData::new(168, 1, &solana_pubkey::new_rand()); + let loader_account = AccountSharedData::new(0, 1, &native_loader::id()); + let mut program_account = AccountSharedData::new(1, 1, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (solana_pubkey::new_rand(), owned_account), + (solana_pubkey::new_rand(), not_owned_account), + (solana_pubkey::new_rand(), readonly_account), + (callee_program_id, program_account), + (solana_pubkey::new_rand(), loader_account), + ]; + let metas = vec![ + AccountMeta::new(transaction_accounts.first().unwrap().0, false), + AccountMeta::new(transaction_accounts.get(1).unwrap().0, false), + AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false), + ]; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new( + instruction_account_index, + false, + instruction_account_index < 2, + ) + }) + .collect::>(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + callee_program_id, + Arc::new(ProgramCacheEntry::new_builtin(0, 1, MockBuiltin::register)), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + // Account modification tests + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(4, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let inner_instruction = + Instruction::new_with_bincode(callee_program_id, &instruction, metas); + let result = invoke_context + .native_invoke_signed(inner_instruction, &[]) + .and(invoke_context.pop()); + assert_eq!(result, expected_result); + } + + #[test_case(Ok(()); "Ok")] + #[test_case(Err(InstructionError::GenericError); "GenericError")] + fn test_process_instruction_compute_unit_consumption( + expected_result: Result<(), InstructionError>, + ) { + let callee_program_id = solana_pubkey::new_rand(); + let owned_account = AccountSharedData::new(42, 1, &callee_program_id); + let not_owned_account = AccountSharedData::new(84, 1, &solana_pubkey::new_rand()); + let readonly_account = AccountSharedData::new(168, 1, &solana_pubkey::new_rand()); + let loader_account = AccountSharedData::new(0, 1, &native_loader::id()); + let mut program_account = AccountSharedData::new(1, 1, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (solana_pubkey::new_rand(), owned_account), + (solana_pubkey::new_rand(), not_owned_account), + (solana_pubkey::new_rand(), readonly_account), + (callee_program_id, program_account), + (solana_pubkey::new_rand(), loader_account), + ]; + let metas = vec![ + AccountMeta::new(transaction_accounts.first().unwrap().0, false), + AccountMeta::new(transaction_accounts.get(1).unwrap().0, false), + AccountMeta::new_readonly(transaction_accounts.get(2).unwrap().0, false), + ]; + let instruction_accounts = (0..4) + .map(|instruction_account_index| { + InstructionAccount::new( + instruction_account_index, + false, + instruction_account_index < 2, + ) + }) + .collect::>(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + callee_program_id, + Arc::new(ProgramCacheEntry::new_builtin(0, 1, MockBuiltin::register)), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + // Compute unit consumption tests + let compute_units_to_consume = 10; + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(4, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let inner_instruction = Instruction::new_with_bincode( + callee_program_id, + &MockInstruction::ConsumeComputeUnits { + compute_units_to_consume, + desired_result: expected_result.clone(), + }, + metas, + ); + invoke_context + .prepare_next_cpi_instruction(inner_instruction, &[]) + .unwrap(); + + let mut compute_units_consumed = 0; + let result = invoke_context + .process_instruction(&mut compute_units_consumed, &mut ExecuteTimings::default()); + + // Because the instruction had compute cost > 0, then regardless of the execution result, + // the number of compute units consumed should be a non-default which is something greater + // than zero. + assert!(compute_units_consumed > 0); + assert_eq!( + compute_units_consumed, + compute_units_to_consume.saturating_add(MOCK_BUILTIN_COMPUTE_UNIT_COST), + ); + assert_eq!(result, expected_result); + + invoke_context.pop().unwrap(); + } + + #[test] + fn test_invoke_context_compute_budget() { + let transaction_accounts = vec![(solana_pubkey::new_rand(), AccountSharedData::default())]; + let execution_budget = SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ..SVMTransactionExecutionBudget::default() + }; + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context.compute_budget = execution_budget; + + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(0, vec![], vec![]) + .unwrap(); + invoke_context.push().unwrap(); + assert_eq!(*invoke_context.get_compute_budget(), execution_budget); + invoke_context.pop().unwrap(); + } + + #[test_case(0; "Resize the account to *the same size*, so not consuming any additional size")] + #[test_case(1; "Resize the account larger")] + #[test_case(-1; "Resize the account smaller")] + fn test_process_instruction_accounts_resize_delta(resize_delta: i64) { + let program_key = Pubkey::new_unique(); + let user_account_data_len = 123u64; + let user_account = + AccountSharedData::new(100, user_account_data_len as usize, &program_key); + let dummy_account = AccountSharedData::new(10, 0, &program_key); + let mut program_account = AccountSharedData::new(500, 500, &native_loader::id()); + program_account.set_executable(true); + let transaction_accounts = vec![ + (Pubkey::new_unique(), user_account), + (Pubkey::new_unique(), dummy_account), + (program_key, program_account), + ]; + let instruction_accounts = vec![ + InstructionAccount::new(0, false, true), + InstructionAccount::new(1, false, false), + ]; + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + program_key, + Arc::new(ProgramCacheEntry::new_builtin(0, 0, MockBuiltin::register)), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + let new_len = (user_account_data_len as i64).saturating_add(resize_delta) as u64; + let instruction_data = bincode::serialize(&MockInstruction::Resize { new_len }).unwrap(); + + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(2, instruction_accounts, instruction_data) + .unwrap(); + let result = invoke_context.process_instruction(&mut 0, &mut ExecuteTimings::default()); + + assert!(result.is_ok()); + assert_eq!( + invoke_context.transaction_context.accounts().resize_delta(), + resize_delta + ); + } + + #[test] + fn test_prepare_instruction_maximum_accounts() { + const MAX_ACCOUNTS_REFERENCED: usize = u16::MAX as usize; + let mut transaction_accounts: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_TRANSACTION); + let mut account_metas: Vec = Vec::with_capacity(MAX_ACCOUNTS_REFERENCED); + + // Fee-payer + let fee_payer = Keypair::new(); + transaction_accounts.push(( + fee_payer.pubkey(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )); + account_metas.push(AccountMeta::new(fee_payer.pubkey(), true)); + + let program_id = Pubkey::new_unique(); + let mut program_account = AccountSharedData::new(1, 1, &Pubkey::new_unique()); + program_account.set_executable(true); + transaction_accounts.push((program_id, program_account)); + account_metas.push(AccountMeta::new_readonly(program_id, false)); + + for i in 2..MAX_ACCOUNTS_REFERENCED { + // Let's reference 256 unique accounts, and the rest is repeated. + if i < MAX_ACCOUNTS_PER_TRANSACTION { + let key = Pubkey::new_unique(); + transaction_accounts + .push((key, AccountSharedData::new(1, 1, &Pubkey::new_unique()))); + account_metas.push(AccountMeta::new_readonly(key, false)); + } else { + let repeated_key = transaction_accounts + .get(i % MAX_ACCOUNTS_PER_TRANSACTION) + .unwrap() + .0; + account_metas.push(AccountMeta::new_readonly(repeated_key, false)); + } + } + + with_mock_invoke_context!(invoke_context, transaction_context, 2, transaction_accounts); + + let instruction_1 = Instruction::new_with_bytes(program_id, &[20], account_metas.clone()); + + let instruction_2 = Instruction::new_with_bytes( + program_id, + &[20], + account_metas.iter().rev().cloned().collect(), + ); + + let transaction = Transaction::new_with_payer( + &[instruction_1.clone(), instruction_2.clone()], + Some(&fee_payer.pubkey()), + ); + + let sanitized = + SanitizedTransaction::try_from_legacy_transaction(transaction, &HashSet::new()) + .unwrap(); + + fn test_case_1(invoke_context: &InvokeContext) { + let instruction_context = invoke_context + .transaction_context + .get_next_instruction_context() + .unwrap(); + for index_in_instruction in 0..MAX_ACCOUNTS_REFERENCED as IndexOfAccount { + let index_in_transaction = instruction_context + .get_index_of_instruction_account_in_transaction(index_in_instruction) + .unwrap(); + let other_ix_index = instruction_context + .get_index_of_account_in_instruction(index_in_transaction) + .unwrap(); + if (index_in_instruction as usize) < MAX_ACCOUNTS_PER_TRANSACTION { + assert_eq!(index_in_instruction, index_in_transaction); + assert_eq!(index_in_instruction, other_ix_index); + } else { + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + index_in_transaction as usize + ); + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + other_ix_index as usize + ); + } + } + } + + fn test_case_2(invoke_context: &InvokeContext) { + let instruction_context = invoke_context + .transaction_context + .get_next_instruction_context() + .unwrap(); + for index_in_instruction in 0..MAX_ACCOUNTS_REFERENCED as IndexOfAccount { + let index_in_transaction = instruction_context + .get_index_of_instruction_account_in_transaction(index_in_instruction) + .unwrap(); + let other_ix_index = instruction_context + .get_index_of_account_in_instruction(index_in_transaction) + .unwrap(); + assert_eq!( + index_in_transaction, + (MAX_ACCOUNTS_REFERENCED as u16) + .saturating_sub(index_in_instruction) + .saturating_sub(1) + .overflowing_rem(MAX_ACCOUNTS_PER_TRANSACTION as u16) + .0 + ); + if (index_in_instruction as usize) < MAX_ACCOUNTS_PER_TRANSACTION { + assert_eq!(index_in_instruction, other_ix_index); + } else { + assert_eq!( + index_in_instruction as usize % MAX_ACCOUNTS_PER_TRANSACTION, + other_ix_index as usize + ); + } + } + } + + invoke_context + .prepare_top_level_instructions(&sanitized) + .unwrap(); + + test_case_1(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + invoke_context.transaction_context.pop().unwrap(); + + test_case_2(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + invoke_context + .prepare_next_cpi_instruction(instruction_1, &[fee_payer.pubkey()]) + .unwrap(); + test_case_1(&invoke_context); + + invoke_context.transaction_context.push().unwrap(); + invoke_context + .prepare_next_cpi_instruction(instruction_2, &[fee_payer.pubkey()]) + .unwrap(); + test_case_2(&invoke_context); + } + + #[test] + fn test_duplicated_accounts() { + let mut transaction_accounts: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_TRANSACTION); + let mut account_metas: Vec = + Vec::with_capacity(MAX_ACCOUNTS_PER_INSTRUCTION.saturating_sub(1)); + + // Fee-payer + let fee_payer = Keypair::new(); + transaction_accounts.push(( + fee_payer.pubkey(), + AccountSharedData::new(1, 1, &Pubkey::new_unique()), + )); + account_metas.push(AccountMeta::new(fee_payer.pubkey(), true)); + + let program_id = Pubkey::new_unique(); + let mut program_account = AccountSharedData::new(1, 1, &Pubkey::new_unique()); + program_account.set_executable(true); + transaction_accounts.push((program_id, program_account)); + account_metas.push(AccountMeta::new_readonly(program_id, false)); + + for i in 2..account_metas.capacity() { + if i % 2 == 0 { + let key = Pubkey::new_unique(); + transaction_accounts + .push((key, AccountSharedData::new(1, 1, &Pubkey::new_unique()))); + account_metas.push(AccountMeta::new_readonly(key, false)); + } else { + let last_key = transaction_accounts.last().unwrap().0; + account_metas.push(AccountMeta::new_readonly(last_key, false)); + } + } + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + + let instruction = Instruction::new_with_bytes(program_id, &[20], account_metas.clone()); + + let transaction = Transaction::new_with_payer(&[instruction], Some(&fee_payer.pubkey())); + + let sanitized = + SanitizedTransaction::try_from_legacy_transaction(transaction, &HashSet::new()) + .unwrap(); + + invoke_context + .prepare_top_level_instructions(&sanitized) + .unwrap(); + + { + let instruction_context = invoke_context + .transaction_context + .get_next_instruction_context() + .unwrap(); + for index_in_instruction in 2..account_metas.len() as IndexOfAccount { + let is_duplicate = instruction_context + .is_instruction_account_duplicate(index_in_instruction) + .unwrap(); + if index_in_instruction % 2 == 0 { + assert!(is_duplicate.is_none()); + } else { + assert_eq!(is_duplicate, Some(index_in_instruction.saturating_sub(1))); + } + } + } + + invoke_context.transaction_context.push().unwrap(); + + let instruction = Instruction::new_with_bytes( + program_id, + &[20], + account_metas.iter().cloned().rev().collect(), + ); + + invoke_context + .prepare_next_cpi_instruction(instruction, &[fee_payer.pubkey()]) + .unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_next_instruction_context() + .unwrap(); + for index_in_instruction in 2..account_metas.len().saturating_sub(1) as u16 { + let is_duplicate = instruction_context + .is_instruction_account_duplicate(index_in_instruction) + .unwrap(); + if index_in_instruction % 2 == 0 { + assert!(is_duplicate.is_none()); + } else { + assert_eq!(is_duplicate, Some(index_in_instruction.saturating_sub(1))); + } + } + } + + // Used for native_invoke_signed tests below. + const TEST_CALLER_PROGRAM_ID: Pubkey = Pubkey::new_from_array([1u8; 32]); + const TEST_CALLEE_PROGRAM_ID: Pubkey = Pubkey::new_from_array([2u8; 32]); + const TEST_WRONG_PROGRAM_ID: Pubkey = Pubkey::new_from_array([3u8; 32]); + const TEST_MOCK_EXTRA_KEY: Pubkey = Pubkey::new_from_array([4u8; 32]); + const TEST_ACCOUNT_KEY: Pubkey = Pubkey::new_from_array([5u8; 32]); + + /// Runs a `native_invoke_signed` call with the standard test setup and returns + /// the result. + /// + /// Same layout for all tests: + /// 0: target account (writable, signer iff `target_is_signer`) + /// 1: caller program (executable) + /// 2: mock extra (satisfies MockBuiltin's 2-account requirement) + /// 3: callee program (executable) + fn run_native_invoke_signed_test( + target_key: Pubkey, + target_is_signer: bool, + inner_instruction: Instruction, + signer_seeds: &[&[&[u8]]], + ) -> Result<(), InstructionError> { + let target_account = AccountSharedData::new(100, 0, &TEST_CALLEE_PROGRAM_ID); + let mock_extra_account = AccountSharedData::new(0, 1, &system_program::id()); + let mut caller_program_account = AccountSharedData::new(1, 1, &native_loader::id()); + caller_program_account.set_executable(true); + let mut callee_program_account = AccountSharedData::new(1, 1, &native_loader::id()); + callee_program_account.set_executable(true); + let transaction_accounts = vec![ + (target_key, target_account), + (TEST_CALLER_PROGRAM_ID, caller_program_account), + (TEST_MOCK_EXTRA_KEY, mock_extra_account), + (TEST_CALLEE_PROGRAM_ID, callee_program_account), + ]; + + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + TEST_CALLEE_PROGRAM_ID, + Arc::new(ProgramCacheEntry::new_builtin(0, 1, MockBuiltin::register)), + ); + invoke_context.program_cache_for_tx_batch = &mut program_cache_for_tx_batch; + + let instruction_accounts = (0..4) + .map(|i| InstructionAccount::new(i, i == 0 && target_is_signer, i < 2)) + .collect::>(); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(1, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + + let result = invoke_context.native_invoke_signed(inner_instruction, signer_seeds); + invoke_context.pop().unwrap(); + result + } + + // Valid PDA seeds grant signer privilege to the derived address. + #[test] + fn test_native_invoke_signed_with_valid_pda_signer() { + let (pda_key, bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![ + AccountMeta::new(pda_key, true), + AccountMeta::new_readonly(TEST_MOCK_EXTRA_KEY, false), + ], + ); + let result = + run_native_invoke_signed_test(pda_key, false, instruction, &[&[b"seed", &[bump_seed]]]); + assert!( + result.is_ok(), + "valid PDA signer should succeed: {result:?}" + ); + } + + // Oversized seeds (>MAX_SEED_LEN) hit `MaxSeedLengthExceeded` + // (discriminant 0) which the broken `as u64` num-traits conversion + // maps to `Custom(0)`. + #[test] + fn test_native_invoke_signed_with_invalid_seeds() { + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(TEST_ACCOUNT_KEY, true)], + ); + let oversized_seed = [0u8; 33]; + let result = run_native_invoke_signed_test( + TEST_ACCOUNT_KEY, + false, + instruction, + &[&[&oversized_seed]], + ); + assert_eq!(result, Err(InstructionError::Custom(0))); + } + + // CPI marks an account as signer but caller provides no seeds — + // signer privilege escalation. + #[test] + fn test_native_invoke_signed_pda_privilege_escalation_without_seeds() { + let (pda_key, _bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(pda_key, true)], + ); + let result = run_native_invoke_signed_test(pda_key, false, instruction, &[]); + assert_eq!(result, Err(InstructionError::PrivilegeEscalation)); + } + + // Seeds valid for a different program ID don't grant signer privilege + // because native_invoke_signed derives against the caller's own program ID. + #[test] + fn test_native_invoke_signed_uses_caller_program_id_for_pda() { + let (pda_key, bump_seed) = Pubkey::find_program_address(&[b"seed"], &TEST_WRONG_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![AccountMeta::new(pda_key, true)], + ); + let result = + run_native_invoke_signed_test(pda_key, false, instruction, &[&[b"seed", &[bump_seed]]]); + assert_eq!(result, Err(InstructionError::PrivilegeEscalation)); + } + + // Top-level signer privilege carries through CPI without needing seeds. + #[test] + fn test_native_invoke_signed_top_level_signer_needs_no_seeds() { + let (pda_key, _bump_seed) = + Pubkey::find_program_address(&[b"seed"], &TEST_CALLER_PROGRAM_ID); + let instruction = Instruction::new_with_bincode( + TEST_CALLEE_PROGRAM_ID, + &MockInstruction::NoopSuccess, + vec![ + AccountMeta::new(pda_key, true), + AccountMeta::new_readonly(TEST_MOCK_EXTRA_KEY, false), + ], + ); + let result = run_native_invoke_signed_test(pda_key, true, instruction, &[]); + assert!( + result.is_ok(), + "top-level signer should not need seeds: {result:?}" + ); + } + + #[test] + fn test_compile_message() { + let program_id = Pubkey::new_from_array([1u8; 32]); + let writable = Pubkey::new_from_array([2u8; 32]); + let loader_key = Pubkey::new_from_array([3u8; 32]); + + let instruction = Instruction { + program_id, + accounts: vec![AccountMeta::new(writable, false)], + data: vec![1, 2, 3], + }; + + let accounts = vec![( + writable, + Account { + lamports: 100, + ..Account::default() + }, + )]; + + let (message, tx_accounts) = + mock_compile_message(&instruction, &accounts, &program_id, &loader_key).unwrap(); + + assert_eq!(message.instructions().len(), 1); + assert_eq!(tx_accounts.len(), 2); + assert_eq!(tx_accounts.first().unwrap().0, writable); + assert_eq!(tx_accounts.get(1).unwrap().0, program_id); + + // Verify the writable account is NOT promoted to signer. + assert!(!message.is_signer(0)); + } +} diff --git a/solana/program-runtime/src/lib.rs b/solana/program-runtime/src/lib.rs new file mode 100644 index 0000000..4bc7e25 --- /dev/null +++ b/solana/program-runtime/src/lib.rs @@ -0,0 +1,32 @@ +#![cfg(feature = "agave-unstable-api")] +#![deny(clippy::arithmetic_side_effects)] +#![deny(clippy::indexing_slicing)] + +pub use solana_sbpf; +pub mod cpi; +pub mod deploy; +pub mod execution_budget; +pub mod invoke_context; +pub mod loaded_programs; +pub mod loading_task; +pub mod mem_pool; +pub mod memory; +pub mod memory_context; +pub mod program_cache_entry; +pub mod program_metrics; +pub mod serialization; +pub mod stable_log; +pub mod sysvar_cache; +pub mod vm; + +// re-exports for macros +pub mod __private { + pub use { + crate::vm::{MEMORY_POOL, calculate_heap_cost, create_vm}, + solana_account::ReadableAccount, + solana_hash::Hash, + solana_instruction::error::InstructionError, + solana_rent::Rent, + solana_transaction_context::transaction::TransactionContext, + }; +} diff --git a/solana/program-runtime/src/loaded_programs.rs b/solana/program-runtime/src/loaded_programs.rs new file mode 100644 index 0000000..183f98c --- /dev/null +++ b/solana/program-runtime/src/loaded_programs.rs @@ -0,0 +1,2480 @@ +use { + crate::{ + invoke_context::InvokeContext, + loading_task::LoadingTaskWaiter, + program_cache_entry::{ProgramCacheEntry, ProgramCacheEntryType, retention_score}, + program_metrics::{EMA_SCALE, ProgramCacheStats}, + }, + log::error, + percentage::PercentageInteger, + solana_clock::{Epoch, Slot}, + solana_pubkey::Pubkey, + solana_sbpf::program::BuiltinProgram, + solana_svm_type_overrides::{ + rand::{Rng, rng}, + sync::{Arc, Mutex, RwLock, atomic::Ordering}, + thread, + }, + std::{ + collections::{HashMap, hash_map::Entry}, + sync::Weak, + }, +}; + +#[repr(transparent)] +#[derive(Clone, Debug)] +pub struct ProgramRuntimeEnvironment(Arc>>); +impl std::hash::Hash for ProgramRuntimeEnvironment { + fn hash(&self, state: &mut H) { + Arc::>>::as_ptr(&self.0).hash(state); + } +} +impl PartialEq for ProgramRuntimeEnvironment { + fn eq(&self, other: &Self) -> bool { + Arc::ptr_eq(&self.0, &other.0) + } +} +impl Eq for ProgramRuntimeEnvironment {} +impl std::ops::Deref for ProgramRuntimeEnvironment { + type Target = Arc>>; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} +impl ProgramRuntimeEnvironment { + pub fn from(inner: BuiltinProgram>) -> Self { + Self(Arc::new(inner)) + } + + pub const fn from_ref<'a>( + inner: &'a Arc>>, + ) -> &'a Self { + // Safety: This wrapper type is transparent and shares the same representation as the underlying type + unsafe { std::mem::transmute(inner) } + } +} + +/// Paired execution and deployment environments. +/// +/// Registered functions within each program runtime environment (syscalls) +/// depend on per-epoch feature gate statuses. In most cases, the list of +/// registered functions in the two environments will be the same. However, +/// it's possible that the effective epoch of deployment could be in the +/// *next epoch*. +pub struct ProgramRuntimeEnvironments { + /// Environment compiled for the current epoch in which programs are + /// executing. + execution: ProgramRuntimeEnvironment, + /// Environment compiled for the epoch of the next slot at which a program + /// deployed in the current slot will execute. + deployment: ProgramRuntimeEnvironment, +} + +impl ProgramRuntimeEnvironments { + /// Create a new ProgramRuntimeEnvironments from an `execution` and + /// `deployment` environment. + pub fn new( + execution: ProgramRuntimeEnvironment, + deployment: ProgramRuntimeEnvironment, + ) -> Self { + Self { + execution, + deployment, + } + } + + /// Get the program runtime environment for execution. + pub fn get_env_for_execution(&self) -> &ProgramRuntimeEnvironment { + &self.execution + } + + /// Get the program runtime environment for deployment. + pub fn get_env_for_deployment(&self) -> &ProgramRuntimeEnvironment { + &self.deployment + } + + #[cfg(feature = "dev-context-only-utils")] + pub fn mock() -> Self { + Self { + execution: get_mock_program_runtime_environment(), + deployment: get_mock_program_runtime_environment(), + } + } +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn get_mock_program_runtime_environment() -> ProgramRuntimeEnvironment { + static MOCK_ENVIRONMENT: std::sync::OnceLock = + std::sync::OnceLock::::new(); + MOCK_ENVIRONMENT + .get_or_init(|| ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock())) + .clone() +} + +pub const MAX_LOADED_ENTRY_COUNT: usize = 512; + +/// Relationship between two fork IDs +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum BlockRelation { + /// The slot is on the same fork and is an ancestor of the other slot + Ancestor, + /// The two slots are equal and are on the same fork + Equal, + /// The slot is on the same fork and is a descendant of the other slot + Descendant, + /// The slots are on two different forks and may have had a common ancestor at some point + Unrelated, + /// Either one or both of the slots are either older than the latest root, or are in future + Unknown, +} + +/// Maps relationship between two slots. +pub trait ForkGraph { + /// Returns the BlockRelation of A to B + fn relationship(&self, a: Slot, b: Slot) -> BlockRelation; +} + +/// Globally manages the transition between environments at the epoch boundary +#[derive(Debug, Default)] +pub struct EpochBoundaryPreparation { + /// The epoch of the upcoming_environment + pub upcoming_epoch: Epoch, + /// Anticipated replacement for `environments` at the next epoch + /// + /// This is `None` during most of an epoch, and only `Some` around the boundaries (at the end and beginning of an epoch). + /// More precisely, it starts with the cache preparation phase a few hundred slots before the epoch boundary, + /// and it ends with the first rerooting after the epoch boundary. + pub upcoming_environment: Option, + /// List of loaded programs which should be recompiled before the next epoch (but don't have to). + pub programs_to_recompile: Vec<(Pubkey, Arc)>, +} + +impl EpochBoundaryPreparation { + pub fn new(epoch: Epoch) -> Self { + Self { + upcoming_epoch: epoch, + upcoming_environment: None, + programs_to_recompile: Vec::default(), + } + } + + /// Returns the upcoming environments depending on the given epoch + pub fn get_upcoming_environment_for_epoch( + &self, + epoch: Epoch, + ) -> Option { + if epoch == self.upcoming_epoch { + return self.upcoming_environment.clone(); + } + None + } + + /// Before rerooting the blockstore this concludes the epoch boundary preparation + pub fn reroot(&mut self, epoch: Epoch) -> Option { + if epoch == self.upcoming_epoch + && let Some(upcoming_environment) = self.upcoming_environment.take() + { + self.programs_to_recompile.clear(); + return Some(upcoming_environment); + } + + None + } +} + +#[derive(Debug)] +pub(crate) enum IndexImplementation { + /// Fork-graph aware index implementation + V1 { + /// A two level index: + /// + /// - the first level is for the address at which programs are deployed + /// - the second level for the slot (and thus also fork), sorted by slot number. + entries: HashMap>>, + /// The entries that are getting loaded and have not yet finished loading. + /// + /// The key is the program address, the value is a tuple of the slot in which the program is + /// being loaded and the thread ID doing the load. + /// + /// It is possible that multiple TX batches from different slots need different versions of a + /// program. The deployment slot of a program is only known after load tho, + /// so all loads for a given program key are serialized. + loading_entries: Mutex>, + }, +} + +/// This structure is the global cache of loaded, verified and compiled programs. +/// +/// It ... +/// - is validator global and fork graph aware, so it can optimize the commonalities across banks. +/// - handles the visibility rules of un/re/deployments. +/// - stores the usage statistics and verification status of each program. +/// - is elastic and uses a probabilistic eviction strategy based on the usage statistics. +/// - also keeps the compiled executables around, but only for the most used programs. +/// - supports various kinds of tombstones to avoid loading programs which can not be loaded. +/// - cleans up entries on orphan branches when the block store is rerooted. +/// - supports the cache preparation phase before feature activations which can change cached programs. +/// - manages the environments of the programs and upcoming environments for the next epoch. +/// - allows for cooperative loading of TX batches which hit the same missing programs simultaneously. +/// - enforces that all programs used in a batch are eagerly loaded ahead of execution. +/// - is not persisted to disk or a snapshot, so it needs to cold start and warm up first. +pub struct ProgramCache { + /// Index of the cached entries and cooperative loading tasks + pub(crate) index: IndexImplementation, + /// The slot of the last rerooting + pub latest_root_slot: Slot, + /// Statistics counters + pub stats: ProgramCacheStats, + /// Reference to the block store + pub fork_graph: Option>>, + /// Coordinates TX batches waiting for others to complete their task during cooperative loading + pub loading_task_waiter: Arc, +} + +impl std::fmt::Debug for ProgramCache { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ProgramCache") + .field("root slot", &self.latest_root_slot) + .field("stats", &self.stats) + .field("index", &self.index) + .finish() + } +} + +/// Local view into [ProgramCache] which was extracted for a specific TX batch. +/// +/// This isolation enables the global [ProgramCache] to continue to evolve (e.g. evictions), +/// while the TX batch is guaranteed it will continue to find all the programs it requires. +/// For program management instructions this also buffers them before they are merged back into the global [ProgramCache]. +#[derive(Clone, Debug, Default)] +pub struct ProgramCacheForTxBatch { + /// Pubkey is the address of a program. + /// ProgramCacheEntry is the corresponding program entry valid for the slot in which a transaction is being executed. + entries: HashMap>, + /// Program entries modified during the transaction batch. + modified_entries: HashMap>, + slot: Slot, + pub hit_max_limit: bool, + pub loaded_missing: bool, + pub merged_modified: bool, +} + +impl ProgramCacheForTxBatch { + pub fn new(slot: Slot) -> Self { + Self { + entries: HashMap::new(), + modified_entries: HashMap::new(), + slot, + hit_max_limit: false, + loaded_missing: false, + merged_modified: false, + } + } + + /// Refill the cache with a single entry. It's typically called during transaction loading, and + /// transaction processing (for program management instructions). + /// It replaces the existing entry (if any) with the provided entry. The return value contains + /// `true` if an entry existed. + /// The function also returns the newly inserted value. + pub fn replenish( + &mut self, + key: Pubkey, + entry: Arc, + ) -> (bool, Arc) { + (self.entries.insert(key, entry.clone()).is_some(), entry) + } + + /// Store an entry in `modified_entries` for a program modified during the + /// transaction batch. + pub fn store_modified_entry(&mut self, key: Pubkey, entry: Arc) { + self.modified_entries.insert(key, entry); + } + + /// Drain the program cache's modified entries, returning the owned + /// collection. + pub fn drain_modified_entries(&mut self) -> HashMap> { + std::mem::take(&mut self.modified_entries) + } + + pub fn find(&self, key: &Pubkey) -> Option> { + // First lookup the cache of the programs modified by the current + // transaction. If not found, lookup the cache of the cache of the + // programs that are loaded for the transaction batch. + self.modified_entries + .get(key) + .or_else(|| self.entries.get(key)) + .map(|entry| { + if entry.is_implicit_delay_visibility_tombstone(self.slot) { + // Found a program entry on the current fork, but it's not effective + // yet. It indicates that the program has delayed visibility. Return + // the tombstone to reflect that. + Arc::new(ProgramCacheEntry::new_tombstone_with_stats( + entry.deployment_slot, + entry.account_owner, + ProgramCacheEntryType::DelayVisibility, + Arc::clone(&entry.stats), + )) + } else { + entry.clone() + } + }) + } + + pub fn slot(&self) -> Slot { + self.slot + } + + pub fn set_slot_for_tests(&mut self, slot: Slot) { + self.slot = slot; + } + + pub fn merge(&mut self, modified_entries: &HashMap>) { + modified_entries.iter().for_each(|(key, entry)| { + self.merged_modified = true; + self.replenish(*key, entry.clone()); + }) + } + + pub fn is_empty(&self) -> bool { + self.entries.is_empty() + } +} + +pub enum ProgramCacheMatchCriteria { + DeployedOnOrAfterSlot(Slot), + Tombstone, + NoCriteria, +} + +impl ProgramCache { + pub fn new(root_slot: Slot) -> Self { + Self { + index: IndexImplementation::V1 { + entries: HashMap::new(), + loading_entries: Mutex::new(HashMap::new()), + }, + latest_root_slot: root_slot, + stats: ProgramCacheStats::default(), + fork_graph: None, + loading_task_waiter: Arc::new(LoadingTaskWaiter::default()), + } + } + + pub fn set_fork_graph(&mut self, fork_graph: Weak>) { + self.fork_graph = Some(fork_graph); + } + + /// Insert a single entry. It's typically called during transaction loading, + /// when the cache doesn't contain the entry corresponding to program `key`. + pub fn assign_program( + &mut self, + program_runtime_environment: &ProgramRuntimeEnvironment, + key: Pubkey, + _last_modification_slot: Slot, + entry: Arc, + ) -> bool { + debug_assert!(!matches!( + &entry.program, + ProgramCacheEntryType::DelayVisibility + )); + // This function always returns `true` during normal operation. + // Only during the cache preparation phase this can return `false` + // for entries with `upcoming_environment`. + fn is_current_env( + program_runtime_environment: &ProgramRuntimeEnvironment, + env_opt: Option<&ProgramRuntimeEnvironment>, + ) -> bool { + env_opt + .map(|env| env == program_runtime_environment) + .unwrap_or(true) + } + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + let slot_versions = &mut entries.entry(key).or_default(); + let insertion_point = slot_versions.binary_search_by(|at| { + at.effective_slot + .cmp(&entry.effective_slot) + .then(at.deployment_slot.cmp(&entry.deployment_slot)) + .then( + // This `.then()` has no effect during normal operation. + // Only during the cache preparation phase this does allow entries + // which only differ in their environment to be interleaved in `slot_versions`. + is_current_env( + program_runtime_environment, + at.program.get_environment(), + ) + .cmp(&is_current_env( + program_runtime_environment, + entry.program.get_environment(), + )), + ) + }); + match insertion_point { + Ok(index) => { + let existing = slot_versions.get_mut(index).unwrap(); + match (&existing.program, &entry.program) { + ( + ProgramCacheEntryType::Builtin(_), + ProgramCacheEntryType::Builtin(_), + ) + | ( + ProgramCacheEntryType::Unloaded(_), + ProgramCacheEntryType::Loaded(_), + ) => {} + (ProgramCacheEntryType::Closed, ProgramCacheEntryType::Closed) + if existing.account_owner != entry.account_owner => {} + _ => { + // Something is wrong, I can feel it ... + error!( + "ProgramCache::assign_program() failed key={key:?} \ + existing={slot_versions:?} entry={entry:?}" + ); + debug_assert!(false, "Unexpected replacement of an entry"); + self.stats.replacements.fetch_add(1, Ordering::Relaxed); + return true; + } + } + entry.stats.merge_from(&existing.stats); + *existing = Arc::clone(&entry); + self.stats.reloads.fetch_add(1, Ordering::Relaxed); + } + Err(index) => { + self.stats.insertions.fetch_add(1, Ordering::Relaxed); + slot_versions.insert(index, Arc::clone(&entry)); + } + } + // Remove existing entries in the same deployment slot unless they are for a different + // environment. + // This overwrites the current status of a program in program management instructions. + slot_versions.retain(|existing| { + existing.deployment_slot != entry.deployment_slot + || existing + .program + .get_environment() + .zip(entry.program.get_environment()) + .map(|(a, b)| a != b) + .unwrap_or(false) + || existing == &entry + }); + } + } + false + } + + pub fn prune_by_deployment_slot(&mut self, slot: Slot) { + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + for second_level in entries.values_mut() { + second_level.retain(|entry| entry.deployment_slot != slot); + } + self.remove_programs_with_no_entries(); + } + } + } + + /// Before rerooting the blockstore this removes all superfluous entries + pub fn prune( + &mut self, + new_root_slot: Slot, + upcoming_environment: Option, + fork_graph: &FG, + ) { + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + for second_level in entries.values_mut() { + // Remove entries un/re/deployed on orphan forks + let mut first_ancestor_found = false; + let mut first_ancestor_env = None; + *second_level = second_level + .iter() + .rev() + .filter(|entry| { + let relation = + fork_graph.relationship(entry.deployment_slot, new_root_slot); + if entry.deployment_slot >= new_root_slot { + matches!(relation, BlockRelation::Equal | BlockRelation::Descendant) + } else if matches!(relation, BlockRelation::Ancestor) + || entry.deployment_slot <= self.latest_root_slot + { + if !first_ancestor_found { + first_ancestor_found = true; + first_ancestor_env = entry.program.get_environment(); + return true; + } + // Do not prune the entry if the runtime environment of the entry is + // different than the entry that was previously found (stored in + // first_ancestor_env). Different environment indicates that this entry + // might belong to an older epoch that had a different environment (e.g. + // different feature set). Once the root moves to the new/current epoch, + // the entry will get pruned. But, until then the entry might still be + // getting used by an older slot. + if let Some(entry_env) = entry.program.get_environment() + && let Some(env) = first_ancestor_env + && entry_env != env + { + return true; + } + self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed); + false + } else { + self.stats.prunes_orphan.fetch_add(1, Ordering::Relaxed); + false + } + }) + .filter(|entry| { + // Remove outdated environment of previous feature set + if let Some(upcoming_environment) = upcoming_environment.as_ref() + && !Self::matches_environment(entry, upcoming_environment) + { + self.stats + .prunes_environment + .fetch_add(1, Ordering::Relaxed); + return false; + } + true + }) + .cloned() + .collect(); + second_level.reverse(); + } + } + } + self.remove_programs_with_no_entries(); + debug_assert!(self.latest_root_slot <= new_root_slot); + self.latest_root_slot = new_root_slot; + } + + fn matches_environment( + entry: &Arc, + program_runtime_environment: &ProgramRuntimeEnvironment, + ) -> bool { + let Some(environment) = entry.program.get_environment() else { + return true; + }; + environment == program_runtime_environment + } + + fn matches_criteria( + program: &Arc, + criteria: &ProgramCacheMatchCriteria, + ) -> bool { + match criteria { + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) => { + program.deployment_slot >= *slot + } + ProgramCacheMatchCriteria::Tombstone => program.is_tombstone(), + ProgramCacheMatchCriteria::NoCriteria => true, + } + } + + /// Extracts a subset of the programs relevant to a transaction batch + /// and returns which program accounts the accounts DB needs to load. + pub fn extract( + &self, + search_for: &mut Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)>, + loaded_programs_for_tx_batch: &mut ProgramCacheForTxBatch, + program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, + increment_usage_counter: bool, + count_hits_and_misses: bool, + ) -> Option { + debug_assert!(self.fork_graph.is_some()); + let fork_graph = self.fork_graph.as_ref().unwrap().upgrade().unwrap(); + let locked_fork_graph = fork_graph.read().unwrap(); + let mut cooperative_loading_task = None; + match &self.index { + IndexImplementation::V1 { + entries, + loading_entries, + } => { + search_for.retain(|(key, match_criteria, _slot)| { + if let Some(second_level) = entries.get(key) { + let mut filter_by_deployment_slot = None; + for entry in second_level.iter().rev() { + let required_deployment_slot = + filter_by_deployment_slot.unwrap_or(entry.deployment_slot); + if required_deployment_slot != entry.deployment_slot { + continue; + } + let entry_in_same_branch = entry.deployment_slot + <= self.latest_root_slot + || matches!( + locked_fork_graph.relationship( + entry.deployment_slot, + loaded_programs_for_tx_batch.slot + ), + BlockRelation::Equal | BlockRelation::Ancestor + ); + if entry_in_same_branch { + let entry_is_effective = + loaded_programs_for_tx_batch.slot >= entry.effective_slot; + let entry_to_return = if entry_is_effective { + if !Self::matches_environment( + entry, + program_runtime_environment_for_execution, + ) { + // We found an entry that would work, had its environment matched + // the one we're planning to use for this slot. + // + // At this point we know that whatever the "current version" of + // program is, it must have had a deployment slot equal to the + // program we're looking at in this iteration. We just have to find + // one with the correct environment and can skip entries for any + // other deployment slot while searching further. + filter_by_deployment_slot = filter_by_deployment_slot + .or(Some(entry.deployment_slot)); + continue; + } + if !Self::matches_criteria(entry, match_criteria) { + break; + } + if let ProgramCacheEntryType::Unloaded(_environment) = + &entry.program + { + break; + } + entry.clone() + } else if entry.is_implicit_delay_visibility_tombstone( + loaded_programs_for_tx_batch.slot, + ) { + // Found a program entry on the current fork, but it's not effective + // yet. It indicates that the program has delayed visibility. Return + // the tombstone to reflect that. + Arc::new(ProgramCacheEntry::new_tombstone_with_stats( + entry.deployment_slot, + entry.account_owner, + ProgramCacheEntryType::DelayVisibility, + Arc::clone(&entry.stats), + )) + } else { + continue; + }; + entry_to_return + .update_access_slot(loaded_programs_for_tx_batch.slot); + if increment_usage_counter { + entry_to_return.stats.uses.fetch_add(1, Ordering::Relaxed); + } + loaded_programs_for_tx_batch + .entries + .insert(*key, entry_to_return); + return false; + } + } + } + if cooperative_loading_task.is_none() { + let mut loading_entries = loading_entries.lock().unwrap(); + let entry = loading_entries.entry(*key); + if let Entry::Vacant(entry) = entry { + entry.insert(( + loaded_programs_for_tx_batch.slot, + thread::current().id(), + )); + cooperative_loading_task = Some(*key); + } + } + true + }); + } + } + drop(locked_fork_graph); + if count_hits_and_misses { + self.stats + .misses + .fetch_add(search_for.len() as u64, Ordering::Relaxed); + self.stats.hits.fetch_add( + loaded_programs_for_tx_batch.entries.len() as u64, + Ordering::Relaxed, + ); + } + cooperative_loading_task + } + + /// Called by Bank::replenish_program_cache() for each program that is done loading. + pub fn finish_cooperative_loading_task( + &mut self, + program_runtime_environment: &ProgramRuntimeEnvironment, + current_slot: Slot, + key: Pubkey, + last_modification_slot: Slot, + loaded_program: Arc, + ) -> bool { + match &mut self.index { + IndexImplementation::V1 { + loading_entries, .. + } => { + let loading_thread = loading_entries.get_mut().unwrap().remove(&key); + debug_assert_eq!(loading_thread, Some((current_slot, thread::current().id()))); + // Check that it will be visible to our own fork once inserted + if loaded_program.deployment_slot > self.latest_root_slot + && !matches!( + self.fork_graph + .as_ref() + .unwrap() + .upgrade() + .unwrap() + .read() + .unwrap() + .relationship(loaded_program.deployment_slot, current_slot), + BlockRelation::Equal | BlockRelation::Ancestor + ) + { + self.stats.lost_insertions.fetch_add(1, Ordering::Relaxed); + } + let was_occupied = self.assign_program( + program_runtime_environment, + key, + last_modification_slot, + loaded_program, + ); + self.loading_task_waiter.notify(); + was_occupied + } + } + } + + pub fn merge( + &mut self, + program_runtime_environment: &ProgramRuntimeEnvironment, + current_slot: Slot, + modified_entries: &HashMap>, + ) { + modified_entries.iter().for_each(|(key, entry)| { + self.assign_program( + program_runtime_environment, + *key, + current_slot, + entry.clone(), + ); + }) + } + + /// Returns the list of entries which are verified and compiled. + pub fn get_flattened_entries(&self) -> Vec<(Pubkey, Slot, Arc)> { + match &self.index { + IndexImplementation::V1 { entries, .. } => entries + .iter() + .flat_map(|(id, second_level)| { + second_level + .iter() + .filter_map(move |program| match program.program { + ProgramCacheEntryType::Loaded(_) => Some((*id, 0, program.clone())), + _ => None, + }) + }) + .collect(), + } + } + + /// Returns the list of all entries in the cache. + #[cfg(feature = "dev-context-only-utils")] + pub fn get_flattened_entries_for_tests(&self) -> Vec<(Pubkey, Arc)> { + match &self.index { + IndexImplementation::V1 { entries, .. } => entries + .iter() + .flat_map(|(id, second_level)| { + second_level.iter().map(|program| (*id, program.clone())) + }) + .collect(), + } + } + + /// Returns the slot versions for the given program id. + pub fn get_slot_versions_for_tests(&self, key: &Pubkey) -> &[Arc] { + match &self.index { + IndexImplementation::V1 { entries, .. } => entries + .get(key) + .map(|second_level| second_level.as_ref()) + .unwrap_or(&[]), + } + } + + /// Unloads programs which were used infrequently + pub fn sort_and_unload(&mut self, shrink_to: PercentageInteger) { + let mut sorted_candidates = self.get_flattened_entries(); + sorted_candidates.sort_by_cached_key(|(_id, _last_modification_slot, program)| { + program.stats.uses.load(Ordering::Relaxed) + }); + let num_to_unload = sorted_candidates + .len() + .saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT)); + for (program, last_modification_slot, entry) in sorted_candidates.iter().take(num_to_unload) + { + self.unload_program_entry(*program, *last_modification_slot, entry); + } + } + + /// Evicts programs using random selection, choosing the worst scoring program out of the + /// entries sampled. + /// + /// The eviction is performed enough number of times to reduce the cache usage to the given + /// percentage. + pub fn evict_using_random_selection(&mut self, shrink_to: PercentageInteger, now: Slot) { + let mut candidates = self.get_flattened_entries(); + let mut rng = rng(); + self.stats + .water_level + .store(candidates.len() as u64, Ordering::Relaxed); + let num_to_unload = candidates + .len() + .saturating_sub(shrink_to.apply_to(MAX_LOADED_ENTRY_COUNT)); + let mut sample_entry = |candidates: &Vec<(Pubkey, u64, Arc)>| { + // gen_range is deprecated in favor of random_range in rand>=0.9, but we also get + // rnd() from shuttle, which doesn't yet support rand 0.9 APIs + #[cfg(feature = "shuttle-test")] + let index = rng.gen_range(0..candidates.len()); + #[cfg(not(feature = "shuttle-test"))] + let index = rng.random_range(0..candidates.len()); + let usage_counter = candidates + .get(index) + .expect("Failed to get cached entry") + .2 + .retention_score(); + (index, usage_counter) + }; + + // Random sampling with just 2 choices can frequently lead to a situation where both + // entries chosen have relatively high retention scores, having us to pick one out of two + // poor options. We can tell what a relatively high retention score is, so we can make a + // few additional samples until we hit some other entry that isn't as highly scoring. + // + // Note that the "high enough" compilation time and use count numbers used here are + // relatively arbitrary. + const MAX_ADDITIONAL_SAMPLES: usize = 3; + let avoid_evicting_above_score = retention_score(now, 500 * EMA_SCALE, 500); + for _ in 0..num_to_unload { + let (mut index, mut score) = sample_entry(&candidates); + for _ in 0..MAX_ADDITIONAL_SAMPLES { + let (sample_index, sample_score) = sample_entry(&candidates); + if score > sample_score { + index = sample_index; + score = sample_score; + } + if score < avoid_evicting_above_score { + break; + } + } + let (id, last_modification_slot, entry) = candidates.swap_remove(index); + self.unload_program_entry(id, last_modification_slot, &entry); + } + } + + /// Removes all the entries at the given keys, if they exist + pub fn remove_programs(&mut self, keys: impl Iterator) { + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + for k in keys { + entries.remove(&k); + } + } + } + } + + /// This function removes the given entry for the given program from the cache. + /// The function expects that the program and entry exists in the cache. Otherwise it'll panic. + fn unload_program_entry( + &mut self, + id: Pubkey, + _last_modification_slot: Slot, + remove_entry: &Arc, + ) { + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + let second_level = entries.get_mut(&id).expect("Cache lookup failed"); + let candidate = second_level + .iter_mut() + .find(|entry| entry == &remove_entry) + .expect("Program entry not found"); + + // Certain entry types cannot be unloaded, such as tombstones, or already unloaded entries. + // For such entries, `to_unloaded()` will return None. + // These entry types do not occupy much memory. + if let Some(unloaded) = candidate.to_unloaded() { + if candidate.stats.uses.load(Ordering::Relaxed) == 1 { + self.stats.one_hit_wonders.fetch_add(1, Ordering::Relaxed); + } + self.stats + .evictions + .entry(id) + .and_modify(|c| *c = c.saturating_add(1)) + .or_insert(1); + *candidate = Arc::new(unloaded); + } + } + } + } + + fn remove_programs_with_no_entries(&mut self) { + match &mut self.index { + IndexImplementation::V1 { entries, .. } => { + let num_programs_before_removal = entries.len(); + entries.retain(|_key, second_level| !second_level.is_empty()); + if entries.len() < num_programs_before_removal { + self.stats.empty_entries.fetch_add( + num_programs_before_removal.saturating_sub(entries.len()) as u64, + Ordering::Relaxed, + ); + } + } + } + } +} + +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::abi_example::AbiExample for ProgramCacheEntry { + fn example() -> Self { + // ProgramCacheEntry isn't serializable by definition. + Self::default() + } +} + +#[cfg(feature = "frozen-abi")] +impl solana_frozen_abi::abi_example::AbiExample for ProgramCache { + fn example() -> Self { + // ProgramCache isn't serializable by definition. + Self::new(Slot::default()) + } +} + +#[cfg(test)] +pub(crate) mod tests { + use { + crate::{ + loaded_programs::{ + BlockRelation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, + ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, + get_mock_program_runtime_environment, + }, + program_cache_entry::{ + DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner, + ProgramCacheEntryType, + }, + program_metrics::ProgramStatistics, + }, + assert_matches::assert_matches, + percentage::Percentage, + solana_clock::Slot, + solana_pubkey::Pubkey, + solana_sbpf::{elf::Executable, program::BuiltinProgram}, + std::{ + fs::File, + io::Read, + ops::ControlFlow, + sync::{ + Arc, RwLock, + atomic::{AtomicU64, Ordering}, + }, + }, + test_case::{test_case, test_matrix}, + }; + + fn new_test_entry(deployment_slot: Slot, effective_slot: Slot) -> Arc { + new_test_entry_with_usage( + deployment_slot, + effective_slot, + ProgramStatistics::default(), + ) + } + + fn new_loaded_entry(env: ProgramRuntimeEnvironment) -> ProgramCacheEntryType { + let mut elf = Vec::new(); + File::open("../programs/bpf_loader/test_elfs/out/noop_aligned.so") + .unwrap() + .read_to_end(&mut elf) + .unwrap(); + let executable = Executable::load(&elf, Arc::clone(&*env)).unwrap(); + ProgramCacheEntryType::Loaded(executable) + } + + pub(crate) fn new_test_entry_with_usage( + deployment_slot: Slot, + effective_slot: Slot, + stats: ProgramStatistics, + ) -> Arc { + Arc::new(ProgramCacheEntry { + program: new_loaded_entry(get_mock_program_runtime_environment()), + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot, + effective_slot, + stats: Arc::new(stats), + latest_access_slot: AtomicU64::new(deployment_slot), + }) + } + + fn new_test_builtin_entry( + deployment_slot: Slot, + effective_slot: Slot, + ) -> Arc { + Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + account_owner: ProgramCacheEntryOwner::NativeLoader, + account_size: 0, + deployment_slot, + effective_slot, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }) + } + + fn set_tombstone( + cache: &mut ProgramCache, + key: Pubkey, + current_slot: Slot, + reason: ProgramCacheEntryType, + ) -> Arc { + let env = get_mock_program_runtime_environment(); + let program = Arc::new(ProgramCacheEntry::new_tombstone( + current_slot, + ProgramCacheEntryOwner::LoaderV2, + reason, + )); + cache.assign_program(&env, key, current_slot, program.clone()); + program + } + + fn insert_unloaded_entry( + cache: &mut ProgramCache, + key: Pubkey, + current_slot: Slot, + ) -> Arc { + let env = get_mock_program_runtime_environment(); + let loaded = new_test_entry_with_usage( + current_slot, + current_slot.saturating_add(1), + ProgramStatistics::default(), + ); + let unloaded = Arc::new(loaded.to_unloaded().expect("Failed to unload the program")); + cache.assign_program(&env, key, current_slot, unloaded.clone()); + unloaded + } + + fn num_matching_entries(cache: &ProgramCache, predicate: P) -> usize + where + P: Fn(&ProgramCacheEntryType) -> bool, + FG: ForkGraph, + { + cache + .get_flattened_entries_for_tests() + .iter() + .filter(|(_key, program)| predicate(&program.program)) + .count() + } + + fn program_deploy_test_helper( + cache: &mut ProgramCache, + program: Pubkey, + deployment_slots: Vec, + usage_counters: Vec, + programs: &mut Vec<(Pubkey, Slot, u64)>, + ) { + let env = get_mock_program_runtime_environment(); + // Add multiple entries for program + deployment_slots + .iter() + .enumerate() + .for_each(|(i, deployment_slot)| { + let usage_counter = *usage_counters.get(i).unwrap_or(&0); + let stats = ProgramStatistics { + uses: usage_counter.into(), + ..Default::default() + }; + cache.assign_program( + &env, + program, + *deployment_slot, + new_test_entry_with_usage( + *deployment_slot, + (*deployment_slot).saturating_add(2), + stats, + ), + ); + programs.push((program, *deployment_slot, usage_counter)); + }); + + // Add tombstones entries for program + let env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + for slot in 21..31 { + set_tombstone( + cache, + program, + slot, + ProgramCacheEntryType::FailedVerification(env.clone()), + ); + } + + // Add unloaded entries for program + for slot in 31..41 { + insert_unloaded_entry(cache, program, slot); + } + } + + #[test] + fn test_random_eviction() { + let mut programs = vec![]; + let mut cache = ProgramCache::::new(0); + + // This test adds different kind of entries to the cache. + // Tombstones and unloaded entries are expected to not be evicted. + // It also adds multiple entries for three programs as it tries to create a typical cache instance. + + // Program 1 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![0, 10, 20], + vec![4, 5, 25], + &mut programs, + ); + + // Program 2 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![5, 11], + vec![0, 2], + &mut programs, + ); + + // Program 3 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![0, 5, 15], + vec![100, 3, 20], + &mut programs, + ); + + // 1 for each deployment slot + let num_loaded_expected = 8; + // 10 for each program + let num_unloaded_expected = 30; + // 10 for each program + let num_tombstones_expected = 30; + + // Count the number of loaded, unloaded and tombstone entries. + programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count); + let num_loaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Loaded(_)) + }); + let num_unloaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Unloaded(_)) + }); + let num_tombstones = num_matching_entries(&cache, |program_type| { + matches!( + program_type, + ProgramCacheEntryType::DelayVisibility + | ProgramCacheEntryType::FailedVerification(_) + | ProgramCacheEntryType::Closed + ) + }); + + // Test that the cache is constructed with the expected number of entries. + assert_eq!(num_loaded, num_loaded_expected); + assert_eq!(num_unloaded, num_unloaded_expected); + assert_eq!(num_tombstones, num_tombstones_expected); + + // Evict entries from the cache + let eviction_pct = 1; + + let num_loaded_expected = + Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT); + let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected; + cache.evict_using_random_selection(Percentage::from(eviction_pct), 21); + + // Count the number of loaded, unloaded and tombstone entries. + let num_loaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Loaded(_)) + }); + let num_unloaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Unloaded(_)) + }); + let num_tombstones = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::FailedVerification(_)) + }); + + // However many entries are left after the shrink + assert_eq!(num_loaded, num_loaded_expected); + // The original unloaded entries + the evicted loaded entries + assert_eq!(num_unloaded, num_unloaded_expected); + // The original tombstones are not evicted + assert_eq!(num_tombstones, num_tombstones_expected); + } + + #[test] + fn test_eviction() { + let mut programs = vec![]; + let mut cache = ProgramCache::::new(0); + + // Program 1 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![0, 10, 20], + vec![4, 5, 25], + &mut programs, + ); + + // Program 2 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![5, 11], + vec![0, 2], + &mut programs, + ); + + // Program 3 + program_deploy_test_helper( + &mut cache, + Pubkey::new_unique(), + vec![0, 5, 15], + vec![100, 3, 20], + &mut programs, + ); + + // 1 for each deployment slot + let num_loaded_expected = 8; + // 10 for each program + let num_unloaded_expected = 30; + // 10 for each program + let num_tombstones_expected = 30; + + // Count the number of loaded, unloaded and tombstone entries. + programs.sort_by_key(|(_id, _slot, usage_count)| *usage_count); + let num_loaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Loaded(_)) + }); + let num_unloaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Unloaded(_)) + }); + let num_tombstones = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::FailedVerification(_)) + }); + + // Test that the cache is constructed with the expected number of entries. + assert_eq!(num_loaded, num_loaded_expected); + assert_eq!(num_unloaded, num_unloaded_expected); + assert_eq!(num_tombstones, num_tombstones_expected); + + // Evict entries from the cache + let eviction_pct = 1; + + let num_loaded_expected = + Percentage::from(eviction_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT); + let num_unloaded_expected = num_unloaded_expected + num_loaded - num_loaded_expected; + + cache.sort_and_unload(Percentage::from(eviction_pct)); + + // Check that every program is still in the cache. + let entries = cache.get_flattened_entries_for_tests(); + programs.iter().for_each(|entry| { + assert!(entries.iter().any(|(key, _entry)| key == &entry.0)); + }); + + let unloaded = entries + .iter() + .filter_map(|(key, program)| { + matches!(program.program, ProgramCacheEntryType::Unloaded(_)) + .then_some((*key, program.stats.uses.load(Ordering::Relaxed))) + }) + .collect::>(); + + for index in 0..3 { + let expected = programs.get(index).expect("Missing program"); + assert!(unloaded.contains(&(expected.0, expected.2))); + } + + // Count the number of loaded, unloaded and tombstone entries. + let num_loaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Loaded(_)) + }); + let num_unloaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Unloaded(_)) + }); + let num_tombstones = num_matching_entries(&cache, |program_type| { + matches!( + program_type, + ProgramCacheEntryType::DelayVisibility + | ProgramCacheEntryType::FailedVerification(_) + | ProgramCacheEntryType::Closed + ) + }); + + // However many entries are left after the shrink + assert_eq!(num_loaded, num_loaded_expected); + // The original unloaded entries + the evicted loaded entries + assert_eq!(num_unloaded, num_unloaded_expected); + // The original tombstones are not evicted + assert_eq!(num_tombstones, num_tombstones_expected); + } + + #[test] + fn test_usage_count_of_unloaded_program() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + let program = Pubkey::new_unique(); + let evict_to_pct = 2; + let cache_capacity_after_shrink = + Percentage::from(evict_to_pct).apply_to(crate::loaded_programs::MAX_LOADED_ENTRY_COUNT); + // Add enough programs to the cache to trigger 1 eviction after shrinking. + let num_total_programs = (cache_capacity_after_shrink + 1) as u64; + (0..num_total_programs).for_each(|i| { + let stats = ProgramStatistics { + uses: (i + 10).into(), + ..Default::default() + }; + let entry = new_test_entry_with_usage(i, i + 2, stats); + cache.assign_program(&env, program, i, entry); + }); + + cache.sort_and_unload(Percentage::from(evict_to_pct)); + + let num_unloaded = num_matching_entries(&cache, |program_type| { + matches!(program_type, ProgramCacheEntryType::Unloaded(_)) + }); + assert_eq!(num_unloaded, 1); + + cache + .get_flattened_entries_for_tests() + .iter() + .for_each(|(_key, program)| { + if matches!(program.program, ProgramCacheEntryType::Unloaded(_)) { + // Test that the usage counter is retained for the unloaded program + assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10); + assert_eq!(program.deployment_slot, 0); + assert_eq!(program.effective_slot, 2); + } + }); + + // Replenish the program that was just unloaded. Use 0 as the usage counter. This should be + // updated with the usage counter from the unloaded program. + cache.assign_program( + &env, + program, + 0, + new_test_entry_with_usage(0, 2, ProgramStatistics::default()), + ); + + cache + .get_flattened_entries_for_tests() + .iter() + .for_each(|(_key, program)| { + if matches!(program.program, ProgramCacheEntryType::Unloaded(_)) + && program.deployment_slot == 0 + && program.effective_slot == 2 + { + // Test that the usage counter was correctly updated. + assert_eq!(program.stats.uses.load(Ordering::Relaxed), 10); + } + }); + } + + #[test] + fn test_fuzz_assign_program_order() { + use rand::prelude::SliceRandom; + const EXPECTED_ENTRIES: [(u64, u64); 7] = + [(1, 2), (5, 5), (5, 6), (5, 10), (9, 10), (10, 10), (3, 12)]; + let mut rng = rand::rng(); + let program_id = Pubkey::new_unique(); + let env = get_mock_program_runtime_environment(); + for _ in 0..1000 { + let mut entries = EXPECTED_ENTRIES.to_vec(); + entries.shuffle(&mut rng); + let mut cache = ProgramCache::::new(0); + for (deployment_slot, effective_slot) in entries { + let entry = Arc::new(ProgramCacheEntry { + program: new_loaded_entry(ProgramRuntimeEnvironment::from( + BuiltinProgram::new_mock(), + )), // Assign them different environments + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot, + effective_slot, + stats: Arc::default(), + latest_access_slot: AtomicU64::new(deployment_slot), + }); + assert!(!cache.assign_program(&env, program_id, deployment_slot, entry)); + } + for ((deployment_slot, effective_slot), entry) in EXPECTED_ENTRIES + .iter() + .zip(cache.get_slot_versions_for_tests(&program_id).iter()) + { + assert_eq!(entry.deployment_slot, *deployment_slot); + assert_eq!(entry.effective_slot, *effective_slot); + } + } + } + + #[test_matrix( + ( + ProgramCacheEntryType::Closed, + ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()), + new_loaded_entry(get_mock_program_runtime_environment()), + ), + ( + ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Closed, + ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + new_loaded_entry(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + ) + )] + #[test_matrix( + ( + ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + ), + ( + ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Closed, + ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + ) + )] + #[test_matrix( + (ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()),), + ( + ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Closed, + ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + new_loaded_entry(get_mock_program_runtime_environment()), + ) + )] + #[should_panic(expected = "Unexpected replacement of an entry")] + fn test_assign_program_failure(old: ProgramCacheEntryType, new: ProgramCacheEntryType) { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + let program_id = Pubkey::new_unique(); + assert!(!cache.assign_program( + &env, + program_id, + 10, + Arc::new(ProgramCacheEntry { + program: old, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + )); + cache.assign_program( + &env, + program_id, + 10, + Arc::new(ProgramCacheEntry { + program: new, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + ); + } + + #[test_case( + ProgramCacheEntryType::Unloaded(ProgramRuntimeEnvironment::from( + BuiltinProgram::new_mock() + )), + new_loaded_entry(get_mock_program_runtime_environment()) + )] + #[test_case( + ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()) + )] + fn test_assign_program_success(old: ProgramCacheEntryType, new: ProgramCacheEntryType) { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + let program_id = Pubkey::new_unique(); + assert!(!cache.assign_program( + &env, + program_id, + 10, + Arc::new(ProgramCacheEntry { + program: old, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + )); + assert!(!cache.assign_program( + &env, + program_id, + 10, + Arc::new(ProgramCacheEntry { + program: new, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }), + )); + } + + #[test] + fn test_assign_program_removes_entries_in_same_slot() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + let program_id = Pubkey::new_unique(); + let closed_other_slot = Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Closed, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 9, + effective_slot: 9, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }); + let closed_current_slot = Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Closed, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 10, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }); + let loaded_entry_current_env = Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }); + let loaded_entry_upcoming_env = Arc::new(ProgramCacheEntry { + program: ProgramCacheEntryType::Unloaded(ProgramRuntimeEnvironment::from( + BuiltinProgram::new_mock(), + )), + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 10, + effective_slot: 11, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }); + assert!(!cache.assign_program(&env, program_id, 9, closed_other_slot.clone())); + assert!(!cache.assign_program(&env, program_id, 10, closed_current_slot)); + assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_upcoming_env.clone())); + assert!(!cache.assign_program(&env, program_id, 10, loaded_entry_current_env.clone())); + // Only the conflicting entry in the same slot which does not have a different environment is removed + assert_eq!( + cache.get_slot_versions_for_tests(&program_id), + &[ + closed_other_slot, + loaded_entry_current_env, + loaded_entry_upcoming_env + ] + ); + } + + #[test] + fn test_tombstone() { + let env = get_mock_program_runtime_environment(); + let tombstone = ProgramCacheEntry::new_tombstone( + 0, + ProgramCacheEntryOwner::LoaderV2, + ProgramCacheEntryType::FailedVerification(env.clone()), + ); + assert_matches!( + tombstone.program, + ProgramCacheEntryType::FailedVerification(_) + ); + assert!(tombstone.is_tombstone()); + assert_eq!(tombstone.deployment_slot, 0); + assert_eq!(tombstone.effective_slot, 0); + + let tombstone = ProgramCacheEntry::new_tombstone( + 100, + ProgramCacheEntryOwner::LoaderV2, + ProgramCacheEntryType::Closed, + ); + assert_matches!(tombstone.program, ProgramCacheEntryType::Closed); + assert!(tombstone.is_tombstone()); + assert_eq!(tombstone.deployment_slot, 100); + assert_eq!(tombstone.effective_slot, 100); + + let mut cache = ProgramCache::::new(0); + let program1 = Pubkey::new_unique(); + let tombstone = set_tombstone( + &mut cache, + program1, + 10, + ProgramCacheEntryType::FailedVerification(env.clone()), + ); + let slot_versions = cache.get_slot_versions_for_tests(&program1); + assert_eq!(slot_versions.len(), 1); + assert!(slot_versions.first().unwrap().is_tombstone()); + assert_eq!(tombstone.deployment_slot, 10); + assert_eq!(tombstone.effective_slot, 10); + + // Add a program at slot 50, and a tombstone for the program at slot 60 + let program2 = Pubkey::new_unique(); + cache.assign_program(&env, program2, 50, new_test_builtin_entry(50, 51)); + let slot_versions = cache.get_slot_versions_for_tests(&program2); + assert_eq!(slot_versions.len(), 1); + assert!(!slot_versions.first().unwrap().is_tombstone()); + + let tombstone = set_tombstone( + &mut cache, + program2, + 60, + ProgramCacheEntryType::FailedVerification(env), + ); + let slot_versions = cache.get_slot_versions_for_tests(&program2); + assert_eq!(slot_versions.len(), 2); + assert!(!slot_versions.first().unwrap().is_tombstone()); + assert!(slot_versions.get(1).unwrap().is_tombstone()); + assert!(tombstone.is_tombstone()); + assert_eq!(tombstone.deployment_slot, 60); + assert_eq!(tombstone.effective_slot, 60); + } + + struct TestForkGraph { + relation: BlockRelation, + } + impl ForkGraph for TestForkGraph { + fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { + self.relation + } + } + + #[test] + fn test_prune_empty() { + let mut cache = ProgramCache::::new(0); + let fork_graph = Arc::new(RwLock::new(TestForkGraph { + relation: BlockRelation::Unrelated, + })); + + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + cache.prune(0, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + cache.prune(10, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + let mut cache = ProgramCache::::new(0); + let fork_graph = Arc::new(RwLock::new(TestForkGraph { + relation: BlockRelation::Ancestor, + })); + + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + cache.prune(0, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + cache.prune(10, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + let mut cache = ProgramCache::::new(0); + let fork_graph = Arc::new(RwLock::new(TestForkGraph { + relation: BlockRelation::Descendant, + })); + + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + cache.prune(0, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + cache.prune(10, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + let mut cache = ProgramCache::::new(0); + let fork_graph = Arc::new(RwLock::new(TestForkGraph { + relation: BlockRelation::Unknown, + })); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + cache.prune(0, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + + cache.prune(10, None, &fork_graph.read().unwrap()); + assert!(cache.get_flattened_entries_for_tests().is_empty()); + } + + #[test] + fn test_prune_different_env() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + let fork_graph = Arc::new(RwLock::new(TestForkGraph { + relation: BlockRelation::Ancestor, + })); + + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 10, new_test_entry(10, 10)); + let new_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + let upcoming_environment = Some(new_env.clone()); + let updated_program = Arc::new(ProgramCacheEntry { + program: new_loaded_entry(new_env.clone()), + deployment_slot: 20, + effective_slot: 20, + ..Default::default() + }); + cache.assign_program( + &env, + program1, + updated_program.deployment_slot, + updated_program.clone(), + ); + + // Test that there are 2 entries for the program + assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2); + + cache.prune(21, None, &fork_graph.read().unwrap()); + + // Test that prune didn't remove the entry, since environments are different. + assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 2); + + cache.prune(22, upcoming_environment, &fork_graph.read().unwrap()); + + // Test that prune removed 1 entry, since epoch changed + assert_eq!(cache.get_slot_versions_for_tests(&program1).len(), 1); + + let entry = cache + .get_slot_versions_for_tests(&program1) + .first() + .expect("Failed to get the program") + .clone(); + // Test that the correct entry remains in the cache + assert_eq!(entry, updated_program); + } + + #[derive(Default)] + struct TestForkGraphSpecific { + forks: Vec>, + } + + impl TestForkGraphSpecific { + fn insert_fork(&mut self, fork: &[Slot]) { + let mut fork = fork.to_vec(); + fork.sort(); + self.forks.push(fork) + } + } + + impl ForkGraph for TestForkGraphSpecific { + fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { + match self.forks.iter().try_for_each(|fork| { + let relation = fork + .iter() + .position(|x| *x == a) + .and_then(|a_pos| { + fork.iter().position(|x| *x == b).and_then(|b_pos| { + (a_pos == b_pos) + .then_some(BlockRelation::Equal) + .or_else(|| (a_pos < b_pos).then_some(BlockRelation::Ancestor)) + .or(Some(BlockRelation::Descendant)) + }) + }) + .unwrap_or(BlockRelation::Unrelated); + + if relation != BlockRelation::Unrelated { + return ControlFlow::Break(relation); + } + + ControlFlow::Continue(()) + }) { + ControlFlow::Break(relation) => relation, + _ => BlockRelation::Unrelated, + } + } + } + + fn get_entries_to_load( + cache: &ProgramCache, + loading_slot: Slot, + keys: &[Pubkey], + ) -> Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> { + let fork_graph = cache.fork_graph.as_ref().unwrap().upgrade().unwrap(); + let locked_fork_graph = fork_graph.read().unwrap(); + let entries = cache.get_flattened_entries_for_tests(); + keys.iter() + .filter_map(|key| { + entries + .iter() + .rev() + .find(|(program_id, entry)| { + program_id == key + && matches!( + locked_fork_graph.relationship(entry.deployment_slot, loading_slot), + BlockRelation::Equal | BlockRelation::Ancestor, + ) + }) + .map(|(program_id, entry)| { + ( + *program_id, + ProgramCacheMatchCriteria::NoCriteria, + entry.deployment_slot, + ) + }) + }) + .collect() + } + + fn match_slot( + extracted: &ProgramCacheForTxBatch, + program: &Pubkey, + deployment_slot: Slot, + working_slot: Slot, + ) -> bool { + assert_eq!(extracted.slot, working_slot); + extracted + .entries + .get(program) + .map(|entry| entry.deployment_slot == deployment_slot) + .unwrap_or(false) + } + + fn match_missing( + missing: &[(Pubkey, ProgramCacheMatchCriteria, Slot)], + program: &Pubkey, + expected_result: bool, + ) -> bool { + missing.iter().any(|(key, _, _)| key == program) == expected_result + } + + #[test] + fn test_fork_extract_and_prune() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + // Fork graph created for the test + // 0 + // / \ + // 10 5 + // | | + // 20 11 + // | | \ + // 22 15 25 + // | | + // 16 27 + // | + // 19 + // | + // 23 + + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20, 22]); + fork_graph.insert_fork(&[0, 5, 11, 15, 16, 18, 19, 21, 23]); + fork_graph.insert_fork(&[0, 5, 11, 25, 27]); + + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program1, 10, new_test_entry(10, 11)); + cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); + + let program2 = Pubkey::new_unique(); + cache.assign_program(&env, program2, 5, new_test_entry(5, 6)); + cache.assign_program( + &env, + program2, + 11, + new_test_entry(11, 11 + DELAY_VISIBILITY_SLOT_OFFSET), + ); + + let program3 = Pubkey::new_unique(); + cache.assign_program(&env, program3, 25, new_test_entry(25, 26)); + + let program4 = Pubkey::new_unique(); + cache.assign_program(&env, program4, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program4, 5, new_test_entry(5, 6)); + // The following is a special case, where effective slot is 3 slots in the future + cache.assign_program( + &env, + program4, + 15, + new_test_entry(15, 15 + DELAY_VISIBILITY_SLOT_OFFSET), + ); + + // Current fork graph + // 0 + // / \ + // 10 5 + // | | + // 20 11 + // | | \ + // 22 15 25 + // | | + // 16 27 + // | + // 19 + // | + // 23 + + // Testing fork 0 - 10 - 20 - 22 with current slot at 22 + let mut missing = + get_entries_to_load(&cache, 22, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program2, false)); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(22); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 20, 22)); + assert!(match_slot(&extracted, &program4, 0, 22)); + + // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 15 + let mut missing = + get_entries_to_load(&cache, 15, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(15); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 15)); + assert!(match_slot(&extracted, &program2, 11, 15)); + // The effective slot of program4 deployed in slot 15 is 19. So it should not be usable in slot 16. + // A delay visibility tombstone should be returned here. + let tombstone = extracted + .find(&program4) + .expect("Failed to find the tombstone"); + assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility); + assert_eq!(tombstone.deployment_slot, 15); + + // Testing the same fork above, but current slot is now 18 (equal to effective slot of program4). + let mut missing = + get_entries_to_load(&cache, 18, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(18); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 18)); + assert!(match_slot(&extracted, &program2, 11, 18)); + // The effective slot of program4 deployed in slot 15 is 18. So it should be usable in slot 18. + assert!(match_slot(&extracted, &program4, 15, 18)); + + // Testing the same fork above, but current slot is now 23 (future slot than effective slot of program4). + let mut missing = + get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(23); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 23)); + assert!(match_slot(&extracted, &program2, 11, 23)); + // The effective slot of program4 deployed in slot 15 is 19. So it should be usable in slot 23. + assert!(match_slot(&extracted, &program4, 15, 23)); + + // Testing fork 0 - 5 - 11 - 15 - 16 with current slot at 11 + let mut missing = + get_entries_to_load(&cache, 11, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(11); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 11)); + // program2 was updated at slot 11, but is not effective till slot 12. The result should contain a tombstone. + let tombstone = extracted + .find(&program2) + .expect("Failed to find the tombstone"); + assert_matches!(tombstone.program, ProgramCacheEntryType::DelayVisibility); + assert_eq!(tombstone.deployment_slot, 11); + assert!(match_slot(&extracted, &program4, 5, 11)); + + cache.prune(5, None, &fork_graph.read().unwrap()); + + // Fork graph after pruning + // 0 + // | + // 5 + // | + // 11 + // | \ + // 15 25 + // | | + // 16 27 + // | + // 19 + // | + // 23 + + // Testing fork 11 - 15 - 16- 19 - 22 with root at 5 and current slot at 22 + let mut missing = + get_entries_to_load(&cache, 21, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(21); + cache.extract(&mut missing, &mut extracted, &env, true, true); + // Since the fork was pruned, we should not find the entry deployed at slot 20. + assert!(match_slot(&extracted, &program1, 0, 21)); + assert!(match_slot(&extracted, &program2, 11, 21)); + assert!(match_slot(&extracted, &program4, 15, 21)); + + // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 + let mut missing = + get_entries_to_load(&cache, 27, &[program1, program2, program3, program4]); + let mut extracted = ProgramCacheForTxBatch::new(27); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 27)); + assert!(match_slot(&extracted, &program2, 11, 27)); + assert!(match_slot(&extracted, &program3, 25, 27)); + assert!(match_slot(&extracted, &program4, 5, 27)); + + cache.prune(15, None, &fork_graph.read().unwrap()); + + // Fork graph after pruning + // 0 + // | + // 5 + // | + // 11 + // | + // 15 + // | + // 16 + // | + // 19 + // | + // 23 + + // Testing fork 16, 19, 23, with root at 15, current slot at 23 + let mut missing = + get_entries_to_load(&cache, 23, &[program1, program2, program3, program4]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(23); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 23)); + assert!(match_slot(&extracted, &program2, 11, 23)); + assert!(match_slot(&extracted, &program4, 15, 23)); + } + + #[test] + fn test_extract_using_deployment_slot() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + // Fork graph created for the test + // 0 + // / \ + // 10 5 + // | | + // 20 11 + // | | \ + // 22 15 25 + // | | + // 16 27 + // | + // 19 + // | + // 23 + + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20, 22]); + fork_graph.insert_fork(&[0, 5, 11, 12, 15, 16, 18, 19, 21, 23]); + fork_graph.insert_fork(&[0, 5, 11, 25, 27]); + + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); + + let program2 = Pubkey::new_unique(); + cache.assign_program(&env, program2, 5, new_test_entry(5, 6)); + cache.assign_program(&env, program2, 11, new_test_entry(11, 12)); + + let program3 = Pubkey::new_unique(); + cache.assign_program(&env, program3, 25, new_test_entry(25, 26)); + + // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 + let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(12); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 12)); + assert!(match_slot(&extracted, &program2, 11, 12)); + + // Test the same fork, but request the program modified at a later slot than what's in the cache. + let mut missing = get_entries_to_load(&cache, 12, &[program1, program2, program3]); + missing.get_mut(0).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + missing.get_mut(1).unwrap().1 = ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(5); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(12); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_missing(&missing, &program1, true)); + assert!(match_slot(&extracted, &program2, 11, 12)); + } + + #[test] + fn test_extract_unloaded() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + // Fork graph created for the test + // 0 + // / \ + // 10 5 + // | | + // 20 11 + // | | \ + // 22 15 25 + // | | + // 16 27 + // | + // 19 + // | + // 23 + + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20, 22]); + fork_graph.insert_fork(&[0, 5, 11, 15, 16, 19, 21, 23]); + fork_graph.insert_fork(&[0, 5, 11, 25, 27]); + + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); + + let program2 = Pubkey::new_unique(); + cache.assign_program(&env, program2, 5, new_test_entry(5, 6)); + cache.assign_program(&env, program2, 11, new_test_entry(11, 12)); + + let program3 = Pubkey::new_unique(); + // Insert an unloaded program with correct/cache's environment at slot 25 + let _ = insert_unloaded_entry(&mut cache, program3, 25); + + // Insert another unloaded program with a different environment at slot 20 + // Since this entry's environment won't match cache's environment, looking up this + // entry should return missing instead of unloaded entry. + cache.assign_program( + &env, + program3, + 20, + Arc::new( + new_test_entry(20, 21) + .to_unloaded() + .expect("Failed to create unloaded program"), + ), + ); + + // Testing fork 0 - 5 - 11 - 15 - 16 - 19 - 21 - 23 with current slot at 19 + let mut missing = get_entries_to_load(&cache, 19, &[program1, program2, program3]); + assert!(match_missing(&missing, &program3, false)); + let mut extracted = ProgramCacheForTxBatch::new(19); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 19)); + assert!(match_slot(&extracted, &program2, 11, 19)); + + // Testing fork 0 - 5 - 11 - 25 - 27 with current slot at 27 + let mut missing = get_entries_to_load(&cache, 27, &[program1, program2, program3]); + let mut extracted = ProgramCacheForTxBatch::new(27); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 27)); + assert!(match_slot(&extracted, &program2, 11, 27)); + assert!(match_missing(&missing, &program3, true)); + + // Testing fork 0 - 10 - 20 - 22 with current slot at 22 + let mut missing = get_entries_to_load(&cache, 22, &[program1, program2, program3]); + assert!(match_missing(&missing, &program2, false)); + let mut extracted = ProgramCacheForTxBatch::new(22); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 20, 22)); + assert!(match_missing(&missing, &program3, true)); + } + + #[test] + fn test_extract_different_environment() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + let other_env = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + + // Fork graph created for the test + // 0 + // | + // 10 + // | + // 20 + // | + // 22 + + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20, 22]); + + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program( + &env, + program1, + 10, + Arc::new(ProgramCacheEntry::new_tombstone( + 10, + ProgramCacheEntryOwner::LoaderV3, + ProgramCacheEntryType::Closed, + )), + ); + cache.assign_program(&env, program1, 20, new_test_entry(20, 21)); + + // Testing fork 0 - 10 - 20 - 22 with current slot at 22 + let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let mut extracted = ProgramCacheForTxBatch::new(22); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 20, 22)); + + // Looking for a different environment + let mut missing = get_entries_to_load(&cache, 22, &[program1]); + let mut extracted = ProgramCacheForTxBatch::new(22); + cache.extract(&mut missing, &mut extracted, &other_env, true, true); + assert!(match_missing(&missing, &program1, true)); + } + + #[test] + fn test_extract_nonexistent() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + let fork_graph = TestForkGraphSpecific::default(); + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + let mut missing = vec![(program1, ProgramCacheMatchCriteria::NoCriteria, 0)]; + let mut extracted = ProgramCacheForTxBatch::new(0); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_missing(&missing, &program1, true)); + } + + #[test] + fn test_unloaded() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + for program_cache_entry_type in [ + ProgramCacheEntryType::FailedVerification(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Closed, + ProgramCacheEntryType::Unloaded(get_mock_program_runtime_environment()), + ProgramCacheEntryType::Builtin(BuiltinProgram::new_mock()), + ] { + let entry = Arc::new(ProgramCacheEntry { + program: program_cache_entry_type, + account_owner: ProgramCacheEntryOwner::LoaderV2, + account_size: 0, + deployment_slot: 0, + effective_slot: 0, + stats: Arc::default(), + latest_access_slot: AtomicU64::default(), + }); + assert!(entry.to_unloaded().is_none()); + + // Check that unload_program_entry() does nothing for this entry + let program_id = Pubkey::new_unique(); + cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone()); + cache.unload_program_entry(program_id, entry.deployment_slot, &entry); + assert_eq!(cache.get_slot_versions_for_tests(&program_id).len(), 1); + assert!(cache.stats.evictions.is_empty()); + } + + let stats = ProgramStatistics { + uses: 3.into(), + ..Default::default() + }; + let entry = new_test_entry_with_usage(1, 2, stats); + let unloaded_entry = entry.to_unloaded().unwrap(); + assert_eq!(unloaded_entry.deployment_slot, 1); + assert_eq!(unloaded_entry.effective_slot, 2); + assert_eq!(unloaded_entry.latest_access_slot.load(Ordering::Relaxed), 1); + assert_eq!(unloaded_entry.stats.uses.load(Ordering::Relaxed), 3); + + // Check that unload_program_entry() does its work + let program_id = Pubkey::new_unique(); + cache.assign_program(&env, program_id, entry.deployment_slot, entry.clone()); + cache.unload_program_entry(program_id, entry.deployment_slot, &entry); + assert!(cache.stats.evictions.contains_key(&program_id)); + } + + #[test] + fn test_fork_prune_find_first_ancestor() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + // Fork graph created for the test + // 0 + // / \ + // 10 5 + // | + // 20 + + // Deploy program on slot 0, and slot 5. + // Prune the fork that has slot 5. The cache should still have the program + // deployed at slot 0. + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20]); + fork_graph.insert_fork(&[0, 5]); + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program1, 5, new_test_entry(5, 6)); + + cache.prune(10, None, &fork_graph.read().unwrap()); + + let mut missing = get_entries_to_load(&cache, 20, &[program1]); + let mut extracted = ProgramCacheForTxBatch::new(20); + cache.extract(&mut missing, &mut extracted, &env, true, true); + + // The cache should have the program deployed at slot 0 + assert_eq!( + extracted + .find(&program1) + .expect("Did not find the program") + .deployment_slot, + 0 + ); + } + + #[test] + fn test_prune_by_deployment_slot() { + let mut cache = ProgramCache::::new(0); + let env = get_mock_program_runtime_environment(); + + // Fork graph created for the test + // 0 + // / \ + // 10 5 + // | + // 20 + + // Deploy program on slot 0, and slot 5. + // Prune the fork that has slot 5. The cache should still have the program + // deployed at slot 0. + let mut fork_graph = TestForkGraphSpecific::default(); + fork_graph.insert_fork(&[0, 10, 20]); + fork_graph.insert_fork(&[0, 5, 6]); + let fork_graph = Arc::new(RwLock::new(fork_graph)); + cache.set_fork_graph(Arc::downgrade(&fork_graph)); + + let program1 = Pubkey::new_unique(); + cache.assign_program(&env, program1, 0, new_test_entry(0, 1)); + cache.assign_program(&env, program1, 5, new_test_entry(5, 6)); + + let program2 = Pubkey::new_unique(); + cache.assign_program(&env, program2, 10, new_test_entry(10, 11)); + + let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut extracted = ProgramCacheForTxBatch::new(20); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 20)); + assert!(match_slot(&extracted, &program2, 10, 20)); + + let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + assert!(match_missing(&missing, &program2, false)); + let mut extracted = ProgramCacheForTxBatch::new(6); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 5, 6)); + + // Pruning slot 5 will remove program1 entry deployed at slot 5. + // On fork chaining from slot 5, the entry deployed at slot 0 will become visible. + cache.prune_by_deployment_slot(5); + + let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + let mut extracted = ProgramCacheForTxBatch::new(20); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 20)); + assert!(match_slot(&extracted, &program2, 10, 20)); + + let mut missing = get_entries_to_load(&cache, 6, &[program1, program2]); + assert!(match_missing(&missing, &program2, false)); + let mut extracted = ProgramCacheForTxBatch::new(6); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 6)); + + // Pruning slot 10 will remove program2 entry deployed at slot 10. + // As there is no other entry for program2, extract() will return it as missing. + cache.prune_by_deployment_slot(10); + + let mut missing = get_entries_to_load(&cache, 20, &[program1, program2]); + assert!(match_missing(&missing, &program2, false)); + let mut extracted = ProgramCacheForTxBatch::new(20); + cache.extract(&mut missing, &mut extracted, &env, true, true); + assert!(match_slot(&extracted, &program1, 0, 20)); + } + + #[test] + fn test_usable_entries_for_slot() { + ProgramCache::::new(0); + let tombstone = Arc::new(ProgramCacheEntry::new_tombstone( + 0, + ProgramCacheEntryOwner::LoaderV2, + ProgramCacheEntryType::Closed, + )); + + assert!(ProgramCache::::matches_criteria( + &tombstone, + &ProgramCacheMatchCriteria::NoCriteria + )); + + assert!(ProgramCache::::matches_criteria( + &tombstone, + &ProgramCacheMatchCriteria::Tombstone + )); + + assert!(ProgramCache::::matches_criteria( + &tombstone, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0) + )); + + assert!(!ProgramCache::::matches_criteria( + &tombstone, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1) + )); + + let program = new_test_entry(0, 1); + + assert!(ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::NoCriteria + )); + + assert!(!ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::Tombstone + )); + + assert!(ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0) + )); + + assert!(!ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1) + )); + + let program = Arc::new(new_test_entry_with_usage( + 0, + 1, + ProgramStatistics::default(), + )); + + assert!(ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::NoCriteria + )); + + assert!(!ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::Tombstone + )); + + assert!(ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(0) + )); + + assert!(!ProgramCache::::matches_criteria( + &program, + &ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(1) + )); + } +} diff --git a/solana/program-runtime/src/loading_task.rs b/solana/program-runtime/src/loading_task.rs new file mode 100644 index 0000000..fe9ed92 --- /dev/null +++ b/solana/program-runtime/src/loading_task.rs @@ -0,0 +1,49 @@ +use solana_svm_type_overrides::sync::{Condvar, Mutex}; + +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)] +pub struct LoadingTaskCookie(u64); + +impl LoadingTaskCookie { + fn new() -> Self { + Self(0) + } + + fn update(&mut self) { + let LoadingTaskCookie(cookie) = self; + *cookie = cookie.wrapping_add(1); + } +} + +/// Suspends the thread in case no cooprative loading task was assigned +#[derive(Debug, Default)] +pub struct LoadingTaskWaiter { + cookie: Mutex, + cond: Condvar, +} + +impl LoadingTaskWaiter { + pub fn new() -> Self { + Self { + cookie: Mutex::new(LoadingTaskCookie::new()), + cond: Condvar::new(), + } + } + + pub fn cookie(&self) -> LoadingTaskCookie { + *self.cookie.lock().unwrap() + } + + pub fn notify(&self) { + let mut cookie = self.cookie.lock().unwrap(); + cookie.update(); + self.cond.notify_all(); + } + + pub fn wait(&self, cookie: LoadingTaskCookie) -> LoadingTaskCookie { + let cookie_guard = self.cookie.lock().unwrap(); + *self + .cond + .wait_while(cookie_guard, |current_cookie| *current_cookie == cookie) + .unwrap() + } +} diff --git a/solana/program-runtime/src/mem_pool.rs b/solana/program-runtime/src/mem_pool.rs new file mode 100644 index 0000000..9907baa --- /dev/null +++ b/solana/program-runtime/src/mem_pool.rs @@ -0,0 +1,194 @@ +use { + crate::execution_budget::{ + MAX_CALL_DEPTH, MAX_HEAP_FRAME_BYTES, MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268, + MIN_HEAP_FRAME_BYTES, + }, + solana_sbpf::{aligned_memory::AlignedMemory, ebpf::HOST_ALIGN, vm::CallFrame}, + std::{ + array, + ops::{Deref, DerefMut}, + }, +}; + +trait Reset { + fn reset(&mut self); +} + +struct Pool { + items: [Option; SIZE], + next_empty: usize, +} + +impl Pool { + fn new(items: [T; SIZE]) -> Self { + Self { + items: items.map(|i| Some(i)), + next_empty: SIZE, + } + } + + fn len(&self) -> usize { + SIZE + } + + fn get(&mut self) -> Option { + if self.next_empty == 0 { + return None; + } + self.next_empty = self.next_empty.saturating_sub(1); + self.items + .get_mut(self.next_empty) + .and_then(|item| item.take()) + } + + fn put(&mut self, mut value: T) -> bool { + self.items + .get_mut(self.next_empty) + .map(|item| { + value.reset(); + item.replace(value); + self.next_empty = self.next_empty.saturating_add(1); + true + }) + .unwrap_or(false) + } +} + +impl Reset for AlignedMemory<{ HOST_ALIGN }> { + fn reset(&mut self) { + self.as_slice_mut().fill(0) + } +} + +pub struct CallFrameBuffer(Box<[CallFrame; MAX_CALL_DEPTH]>); + +impl Default for CallFrameBuffer { + fn default() -> Self { + let mut mem = Box::<[CallFrame; MAX_CALL_DEPTH]>::new_uninit(); + let ptr = mem.as_mut_ptr().cast::(); + for i in 0..MAX_CALL_DEPTH { + unsafe { ptr.add(i).write(CallFrame::default()) } + } + Self(unsafe { mem.assume_init() }) + } +} + +impl Reset for CallFrameBuffer { + fn reset(&mut self) { + self.fill(CallFrame::default()) + } +} + +impl Deref for CallFrameBuffer { + type Target = [CallFrame]; + + fn deref(&self) -> &Self::Target { + self.0.as_slice() + } +} + +impl DerefMut for CallFrameBuffer { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.as_mut_slice() + } +} + +pub struct VmMemoryPool { + stack: Pool, MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268>, + heap: Pool, MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268>, + call_frame: Pool, +} + +impl VmMemoryPool { + pub fn new() -> Self { + Self { + stack: Pool::new(array::from_fn(|_| { + #[allow(clippy::arithmetic_side_effects)] + AlignedMemory::zero_filled(solana_sbpf::vm::get_stack_frame_size() * MAX_CALL_DEPTH) + })), + heap: Pool::new(array::from_fn(|_| { + AlignedMemory::zero_filled(MAX_HEAP_FRAME_BYTES as usize) + })), + call_frame: Pool::new(array::from_fn(|_| CallFrameBuffer::default())), + } + } + + pub fn stack_len(&self) -> usize { + self.stack.len() + } + + pub fn heap_len(&self) -> usize { + self.heap.len() + } + + #[allow(clippy::arithmetic_side_effects)] + pub fn get_stack(&mut self, size: usize) -> AlignedMemory<{ HOST_ALIGN }> { + debug_assert!(size == solana_sbpf::vm::get_stack_frame_size() * MAX_CALL_DEPTH); + self.stack + .get() + .unwrap_or_else(|| AlignedMemory::zero_filled(size)) + } + + pub fn put_stack(&mut self, stack: AlignedMemory<{ HOST_ALIGN }>) -> bool { + self.stack.put(stack) + } + + pub fn get_heap(&mut self, heap_size: u32) -> AlignedMemory<{ HOST_ALIGN }> { + debug_assert!((MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&heap_size)); + self.heap + .get() + .unwrap_or_else(|| AlignedMemory::zero_filled(MAX_HEAP_FRAME_BYTES as usize)) + } + + pub fn put_heap(&mut self, heap: AlignedMemory<{ HOST_ALIGN }>) -> bool { + let heap_size = heap.len(); + debug_assert!( + heap_size >= MIN_HEAP_FRAME_BYTES as usize + && heap_size <= MAX_HEAP_FRAME_BYTES as usize + ); + self.heap.put(heap) + } + + pub fn get_call_frames(&mut self) -> CallFrameBuffer { + self.call_frame.get().unwrap_or_default() + } + + pub fn put_call_frames(&mut self, call_frame: CallFrameBuffer) -> bool { + self.call_frame.put(call_frame) + } +} + +impl Default for VmMemoryPool { + fn default() -> Self { + Self::new() + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[derive(Debug, Eq, PartialEq)] + struct Item(u8, u8); + impl Reset for Item { + fn reset(&mut self) { + self.1 = 0; + } + } + + #[test] + fn test_pool() { + let mut pool = Pool::::new([Item(0, 1), Item(1, 1)]); + assert_eq!(pool.get(), Some(Item(1, 1))); + assert_eq!(pool.get(), Some(Item(0, 1))); + assert_eq!(pool.get(), None); + pool.put(Item(1, 1)); + assert_eq!(pool.get(), Some(Item(1, 0))); + pool.put(Item(2, 2)); + pool.put(Item(3, 3)); + assert!(!pool.put(Item(4, 4))); + assert_eq!(pool.get(), Some(Item(3, 0))); + assert_eq!(pool.get(), Some(Item(2, 0))); + assert_eq!(pool.get(), None); + } +} diff --git a/solana/program-runtime/src/memory.rs b/solana/program-runtime/src/memory.rs new file mode 100644 index 0000000..508a1b5 --- /dev/null +++ b/solana/program-runtime/src/memory.rs @@ -0,0 +1,138 @@ +//! Memory translation utilities. + +use { + solana_sbpf::memory_region::{AccessType, MemoryMapping}, + solana_transaction_context::vm_slice::VmSlice, + std::{mem::align_of, slice::from_raw_parts_mut}, +}; + +/// Error types for memory translation operations. +#[derive(Debug, thiserror::Error, PartialEq, Eq, Clone)] +pub enum MemoryTranslationError { + #[error("Unaligned pointer")] + UnalignedPointer, + #[error("InvalidLength")] + InvalidLength, +} + +pub fn address_is_aligned(address: u64) -> bool { + (address as *mut T as usize) + .checked_rem(align_of::()) + .map(|rem| rem == 0) + .expect("T to be non-zero aligned") +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_inner { + ($memory_mapping:expr, $map:ident, $access_type:expr, $vm_addr:expr, $len:expr $(,)?) => { + Result::>::from( + $memory_mapping + .$map($access_type, $vm_addr, $len) + .map_err(|err| err.into()), + ) + }; +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_type_inner { + ($memory_mapping:expr, $access_type:expr, $vm_addr:expr, $T:ty, $check_aligned:expr $(,)?) => {{ + let host_addr = $crate::translate_inner!( + $memory_mapping, + map, + $access_type, + $vm_addr, + size_of::<$T>() as u64 + )?; + if !$check_aligned { + Ok(unsafe { std::mem::transmute::(host_addr) }) + } else if !$crate::memory::address_is_aligned::<$T>(host_addr) { + Err($crate::memory::MemoryTranslationError::UnalignedPointer.into()) + } else { + Ok(unsafe { &mut *(host_addr as *mut $T) }) + } + }}; +} + +// Do not use this directly +#[macro_export] +macro_rules! translate_slice_inner { + ($memory_mapping:expr, $access_type:expr, $vm_addr:expr, $len:expr, $T:ty, $check_aligned:expr $(,)?) => {{ + if $len == 0 { + return Ok(&mut []); + } + let total_size = $len.saturating_mul(size_of::<$T>() as u64); + if isize::try_from(total_size).is_err() { + return Err($crate::memory::MemoryTranslationError::InvalidLength.into()); + } + let host_addr = + $crate::translate_inner!($memory_mapping, map, $access_type, $vm_addr, total_size)?; + if $check_aligned && !$crate::memory::address_is_aligned::<$T>(host_addr) { + return Err($crate::memory::MemoryTranslationError::UnalignedPointer.into()); + } + Ok(unsafe { from_raw_parts_mut(host_addr as *mut $T, $len as usize) }) + }}; +} + +pub fn translate_type<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + check_aligned: bool, +) -> Result<&'a T, Box> { + translate_type_inner!(memory_mapping, AccessType::Load, vm_addr, T, check_aligned) + .map(|value| &*value) +} + +pub fn translate_slice( + memory_mapping: &MemoryMapping, + vm_addr: u64, + len: u64, + check_aligned: bool, +) -> Result<&[T], Box> { + translate_slice_inner!( + memory_mapping, + AccessType::Load, + vm_addr, + len, + T, + check_aligned, + ) + .map(|value| &*value) +} + +/// CPI-specific version with intentionally different lifetime signature. +/// This version is missing lifetime 'a of the return type in the parameter &MemoryMapping. +pub fn translate_type_mut_for_cpi<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + check_aligned: bool, +) -> Result<&'a mut T, Box> { + translate_type_inner!(memory_mapping, AccessType::Store, vm_addr, T, check_aligned) +} + +/// CPI-specific version with intentionally different lifetime signature. +/// This version is missing lifetime 'a of the return type in the parameter &MemoryMapping. +pub fn translate_slice_mut_for_cpi<'a, T>( + memory_mapping: &MemoryMapping, + vm_addr: u64, + len: u64, + check_aligned: bool, +) -> Result<&'a mut [T], Box> { + translate_slice_inner!( + memory_mapping, + AccessType::Store, + vm_addr, + len, + T, + check_aligned, + ) +} + +pub fn translate_vm_slice<'a, T>( + slice: &VmSlice, + memory_mapping: &'a MemoryMapping, + check_aligned: bool, +) -> Result<&'a [T], Box> { + translate_slice::(memory_mapping, slice.ptr(), slice.len(), check_aligned) +} diff --git a/solana/program-runtime/src/memory_context.rs b/solana/program-runtime/src/memory_context.rs new file mode 100644 index 0000000..2a91946 --- /dev/null +++ b/solana/program-runtime/src/memory_context.rs @@ -0,0 +1,133 @@ +use { + crate::invoke_context::BpfAllocator, solana_instruction::error::InstructionError, + solana_sbpf::memory_region::MemoryMapping, +}; + +enum MemoryContextType { + ABIv1(MemoryContext), + Placeholder, +} + +pub struct MemoryContexts { + contexts: Vec, +} + +impl MemoryContexts { + pub(crate) fn new() -> Self { + Self { + contexts: Vec::new(), + } + } + + /// Set this instruction's [`MemoryContext`]. + pub fn set_memory_context_abi_v1( + &mut self, + memory_context: MemoryContext, + ) -> Result<(), InstructionError> { + *self + .contexts + .last_mut() + .ok_or(InstructionError::CallDepth)? = MemoryContextType::ABIv1(memory_context); + Ok(()) + } + + /// Get current instruction's [`MemoryContext`] + pub fn memory_context_abi_v1(&self) -> Result<&MemoryContext, InstructionError> { + match self.contexts.last().ok_or(InstructionError::CallDepth)? { + MemoryContextType::ABIv1(ctx) => Ok(ctx), + MemoryContextType::Placeholder => Err(InstructionError::ProgramEnvironmentSetupFailure), + } + } + + /// Get current instruction's [`MemoryContext`] for mutable use. + pub fn memory_context_mut_abi_v1(&mut self) -> Result<&mut MemoryContext, InstructionError> { + let context = self + .contexts + .last_mut() + .ok_or(InstructionError::CallDepth)?; + + match context { + MemoryContextType::ABIv1(ctx) => Ok(ctx), + MemoryContextType::Placeholder => Err(InstructionError::ProgramEnvironmentSetupFailure), + } + } + + pub fn memory_mapping(&self) -> Result<&MemoryMapping, InstructionError> { + let mapping = match self.contexts.last().ok_or(InstructionError::CallDepth)? { + MemoryContextType::ABIv1(ctx) => &ctx.memory_mapping, + MemoryContextType::Placeholder => { + return Err(InstructionError::ProgramEnvironmentSetupFailure); + } + }; + + Ok(mapping) + } + + pub fn memory_mapping_mut(&mut self) -> Result<&mut MemoryMapping, InstructionError> { + let mapping = match self + .contexts + .last_mut() + .ok_or(InstructionError::CallDepth)? + { + MemoryContextType::ABIv1(ctx) => &mut ctx.memory_mapping, + MemoryContextType::Placeholder => { + return Err(InstructionError::ProgramEnvironmentSetupFailure); + } + }; + + Ok(mapping) + } + + #[cfg(feature = "dev-context-only-utils")] + pub fn mock_set_mapping_abi_v1(&mut self, memory_mapping: MemoryMapping) { + self.contexts = vec![MemoryContextType::ABIv1(MemoryContext { + allocator: BpfAllocator::new(0), + accounts_metadata: vec![], + memory_mapping: Box::new(memory_mapping), + })]; + } + + pub fn push_placeholder(&mut self) { + // We are only pushing a placeholder to be configured later + self.contexts.push(MemoryContextType::Placeholder); + } + + pub fn pop(&mut self) { + self.contexts.pop(); + } +} + +/// This structure contains metadata about the memory for each instruction under execution. +/// The BpfAllocator, accounts addresses in the guest and the memory mapping. +pub struct MemoryContext { + pub allocator: BpfAllocator, + pub accounts_metadata: Vec, + memory_mapping: Box, +} + +impl MemoryContext { + /// Creates a new memory context + pub fn new( + allocator: BpfAllocator, + accounts_metadata: Vec, + memory_mapping: MemoryMapping, + ) -> Self { + Self { + allocator, + accounts_metadata, + memory_mapping: Box::new(memory_mapping), + } + } +} + +#[derive(Debug, Clone)] +pub struct SerializedAccountMetadata { + /// Address of the first byte of the serialized account record (the + /// `NON_DUP_MARKER`/duplicate-marker byte). + pub vm_addr: u64, + pub original_data_len: usize, + pub vm_data_addr: u64, + pub vm_key_addr: u64, + pub vm_lamports_addr: u64, + pub vm_owner_addr: u64, +} diff --git a/solana/program-runtime/src/program_cache_entry.rs b/solana/program-runtime/src/program_cache_entry.rs new file mode 100644 index 0000000..35764a7 --- /dev/null +++ b/solana/program-runtime/src/program_cache_entry.rs @@ -0,0 +1,522 @@ +#[cfg(feature = "metrics")] +use crate::program_metrics::LoadProgramMetrics; +use { + crate::{ + invoke_context::{BuiltinFunctionRegisterer, InvokeContext}, + loaded_programs::ProgramRuntimeEnvironment, + program_metrics::ProgramStatistics, + }, + solana_clock::Slot, + solana_pubkey::Pubkey, + solana_sbpf::{elf::Executable, program::BuiltinProgram, verifier::RequisiteVerifier}, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + }, + solana_svm_type_overrides::sync::{ + Arc, + atomic::{AtomicU64, Ordering}, + }, +}; + +pub const DELAY_VISIBILITY_SLOT_OFFSET: Slot = 1; + +/// The owner of a programs accounts, thus the loader of a program +#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)] +pub enum ProgramCacheEntryOwner { + #[default] + NativeLoader, + LoaderV1, + LoaderV2, + LoaderV3, + LoaderV4, +} + +impl TryFrom<&Pubkey> for ProgramCacheEntryOwner { + type Error = (); + fn try_from(loader_key: &Pubkey) -> Result { + if native_loader::check_id(loader_key) { + Ok(ProgramCacheEntryOwner::NativeLoader) + } else if bpf_loader_deprecated::check_id(loader_key) { + Ok(ProgramCacheEntryOwner::LoaderV1) + } else if bpf_loader::check_id(loader_key) { + Ok(ProgramCacheEntryOwner::LoaderV2) + } else if bpf_loader_upgradeable::check_id(loader_key) { + Ok(ProgramCacheEntryOwner::LoaderV3) + } else if loader_v4::check_id(loader_key) { + Ok(ProgramCacheEntryOwner::LoaderV4) + } else { + Err(()) + } + } +} + +impl From for Pubkey { + fn from(program_cache_entry_owner: ProgramCacheEntryOwner) -> Self { + match program_cache_entry_owner { + ProgramCacheEntryOwner::NativeLoader => native_loader::id(), + ProgramCacheEntryOwner::LoaderV1 => bpf_loader_deprecated::id(), + ProgramCacheEntryOwner::LoaderV2 => bpf_loader::id(), + ProgramCacheEntryOwner::LoaderV3 => bpf_loader_upgradeable::id(), + ProgramCacheEntryOwner::LoaderV4 => loader_v4::id(), + } + } +} + +/* + The possible ProgramCacheEntryType transitions: + + DelayVisibility is special in that it is never stored in the cache. + It is only returned by ProgramCacheForTxBatch::find() when a Loaded entry + is encountered which is not effective yet. + + Builtin re/deployment: + - Empty => Builtin in TransactionBatchProcessor::add_builtin + - Builtin => Builtin in TransactionBatchProcessor::add_builtin + + Un/re/deployment (with delay and cooldown): + - Empty / Closed => Loaded in UpgradeableLoaderInstruction::DeployWithMaxDataLen + - Loaded / FailedVerification => Loaded in UpgradeableLoaderInstruction::Upgrade + - Loaded / FailedVerification => Closed in UpgradeableLoaderInstruction::Close + + Loader migration: + - Closed => Closed (in the same slot) + - FailedVerification => FailedVerification (with different account_owner) + - Loaded => Loaded (with different account_owner) + + Eviction and unloading (in the same slot): + - Unloaded => Loaded in ProgramCache::assign_program + - Loaded => Unloaded in ProgramCache::unload_program_entry + + At epoch boundary (when feature set and environment changes): + - Loaded => FailedVerification in Bank::_new_from_parent + - FailedVerification => Loaded in Bank::_new_from_parent + + Through pruning (when on orphan fork or overshadowed on the rooted fork): + - Closed / Unloaded / Loaded / Builtin => Empty in ProgramCache::prune +*/ + +/// Actual payload of [ProgramCacheEntry]. +#[derive(Default)] +pub enum ProgramCacheEntryType { + /// Tombstone for programs which currently do not pass the verifier but could if the feature set changed. + FailedVerification(ProgramRuntimeEnvironment), + /// Tombstone for programs that were either explicitly closed or never deployed. + /// + /// It's also used for accounts belonging to program loaders, that don't actually contain program code (e.g. buffer accounts for LoaderV3 programs). + #[default] + Closed, + /// Tombstone for programs which have recently been modified but the new version is not visible yet. + DelayVisibility, + /// Successfully verified but not currently compiled. + /// + /// It continues to track usage statistics even when the compiled executable of the program is evicted from memory. + Unloaded(ProgramRuntimeEnvironment), + /// Verified program. + /// + /// It may or may not be JIT compiled. + Loaded(Executable>), + /// A built-in program which is not stored on-chain but backed into and distributed with the validator + Builtin(BuiltinProgram>), +} + +impl std::fmt::Debug for ProgramCacheEntryType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct(match self { + ProgramCacheEntryType::FailedVerification(_) => { + "ProgramCacheEntryType::FailedVerification" + } + ProgramCacheEntryType::Closed => "ProgramCacheEntryType::Closed", + ProgramCacheEntryType::DelayVisibility => "ProgramCacheEntryType::DelayVisibility", + ProgramCacheEntryType::Unloaded(_) => "ProgramCacheEntryType::Unloaded", + ProgramCacheEntryType::Loaded(_) => "ProgramCacheEntryType::Loaded", + ProgramCacheEntryType::Builtin(_) => "ProgramCacheEntryType::Builtin", + }) + .finish() + } +} + +impl ProgramCacheEntryType { + /// Returns a reference to its environment if it has one + pub fn get_environment(&self) -> Option<&ProgramRuntimeEnvironment> { + match self { + ProgramCacheEntryType::Loaded(program) => { + Some(ProgramRuntimeEnvironment::from_ref(program.get_loader())) + } + ProgramCacheEntryType::FailedVerification(env) + | ProgramCacheEntryType::Unloaded(env) => Some(env), + _ => None, + } + } +} + +/// Holds a program version at a specific address and on a specific slot / fork. +/// +/// It contains the actual program in [ProgramCacheEntryType] and a bunch of meta-data. +#[derive(Debug, Default)] +pub struct ProgramCacheEntry { + /// The program of this entry + pub program: ProgramCacheEntryType, + /// The loader of this entry + pub account_owner: ProgramCacheEntryOwner, + /// Size of account that stores the program and program data + pub account_size: usize, + /// Slot in which the program was (re)deployed + pub deployment_slot: Slot, + /// Slot in which this entry will become active (can be in the future) + pub effective_slot: Slot, + /// How often this entry was used by a transaction + pub stats: Arc, + pub latest_access_slot: AtomicU64, +} + +impl PartialEq for ProgramCacheEntry { + fn eq(&self, other: &Self) -> bool { + self.effective_slot == other.effective_slot + && self.deployment_slot == other.deployment_slot + && self.is_tombstone() == other.is_tombstone() + } +} + +impl ProgramCacheEntry { + /// Creates a new user program + pub fn new( + loader_key: &Pubkey, + program_runtime_environment: ProgramRuntimeEnvironment, + deployment_slot: Slot, + effective_slot: Slot, + elf_bytes: &[u8], + account_size: usize, + #[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics, + ) -> Result> { + Self::new_internal( + loader_key, + program_runtime_environment, + deployment_slot, + effective_slot, + elf_bytes, + account_size, + #[cfg(feature = "metrics")] + metrics, + false, /* reloading */ + ) + } + + /// Reloads a user program, *without* running the verifier. + /// + /// # Safety + /// + /// This method is unsafe since it assumes that the program has already been verified. Should + /// only be called when the program was previously verified and loaded in the cache, but was + /// unloaded due to inactivity. It should also be checked that the `program_runtime_environment` + /// hasn't changed since it was unloaded. + pub unsafe fn reload( + loader_key: &Pubkey, + program_runtime_environment: ProgramRuntimeEnvironment, + deployment_slot: Slot, + effective_slot: Slot, + elf_bytes: &[u8], + account_size: usize, + #[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics, + ) -> Result> { + Self::new_internal( + loader_key, + program_runtime_environment, + deployment_slot, + effective_slot, + elf_bytes, + account_size, + #[cfg(feature = "metrics")] + metrics, + true, /* reloading */ + ) + } + + fn new_internal( + loader_key: &Pubkey, + program_runtime_environment: ProgramRuntimeEnvironment, + deployment_slot: Slot, + effective_slot: Slot, + elf_bytes: &[u8], + account_size: usize, + #[cfg(feature = "metrics")] metrics: &mut LoadProgramMetrics, + reloading: bool, + ) -> Result> { + let entry_stats = ProgramStatistics::default(); + #[cfg(feature = "metrics")] + let load_elf_time = solana_svm_measure::measure::Measure::start("load_elf_time"); + let executable = Executable::load(elf_bytes, Arc::clone(&*program_runtime_environment))?; + + #[cfg(feature = "metrics")] + { + metrics.load_elf_us = load_elf_time.end_as_us(); + } + + if !reloading { + #[cfg(feature = "metrics")] + let verify_code_time = solana_svm_measure::measure::Measure::start("verify_code_time"); + executable.verify::()?; + #[cfg(feature = "metrics")] + { + metrics.verify_code_us = verify_code_time.end_as_us(); + } + } + + #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] + { + let jit_compile_time = solana_svm_measure::measure::Measure::start("jit_compile_time"); + executable.jit_compile()?; + let jit_compile_time = jit_compile_time.end_as_us(); + entry_stats.jit_compiled(jit_compile_time); + #[cfg(feature = "metrics")] + { + metrics.jit_compile_us = jit_compile_time; + } + } + + Ok(Self { + deployment_slot, + account_owner: ProgramCacheEntryOwner::try_from(loader_key).unwrap(), + account_size, + effective_slot, + program: ProgramCacheEntryType::Loaded(executable), + stats: entry_stats.into(), + latest_access_slot: AtomicU64::new(0), + }) + } + + pub fn to_unloaded(&self) -> Option { + match &self.program { + ProgramCacheEntryType::Loaded(_) => {} + ProgramCacheEntryType::FailedVerification(_) + | ProgramCacheEntryType::Closed + | ProgramCacheEntryType::DelayVisibility + | ProgramCacheEntryType::Unloaded(_) + | ProgramCacheEntryType::Builtin(_) => { + return None; + } + } + Some(Self { + program: ProgramCacheEntryType::Unloaded(self.program.get_environment()?.clone()), + account_owner: self.account_owner, + account_size: self.account_size, + deployment_slot: self.deployment_slot, + effective_slot: self.effective_slot, + stats: Arc::clone(&self.stats), + latest_access_slot: AtomicU64::new(self.latest_access_slot.load(Ordering::Relaxed)), + }) + } + + /// Creates a new built-in program + pub fn new_builtin( + deployment_slot: Slot, + account_size: usize, + register_fn: BuiltinFunctionRegisterer, + ) -> Self { + let mut program = BuiltinProgram::new_builtin(); + register_fn(&mut program, "entrypoint").unwrap(); + Self { + deployment_slot, + account_owner: ProgramCacheEntryOwner::NativeLoader, + account_size, + effective_slot: deployment_slot, + program: ProgramCacheEntryType::Builtin(program), + stats: Arc::default(), + latest_access_slot: AtomicU64::new(0), + } + } + + pub fn new_tombstone( + slot: Slot, + account_owner: ProgramCacheEntryOwner, + reason: ProgramCacheEntryType, + ) -> Self { + Self::new_tombstone_with_stats(slot, account_owner, reason, Arc::default()) + } + + pub fn new_tombstone_with_stats( + slot: Slot, + account_owner: ProgramCacheEntryOwner, + reason: ProgramCacheEntryType, + stats: Arc, + ) -> Self { + let tombstone = Self { + program: reason, + account_owner, + account_size: 0, + deployment_slot: slot, + effective_slot: slot, + stats, + latest_access_slot: AtomicU64::new(0), + }; + debug_assert!(tombstone.is_tombstone()); + tombstone + } + + pub fn is_tombstone(&self) -> bool { + matches!( + self.program, + ProgramCacheEntryType::FailedVerification(_) + | ProgramCacheEntryType::Closed + | ProgramCacheEntryType::DelayVisibility + ) + } + + pub(crate) fn is_implicit_delay_visibility_tombstone(&self, slot: Slot) -> bool { + !matches!(self.program, ProgramCacheEntryType::Builtin(_)) + && self.effective_slot.saturating_sub(self.deployment_slot) + == DELAY_VISIBILITY_SLOT_OFFSET + && slot >= self.deployment_slot + && slot < self.effective_slot + } + + pub fn update_access_slot(&self, slot: Slot) { + let _ = self.latest_access_slot.fetch_max(slot, Ordering::Relaxed); + } + + /// Compute a retention score. + /// + /// Eviction uses an adapted GDSF scheme which incorporates frequency, recovery cost + /// (recompilation) and time-based decay. + /// + /// How hard should we try to retain this entry. Higher number -> retention more likely. + pub fn retention_score(&self) -> u64 { + let last_access = self.latest_access_slot.load(Ordering::Relaxed); + let recovery_cost = self.stats.compilation_time_ema.load(Ordering::Relaxed); + let frequency = self.stats.uses.load(Ordering::Relaxed); + retention_score(last_access, recovery_cost, frequency) + } + + pub fn account_owner(&self) -> Pubkey { + self.account_owner.into() + } +} + +/// See [`ProgramCacheEntry::retention_score`]. +pub(crate) const fn retention_score(last_access: u64, recovery_cost: u64, frequency: u64) -> u64 { + // Traditionally GDSF uses the following logic: + // + // on_access: + // entry.frequency += 1 + // entry.H := cache.L + (entry.cost * entry.frequency) / entry.size + // + // on_eviction: + // victim = pick_victim_minimizing_H() + // cache.L := victim.H + // + // It achieves decay by virtue of L increasing over time (and therefore the “value” of + // stored score of each entry decreasing over time.) Entry recovery and frequency, as well + // as size are otherwise also accounted for by them inflating the overall score by a bit. + // + // We adapt this algorithm slightly: we already have a kind of `L` – access slot. It does + // not include the weight of the evicted entry as the original algorithm does, that is + // *probably* fine (the author has not done any empirical experiments to verify it it + // actually matters.) + // + // Additionally we ignore the size component altogether as irrelevant and instead of + // applying entry weight linearly, we use a `log_2`. We can't use plain `weight*frequency` + // as the most heavily used entries would never ever get evicted after just some runtime, + // even if they're no longer used. With `log_2` weight and frequency can contribute to + // up-to 128 slots of "bonus" towards their retention compared to rarely used peers. + // + // Feel free to adjust the specific formulae used. + let weight = (recovery_cost as u128).wrapping_mul(frequency as u128); + let weight_log = u128::BITS.wrapping_sub(weight.leading_zeros()); + last_access.saturating_add(weight_log as u64) +} + +#[cfg(test)] +mod tests { + use { + crate::{ + loaded_programs::tests::new_test_entry_with_usage, program_metrics::ProgramStatistics, + }, + std::sync::atomic::{AtomicU64, Ordering}, + }; + + #[test] + fn test_retention_score_decay_horizon() { + let stats = ProgramStatistics { + uses: AtomicU64::new(u64::MAX), + compilation_time_ema: AtomicU64::new(u64::MAX), + ..Default::default() + }; + let program = new_test_entry_with_usage(0, 0, stats); + program.update_access_slot(1); + assert!( + dbg!(program.retention_score()) <= 129, + "retention score should remain within sensible boundaries even for very frequently \ + used entries." + ); + } + + #[test] + fn test_retention_score_frequency_preference() { + let stats = ProgramStatistics { + uses: AtomicU64::new(16), + compilation_time_ema: AtomicU64::new(1), + ..Default::default() + }; + let program = new_test_entry_with_usage(10, 11, stats); + program.update_access_slot(15); + let less_used_retention_score = program.retention_score(); + program.stats.uses.fetch_max(1024, Ordering::Relaxed); + let more_used_retention_score = program.retention_score(); + assert!( + less_used_retention_score > 15, + "frequency should count for entry retention score" + ); + assert!( + dbg!(more_used_retention_score) > dbg!(less_used_retention_score), + "retention score should prefer evicting less used entry over the more used one if \ + possible" + ); + } + + #[test] + fn test_retention_score_recovery_time_preference() { + let stats = ProgramStatistics { + uses: AtomicU64::new(1), + compilation_time_ema: AtomicU64::new(1000), + ..Default::default() + }; + let program = new_test_entry_with_usage(10, 11, stats); + program.update_access_slot(15); + let cheaper_to_compile_score = program.retention_score(); + program + .stats + .compilation_time_ema + .fetch_max(2000, Ordering::Relaxed); + let more_expensive_to_compile_score = program.retention_score(); + assert!( + cheaper_to_compile_score > 15, + "compile time should count for entry retention score" + ); + assert!( + dbg!(more_expensive_to_compile_score) > dbg!(cheaper_to_compile_score), + "retention score should prefer evicting cheaper-to-compile entries" + ); + } + + #[test] + fn test_retention_weight_metric_does_not_outweight_smaller_metric() { + // Compilation time generally stays in the scale of 4 digits, while the uses counter can + // become many millions. Neither should overshadow other too much. + let stats = ProgramStatistics { + uses: AtomicU64::new(100_000_000), + compilation_time_ema: AtomicU64::new(1000), + ..Default::default() + }; + let program = new_test_entry_with_usage(10, 11, stats); + program.update_access_slot(15); + let previous_score = program.retention_score(); + program + .stats + .compilation_time_ema + .fetch_max(2000, Ordering::Relaxed); + let new_score = program.retention_score(); + assert!( + dbg!(previous_score) != dbg!(new_score), + "retention weight components shouldn't overshadow the other due to scale differences" + ); + } +} diff --git a/solana/program-runtime/src/program_metrics.rs b/solana/program-runtime/src/program_metrics.rs new file mode 100644 index 0000000..31d3626 --- /dev/null +++ b/solana/program-runtime/src/program_metrics.rs @@ -0,0 +1,326 @@ +#[cfg(feature = "metrics")] +use solana_svm_timings::ExecuteDetailsTimings; +use { + crate::loaded_programs::ForkGraph, + log::{debug, log_enabled, trace}, + solana_pubkey::Pubkey, + std::{ + collections::HashMap, + sync::atomic::{AtomicU64, Ordering}, + }, +}; + +#[derive(Debug, Default)] +pub struct ProgramStatistics { + pub uses: AtomicU64, + + pub compilations: AtomicU64, + pub total_compilation_time_us: AtomicU64, + /// Exponential moving average of the compilation time. + pub compilation_time_ema: AtomicU64, + + pub jit_invocations: AtomicU64, + pub total_jit_execution_time_us: AtomicU64, + /// Exponential moving average of the JIT execution time. + pub jit_execution_time_ema: AtomicU64, + + pub interpreted_invocations: AtomicU64, + pub total_interpretation_time_us: AtomicU64, + /// Exponential moving average of the interpreted execution time. + pub interpretation_time_ema: AtomicU64, +} + +/// Number of compilation observations contributing to the the [`Self::compilation_time_ema`]. +const COMPILATION_EMA_WINDOW_SIZE: u64 = 10; +/// Number of execution observations contributing to the execution EMA stats. +const EXECUTION_EMA_WINDOW_SIZE: u64 = 500; +/// Track exponential moving average in scaled-up units. +/// +/// Doing so allows to mitigate error from rounding-towards-zero we get when using integer math. +pub(crate) const EMA_SCALE: u64 = 1_000; + +impl ProgramStatistics { + fn observe_ema(counter: &AtomicU64, duration_us: u64) { + let duration_ema = duration_us.saturating_mul(EMA_SCALE); + counter + .fetch_update(Ordering::Relaxed, Ordering::Relaxed, |ema| { + // Exponential moving average iteratively is computed as $ema' = alpha * + // observation + (1 - alpha) * ema$. This works great for floating point, but we + // want integers. For purposes of convenience we also want to really think in terms + // of simple moving average window sizes as that is easier to reason about. + // + // Exponential moving average and simple moving average of window N has a rough + // equivalence of `alpha ≈ 2 / (N + 1)`. Slotting this into our original iterative + // formula: + // + // $$ ema' = 2 / (N+1) * observation + (1 - 2/(N+1)) * ema $$ + // + // we get + // + // $$ ema' = (2*observation)/(N+1) + (N+1-2)*ema/(N+1) $$ + let (numer, denom) = const { (2, 1 + WINDOW_SIZE) }; + Some(if ema == 0 { + duration_ema + } else { + let weighted_observation = duration_ema.saturating_mul(numer); + let previous_observations = ema.saturating_mul(denom.saturating_sub(numer)); + weighted_observation + .saturating_add(previous_observations) + .checked_div(denom) + .expect("unreachable: denom is >= 1") + }) + }) + .expect("unreachable: closure always returns a Some"); + } + + /// Record information about JIT compilation. + pub fn jit_compiled(&self, duration_us: u64) { + let ord = Ordering::Relaxed; + self.compilations.fetch_add(1, ord); + self.total_compilation_time_us.fetch_add(duration_us, ord); + Self::observe_ema::(&self.compilation_time_ema, duration_us); + } + + /// Record information about JIT-compiled program having been executed. + pub fn jit_executed(&self, duration_us: u64) { + let ord = Ordering::Relaxed; + self.jit_invocations.fetch_add(1, ord); + self.total_jit_execution_time_us.fetch_add(duration_us, ord); + Self::observe_ema::(&self.jit_execution_time_ema, duration_us); + } + + /// Record information about program executed with the interpreter. + pub fn interpreter_executed(&self, duration_us: u64) { + let ord = Ordering::Relaxed; + self.interpreted_invocations.fetch_add(1, ord); + self.total_interpretation_time_us + .fetch_add(duration_us, ord); + Self::observe_ema::(&self.interpretation_time_ema, duration_us); + } + + pub fn merge_from(&self, other: &ProgramStatistics) { + let ord = Ordering::Relaxed; + self.uses.fetch_add(other.uses.load(ord), ord); + let other_compilations = other.compilations.load(ord); + let this_compilations = self.compilations.fetch_add(other_compilations, ord); + self.total_compilation_time_us + .fetch_add(other.total_compilation_time_us.load(ord), ord); + let other_jit_invocations = other.jit_invocations.load(ord); + let this_jit_invocations = self.jit_invocations.fetch_add(other_jit_invocations, ord); + self.total_jit_execution_time_us + .fetch_add(other.total_jit_execution_time_us.load(ord), ord); + let other_interpretations = other.interpreted_invocations.load(ord); + let this_interpretations = self + .interpreted_invocations + .fetch_add(other_interpretations, ord); + self.total_interpretation_time_us + .fetch_add(other.total_interpretation_time_us.load(ord), ord); + if let Some(comp_ema) = ProgramCacheStats::combined_ema::< + COMPILATION_EMA_WINDOW_SIZE, + COMPILATION_EMA_WINDOW_SIZE, + >( + &self.compilation_time_ema, + &other.compilation_time_ema, + this_compilations, + other_compilations, + ) { + self.compilation_time_ema.store(comp_ema, ord); + } + if let Some(exec_ema) = + ProgramCacheStats::combined_ema::( + &self.jit_execution_time_ema, + &other.jit_execution_time_ema, + this_jit_invocations, + other_jit_invocations, + ) + { + self.jit_execution_time_ema.store(exec_ema, ord); + } + if let Some(interp_ema) = + ProgramCacheStats::combined_ema::( + &self.interpretation_time_ema, + &other.interpretation_time_ema, + this_interpretations, + other_interpretations, + ) + { + self.interpretation_time_ema.store(interp_ema, ord); + } + } +} + +/// Global cache statistics for [ProgramCache]. +#[derive(Debug, Default)] +pub struct ProgramCacheStats { + /// a program was already in the cache + pub hits: AtomicU64, + /// a program was not found and loaded instead + pub misses: AtomicU64, + /// a compiled executable was unloaded + pub evictions: HashMap, + /// an unloaded program was loaded again (opposite of eviction) + pub reloads: AtomicU64, + /// a program was loaded or un/re/deployed + pub insertions: AtomicU64, + /// a program was loaded but can not be extracted on its own fork anymore + pub lost_insertions: AtomicU64, + /// a program which was already in the cache was reloaded by mistake + pub replacements: AtomicU64, + /// a program was only used once before being unloaded + pub one_hit_wonders: AtomicU64, + /// a program became unreachable in the fork graph because of rerooting + pub prunes_orphan: AtomicU64, + /// a program got pruned because it was not recompiled for the next epoch + pub prunes_environment: AtomicU64, + /// a program had no entries because all slot versions got pruned + pub empty_entries: AtomicU64, + /// water level of loaded entries currently cached + pub water_level: AtomicU64, +} + +impl ProgramCacheStats { + pub fn reset(&mut self) { + *self = ProgramCacheStats::default(); + } + pub fn log(&self) { + let hits = self.hits.load(Ordering::Relaxed); + let misses = self.misses.load(Ordering::Relaxed); + let evictions: u64 = self.evictions.values().sum(); + let reloads = self.reloads.load(Ordering::Relaxed); + let insertions = self.insertions.load(Ordering::Relaxed); + let lost_insertions = self.lost_insertions.load(Ordering::Relaxed); + let replacements = self.replacements.load(Ordering::Relaxed); + let one_hit_wonders = self.one_hit_wonders.load(Ordering::Relaxed); + let prunes_orphan = self.prunes_orphan.load(Ordering::Relaxed); + let prunes_environment = self.prunes_environment.load(Ordering::Relaxed); + let empty_entries = self.empty_entries.load(Ordering::Relaxed); + let water_level = self.water_level.load(Ordering::Relaxed); + debug!( + "Loaded Programs Cache Stats -- Hits: {hits}, Misses: {misses}, Evictions: \ + {evictions}, Reloads: {reloads}, Insertions: {insertions}, Lost-Insertions: \ + {lost_insertions}, Replacements: {replacements}, One-Hit-Wonders: {one_hit_wonders}, \ + Prunes-Orphan: {prunes_orphan}, Prunes-Environment: {prunes_environment}, Empty: \ + {empty_entries}, Water-Level: {water_level}" + ); + + if log_enabled!(log::Level::Trace) && !self.evictions.is_empty() { + let mut evictions = self.evictions.iter().collect::>(); + evictions.sort_by_key(|e| e.1); + let evictions = evictions + .into_iter() + .rev() + .map(|(program_id, evictions)| { + format!(" {:<44} {}", program_id.to_string(), evictions) + }) + .collect::>(); + let evictions = evictions.join("\n"); + trace!( + "Eviction Details:\n {:<44} {}\n{}", + "Program", "Count", evictions + ); + } + } + + fn combined_ema( + into_ema: &AtomicU64, + from_ema: &AtomicU64, + into_observations: u64, + from_observations: u64, + ) -> Option { + // This is a mild non-sense, but there is no good mathematically rigorous way to merge + // two independent EMA trackers AFAICT and this is the best I (nagisa) could come up + // with… + let other_ema_val = from_ema.load(Ordering::Relaxed); + let other_ema_weight = std::cmp::max(WINDOW1, from_observations); + let this_ema_val = into_ema.load(Ordering::Relaxed); + let this_ema_weight = std::cmp::max(WINDOW2, into_observations); + other_ema_val + .wrapping_mul(other_ema_weight) + .wrapping_add(this_ema_val.wrapping_mul(this_ema_weight)) + .checked_div(other_ema_weight.wrapping_add(this_ema_weight)) + } +} + +#[cfg(feature = "metrics")] +/// Time measurements for loading a single [ProgramCacheEntry]. +#[derive(Debug, Default)] +pub struct LoadProgramMetrics { + /// Program address, but as text + pub program_id: String, + /// Microseconds it took to `create_program_runtime_environment` + pub register_syscalls_us: u64, + /// Microseconds it took to `Executable::::load` + pub load_elf_us: u64, + /// Microseconds it took to `executable.verify::` + pub verify_code_us: u64, + /// Microseconds it took to `executable.jit_compile` + pub jit_compile_us: u64, +} + +#[cfg(feature = "metrics")] +impl LoadProgramMetrics { + pub fn submit_datapoint(&self, timings: &mut ExecuteDetailsTimings) { + timings.create_executor_register_syscalls_us += self.register_syscalls_us; + timings.create_executor_load_elf_us += self.load_elf_us; + timings.create_executor_verify_code_us += self.verify_code_us; + timings.create_executor_jit_compile_us += self.jit_compile_us; + } +} + +impl crate::loaded_programs::ProgramCache { + /// Log per-entry statistics for each entry in the global cache. + #[cfg(feature = "dev-context-only-utils")] + pub fn output_entry_stats(&self) { + use {crate::program_cache_entry::ProgramCacheEntryType, std::fmt::Write}; + // The entry stats can become very verbose after some runtime. Rather than dumping them + // to the log, we'd rather maintain a continuously updated file instead... + static ENTRY_STAT_PATH: std::sync::LazyLock> = + std::sync::LazyLock::new(|| std::env::var_os("AGAVE_PROGRAM_CACHE_ENTRY_STATS_PATH")); + let Some(stat_path) = &*ENTRY_STAT_PATH else { + log::trace!("Set AGAVE_PROGRAM_CACHE_ENTRY_STATS_PATH to write per-entry stats"); + return; + }; + let mut output = String::new(); + let entries = self.get_flattened_entries_for_tests(); + for (addr, entry) in entries { + let entry_ty = match &entry.program { + ProgramCacheEntryType::FailedVerification(_) => "FailedVerification", + ProgramCacheEntryType::Closed => "Closed", + ProgramCacheEntryType::DelayVisibility => "DelayVisibility", + ProgramCacheEntryType::Unloaded(_) => "Unloaded", + ProgramCacheEntryType::Builtin(_) => "Builtin", + #[cfg(not(all(not(target_os = "windows"), target_arch = "x86_64")))] + ProgramCacheEntryType::Loaded(_) => "Loaded", + #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] + ProgramCacheEntryType::Loaded(executable) => { + if executable.get_compiled_program().is_some() { + "JitCompiled" + } else { + "Loaded" + } + } + }; + let stats = &entry.stats; + let uses = stats.uses.load(Ordering::Relaxed); + let compiles = stats.compilations.load(Ordering::Relaxed); + let comptime = stats.total_compilation_time_us.load(Ordering::Relaxed); + let comptime_ema = stats.compilation_time_ema.load(Ordering::Relaxed) / EMA_SCALE; + let invokes = stats.jit_invocations.load(Ordering::Relaxed); + let jittime = stats.total_jit_execution_time_us.load(Ordering::Relaxed); + let jittime_ema = stats.jit_execution_time_ema.load(Ordering::Relaxed) / EMA_SCALE; + let interps = stats.interpreted_invocations.load(Ordering::Relaxed); + let interptime = stats.total_interpretation_time_us.load(Ordering::Relaxed); + let interpema = stats.interpretation_time_ema.load(Ordering::Relaxed) / EMA_SCALE; + let _ = writeln!( + &mut output, + "{addr},{entry_ty},{uses},{compiles},{comptime},{comptime_ema},{invokes},\ + {jittime},{jittime_ema},{interps},{interptime},{interpema}" + ); + } + if let Err(e) = std::fs::write(stat_path, output) { + log::info!("Writing entry stats to {stat_path:?} failed: {e:?}"); + } else { + log::debug!("Entry stats written to {stat_path:?}"); + } + } +} diff --git a/solana/program-runtime/src/serialization.rs b/solana/program-runtime/src/serialization.rs new file mode 100644 index 0000000..0ddd77a --- /dev/null +++ b/solana/program-runtime/src/serialization.rs @@ -0,0 +1,1669 @@ +#![allow(clippy::arithmetic_side_effects)] + +use { + crate::memory_context::SerializedAccountMetadata, + solana_instruction::error::InstructionError, + solana_program_entrypoint::{BPF_ALIGN_OF_U128, MAX_PERMITTED_DATA_INCREASE, NON_DUP_MARKER}, + solana_pubkey::Pubkey, + solana_sbpf::{ + aligned_memory::{AlignedMemory, Pod}, + ebpf::{HOST_ALIGN, MM_INPUT_START}, + memory_region::MemoryRegion, + }, + solana_sdk_ids::bpf_loader_deprecated, + solana_system_interface::MAX_PERMITTED_DATA_LENGTH, + solana_transaction_context::{ + IndexOfAccount, MAX_ACCOUNTS_PER_INSTRUCTION, instruction::InstructionContext, + instruction_accounts::BorrowedInstructionAccount, + }, + std::mem::{self, size_of}, +}; + +/// Modifies the memory mapping in serialization and CPI return for virtual_address_space_adjustments +pub fn modify_memory_region_of_account( + account: &mut BorrowedInstructionAccount<'_, '_>, + region: &mut MemoryRegion, +) { + region.len = account.get_data().len() as u64; + if account.can_data_be_changed().is_ok() { + region.writable = true; + region.access_violation_handler_payload = Some(account.get_index_in_transaction()); + } else { + region.writable = false; + region.access_violation_handler_payload = None; + } +} + +/// Creates the memory mapping in serialization and CPI return for account_data_direct_mapping +pub fn create_memory_region_of_account( + account: &mut BorrowedInstructionAccount<'_, '_>, + vaddr: u64, +) -> Result { + let can_data_be_changed = account.can_data_be_changed().is_ok(); + let mut memory_region = if can_data_be_changed && !account.is_shared() { + MemoryRegion::new(&raw mut account.get_data_mut()?[..], vaddr) + } else { + MemoryRegion::new(&raw const account.get_data()[..], vaddr) + }; + if can_data_be_changed { + memory_region.access_violation_handler_payload = Some(account.get_index_in_transaction()); + } + Ok(memory_region) +} + +#[expect(dead_code)] +enum SerializeAccount<'a, 'ix_data> { + Account(IndexOfAccount, BorrowedInstructionAccount<'a, 'ix_data>), + Duplicate(IndexOfAccount), +} + +struct Serializer { + buffer: AlignedMemory, + regions: Vec, + vaddr: u64, + region_start: usize, + is_loader_v1: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, +} + +impl Serializer { + fn new( + size: usize, + start_addr: u64, + is_loader_v1: bool, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) -> Serializer { + Serializer { + buffer: AlignedMemory::with_capacity(size), + regions: Vec::new(), + region_start: 0, + vaddr: start_addr, + is_loader_v1, + virtual_address_space_adjustments, + account_data_direct_mapping, + } + } + + fn fill_write(&mut self, num: usize, value: u8) -> std::io::Result<()> { + self.buffer.fill_write(num, value) + } + + fn write(&mut self, value: T) -> u64 { + self.debug_assert_alignment::(); + let vaddr = self + .vaddr + .saturating_add(self.buffer.len() as u64) + .saturating_sub(self.region_start as u64); + // Safety: + // in serialize_parameters_(aligned|unaligned) first we compute the + // required size then we write into the newly allocated buffer. There's + // no need to check bounds at every write. + // + // AlignedMemory::write_unchecked _does_ debug_assert!() that the capacity + // is enough, so in the unlikely case we introduce a bug in the size + // computation, tests will abort. + unsafe { + self.buffer.write_unchecked(value); + } + + vaddr + } + + fn write_all(&mut self, value: &[u8]) -> u64 { + let vaddr = self + .vaddr + .saturating_add(self.buffer.len() as u64) + .saturating_sub(self.region_start as u64); + // Safety: + // see write() - the buffer is guaranteed to be large enough + unsafe { + self.buffer.write_all_unchecked(value); + } + + vaddr + } + + fn write_account( + &mut self, + account: &mut BorrowedInstructionAccount<'_, '_>, + ) -> Result { + if !self.virtual_address_space_adjustments { + let vm_data_addr = self.vaddr.saturating_add(self.buffer.len() as u64); + self.write_all(account.get_data()); + if !self.is_loader_v1 { + let align_offset = + (account.get_data().len() as *const u8).align_offset(BPF_ALIGN_OF_U128); + self.fill_write(MAX_PERMITTED_DATA_INCREASE + align_offset, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + } + Ok(vm_data_addr) + } else { + self.push_region(); + let vm_data_addr = self.vaddr; + if !self.account_data_direct_mapping { + self.write_all(account.get_data()); + if !self.is_loader_v1 { + self.fill_write(MAX_PERMITTED_DATA_INCREASE, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + } + } + let address_space_reserved_for_account = if !self.is_loader_v1 { + account + .get_data() + .len() + .saturating_add(MAX_PERMITTED_DATA_INCREASE) + } else { + account.get_data().len() + }; + if address_space_reserved_for_account > 0 { + if !self.account_data_direct_mapping { + self.push_region(); + let region = self.regions.last_mut().unwrap(); + modify_memory_region_of_account(account, region); + } else { + let new_region = create_memory_region_of_account(account, self.vaddr)?; + self.vaddr += address_space_reserved_for_account as u64; + self.regions.push(new_region); + } + } + if !self.is_loader_v1 { + let align_offset = + (account.get_data().len() as *const u8).align_offset(BPF_ALIGN_OF_U128); + if !self.account_data_direct_mapping { + self.fill_write(align_offset, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + } else { + // The deserialization code is going to align the vm_addr to + // BPF_ALIGN_OF_U128. Always add one BPF_ALIGN_OF_U128 worth of + // padding and shift the start of the next region, so that once + // vm_addr is aligned, the corresponding host_addr is aligned + // too. + self.fill_write(BPF_ALIGN_OF_U128, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + self.region_start += BPF_ALIGN_OF_U128.saturating_sub(align_offset); + } + } + Ok(vm_data_addr) + } + } + + fn push_region(&mut self) { + let range = self.region_start..self.buffer.len(); + let region_slice = self.buffer.as_slice_mut().get_mut(range.clone()).unwrap(); + self.regions + .push(MemoryRegion::new(&raw mut region_slice[..], self.vaddr)); + self.region_start = range.end; + self.vaddr += range.len() as u64; + } + + fn finish(mut self) -> (AlignedMemory, Vec) { + self.push_region(); + debug_assert_eq!(self.region_start, self.buffer.len()); + (self.buffer, self.regions) + } + + fn debug_assert_alignment(&self) { + debug_assert!( + self.is_loader_v1 + || self + .buffer + .as_slice() + .as_ptr_range() + .end + .align_offset(mem::align_of::()) + == 0 + ); + } +} + +pub fn serialize_parameters( + instruction_context: &InstructionContext, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + direct_account_pointers_in_program_input: bool, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + let num_ix_accounts = instruction_context.get_number_of_instruction_accounts(); + if num_ix_accounts > MAX_ACCOUNTS_PER_INSTRUCTION as IndexOfAccount { + return Err(InstructionError::MaxAccountsExceeded); + } + + let program_id = *instruction_context.get_program_key()?; + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + + let accounts = (0..instruction_context.get_number_of_instruction_accounts()) + .map(|instruction_account_index| { + if let Some(index) = instruction_context + .is_instruction_account_duplicate(instruction_account_index) + .unwrap() + { + SerializeAccount::Duplicate(index) + } else { + let account = instruction_context + .try_borrow_instruction_account(instruction_account_index) + .unwrap(); + SerializeAccount::Account(instruction_account_index, account) + } + }) + // fun fact: jemalloc is good at caching tiny allocations like this one, + // so collecting here is actually faster than passing the iterator + // around, since the iterator does the work to produce its items each + // time it's iterated on. + .collect::>(); + + if is_loader_deprecated { + // Used by loader-v1 (bpf_loader_deprecated) + serialize_parameters_for_abiv0( + accounts, + instruction_context.get_instruction_data(), + &program_id, + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + } else { + // Used by loader-v2 (bpf_loader) and loader-v3 (bpf_loader_upgradeable) + serialize_parameters_for_abiv1( + accounts, + instruction_context.get_instruction_data(), + &program_id, + virtual_address_space_adjustments, + account_data_direct_mapping, + // SIMD-0449: only available on ABIv1 + direct_account_pointers_in_program_input, + ) + } +} + +pub fn deserialize_parameters( + instruction_context: &InstructionContext, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + buffer: &[u8], + accounts_metadata: &[SerializedAccountMetadata], +) -> Result<(), InstructionError> { + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + let account_lengths = accounts_metadata.iter().map(|a| a.original_data_len); + if is_loader_deprecated { + // Used by loader-v1 (bpf_loader_deprecated) + deserialize_parameters_for_abiv0( + instruction_context, + virtual_address_space_adjustments, + account_data_direct_mapping, + buffer, + account_lengths, + ) + } else { + // Used by loader-v2 (bpf_loader) and loader-v3 (bpf_loader_upgradeable) + deserialize_parameters_for_abiv1( + instruction_context, + virtual_address_space_adjustments, + account_data_direct_mapping, + buffer, + account_lengths, + ) + } +} + +fn serialize_parameters_for_abiv0( + accounts: Vec, + instruction_data: &[u8], + program_id: &Pubkey, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + // Calculate size in order to alloc once + let mut size = size_of::(); + for account in &accounts { + size += 1; // dup + match account { + SerializeAccount::Duplicate(_) => {} + SerializeAccount::Account(_, account) => { + size += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // key + + size_of::() // lamports + + size_of::() // data len + + size_of::() // owner + + size_of::() // executable + + size_of::(); // rent_epoch + if !(virtual_address_space_adjustments && account_data_direct_mapping) { + size += account.get_data().len(); + } + } + } + } + size += size_of::() // instruction data len + + instruction_data.len() // instruction data + + size_of::(); // program id + + let mut s = Serializer::new( + size, + MM_INPUT_START, + true, + virtual_address_space_adjustments, + account_data_direct_mapping, + ); + + let mut accounts_metadata: Vec = Vec::with_capacity(accounts.len()); + s.write::((accounts.len() as u64).to_le()); + for account in accounts { + match account { + SerializeAccount::Duplicate(position) => { + accounts_metadata.push(accounts_metadata.get(position as usize).unwrap().clone()); + s.write(position as u8); + } + SerializeAccount::Account(_, mut account) => { + let vm_addr = s.write::(NON_DUP_MARKER); + s.write::(account.is_signer() as u8); + s.write::(account.is_writable() as u8); + let vm_key_addr = s.write_all(account.get_key().as_ref()); + let vm_lamports_addr = s.write::(account.get_lamports().to_le()); + s.write::((account.get_data().len() as u64).to_le()); + let vm_data_addr = s.write_account(&mut account)?; + let vm_owner_addr = s.write_all(account.get_owner().as_ref()); + #[expect(deprecated)] + s.write::(account.is_executable() as u8); + let rent_epoch = u64::MAX; + s.write::(rent_epoch.to_le()); + accounts_metadata.push(SerializedAccountMetadata { + vm_addr, + original_data_len: account.get_data().len(), + vm_key_addr, + vm_lamports_addr, + vm_owner_addr, + vm_data_addr, + }); + } + }; + } + s.write::((instruction_data.len() as u64).to_le()); + let instruction_data_offset = s.write_all(instruction_data); + s.write_all(program_id.as_ref()); + + let (mem, regions) = s.finish(); + Ok(( + mem, + regions, + accounts_metadata, + instruction_data_offset as usize, + )) +} + +fn deserialize_parameters_for_abiv0>( + instruction_context: &InstructionContext, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + buffer: &[u8], + account_lengths: I, +) -> Result<(), InstructionError> { + let mut start = size_of::(); // number of accounts + for (instruction_account_index, pre_len) in + (0..instruction_context.get_number_of_instruction_accounts()).zip(account_lengths) + { + let duplicate = + instruction_context.is_instruction_account_duplicate(instruction_account_index)?; + start += 1; // is_dup + if duplicate.is_none() { + let mut borrowed_account = + instruction_context.try_borrow_instruction_account(instruction_account_index)?; + start += size_of::(); // is_signer + start += size_of::(); // is_writable + start += size_of::(); // key + let lamports = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)?; + if borrowed_account.get_lamports() != lamports { + borrowed_account.set_lamports(lamports)?; + } + start += size_of::() // lamports + + size_of::(); // data length + if !virtual_address_space_adjustments { + let data = buffer + .get(start..start + pre_len) + .ok_or(InstructionError::InvalidArgument)?; + // The redundant check helps to avoid the expensive data comparison if we can + match borrowed_account.can_data_be_resized(pre_len) { + Ok(()) => borrowed_account.set_data_from_slice(data)?, + Err(err) if borrowed_account.get_data() != data => return Err(err), + _ => {} + } + } else if !account_data_direct_mapping && borrowed_account.can_data_be_changed().is_ok() + { + let data = buffer + .get(start..start + pre_len) + .ok_or(InstructionError::InvalidArgument)?; + borrowed_account.set_data_from_slice(data)?; + } else if borrowed_account.get_data().len() != pre_len { + borrowed_account.set_data_length(pre_len)?; + } + if !(virtual_address_space_adjustments && account_data_direct_mapping) { + start += pre_len; // data + } + start += size_of::() // owner + + size_of::() // executable + + size_of::(); // rent_epoch + } + } + Ok(()) +} + +fn serialize_parameters_for_abiv1( + accounts: Vec, + instruction_data: &[u8], + program_id: &Pubkey, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + direct_account_pointers_program_input: bool, +) -> Result< + ( + AlignedMemory, + Vec, + Vec, + usize, + ), + InstructionError, +> { + let mut accounts_metadata = Vec::with_capacity(accounts.len()); + // Calculate size in order to alloc once + let mut size = size_of::(); + for account in &accounts { + size += 1; // dup + match account { + SerializeAccount::Duplicate(_) => size += 7, // padding to 64-bit aligned + SerializeAccount::Account(_, account) => { + let data_len = account.get_data().len(); + size += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // executable + + size_of::() // original_data_len + + size_of::() // key + + size_of::() // owner + + size_of::() // lamports + + size_of::() // data len + + size_of::(); // rent epoch + if !(virtual_address_space_adjustments && account_data_direct_mapping) { + size += data_len + + MAX_PERMITTED_DATA_INCREASE + + (data_len as *const u8).align_offset(BPF_ALIGN_OF_U128); + } else { + size += BPF_ALIGN_OF_U128; + } + } + } + } + size += size_of::() // data len + + instruction_data.len() + + size_of::(); // program id; + + // reserve space for account pointer array if SIMD-0449 is enabled + let account_pointers_offset = if direct_account_pointers_program_input { + let offset = (size as *const u8).align_offset(BPF_ALIGN_OF_U128); + size += offset + accounts.len() * size_of::(); + Some(offset) + } else { + None + }; + + let mut s = Serializer::new( + size, + MM_INPUT_START, + false, + virtual_address_space_adjustments, + account_data_direct_mapping, + ); + + // Serialize into the buffer + s.write::((accounts.len() as u64).to_le()); + for account in accounts { + match account { + SerializeAccount::Account(_, mut borrowed_account) => { + let vm_addr = s.write::(NON_DUP_MARKER); + s.write::(borrowed_account.is_signer() as u8); + s.write::(borrowed_account.is_writable() as u8); + #[expect(deprecated)] + s.write::(borrowed_account.is_executable() as u8); + s.write_all(&[0u8, 0, 0, 0]); + 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 vm_lamports_addr = s.write::(borrowed_account.get_lamports().to_le()); + s.write::((borrowed_account.get_data().len() as u64).to_le()); + let vm_data_addr = s.write_account(&mut borrowed_account)?; + let rent_epoch = u64::MAX; + s.write::(rent_epoch.to_le()); + accounts_metadata.push(SerializedAccountMetadata { + vm_addr, + original_data_len: borrowed_account.get_data().len(), + vm_key_addr, + vm_owner_addr, + vm_lamports_addr, + vm_data_addr, + }); + } + SerializeAccount::Duplicate(position) => { + accounts_metadata.push(accounts_metadata.get(position as usize).unwrap().clone()); + s.write::(position as u8); + s.write_all(&[0u8, 0, 0, 0, 0, 0, 0]); + } + }; + } + s.write::((instruction_data.len() as u64).to_le()); + let instruction_data_offset = s.write_all(instruction_data); + s.write_all(program_id.as_ref()); + + if let Some(offset) = account_pointers_offset { + // Add padding before the account pointer array to reach 8-byte alignment + // (BPF_ALIGN_OF_U128). + s.fill_write(offset, 0) + .map_err(|_| InstructionError::InvalidArgument)?; + for entry in accounts_metadata.iter() { + s.write::(entry.vm_addr.to_le()); + } + } + + let (mem, regions) = s.finish(); + Ok(( + mem, + regions, + accounts_metadata, + instruction_data_offset as usize, + )) +} + +fn deserialize_parameters_for_abiv1>( + instruction_context: &InstructionContext, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + buffer: &[u8], + account_lengths: I, +) -> Result<(), InstructionError> { + let mut start = size_of::(); // number of accounts + for (instruction_account_index, pre_len) in + (0..instruction_context.get_number_of_instruction_accounts()).zip(account_lengths) + { + let duplicate = + instruction_context.is_instruction_account_duplicate(instruction_account_index)?; + start += size_of::(); // position + if duplicate.is_some() { + start += 7; // padding to 64-bit aligned + } else { + let mut borrowed_account = + instruction_context.try_borrow_instruction_account(instruction_account_index)?; + start += size_of::() // is_signer + + size_of::() // is_writable + + size_of::() // executable + + size_of::() // original_data_len + + size_of::(); // key + let owner = buffer + .get(start..start + size_of::()) + .ok_or(InstructionError::InvalidArgument)?; + start += size_of::(); // owner + let lamports = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)?; + if borrowed_account.get_lamports() != lamports { + borrowed_account.set_lamports(lamports)?; + } + start += size_of::(); // lamports + let post_len = buffer + .get(start..start.saturating_add(8)) + .map(<[u8; 8]>::try_from) + .and_then(Result::ok) + .map(u64::from_le_bytes) + .ok_or(InstructionError::InvalidArgument)? as usize; + start += size_of::(); // data length + if post_len.saturating_sub(pre_len) > MAX_PERMITTED_DATA_INCREASE + || post_len > MAX_PERMITTED_DATA_LENGTH as usize + { + return Err(InstructionError::InvalidRealloc); + } + if !virtual_address_space_adjustments { + let data = buffer + .get(start..start + post_len) + .ok_or(InstructionError::InvalidArgument)?; + // The redundant check helps to avoid the expensive data comparison if we can + match borrowed_account.can_data_be_resized(post_len) { + Ok(()) => borrowed_account.set_data_from_slice(data)?, + Err(err) if borrowed_account.get_data() != data => return Err(err), + _ => {} + } + } else if !account_data_direct_mapping && borrowed_account.can_data_be_changed().is_ok() + { + let data = buffer + .get(start..start + post_len) + .ok_or(InstructionError::InvalidArgument)?; + borrowed_account.set_data_from_slice(data)?; + } else if borrowed_account.get_data().len() != post_len { + borrowed_account.set_data_length(post_len)?; + } + start += if !(virtual_address_space_adjustments && account_data_direct_mapping) { + let alignment_offset = (pre_len as *const u8).align_offset(BPF_ALIGN_OF_U128); + pre_len // data + .saturating_add(MAX_PERMITTED_DATA_INCREASE) // realloc padding + .saturating_add(alignment_offset) + } else { + // See Serializer::write_account() as to why we have this + BPF_ALIGN_OF_U128 + }; + start += size_of::(); // rent_epoch + 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(()) +} + +#[cfg(test)] +#[allow(clippy::indexing_slicing)] +mod tests { + use { + super::*, + crate::with_mock_invoke_context, + solana_account::{Account, AccountSharedData, ReadableAccount}, + solana_account_info::AccountInfo, + solana_program_entrypoint::deserialize, + solana_rent::Rent, + solana_sbpf::{memory_region::MemoryMapping, program::SBPFVersion, vm::Config}, + solana_sdk_ids::bpf_loader, + solana_system_interface::MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION, + solana_transaction_context::{ + MAX_ACCOUNTS_PER_TRANSACTION, instruction_accounts::InstructionAccount, + transaction::TransactionContext, + }, + std::{ + borrow::Cow, + cell::RefCell, + mem::transmute, + rc::Rc, + slice::{self, from_raw_parts, from_raw_parts_mut}, + }, + test_case::test_case, + }; + + fn deduplicated_instruction_accounts( + transaction_indexes: &[IndexOfAccount], + is_writable: fn(usize) -> bool, + ) -> Vec { + transaction_indexes + .iter() + .enumerate() + .map(|(index_in_instruction, index_in_transaction)| { + InstructionAccount::new( + *index_in_transaction, + false, + is_writable(index_in_instruction), + ) + }) + .collect() + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters_with_many_accounts( + direct_account_pointers_in_program_input: bool, + ) { + struct TestCase { + num_ix_accounts: usize, + append_dup_account: bool, + expected_err: Option, + name: &'static str, + } + + for virtual_address_space_adjustments in [false, true] { + for TestCase { + num_ix_accounts, + append_dup_account, + expected_err, + name, + } in [ + TestCase { + name: "serialize max accounts with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION, + append_dup_account: false, + expected_err: None, + }, + TestCase { + name: "serialize too many accounts with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION + 1, + append_dup_account: false, + expected_err: Some(InstructionError::MaxAccountsExceeded), + }, + TestCase { + name: "serialize too many accounts and append dup with cap", + num_ix_accounts: MAX_ACCOUNTS_PER_INSTRUCTION, + append_dup_account: true, + expected_err: Some(InstructionError::MaxAccountsExceeded), + }, + ] { + let program_id = solana_pubkey::new_rand(); + let mut transaction_accounts = vec![( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + )]; + for _ in 0..num_ix_accounts { + transaction_accounts.push(( + Pubkey::new_unique(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: program_id, + executable: false, + rent_epoch: 0, + }), + )); + } + + let transaction_accounts_indexes: Vec = + (0..num_ix_accounts as u16).collect(); + let mut instruction_accounts = + deduplicated_instruction_accounts(&transaction_accounts_indexes, |_| false); + if append_dup_account { + instruction_accounts.push(instruction_accounts.last().cloned().unwrap()); + } + let instruction_data = vec![]; + + with_mock_invoke_context!( + invoke_context, + transaction_context, + transaction_accounts + ); + if instruction_accounts.len() > MAX_ACCOUNTS_PER_INSTRUCTION { + // Special case implementation of configure_next_instruction_for_tests() + // which avoids the overflow when constructing the dedup_map + // by simply not filling it. + let dedup_map = vec![u16::MAX; MAX_ACCOUNTS_PER_TRANSACTION]; + invoke_context + .transaction_context + .configure_instruction_at_index( + 0, + 0, + instruction_accounts, + dedup_map, + Cow::Owned(instruction_data.clone()), + Some(0), + ) + .unwrap(); + } else { + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + instruction_accounts, + instruction_data.clone(), + ) + .unwrap(); + } + invoke_context.push().unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + let serialization_result = serialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + false, // account_data_direct_mapping + direct_account_pointers_in_program_input, + ); + assert_eq!( + serialization_result.as_ref().err(), + expected_err.as_ref(), + "{name} test case failed", + ); + if expected_err.is_some() { + continue; + } + + let (mut serialized, regions, _account_lengths, _instruction_data_offset) = + serialization_result.unwrap(); + let mut serialized_regions = concat_regions(®ions); + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize( + if !virtual_address_space_adjustments { + serialized.as_slice_mut() + } else { + serialized_regions.as_slice_mut() + } + .first_mut() + .unwrap() as *mut u8, + ) + }; + assert_eq!(de_program_id, &program_id); + assert_eq!(de_instruction_data, &instruction_data); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + // Using the sdk entrypoint, the rent-epoch is skipped + assert_eq!(0, account_info._unused); + } + } + } + } + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters(direct_account_pointers_in_program_input: bool) { + for virtual_address_space_adjustments in [false, true] { + let program_id = solana_pubkey::new_rand(); + let transaction_accounts = vec![ + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 1, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 2, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 3, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 4, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 5, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 6, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + program_id, + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader_deprecated::id(), + executable: true, + rent_epoch: 0, + }), + ), + ]; + let instruction_accounts = + deduplicated_instruction_accounts(&[1, 1, 2, 3, 4, 4, 5, 6], |index| index >= 4); + let instruction_data = vec![1u8, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11]; + let original_accounts = transaction_accounts.clone(); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 0, + instruction_accounts.clone(), + instruction_data.clone(), + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + // check serialize_parameters_for_abiv1 + let (mut serialized, regions, accounts_metadata, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + false, // account_data_direct_mapping + direct_account_pointers_in_program_input, + ) + .unwrap(); + + let mut serialized_regions = concat_regions(®ions); + if !virtual_address_space_adjustments { + assert_eq!(serialized.as_slice(), serialized_regions.as_slice()); + } + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize( + if !virtual_address_space_adjustments { + serialized.as_slice_mut() + } else { + serialized_regions.as_slice_mut() + } + .first_mut() + .unwrap() as *mut u8, + ) + }; + + assert_eq!(&program_id, de_program_id); + assert_eq!(instruction_data, de_instruction_data); + assert_eq!( + (de_instruction_data.first().unwrap() as *const u8).align_offset(BPF_ALIGN_OF_U128), + 0 + ); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + // Using the sdk entrypoint, the rent-epoch is skipped + assert_eq!(0, account_info._unused); + } + + assert_eq!( + (*account_info.lamports.borrow() as *const u64).align_offset(BPF_ALIGN_OF_U128), + 0 + ); + assert_eq!( + account_info + .data + .borrow() + .as_ptr() + .align_offset(BPF_ALIGN_OF_U128), + 0 + ); + } + + deserialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + false, // account_data_direct_mapping + serialized.as_slice(), + &accounts_metadata, + ) + .unwrap(); + for (index_in_transaction, (_key, original_account)) in + original_accounts.iter().enumerate() + { + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction as IndexOfAccount) + .unwrap(); + assert_eq!(&*account, original_account); + } + + invoke_context.pop().unwrap(); + // check serialize_parameters_for_abiv0 + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests( + 7, + instruction_accounts, + instruction_data.clone(), + ) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + let (mut serialized, regions, account_lengths, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + false, // account_data_direct_mapping + direct_account_pointers_in_program_input, + ) + .unwrap(); + let mut serialized_regions = concat_regions(®ions); + + let (de_program_id, de_accounts, de_instruction_data) = unsafe { + deserialize_for_abiv0( + if !virtual_address_space_adjustments { + serialized.as_slice_mut() + } else { + serialized_regions.as_slice_mut() + } + .first_mut() + .unwrap() as *mut u8, + ) + }; + assert_eq!(&program_id, de_program_id); + assert_eq!(instruction_data, de_instruction_data); + for account_info in de_accounts { + let index_in_transaction = invoke_context + .transaction_context + .find_index_of_account(account_info.key) + .unwrap(); + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction) + .unwrap(); + assert_eq!(account.lamports(), account_info.lamports()); + assert_eq!(account.data(), &account_info.data.borrow()[..]); + assert_eq!(account.owner(), account_info.owner); + assert_eq!(account.executable(), account_info.executable); + #[allow(deprecated)] + { + assert_eq!(u64::MAX, account_info._unused); + } + } + + deserialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + false, // account_data_direct_mapping + serialized.as_slice(), + &account_lengths, + ) + .unwrap(); + for (index_in_transaction, (_key, original_account)) in + original_accounts.iter().enumerate() + { + let account = invoke_context + .transaction_context + .accounts() + .try_borrow(index_in_transaction as IndexOfAccount) + .unwrap(); + assert_eq!(&*account, original_account); + } + } + } + + #[test_case(false; "direct_account_pointers_in_program_input disabled")] + #[test_case(true; "direct_account_pointers_in_program_input enabled")] + fn test_serialize_parameters_mask_out_rent_epoch_in_vm_serialization( + direct_account_pointers_in_program_input: bool, + ) { + let transaction_accounts = vec![ + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 0, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 1, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 2, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 3, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 300, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 4, + data: vec![1u8, 2, 3, 4, 5], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 5, + data: vec![11u8, 12, 13, 14, 15, 16, 17, 18, 19], + owner: bpf_loader::id(), + executable: true, + rent_epoch: 200, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 6, + data: vec![], + owner: bpf_loader::id(), + executable: false, + rent_epoch: 3100, + }), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::from(Account { + lamports: 0, + data: vec![], + owner: bpf_loader_deprecated::id(), + executable: true, + rent_epoch: 0, + }), + ), + ]; + let instruction_accounts = + deduplicated_instruction_accounts(&[1, 1, 2, 3, 4, 4, 5, 6], |index| index >= 4); + with_mock_invoke_context!(invoke_context, transaction_context, transaction_accounts); + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(0, instruction_accounts.clone(), vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + // check serialize_parameters_for_abiv1 + let (_serialized, regions, _accounts_metadata, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + true, + false, // account_data_direct_mapping + direct_account_pointers_in_program_input, + ) + .unwrap(); + + let mut serialized_regions = concat_regions(®ions); + let (_de_program_id, de_accounts, _de_instruction_data) = unsafe { + deserialize(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + + for account_info in de_accounts { + // Using program-entrypoint, the rent-epoch will always be 0 + #[allow(deprecated)] + { + assert_eq!(0, account_info._unused); + } + } + + // check serialize_parameters_for_abiv0 + invoke_context + .transaction_context + .configure_top_level_instruction_for_tests(7, instruction_accounts, vec![]) + .unwrap(); + invoke_context.push().unwrap(); + let instruction_context = invoke_context + .transaction_context + .get_current_instruction_context() + .unwrap(); + + let (_serialized, regions, _account_lengths, _instruction_data_offset) = + serialize_parameters( + &instruction_context, + true, + false, // account_data_direct_mapping + direct_account_pointers_in_program_input, + ) + .unwrap(); + let mut serialized_regions = concat_regions(®ions); + + let (_de_program_id, de_accounts, _de_instruction_data) = unsafe { + deserialize_for_abiv0(serialized_regions.as_slice_mut().first_mut().unwrap() as *mut u8) + }; + for account_info in de_accounts { + #[allow(deprecated)] + { + assert_eq!(account_info._unused, u64::MAX); + } + } + } + + // the old bpf_loader in-program deserializer bpf_loader::id() + #[deny(unsafe_op_in_unsafe_fn)] + unsafe fn deserialize_for_abiv0<'a>( + input: *mut u8, + ) -> (&'a Pubkey, Vec>, &'a [u8]) { + // this boring boilerplate struct is needed until inline const... + struct Ptr(std::marker::PhantomData); + impl Ptr { + const COULD_BE_UNALIGNED: bool = std::mem::align_of::() > 1; + + #[inline(always)] + fn read_possibly_unaligned(input: *mut u8, offset: usize) -> T { + unsafe { + let src = input.add(offset) as *const T; + if Self::COULD_BE_UNALIGNED { + src.read_unaligned() + } else { + src.read() + } + } + } + + // rustc inserts debug_assert! for misaligned pointer dereferences when + // deserializing, starting from [1]. so, use std::mem::transmute as the last resort + // while preventing clippy from complaining to suggest not to use it. + // [1]: https://github.com/rust-lang/rust/commit/22a7a19f9333bc1fcba97ce444a3515cb5fb33e6 + // as for the ub nature of the misaligned pointer dereference, this is + // acceptable in this code, given that this is cfg(test) and it's cared only with + // x86-64 and the target only incurs some performance penalty, not like segfaults + // in other targets. + #[inline(always)] + fn ref_possibly_unaligned<'a>(input: *mut u8, offset: usize) -> &'a T { + #[allow(clippy::transmute_ptr_to_ref)] + unsafe { + transmute(input.add(offset) as *const T) + } + } + + // See ref_possibly_unaligned's comment + #[inline(always)] + fn mut_possibly_unaligned<'a>(input: *mut u8, offset: usize) -> &'a mut T { + #[allow(clippy::transmute_ptr_to_ref)] + unsafe { + transmute(input.add(offset) as *mut T) + } + } + } + + let mut offset: usize = 0; + + // number of accounts present + + let num_accounts = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + // account Infos + + let mut accounts = Vec::with_capacity(num_accounts); + for _ in 0..num_accounts { + let dup_info = Ptr::::read_possibly_unaligned(input, offset); + offset += size_of::(); + if dup_info == NON_DUP_MARKER { + let is_signer = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let is_writable = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let key = Ptr::::ref_possibly_unaligned(input, offset); + offset += size_of::(); + + let lamports = Rc::new(RefCell::new(Ptr::mut_possibly_unaligned(input, offset))); + offset += size_of::(); + + let data_len = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + let data = Rc::new(RefCell::new(unsafe { + from_raw_parts_mut(input.add(offset), data_len) + })); + offset += data_len; + + let owner: &Pubkey = Ptr::::ref_possibly_unaligned(input, offset); + offset += size_of::(); + + let executable = Ptr::::read_possibly_unaligned(input, offset) != 0; + offset += size_of::(); + + let unused = Ptr::::read_possibly_unaligned(input, offset); + offset += size_of::(); + + #[allow(deprecated)] + accounts.push(AccountInfo { + key, + is_signer, + is_writable, + lamports, + data, + owner, + executable, + _unused: unused, + }); + } else { + // duplicate account, clone the original + accounts.push(accounts.get(dup_info as usize).unwrap().clone()); + } + } + + // instruction data + + let instruction_data_len = Ptr::::read_possibly_unaligned(input, offset) as usize; + offset += size_of::(); + + let instruction_data = unsafe { from_raw_parts(input.add(offset), instruction_data_len) }; + offset += instruction_data_len; + + // program Id + + let program_id = Ptr::::ref_possibly_unaligned(input, offset); + + (program_id, accounts, instruction_data) + } + + fn concat_regions(regions: &[MemoryRegion]) -> AlignedMemory { + let last_region = regions.last().unwrap(); + let mut mem = AlignedMemory::zero_filled( + (last_region.vm_addr - MM_INPUT_START + last_region.len) as usize, + ); + for region in regions { + let host_slice = unsafe { + slice::from_raw_parts(region.host_addr as *const u8, region.len as usize) + }; + mem.as_slice_mut()[(region.vm_addr - MM_INPUT_START) as usize..][..region.len as usize] + .copy_from_slice(host_slice) + } + mem + } + + #[test] + fn test_access_violation_handler() { + let program_id = Pubkey::new_unique(); + let shared_account = AccountSharedData::new(0, 4, &program_id); + let mut transaction_context = TransactionContext::new( + vec![ + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 4, &program_id), + ), // readonly + (Pubkey::new_unique(), shared_account.clone()), // writable shared + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &program_id), + ), // another writable account + ( + Pubkey::new_unique(), + AccountSharedData::new( + 0, + MAX_PERMITTED_DATA_LENGTH as usize - 0x100, + &program_id, + ), + ), // almost max sized writable account + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0, &program_id), + ), // writable dummy to burn accounts_resize_delta + ( + Pubkey::new_unique(), + AccountSharedData::new(0, 0x3000, &program_id), + ), // writable dummy to burn accounts_resize_delta + (program_id, AccountSharedData::default()), // program + ], + Rent::default(), + /* max_instruction_stack_depth */ 1, + /* max_instruction_trace_length */ 1, + /* number_of_top_level_instructions */ 1, + ); + let transaction_accounts_indexes = [0, 1, 2, 3, 4, 5]; + let instruction_accounts = + deduplicated_instruction_accounts(&transaction_accounts_indexes, |index| index > 0); + transaction_context + .configure_top_level_instruction_for_tests(6, instruction_accounts, vec![]) + .unwrap(); + transaction_context.push().unwrap(); + let instruction_context = transaction_context + .get_current_instruction_context() + .unwrap(); + let account_start_offsets = [ + MM_INPUT_START, + MM_INPUT_START + 4 + MAX_PERMITTED_DATA_INCREASE as u64, + MM_INPUT_START + (4 + MAX_PERMITTED_DATA_INCREASE as u64) * 2, + MM_INPUT_START + (4 + MAX_PERMITTED_DATA_INCREASE as u64) * 3, + ]; + let regions = account_start_offsets + .iter() + .enumerate() + .map(|(index_in_instruction, account_start_offset)| { + create_memory_region_of_account( + &mut instruction_context + .try_borrow_instruction_account(index_in_instruction as IndexOfAccount) + .unwrap(), + *account_start_offset, + ) + .unwrap() + }) + .collect::>(); + let config = Config { + aligned_memory_mapping: false, + ..Config::default() + }; + let mut memory_mapping = unsafe { + MemoryMapping::new_with_access_violation_handler( + regions, + &config, + SBPFVersion::V3, + transaction_context.access_violation_handler(true, true), + ) + .unwrap() + }; + + // Reading readonly account is allowed + memory_mapping + .load::(account_start_offsets[0]) + .unwrap(); + + // Reading writable account is allowed + memory_mapping + .load::(account_start_offsets[1]) + .unwrap(); + + // Reading beyond readonly accounts current size is denied + memory_mapping + .load::(account_start_offsets[0] + 4) + .unwrap_err(); + + // Writing to readonly account is denied + memory_mapping + .store::(0, account_start_offsets[0]) + .unwrap_err(); + + // Writing to shared writable account makes it unique (CoW logic.) + // It has been previously been made non-unique at the beginning of + // the test through a clone. + let _shared_account_ref = shared_account; + assert!( + transaction_context + .accounts() + .try_borrow_mut(1) + .unwrap() + .is_shared() + ); + memory_mapping + .store::(0, account_start_offsets[1]) + .unwrap(); + assert!( + !transaction_context + .accounts() + .try_borrow_mut(1) + .unwrap() + .is_shared() + ); + assert_eq!( + transaction_context + .accounts() + .try_borrow(1) + .unwrap() + .data() + .len(), + 4, + ); + + // Reading beyond writable accounts current size grows is denied + memory_mapping + .load::(account_start_offsets[1] + 4) + .unwrap_err(); + + // Writing beyond writable accounts current size grows it + // to original length plus MAX_PERMITTED_DATA_INCREASE + memory_mapping + .store::(0, account_start_offsets[1] + 4) + .unwrap(); + assert_eq!( + transaction_context + .accounts() + .try_borrow(1) + .unwrap() + .data() + .len(), + 4 + MAX_PERMITTED_DATA_INCREASE, + ); + assert!( + transaction_context + .accounts() + .try_borrow(1) + .unwrap() + .data() + .len() + < 0x3000 + ); + + // Writing beyond almost max sized writable accounts current size only grows it + // to MAX_PERMITTED_DATA_LENGTH + memory_mapping + .store::(0, account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH - 4) + .unwrap(); + assert_eq!( + transaction_context + .accounts() + .try_borrow(3) + .unwrap() + .data() + .len(), + MAX_PERMITTED_DATA_LENGTH as usize, + ); + + // Accessing the rest of the address space reserved for + // the almost max sized writable account is denied + memory_mapping + .load::(account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH) + .unwrap_err(); + memory_mapping + .store::(0, account_start_offsets[3] + MAX_PERMITTED_DATA_LENGTH) + .unwrap_err(); + + // Burn through most of the accounts_resize_delta budget + let remaining_allowed_growth: usize = 0x700; + for index_in_instruction in 4..6 { + let mut borrowed_account = instruction_context + .try_borrow_instruction_account(index_in_instruction) + .unwrap(); + borrowed_account + .set_data_from_slice(&vec![0u8; MAX_PERMITTED_DATA_LENGTH as usize]) + .unwrap(); + } + assert_eq!( + transaction_context.accounts().resize_delta(), + MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION + - remaining_allowed_growth as i64, + ); + + // Writing beyond empty writable accounts current size + // only grows it to fill up MAX_PERMITTED_ACCOUNTS_DATA_ALLOCATIONS_PER_TRANSACTION + memory_mapping + .store::(0, account_start_offsets[2] + 0x500) + .unwrap(); + assert_eq!( + transaction_context + .accounts() + .try_borrow(2) + .unwrap() + .data() + .len(), + remaining_allowed_growth, + ); + } +} diff --git a/solana/program-runtime/src/stable_log.rs b/solana/program-runtime/src/stable_log.rs new file mode 100644 index 0000000..bf31fea --- /dev/null +++ b/solana/program-runtime/src/stable_log.rs @@ -0,0 +1,110 @@ +//! Stable program log messages +//! +//! The format of these log messages should not be modified to avoid breaking downstream consumers +//! of program logging +use { + base64::{Engine, prelude::BASE64_STANDARD}, + itertools::Itertools, + solana_pubkey::Pubkey, + solana_svm_log_collector::{LogCollector, ic_logger_msg}, + std::{cell::RefCell, rc::Rc}, +}; + +/// Log a program invoke. +/// +/// The general form is: +/// +/// ```notrust +/// "Program
invoke []" +/// ``` +pub fn program_invoke( + log_collector: &Option>>, + program_id: &Pubkey, + invoke_depth: usize, +) { + ic_logger_msg!( + log_collector, + "Program {} invoke [{}]", + program_id, + invoke_depth + ); +} + +/// Log a message from the program itself. +/// +/// The general form is: +/// +/// ```notrust +/// "Program log: " +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program log: " +pub fn program_log(log_collector: &Option>>, message: &str) { + ic_logger_msg!(log_collector, "Program log: {}", message); +} + +/// Emit a program data. +/// +/// The general form is: +/// +/// ```notrust +/// "Program data: *" +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program data: " +pub fn program_data(log_collector: &Option>>, data: &[&[u8]]) { + ic_logger_msg!( + log_collector, + "Program data: {}", + data.iter().map(|v| BASE64_STANDARD.encode(v)).join(" ") + ); +} + +/// Log return data as from the program itself. This line will not be present if no return +/// data was set, or if the return data was set to zero length. +/// +/// The general form is: +/// +/// ```notrust +/// "Program return: " +/// ``` +/// +/// That is, any program-generated output is guaranteed to be prefixed by "Program return: " +pub fn program_return( + log_collector: &Option>>, + program_id: &Pubkey, + data: &[u8], +) { + ic_logger_msg!( + log_collector, + "Program return: {} {}", + program_id, + BASE64_STANDARD.encode(data) + ); +} + +/// Log successful program execution. +/// +/// The general form is: +/// +/// ```notrust +/// "Program
success" +/// ``` +pub fn program_success(log_collector: &Option>>, program_id: &Pubkey) { + ic_logger_msg!(log_collector, "Program {} success", program_id); +} + +/// Log program execution failure +/// +/// The general form is: +/// +/// ```notrust +/// "Program
failed: " +/// ``` +pub fn program_failure( + log_collector: &Option>>, + program_id: &Pubkey, + err: &E, +) { + ic_logger_msg!(log_collector, "Program {} failed: {}", program_id, err); +} diff --git a/solana/program-runtime/src/sysvar_cache.rs b/solana/program-runtime/src/sysvar_cache.rs new file mode 100644 index 0000000..8d018cd --- /dev/null +++ b/solana/program-runtime/src/sysvar_cache.rs @@ -0,0 +1,402 @@ +#[expect(deprecated)] +use solana_sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes}; +use { + crate::invoke_context::InvokeContext, + serde::de::DeserializeOwned, + solana_clock::Clock, + solana_epoch_rewards::EpochRewards, + solana_epoch_schedule::EpochSchedule, + solana_instruction::error::InstructionError, + solana_last_restart_slot::LastRestartSlot, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::sysvar, + solana_slot_hashes::SlotHashes, + solana_stake_interface::stake_history::StakeHistory, + solana_svm_type_overrides::sync::Arc, + solana_sysvar::SysvarSerialize, + solana_sysvar_id::SysvarId, + solana_transaction_context::{IndexOfAccount, instruction::InstructionContext}, +}; + +#[cfg(feature = "frozen-abi")] +impl ::solana_frozen_abi::abi_example::AbiExample for SysvarCache { + fn example() -> Self { + // SysvarCache is not Serialize so just rely on Default. + SysvarCache::default() + } +} + +#[derive(Default, Clone, Debug)] +pub struct SysvarCache { + // full account data as provided by bank, including any trailing zero bytes + clock: Option>, + epoch_schedule: Option>, + epoch_rewards: Option>, + rent: Option>, + slot_hashes: Option>, + stake_history: Option>, + last_restart_slot: Option>, + + // object representations of large sysvars for convenience + // these are used by the stake and vote builtin programs + // these should be removed once those programs are ported to bpf + slot_hashes_obj: Option>, + stake_history_obj: Option>, + + // deprecated sysvars, these should be removed once practical + #[expect(deprecated)] + fees: Option, + #[expect(deprecated)] + recent_blockhashes: Option, +} + +// declare_deprecated_sysvar_id doesn't support const. +// These sysvars are going away anyway. +const FEES_ID: Pubkey = Pubkey::from_str_const("SysvarFees111111111111111111111111111111111"); +const RECENT_BLOCKHASHES_ID: Pubkey = + Pubkey::from_str_const("SysvarRecentB1ockHashes11111111111111111111"); + +impl SysvarCache { + /// Overwrite a sysvar. For testing purposes only. + #[expect(deprecated)] + pub fn set_sysvar_for_tests(&mut self, sysvar: &T) { + let data = bincode::serialize(sysvar).expect("Failed to serialize sysvar."); + let sysvar_id = T::id(); + match sysvar_id { + sysvar::clock::ID => { + self.clock = Some(data); + } + sysvar::epoch_rewards::ID => { + self.epoch_rewards = Some(data); + } + sysvar::epoch_schedule::ID => { + self.epoch_schedule = Some(data); + } + FEES_ID => { + let fees: Fees = + bincode::deserialize(&data).expect("Failed to deserialize Fees sysvar."); + self.fees = Some(fees); + } + sysvar::last_restart_slot::ID => { + self.last_restart_slot = Some(data); + } + RECENT_BLOCKHASHES_ID => { + let recent_blockhashes: RecentBlockhashes = bincode::deserialize(&data) + .expect("Failed to deserialize RecentBlockhashes sysvar."); + self.recent_blockhashes = Some(recent_blockhashes); + } + sysvar::rent::ID => { + self.rent = Some(data); + } + sysvar::slot_hashes::ID => { + let slot_hashes: SlotHashes = + bincode::deserialize(&data).expect("Failed to deserialize SlotHashes sysvar."); + self.slot_hashes = Some(data); + self.slot_hashes_obj = Some(Arc::new(slot_hashes)); + } + sysvar::stake_history::ID => { + let stake_history: StakeHistory = bincode::deserialize(&data) + .expect("Failed to deserialize StakeHistory sysvar."); + self.stake_history = Some(data); + self.stake_history_obj = Some(Arc::new(stake_history)); + } + _ => panic!("Unrecognized Sysvar ID: {sysvar_id}"), + } + } + + // this is exposed for SyscallGetSysvar and should not otherwise be used + pub fn sysvar_id_to_buffer(&self, sysvar_id: &Pubkey) -> &Option> { + if Clock::check_id(sysvar_id) { + &self.clock + } else if EpochSchedule::check_id(sysvar_id) { + &self.epoch_schedule + } else if EpochRewards::check_id(sysvar_id) { + &self.epoch_rewards + } else if Rent::check_id(sysvar_id) { + &self.rent + } else if SlotHashes::check_id(sysvar_id) { + &self.slot_hashes + } else if StakeHistory::check_id(sysvar_id) { + &self.stake_history + } else if LastRestartSlot::check_id(sysvar_id) { + &self.last_restart_slot + } else { + &None + } + } + + // most if not all of the obj getter functions can be removed once builtins transition to bpf + // the Arc wrapper is to preserve the existing public interface + fn get_sysvar_obj( + &self, + sysvar_id: &Pubkey, + ) -> Result, InstructionError> { + if let Some(sysvar_buf) = self.sysvar_id_to_buffer(sysvar_id) { + bincode::deserialize(sysvar_buf) + .map(Arc::new) + .map_err(|_| InstructionError::UnsupportedSysvar) + } else { + Err(InstructionError::UnsupportedSysvar) + } + } + + pub fn get_clock(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&Clock::id()) + } + + pub fn get_epoch_schedule(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&EpochSchedule::id()) + } + + pub fn get_epoch_rewards(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&EpochRewards::id()) + } + + pub fn get_rent(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&Rent::id()) + } + + pub fn get_last_restart_slot(&self) -> Result, InstructionError> { + self.get_sysvar_obj(&LastRestartSlot::id()) + } + + pub fn get_stake_history(&self) -> Result, InstructionError> { + self.stake_history_obj + .clone() + .ok_or(InstructionError::UnsupportedSysvar) + } + + pub fn get_slot_hashes(&self) -> Result, InstructionError> { + self.slot_hashes_obj + .clone() + .ok_or(InstructionError::UnsupportedSysvar) + } + + #[deprecated] + #[expect(deprecated)] + pub fn get_fees(&self) -> Result, InstructionError> { + self.fees + .clone() + .ok_or(InstructionError::UnsupportedSysvar) + .map(Arc::new) + } + + #[deprecated] + #[expect(deprecated)] + pub fn get_recent_blockhashes(&self) -> Result, InstructionError> { + self.recent_blockhashes + .clone() + .ok_or(InstructionError::UnsupportedSysvar) + .map(Arc::new) + } + + pub fn fill_missing_entries( + &mut self, + mut get_account_data: F, + ) { + if self.clock.is_none() { + get_account_data(&Clock::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.clock = Some(data.to_vec()); + } + }); + } + + if self.epoch_schedule.is_none() { + get_account_data(&EpochSchedule::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.epoch_schedule = Some(data.to_vec()); + } + }); + } + + if self.epoch_rewards.is_none() { + get_account_data(&EpochRewards::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.epoch_rewards = Some(data.to_vec()); + } + }); + } + + if self.rent.is_none() { + get_account_data(&Rent::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.rent = Some(data.to_vec()); + } + }); + } + + if self.slot_hashes.is_none() { + get_account_data(&SlotHashes::id(), &mut |data: &[u8]| { + if let Ok(obj) = bincode::deserialize::(data) { + self.slot_hashes = Some(data.to_vec()); + self.slot_hashes_obj = Some(Arc::new(obj)); + } + }); + } + + if self.stake_history.is_none() { + get_account_data(&StakeHistory::id(), &mut |data: &[u8]| { + if let Ok(obj) = bincode::deserialize::(data) { + self.stake_history = Some(data.to_vec()); + self.stake_history_obj = Some(Arc::new(obj)); + } + }); + } + + if self.last_restart_slot.is_none() { + get_account_data(&LastRestartSlot::id(), &mut |data: &[u8]| { + if bincode::deserialize::(data).is_ok() { + self.last_restart_slot = Some(data.to_vec()); + } + }); + } + + #[expect(deprecated)] + if self.fees.is_none() { + get_account_data(&Fees::id(), &mut |data: &[u8]| { + if let Ok(fees) = bincode::deserialize(data) { + self.fees = Some(fees); + } + }); + } + + #[expect(deprecated)] + if self.recent_blockhashes.is_none() { + get_account_data(&RecentBlockhashes::id(), &mut |data: &[u8]| { + if let Ok(recent_blockhashes) = bincode::deserialize(data) { + self.recent_blockhashes = Some(recent_blockhashes); + } + }); + } + } + + pub fn reset(&mut self) { + *self = Self::default(); + } +} + +/// These methods facilitate a transition from fetching sysvars from keyed +/// accounts to fetching from the sysvar cache without breaking consensus. In +/// order to keep consistent behavior, they continue to enforce legacy checks +/// despite dynamically loading them instead of deserializing from account data. +pub mod get_sysvar_with_account_check { + use super::*; + + fn check_sysvar_account( + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result<(), InstructionError> { + if !S::check_id( + instruction_context.get_key_of_instruction_account(instruction_account_index)?, + ) { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + + pub fn clock( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.environment_config.sysvar_cache().get_clock() + } + + pub fn rent( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context.environment_config.sysvar_cache().get_rent() + } + + pub fn slot_hashes( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context + .environment_config + .sysvar_cache() + .get_slot_hashes() + } + + #[expect(deprecated)] + pub fn recent_blockhashes( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context + .environment_config + .sysvar_cache() + .get_recent_blockhashes() + } + + pub fn stake_history( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context + .environment_config + .sysvar_cache() + .get_stake_history() + } + + pub fn last_restart_slot( + invoke_context: &InvokeContext, + instruction_context: &InstructionContext, + instruction_account_index: IndexOfAccount, + ) -> Result, InstructionError> { + check_sysvar_account::(instruction_context, instruction_account_index)?; + invoke_context + .environment_config + .sysvar_cache() + .get_last_restart_slot() + } +} + +#[cfg(test)] +mod tests { + use {super::*, test_case::test_case}; + + // sysvar cache provides the full account data of a sysvar + // the setters MUST NOT be changed to serialize an object representation + // it is required that the syscall be able to access the full buffer as it exists onchain + // this is meant to cover the cases: + // * account data is larger than struct sysvar + // * vector sysvar has fewer than its maximum entries + // if at any point the data is roundtripped through bincode, the vector will shrink + #[test_case(Clock::default(); "clock")] + #[test_case(EpochSchedule::default(); "epoch_schedule")] + #[test_case(EpochRewards::default(); "epoch_rewards")] + #[test_case(Rent::default(); "rent")] + #[test_case(SlotHashes::default(); "slot_hashes")] + #[test_case(StakeHistory::default(); "stake_history")] + #[test_case(LastRestartSlot::default(); "last_restart_slot")] + fn test_sysvar_cache_preserves_bytes(_: T) { + let id = T::id(); + let size = T::size_of().saturating_mul(2); + let in_buf = vec![0; size]; + + let mut sysvar_cache = SysvarCache::default(); + sysvar_cache.fill_missing_entries(|pubkey, callback| { + if *pubkey == id { + callback(&in_buf) + } + }); + let sysvar_cache = sysvar_cache; + + let out_buf = sysvar_cache.sysvar_id_to_buffer(&id).clone().unwrap(); + + assert_eq!(out_buf, in_buf); + } +} diff --git a/solana/program-runtime/src/vm.rs b/solana/program-runtime/src/vm.rs new file mode 100644 index 0000000..119c6f0 --- /dev/null +++ b/solana/program-runtime/src/vm.rs @@ -0,0 +1,490 @@ +//! SBF virtual machine provisioning and execution. + +#[cfg(feature = "svm-internal")] +use qualifier_attr::qualifiers; +use { + crate::{ + execution_budget::MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268, + invoke_context::{BpfAllocator, InvokeContext}, + mem_pool::VmMemoryPool, + memory_context::{MemoryContext, SerializedAccountMetadata}, + program_cache_entry::ProgramCacheEntry, + serialization, stable_log, + }, + solana_instruction::error::InstructionError, + solana_program_entrypoint::{MAX_PERMITTED_DATA_INCREASE, SUCCESS}, + solana_sbpf::{ + ebpf::{self, MM_HEAP_START, MM_STACK_START}, + elf::Executable, + error::{EbpfError, ProgramResult}, + memory_region::{AccessType, MemoryMapping, MemoryRegion}, + vm::{ContextObject, EbpfVm, ExecutionMode}, + }, + solana_sdk_ids::bpf_loader_deprecated, + solana_svm_log_collector::ic_logger_msg, + solana_svm_measure::measure::Measure, + solana_transaction_context::IndexOfAccount, + std::{cell::RefCell, mem, time::Duration}, +}; + +thread_local! { + pub static MEMORY_POOL: RefCell = RefCell::new(VmMemoryPool::new()); +} + +/// Only used in macro, do not use directly! +pub fn calculate_heap_cost(heap_size: u32, heap_cost: u64) -> u64 { + const KIBIBYTE: u64 = 1024; + const PAGE_SIZE_KB: u64 = 32; + let mut rounded_heap_size = u64::from(heap_size); + rounded_heap_size = + rounded_heap_size.saturating_add(PAGE_SIZE_KB.saturating_mul(KIBIBYTE).saturating_sub(1)); + rounded_heap_size + .checked_div(PAGE_SIZE_KB.saturating_mul(KIBIBYTE)) + .expect("PAGE_SIZE_KB * KIBIBYTE > 0") + .saturating_sub(1) + .saturating_mul(heap_cost) +} + +/// Only used in macro, do not use directly! +/// +/// # Safety +/// +/// Refer to [`configure_program_regions`]. +#[cfg_attr(feature = "svm-internal", qualifiers(pub))] +pub unsafe fn create_vm<'a, 'b>( + program: &'a Executable>, + invoke_context: &'a mut InvokeContext<'b, 'b>, + stack: *mut [u8], + heap: *mut [u8], +) -> Result>, Box> { + let stack_size = stack.len(); + unsafe { + // SAFETY: invariants delegated to the caller. + configure_program_regions(invoke_context, program, stack, heap)?; + } + Ok(EbpfVm::new( + program.get_loader().clone(), + program.get_sbpf_version(), + invoke_context, + stack_size, + )) +} + +/// # Safety +/// +/// The `executable`, `stack` and `heap` arguments must remain allocated for at least the lifetime +/// of [`MemoryMapping`] (or until after the `MemoryMapping` is reconfigured with different +/// `executable`, `stack` and `heap`). +unsafe fn configure_program_regions( + invoke_context: &mut InvokeContext, + executable: &Executable, + stack: *mut [u8], + heap: *mut [u8], +) -> Result<(), Box> { + let mapping = invoke_context.memory_contexts.memory_mapping_mut()?; + let regions = mapping.get_regions_mut(); + let [ro_area, stack_area, heap_area, ..] = regions else { + panic!("the regions vector must have at least three entries") + }; + *ro_area = executable.get_ro_region(); + let sbpf_version = executable.get_sbpf_version(); + let config = executable.get_config(); + *stack_area = MemoryRegion::new_gapped( + stack, + MM_STACK_START, + if sbpf_version.stack_frame_gaps() && config.enable_stack_frame_gaps { + config.stack_frame_size as u64 + } else { + 0 + }, + ); + *heap_area = MemoryRegion::new(heap, MM_HEAP_START); + mapping + .initialize() + .map_err(|err| Box::new(err) as Box) +} + +/// Create the SBF virtual machine +#[macro_export] +macro_rules! create_vm { + ($vm:ident, $program:expr, $invoke_context:expr $(,)?) => { + let invoke_context = &*$invoke_context; + let stack_size = $program.get_config().stack_size(); + let heap_size = invoke_context.get_compute_budget().heap_size; + let heap_cost_result = + invoke_context + .compute_meter + .consume_checked($crate::__private::calculate_heap_cost( + heap_size, + invoke_context.get_execution_cost().heap_cost, + )); + let $vm = heap_cost_result.and_then(|_| { + let (mut stack, mut heap) = $crate::__private::MEMORY_POOL + .with_borrow_mut(|pool| (pool.get_stack(stack_size), pool.get_heap(heap_size))); + let vm = $crate::__private::create_vm( + $program, + $invoke_context, + stack + .as_slice_mut() + .get_mut(..stack_size) + .expect("invalid stack size"), + heap.as_slice_mut() + .get_mut(..heap_size as usize) + .expect("invalid heap size"), + ); + vm.map(|vm| (vm, stack, heap)) + }); + }; +} + +/// # Safety +/// +/// The [`MemoryRegion`]s must satisfy the safety preconditions for +/// [`MemoryMapping::new_uninitialized`]. +unsafe fn set_memory_context<'b>( + additional_initialized_regions: Vec, + accounts_metadata: Vec, + invoke_context: &mut InvokeContext<'b, 'b>, + executable: &Executable>, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, +) -> Result<(), Box> { + let heap_size = invoke_context.get_compute_budget().heap_size; + let regions = vec![MemoryRegion::default(); 3] + .into_iter() + .chain(additional_initialized_regions) + .collect(); + let memory_mapping = unsafe { + // SAFETY: all memory regions are `default` (and thus implicitly valid) or valid by + // delegating the safety invariant upon the caller. + MemoryMapping::new_uninitialized( + regions, + executable.get_config(), + executable.get_sbpf_version(), + invoke_context.transaction_context.access_violation_handler( + virtual_address_space_adjustments, + account_data_direct_mapping, + ), + ) + }; + + invoke_context + .memory_contexts + .set_memory_context_abi_v1(MemoryContext::new( + BpfAllocator::new(heap_size as u64), + accounts_metadata, + memory_mapping, + )) + .map_err(|err| Box::new(err) as Box) +} + +#[cfg_attr(feature = "svm-internal", qualifiers(pub))] +pub fn execute<'a, 'b: 'a>( + executable: &'a Executable>, + invoke_context: &'a mut InvokeContext<'b, 'b>, + cache_entry: &ProgramCacheEntry, +) -> Result<(), Box> { + // We dropped the lifetime tracking in the Executor by setting it to 'static, + // thus we need to reintroduce the correct lifetime of InvokeContext here again. + let executable = unsafe { + mem::transmute::< + &'a Executable>, + &'a Executable>, + >(executable) + }; + let log_collector = invoke_context.get_log_collector(); + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let program_id = *instruction_context.get_program_key()?; + let is_loader_deprecated = + instruction_context.get_program_owner()? == bpf_loader_deprecated::id(); + let virtual_address_space_adjustments = invoke_context + .get_feature_set() + .virtual_address_space_adjustments; + let account_data_direct_mapping = invoke_context.get_feature_set().account_data_direct_mapping; + let direct_account_pointers_in_program_input = invoke_context + .get_feature_set() + .direct_account_pointers_in_program_input; + + let mut serialize_time = Measure::start("serialize"); + let (parameter_bytes, regions, accounts_metadata, instruction_data_offset) = + serialization::serialize_parameters( + &instruction_context, + virtual_address_space_adjustments, + account_data_direct_mapping, + direct_account_pointers_in_program_input, + )?; + serialize_time.stop(); + + // save the account addresses so in case we hit an AccessViolation error we + // can map to a more specific error + let account_region_addrs = accounts_metadata + .iter() + .map(|m| { + let vm_end = m + .vm_data_addr + .saturating_add(m.original_data_len as u64) + .saturating_add(if !is_loader_deprecated { + MAX_PERMITTED_DATA_INCREASE as u64 + } else { + 0 + }); + m.vm_data_addr..vm_end + }) + .collect::>(); + + #[cfg(feature = "sbpf-debugger")] + let (debug_port, debug_metadata) = if invoke_context.debug_port.is_some() { + ( + invoke_context.debug_port, + Some(format!( + "program_id={};cpi_level={};caller={}", + program_id, + instruction_context.get_stack_height().saturating_sub(1), + invoke_context + .get_stack_height() + .checked_sub(2) + .and_then(|nesting_level| { + transaction_context + .get_instruction_context_at_nesting_level(nesting_level) + .ok() + }) + .and_then(|ctx| ctx.get_program_key().ok()) + .map(|key| key.to_string()) + .unwrap_or_else(|| "none".into()) + )), + ) + } else { + (None, None) + }; + + let mut create_vm_time = Measure::start("create_vm"); + unsafe { + // SAFETY: The memory pointed to by regions is valid for the useful lifetime of + // `invoke_context`, which in turn contains the `MemoryMapping` that allows access to this + // memory. + set_memory_context( + regions, + accounts_metadata, + invoke_context, + executable, + virtual_address_space_adjustments, + account_data_direct_mapping, + )? + }; + + let execution_result = { + let mut execution_mode = ExecutionMode::PreferJit; + + #[cfg(feature = "sbpf-debugger")] + if invoke_context.debug_port.is_some() { + execution_mode = ExecutionMode::Interpreted; + } + + let compute_meter_prev = invoke_context.get_remaining(); + let (mut vm, stack, heap) = unsafe { + // SAFETY: The `stack`, `heap` and `executable` live past the lifetime of + // `invoke_context`. + create_vm!(vm, executable, invoke_context); + match vm { + Ok(info) => info, + Err(e) => { + ic_logger_msg!(log_collector, "Failed to create SBF VM: {}", e); + return Err(Box::new(InstructionError::ProgramEnvironmentSetupFailure)); + } + } + }; + + create_vm_time.stop(); + #[cfg(feature = "sbpf-debugger")] + { + vm.debug_metadata = debug_metadata; + } + + let execute_time = Measure::start("execute"); + let prev_nested_exec_time = vm.context().total_nested_exec_time; + + vm.registers[1] = ebpf::MM_INPUT_START; + vm.registers[2] = instruction_data_offset as u64; + let mut call_frames = + MEMORY_POOL.with_borrow_mut(|memory_pool| memory_pool.get_call_frames()); + let (compute_units_consumed, result) = + vm.execute_program(executable, &mut execution_mode, &mut call_frames); + let register_trace = std::mem::take(&mut vm.register_trace); + MEMORY_POOL.with_borrow_mut(|memory_pool| { + memory_pool.put_stack(stack); + memory_pool.put_heap(heap); + memory_pool.put_call_frames(call_frames); + debug_assert!(memory_pool.stack_len() <= MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268); + debug_assert!(memory_pool.heap_len() <= MAX_INSTRUCTION_STACK_DEPTH_SIMD_0268); + }); + drop(vm); + invoke_context.insert_register_trace(register_trace); + + // This section is a little convoluted due to the nested and sibling (CPI) invocations. + let total_execute_ns = execute_time.end_as_ns(); + let nested_execution_time_delta = invoke_context + .total_nested_exec_time + .saturating_sub(prev_nested_exec_time); + let this_call_ns = + total_execute_ns.saturating_sub(nested_execution_time_delta.as_nanos() as u64); + invoke_context.total_nested_exec_time = invoke_context + .total_nested_exec_time + .saturating_add(Duration::from_nanos(this_call_ns)); + let this_call_us = this_call_ns / 1000; + invoke_context.timings.execute_us += this_call_us; + match execution_mode { + ExecutionMode::Interpreted => cache_entry.stats.interpreter_executed(this_call_us), + ExecutionMode::Jit => cache_entry.stats.jit_executed(this_call_us), + ExecutionMode::PreferJit => { /* not actually executed? */ } + } + + ic_logger_msg!( + log_collector, + "Program {} consumed {} of {} compute units", + &program_id, + compute_units_consumed, + compute_meter_prev + ); + let (_returned_from_program_id, return_data) = + invoke_context.transaction_context.get_return_data(); + if !return_data.is_empty() { + stable_log::program_return(&log_collector, &program_id, return_data); + } + match result { + ProgramResult::Ok(status) if status != SUCCESS => { + let error: InstructionError = status.into(); + Err(Box::new(error) as Box) + } + ProgramResult::Err(mut error) => { + // Don't clean me up!! + // This feature is active on all networks, but we still toggle + // it off during fuzzing. + if invoke_context + .get_feature_set() + .deplete_cu_meter_on_vm_failure + && !matches!(error, EbpfError::SyscallError(_)) + { + // when an exception is thrown during the execution of a + // Basic Block (e.g., a null memory dereference or other + // faults), determining the exact number of CUs consumed + // up to the point of failure requires additional effort + // and is unnecessary since these cases are rare. + // + // In order to simplify CU tracking, simply consume all + // remaining compute units so that the block cost + // tracker uses the full requested compute unit cost for + // this failed transaction. + invoke_context.consume(invoke_context.get_remaining()); + } + + if virtual_address_space_adjustments { + if let EbpfError::SyscallError(err) = error { + error = err + .downcast::() + .map(|err| *err) + .unwrap_or_else(EbpfError::SyscallError); + } + if let EbpfError::AccessViolation(access_type, vm_addr, len, _section_name) = + error + { + // If virtual_address_space_adjustments is enabled and a program tries to write to a readonly + // region we'll get a memory access violation. Map it to a more specific + // error so it's easier for developers to see what happened. + if let Some((instruction_account_index, vm_addr_range)) = + account_region_addrs + .iter() + .enumerate() + .find(|(_, vm_addr_range)| vm_addr_range.contains(&vm_addr)) + { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = + transaction_context.get_current_instruction_context()?; + let account = instruction_context.try_borrow_instruction_account( + instruction_account_index as IndexOfAccount, + )?; + if vm_addr.saturating_add(len) <= vm_addr_range.end { + // The access was within the range of the accounts address space, + // but it might not be within the range of the actual data. + let is_access_outside_of_data = vm_addr + .saturating_add(len) + .saturating_sub(vm_addr_range.start) + as usize + > account.get_data().len(); + error = EbpfError::SyscallError(Box::new(match access_type { + AccessType::Store => { + if let Err(err) = account.can_data_be_changed() { + err + } else { + // The store was allowed but failed, + // thus it must have been an attempt to grow the account. + debug_assert!(is_access_outside_of_data); + InstructionError::InvalidRealloc + } + } + AccessType::Load => { + // Loads should only fail when they are outside of the account data. + debug_assert!(is_access_outside_of_data); + if account.can_data_be_changed().is_err() { + // Load beyond readonly account data happened because the program + // expected more data than there actually is. + InstructionError::AccountDataTooSmall + } else { + // Load beyond writable account data also attempted to grow. + InstructionError::InvalidRealloc + } + } + })); + } + } + } + } + Err(if let EbpfError::SyscallError(err) = error { + err + } else { + error.into() + }) + } + _ => Ok(()), + } + }; + + fn deserialize_parameters( + invoke_context: &mut InvokeContext, + parameter_bytes: &[u8], + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) -> Result<(), InstructionError> { + serialization::deserialize_parameters( + &invoke_context + .transaction_context + .get_current_instruction_context()?, + virtual_address_space_adjustments, + account_data_direct_mapping, + parameter_bytes, + &invoke_context + .memory_contexts + .memory_context_abi_v1()? + .accounts_metadata, + ) + } + + let mut deserialize_time = Measure::start("deserialize"); + let execute_or_deserialize_result = execution_result.and_then(|_| { + deserialize_parameters( + invoke_context, + parameter_bytes.as_slice(), + virtual_address_space_adjustments, + account_data_direct_mapping, + ) + .map_err(|error| Box::new(error) as Box) + }); + deserialize_time.stop(); + + // Update the timings + invoke_context.timings.serialize_us += serialize_time.as_us(); + invoke_context.timings.create_vm_us += create_vm_time.as_us(); + invoke_context.timings.deserialize_us += deserialize_time.as_us(); + + execute_or_deserialize_result +} diff --git a/solana/svm/Cargo.toml b/solana/svm/Cargo.toml new file mode 100644 index 0000000..0916c0d --- /dev/null +++ b/solana/svm/Cargo.toml @@ -0,0 +1,120 @@ +[package] +name = "solana-svm" +description = "Solana SVM" +documentation = "https://docs.rs/solana-svm" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[lib] +crate-type = ["lib"] +name = "solana_svm" + +[features] +default = ["metrics"] +agave-unstable-api = [] +dummy-for-ci-check = ["metrics"] +dev-context-only-utils = ["dep:qualifier_attr", "solana-program-runtime/dev-context-only-utils"] +frozen-abi = [ + "dep:solana-frozen-abi", + "dep:solana-frozen-abi-macro", + "solana-program-runtime/frozen-abi", +] +metrics = [ + "solana-bpf-loader-program/metrics", + "solana-program-runtime/metrics", +] +shuttle-test = [ + "solana-bpf-loader-program/shuttle-test", + "solana-program-runtime/shuttle-test", + "solana-svm-type-overrides/shuttle-test", +] +svm-internal = ["dep:qualifier_attr"] + +[dependencies] +ahash = { workspace = true } +percentage = { workspace = true } +qualifier_attr = { workspace = true, optional = true } +serde = { workspace = true, features = ["rc"] } +solana-account = { workspace = true } +solana-clock = { workspace = true } +solana-fee-structure = { workspace = true } +solana-frozen-abi = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-frozen-abi-macro = { workspace = true, optional = true, features = [ + "frozen-abi", +] } +solana-hash = { workspace = true } +solana-instruction = { workspace = true, features = ["std"] } +solana-instructions-sysvar = { workspace = true } +solana-loader-v3-interface = { workspace = true, features = ["bincode"] } +solana-loader-v4-interface = { workspace = true } +solana-message = { workspace = true } +solana-nonce = { workspace = true } +solana-nonce-account = { workspace = true, features = ["wincode"] } +solana-program-pack = { workspace = true } +solana-program-runtime = { workspace = true } +solana-pubkey = { workspace = true } +solana-rent = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-svm-callback = { workspace = true } +solana-svm-feature-set = { workspace = true } +solana-svm-log-collector = { workspace = true } +solana-svm-measure = { workspace = true } +solana-svm-timings = { workspace = true } +solana-svm-transaction = { workspace = true } +solana-svm-type-overrides = { workspace = true } +solana-system-interface = { workspace = true } +solana-transaction-context = { workspace = true } +solana-transaction-error = { workspace = true } +spl-generic-token = { workspace = true } +thiserror = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +bincode = { workspace = true } +env_logger = { workspace = true } +libsecp256k1 = { workspace = true } +openssl = { workspace = true } +rand = { workspace = true } +shuttle = { workspace = true } +solana-bpf-loader-program = { path = "../programs/bpf_loader", default-features = false, features = ["agave-unstable-api"] } +solana-clock = { workspace = true } +solana-compute-budget = { path = "../compute-budget", features = ["agave-unstable-api"] } +solana-compute-budget-interface = { workspace = true } +solana-compute-budget-program = { path = "../programs/compute-budget", features = ["agave-unstable-api"] } +solana-ed25519-program = { workspace = true } +solana-epoch-schedule = { workspace = true } +solana-fee-calculator = { workspace = true } +solana-keypair = { workspace = true } +solana-native-token = { workspace = true } +solana-precompile-error = { workspace = true } +solana-program-binaries = { path = "../program-binaries", features = ["agave-unstable-api"] } +solana-program-runtime = { path = "../program-runtime", features = ["agave-unstable-api", "dev-context-only-utils"] } +solana-pubkey = { workspace = true, features = ["rand"] } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true, features = ["jit"] } +solana-secp256k1-program = { workspace = true, features = ["bincode"] } +solana-secp256r1-program = { workspace = true, features = ["openssl-vendored"] } +solana-signature = { workspace = true, features = ["rand"] } +solana-signer = { workspace = true } +# See order-crates-for-publishing.py for using this unusual `path = "."` +solana-svm = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils", "svm-internal"] } +solana-syscalls = { path = "../syscalls", features = ["agave-unstable-api"] } +solana-system-program = { path = "../programs/system", features = ["agave-unstable-api"] } +solana-system-transaction = { workspace = true } +solana-sysvar = { workspace = true } +solana-transaction = { workspace = true, features = ["dev-context-only-utils"] } +solana-transaction-context = { path = "../transaction-context", features = ["agave-unstable-api", "bincode", "dev-context-only-utils"] } +spl-token-interface = { workspace = true } +test-case = { workspace = true } + +[lints] +workspace = true diff --git a/solana/svm/doc/diagrams/context.svg b/solana/svm/doc/diagrams/context.svg new file mode 100644 index 0000000..b2ec4f2 --- /dev/null +++ b/solana/svm/doc/diagrams/context.svg @@ -0,0 +1,88 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +bank + +transactionprocessor + +programruntime + +BPFVM + +accounts-db + +runtime + +SVM + + \ No newline at end of file diff --git a/solana/svm/doc/diagrams/context.tex b/solana/svm/doc/diagrams/context.tex new file mode 100644 index 0000000..ec493b9 --- /dev/null +++ b/solana/svm/doc/diagrams/context.tex @@ -0,0 +1,35 @@ +%%\documentclass[dvisvgm]{minimal} +\documentclass{minimal} + +\usepackage{tikz} +\usetikzlibrary{graphs, graphdrawing, shapes.misc} +\usegdlibrary{trees} + +\begin{document} + +\tikzset{terminal/.style={ + % The shape: + rectangle, + rounded corners=3mm, + minimum size=20mm, + % The rest + very thick,draw=black!50, + top color=white,bottom color=black!20, + font=\ttfamily}, +} + +\begin{tikzpicture} + \graph[tree layout, grow'=right, level sep=10mm] { + bank [terminal] + -> { + { a/"transaction processor"[terminal, orient=right, orient tail=bank] + -> b/"program runtime"[terminal] + -> c/"BPF VM"[terminal] }, , , , , , , , + d/"accounts-db"[terminal, orient=down, orient tail=bank] + }; + runtime [draw] // { bank }; + SVM [draw] // { a, b, c } + }; +\end{tikzpicture} + +\end{document} diff --git a/solana/svm/doc/spec.md b/solana/svm/doc/spec.md new file mode 100644 index 0000000..16c311b --- /dev/null +++ b/solana/svm/doc/spec.md @@ -0,0 +1,311 @@ +# Solana Virtual Machine specification + +# Introduction + +Several components of the Solana Validator are involved in processing +a transaction (or a batch of transactions). Collectively, the +components responsible for transaction execution are designated as +Solana Virtual Machine (SVM). SVM packaged as a stand-alone library +can be used in applications outside the Solana Validator. + +This document represents the SVM specification. It covers the API +of using SVM in projects unrelated to Solana Validator and the +internal workings of the SVM, including the descriptions of the inner +data flow, data structures, and algorithms involved in the execution +of transactions. The document’s target audience includes both external +users and the developers of the SVM. + +## Use cases + +We envision the following applications for SVM + +- **Transaction execution in Solana Validator** + + This is the primary use case for the SVM. It remains a major + component of the Agave Validator, but with clear interface and + isolated from dependencies on other components. + + The SVM is currently viewed as realizing two stages of the + Transaction Engine Execution pipeline as described in Solana + Architecture documentation + [https://docs.solana.com/validator/runtime#execution](https://docs.solana.com/validator/runtime#execution), + namely ‘load accounts’ and ‘execute’ stages. + +- **SVM Rollups** + + Rollups that need to execute a block but don’t need the other + components of the validator can benefit from SVM, as it can reduce + hardware requirements and decentralize the network. This is + especially useful for Ephemeral Rollups since the cost of compute + will be higher as a new rollup is created for every user session + in applications like gaming. + +- **SVM Fraud Proofs for Diet Clients** + + A succinct proof of an invalid state transition by the supermajority (SIMD-65) + +- **Validator Sidecar for JSON-RPC** + + The RPC needs to be separated from the validator. + `simulateTransaction` requires replaying the transactions and + accessing necessary account data. + +- **SVM-based Avalanche subnet** + + The SVM would need to be isolated to run within a subnet since the + consensus and networking functionality would rely on Avalanche + modules. + +- **Modified SVM (SVM+)** + + An SVM type with all the current functionality and extended + instructions for custom use cases. This would form a superset of + the current SVM. + +# System Context + +In this section, SVM is represented as a single entity. We describe its +interfaces to the parts of the Solana Validator external to SVM. + +In the context of Solana Validator, the main entity external to SVM is +bank. It creates an SVM, submits transactions for execution and +receives results of transaction execution from SVM. + +![context diagram](/svm/doc/diagrams/context.svg "System Context") + +## Interfaces + +In this section, we describe the API of using the SVM both in Solana +Validator and in third-party applications. + +The interface to SVM is represented by the +`transaction_processor::TransactionBatchProcessor` struct. To create +a `TransactionBatchProcessor` object the client need to specify the +`slot`, `epoch`, and `program_cache`. + +- `slot: Slot` is a u64 value representing the ordinal number of a + particular blockchain state in context of which the transactions + are executed. This value is used to locate the on-chain program + versions used in the transaction execution. +- `epoch: Epoch` is a u64 value representing the ordinal number of + a Solana epoch, in which the slot was created. This is another + index used to locate the onchain programs used in the execution of + transactions in the batch. +- `program_cache: Arc>>` is a reference to + a ProgramCache instance. All on chain programs used in transaction + batch execution are loaded from the program cache. + +In addition, `TransactionBatchProcessor` needs an instance of +`SysvarCache` and a set of pubkeys of builtin program IDs. + +The main entry point to the SVM is the method +`load_and_execute_sanitized_transactions`. + +The method `load_and_execute_sanitized_transactions` takes the +following arguments: + +- `callbacks`: A `TransactionProcessingCallback` trait instance which allows + the transaction processor to summon information about accounts, most + importantly loading them for transaction execution. +- `sanitized_txs`: A slice of sanitized transactions. +- `check_results`: A mutable slice of transaction check results. +- `environment`: The runtime environment for transaction batch processing. +- `config`: Configurations for customizing transaction processing behavior. + +The method returns a `LoadAndExecuteSanitizedTransactionsOutput`, which is +defined below in more detail. + +An integration test `svm_integration` contains an example of +instantiating `TransactionBatchProcessor` and calling its method +`load_and_execute_sanitized_transactions`. + +### `TransactionProcessingCallback` + +Downstream consumers of the SVM must implement the +`TransactionProcessingCallback` trait in order to provide the transaction +processor with the ability to load accounts and retrieve other account-related +information. + +```rust +pub trait TransactionProcessingCallback { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)>; + + fn add_builtin_account(&self, _name: &str, _program_id: &Pubkey) {} +} +``` + +Consumers can customize this plug-in to use their own Solana account source, +caching, and more. + +### `SVMTransaction` + +An SVM transaction is a transaction that has undergone the +various checks required to evaluate a transaction against the Solana protocol +ruleset. Some of these rules include signature verification and validation +of account indices (`num_readonly_signers`, etc.). + +A `SVMTransaction` is a trait that can access: + +- `signatures`: the hash of the transaction message encrypted using + the signing key (for each signer in the transaction). +- `static_account_keys`: Slice of `Pubkey` of accounts used in the transaction. +- `account_keys`: Pubkeys of all accounts used in the transaction, including + those from address table lookups. +- `recent_blockhash`: Hash of a recent block. +- `instructions_iter`: An iterator over the transaction's instructions. +- `message_address_table_lookups`: An iterator over the transaction's + address table lookups. These are only used in V0 transactions, for legacy + transactions the iterator is empty. + +### `TransactionCheckResult` + +Simply stores details about a transaction, including whether or not it contains +a nonce, the nonce it contains (if applicable), and the lamports per signature +to charge for fees. + +### `TransactionProcessingEnvironment` + +The transaction processor requires consumers to provide values describing +the runtime environment to use for processing transactions. + +- `blockhash`: The blockhash to use for the transaction batch. +- `feature_set`: Runtime feature set to use for the transaction batch. +- `epoch_total_stake`: The total stake for the current epoch. +- `fee_structure`: Fee structure to use for assessing transaction fees. +- `lamports_per_signature`: Lamports per signature to charge per transaction. +- `rent_collector`: Rent collector to use for the transaction batch. + +### `TransactionProcessingConfig` + +Consumers can provide various configurations to adjust the default behavior of +the transaction processor. + +- `account_overrides`: Encapsulates overridden accounts, typically used for + transaction simulation. +- `compute_budget`: The compute budget to use for transaction execution. +- `check_program_deployment_slot`: Whether or not to check a program's + deployment slot when replenishing a program cache instance. +- `log_messages_bytes_limit`: The maximum number of bytes that log messages can + consume. +- `limit_to_load_programs`: Whether to limit the number of programs loaded for + the transaction batch. +- `recording_config`: Recording capabilities for transaction execution. + +### `LoadAndExecuteSanitizedTransactionsOutput` + +The output of the transaction batch processor's +`load_and_execute_sanitized_transactions` method. + +- `error_metrics`: Error metrics for transactions that were processed. +- `execute_timings`: Timings for transaction batch execution. +- `processing_results`: Vector of results indicating whether a transaction was + processed or could not be processed for some reason. Note that processed + transactions can still have failed! + +# Functional Model + +In this section, we describe the functionality (logic) of the SVM in +terms of its components, relationships among components, and their +interactions. + +On a high level the control flow of SVM consists of loading program +accounts, checking and verifying the loaded accounts, creating +invocation context and invoking RBPF on programs implementing the +instructions of a transaction. The SVM needs to have access to an account +database, and a sysvar cache via traits implemented for the corresponding +objects passed to it. The results of transaction execution are +consumed by bank in Solana Validator use case. However, bank structure +should not be part of the SVM. + +In bank context `load_and_execute_sanitized_transactions` is called from +`simulate_transaction` where a single transaction is executed, and +from `load_execute_and_commit_transactions` which receives a batch of +transactions from its caller. + +Steps of `load_and_execute_sanitized_transactions` + +1. Steps of preparation for execution + - filter executable program accounts and build program accounts map (explain) + - add builtin programs to program accounts map + - replenish program cache using the program accounts map + - Gather all required programs to load from the cache. + - Lock the global program cache and initialize the local program cache. + - Perform loading tasks to load all required programs from the cache, + loading, verifying, and compiling (where necessary) each program. + - A helper module - `program_loader` - provides utilities for loading + programs from on-chain, namely `load_program_with_pubkey`. + - Return the replenished local program cache. + +2. Load accounts (call to `load_accounts` function) + - For each `SVMTransaction` and `TransactionCheckResult`, we: + - Calculate the number of signatures in transaction and its cost. + - Call `load_transaction_accounts` + - The function is interwined with the struct `SVMInstruction` + - Load accounts from accounts DB + - Extract data from accounts + - Verify if we've reached the maximum account data size + - Validate the fee payer and the loaded accounts + - Validate the programs accounts that have been loaded and checks if they are builtin programs. + - Return `struct LoadedTransaction` containing the accounts (pubkey and data), + indices to the executable accounts in `TransactionContext` (or `InstructionContext`), + the transaction rent, and the `struct RentDebit`. + - Generate a `RollbackAccounts` struct which holds fee-subtracted fee payer account and pre-execution nonce state used for rolling back account state on execution failure. + - Returns `TransactionLoadedResult`, containing the `LoadTransaction` we obtained from `loaded_transaction_accounts` + +3. Execute each loaded transactions + 1. Compute the sum of transaction accounts' balances. This sum is + invariant in the transaction execution. + 2. Obtain rent state of each account before the transaction + execution. This is later used in verifying the account state + changes (step #7). + 3. Create a new log_collector. `LogCollector` is defined in + solana-program-runtime crate. + 4. Obtain last blockhash and lamports per signature. This + information is read from blockhash_queue maintained in Bank. The + information is taken in parameters to + `MessageProcessor::process_message`. + 5. Make two local variables that will be used as output parameters + of `MessageProcessor::process_message`. One will contain the + number of executed units (the number of compute unites consumed + in the transaction). Another is a container of `ProgramCacheForTxBatch`. + The latter is initialized with the slot, and + the clone of environments of `programs_loaded_for_tx_batch` + - `programs_loaded_for_tx_batch` contains a reference to all the `ProgramCacheEntry`s + necessary for the transaction. It maintains an `Arc` to the programs in the global + `ProgramCacheEntry` data structure. + 6. Call `MessageProcessor::process_message` to execute the + transaction. `MessageProcessor` is contained in + solana-program-runtime crate. The result of processing message + is either `ProcessedMessageInfo` which is an i64 wrapped in a + struct meaning the change in accounts data length, or a + `TransactionError`, if any of instructions failed to execute + correctly. + 7. Verify transaction accounts' `RentState` changes (`verify_changes` function) + - If the account `RentState` post-transaction processing is rent exempt or uninitialized, the verification will pass, regardless of the pre-transaction `RentState`. + - If the account `RentState` pre-transaction is rent paying: + - It may remain rent paying only if its size has not changed and its balance has not increased. + - If the account `RentState` pre-transaction is rent exempt or uninitialized: + - It cannot become rent paying. + 8. Extract log messages. + 9. Extract inner instructions (`Vec>`). + 10. Extract `ExecutionRecord` components from transaction context. + 11. Check balances of accounts to match the sum of balances before + transaction execution. + 12. Update loaded transaction accounts to new accounts. + 13. Extract changes in accounts data sizes + 14. Extract return data + 15. Return `TransactionExecutionResult` with wrapping the extracted + information in `TransactionExecutionDetails`. + +4. Prepare the results of loading and executing transactions. + + This includes the following steps for each transactions + 1. Dump flattened result to info log for an account whose pubkey is + in the transaction's debug keys. + 2. Collect logs of the transaction execution for each executed + transaction, unless Bank's `transaction_log_collector_config` is + set to `None`. + 3. Finally, increment various statistical counters, and update + timings passed as a mutable reference to + `load_and_execute_transactions` in arguments. The counters are + packed in the struct `LoadAndExecuteTransactionsOutput`. diff --git a/solana/svm/src/account_loader.rs b/solana/svm/src/account_loader.rs new file mode 100644 index 0000000..a0db387 --- /dev/null +++ b/solana/svm/src/account_loader.rs @@ -0,0 +1,2591 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::{field_qualifiers, qualifiers}; +use { + crate::{ + account_overrides::AccountOverrides, + rent_calculator::{ + RENT_EXEMPT_RENT_EPOCH, check_rent_state_with_account, get_account_rent_state, + }, + rollback_accounts::RollbackAccounts, + transaction_error_metrics::TransactionErrorMetrics, + }, + ahash::{AHashMap, AHashSet}, + solana_account::{ + Account, AccountSharedData, ReadableAccount, WritableAccount, state_traits::StateMut, + }, + solana_clock::Slot, + solana_fee_structure::FeeDetails, + solana_instruction::{BorrowedAccountMeta, BorrowedInstruction}, + solana_instructions_sysvar::construct_instructions_data, + solana_loader_v3_interface::state::UpgradeableLoaderState, + solana_nonce::state::State as NonceState, + solana_nonce_account::{SystemAccountKind, get_system_account_kind}, + solana_program_runtime::execution_budget::{ + SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4, native_loader, + sysvar::{self, slot_history}, + }, + solana_svm_callback::{AccountState, TransactionProcessingCallback}, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_transaction::svm_message::SVMMessage, + solana_transaction_context::{IndexOfAccount, transaction_accounts::KeyedAccountSharedData}, + solana_transaction_error::{TransactionError, TransactionResult as Result}, +}; + +// Per SIMD-0186, all accounts are assigned a base size of 64 bytes to cover +// the storage cost of metadata. +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +pub(crate) const TRANSACTION_ACCOUNT_BASE_SIZE: usize = 64; + +// Valid program owners (loaders). +pub const PROGRAM_OWNERS: &[Pubkey] = &[ + bpf_loader_upgradeable::id(), + bpf_loader::id(), + bpf_loader_deprecated::id(), + loader_v4::id(), +]; + +// Per SIMD-0186, resolved address lookup tables are assigned a base size of 8248 +// bytes: 8192 bytes for the maximum table size plus 56 bytes for metadata. +const ADDRESS_LOOKUP_TABLE_BASE_SIZE: usize = 8248; + +// for the load instructions +pub type TransactionCheckResult = Result; +type TransactionValidationResult = Result; + +#[derive(PartialEq, Eq, Debug)] +pub(crate) enum TransactionLoadResult { + /// All transaction accounts were loaded successfully + Loaded(LoadedTransaction), + /// Some transaction accounts needed for execution were unable to be loaded + /// but the fee payer and any nonce account needed for fee collection were + /// loaded successfully + FeesOnly(FeesOnlyTransaction), + /// Some transaction accounts needed for fee collection were unable to be + /// loaded + NotLoaded(TransactionError), +} + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr( + feature = "svm-internal", + qualifier_attr::field_qualifiers(nonce_address(pub)) +)] +pub struct CheckedTransactionDetails { + pub(crate) nonce_address: Option, + pub(crate) compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for CheckedTransactionDetails { + fn default() -> Self { + Self { + nonce_address: None, + compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits { + budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size_limit: 32, + fee_details: FeeDetails::default(), + }, + } + } +} + +impl CheckedTransactionDetails { + pub fn new( + nonce_address: Option, + compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, + ) -> Self { + Self { + nonce_address, + compute_budget_and_limits, + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub(crate) struct ValidatedTransactionDetails { + pub(crate) rollback_accounts: RollbackAccounts, + pub(crate) compute_budget: SVMTransactionExecutionBudget, + pub(crate) loaded_accounts_bytes_limit: u32, + pub(crate) fee_details: FeeDetails, + pub(crate) loaded_fee_payer_account: LoadedTransactionAccount, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for ValidatedTransactionDetails { + fn default() -> Self { + Self { + rollback_accounts: RollbackAccounts::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_bytes_limit: + solana_program_runtime::execution_budget::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(), + fee_details: FeeDetails::default(), + loaded_fee_payer_account: LoadedTransactionAccount::default(), + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] +pub(crate) struct LoadedTransactionAccount { + pub(crate) account: AccountSharedData, + pub(crate) loaded_size: usize, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +#[cfg_attr(feature = "dev-context-only-utils", derive(Default))] +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(compute_budget(pub)) +)] +pub struct LoadedTransaction { + pub accounts: Vec, + pub fee_details: FeeDetails, + pub rollback_accounts: RollbackAccounts, + pub(crate) compute_budget: SVMTransactionExecutionBudget, + pub loaded_accounts_data_size: u32, +} + +#[derive(PartialEq, Eq, Debug, Clone)] +pub struct FeesOnlyTransaction { + pub load_error: TransactionError, + pub rollback_accounts: RollbackAccounts, + pub fee_details: FeeDetails, + pub loaded_accounts_data_size: u32, +} + +// This is an internal SVM type that tracks account changes throughout a +// transaction batch and obviates the need to load accounts from accounts-db +// more than once. It effectively wraps an `impl TransactionProcessingCallback` +// type, and itself implements `TransactionProcessingCallback`, behaving +// exactly like the implementor of the trait, but also returning up-to-date +// account states mid-batch. +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +pub(crate) struct AccountLoader<'a, CB: TransactionProcessingCallback> { + loaded_accounts: AHashMap, + callbacks: &'a CB, + pub(crate) feature_set: &'a SVMFeatureSet, +} + +impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> { + // create a new AccountLoader for the transaction batch + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + pub(crate) fn new_with_loaded_accounts_capacity( + account_overrides: Option<&'a AccountOverrides>, + callbacks: &'a CB, + feature_set: &'a SVMFeatureSet, + capacity: usize, + ) -> AccountLoader<'a, CB> { + let mut loaded_accounts = AHashMap::with_capacity(capacity); + + // SlotHistory may be overridden for simulation. + // No other uses of AccountOverrides are expected. + if let Some(slot_history) = + account_overrides.and_then(|overrides| overrides.get(&slot_history::id())) + { + loaded_accounts.insert(slot_history::id(), (slot_history.clone(), 0)); + } + + Self { + loaded_accounts, + callbacks, + feature_set, + } + } + + // Load an account either from our own store or accounts-db and inspect it on behalf of Bank. + // Inspection is required prior to any modifications to the account. This function is used + // by load_transaction() and validate_transaction_fee_payer() for that purpose. It returns + // a different type than other AccountLoader load functions, which should prevent accidental + // mix and match of them. + pub(crate) fn load_transaction_account( + &mut self, + account_key: &Pubkey, + is_writable: bool, + ) -> Option { + let account = self.load_account(account_key); + + // Inspect prior to collecting rent, since rent collection can modify + // the account. + // + // Note that though rent collection is disabled, we still set the rent + // epoch of rent exempt if the account is rent-exempt but its rent epoch + // is not set to u64::MAX. In other words, an account can be updated + // during rent collection. Therefore, we must inspect prior to collecting rent. + self.callbacks.inspect_account( + account_key, + if let Some(ref account) = account { + AccountState::Alive(account) + } else { + AccountState::Dead + }, + is_writable, + ); + + account.map(|account| LoadedTransactionAccount { + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE.saturating_add(account.data().len()), + account, + }) + } + + // Load an account as above, with no inspection and no LoadedTransactionAccount wrapper. + // This is a general purpose function suitable for usage outside initial transaction loading. + pub(crate) fn load_account(&mut self, account_key: &Pubkey) -> Option { + match self.do_load(account_key) { + // Exists, from AccountLoader. + (Some((account, _last_modification_slot)), false) => Some(account), + // Not allocated, but has an AccountLoader placeholder already. + (None, false) => None, + // Exists in accounts-db. Store it in AccountLoader for future loads. + (Some((account, last_modification_slot)), true) => { + self.loaded_accounts + .insert(*account_key, (account.clone(), last_modification_slot)); + Some(account) + } + // Does not exist and has never been seen. + (None, true) => { + self.loaded_accounts + .insert(*account_key, (AccountSharedData::default(), 0)); + None + } + } + } + + // Internal helper for core loading logic to prevent code duplication. Returns a bool + // indicating whether an accounts-db lookup was performed, which allows wrappers with + // &mut self to insert the account. Wrappers with &self ignore it. + fn do_load(&self, account_key: &Pubkey) -> (Option<(AccountSharedData, Slot)>, bool) { + if let Some((account, slot)) = self.loaded_accounts.get(account_key) { + // If lamports is 0, a previous transaction deallocated this account. + // We return None instead of the account we found so it can be created fresh. + // We *never* remove accounts, or else we would fetch stale state from accounts-db. + let option_account = if account.lamports() == 0 { + None + } else { + Some((account.clone(), *slot)) + }; + + (option_account, false) + } else if let Some((account, slot)) = self.callbacks.get_account_shared_data(account_key) { + (Some((account, slot)), true) + } else { + (None, true) + } + } + + pub(crate) fn update_accounts_for_failed_tx( + &mut self, + rollback_accounts: &RollbackAccounts, + current_slot: Slot, + ) { + for (account_address, account) in rollback_accounts { + self.loaded_accounts + .insert(*account_address, (account.clone(), current_slot)); + } + } + + pub(crate) fn update_accounts_for_successful_tx( + &mut self, + message: &impl SVMMessage, + transaction_accounts: &[KeyedAccountSharedData], + current_slot: Slot, + ) { + for (i, (address, account)) in (0..message.account_keys().len()).zip(transaction_accounts) { + if !message.is_writable(i) { + continue; + } + + // Accounts that are invoked and also not passed as an instruction + // account to a program don't need to be stored because it's assumed + // to be impossible for a committable transaction to modify an + // invoked account if said account isn't passed to some program. + if message.is_invoked(i) && !message.is_instruction_account(i) { + continue; + } + + self.loaded_accounts + .insert(*address, (account.clone(), current_slot)); + } + } +} + +// Program loaders and parsers require a type that impls TransactionProcessingCallback, +// because they are used in both SVM and by Bank. We impl it, with the consequence +// that if we fall back to accounts-db, we cannot store the state for future loads. +// In practice, all accounts we load this way will already be in our accounts store. +impl TransactionProcessingCallback for AccountLoader<'_, CB> { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.do_load(pubkey).0 + } +} + +// NOTE this is a required subtrait of TransactionProcessingCallback. +// It may make sense to break out a second subtrait just for the above two functions, +// but this would be a nontrivial breaking change and require careful consideration. +impl solana_svm_callback::InvokeContextCallback + for AccountLoader<'_, CB> +{ +} + +/// Set the rent epoch to u64::MAX if the account is rent exempt. +/// +/// TODO: This function is used to update the rent epoch of an account. Once we +/// completely switched to lthash, where rent_epoch is ignored in accounts +/// hashing, we can remove this function. +pub fn update_rent_exempt_status_for_account(rent: &Rent, account: &mut AccountSharedData) { + // Now that rent fee collection is disabled, we won't collect rent for any + // account. If there are any rent paying accounts, their `rent_epoch` won't + // change either. However, if the account itself is rent-exempted but its + // `rent_epoch` is not u64::MAX, we will set its `rent_epoch` to u64::MAX. + // In such case, the behavior stays the same as before. + if account.rent_epoch() != RENT_EXEMPT_RENT_EPOCH + && rent.is_exempt(account.lamports(), account.data().len()) + { + account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + } +} + +/// Check whether the payer_account is capable of paying the fee. The +/// side effect is to subtract the fee amount from the payer_account +/// balance of lamports. If the payer_account is not able to pay the +/// fee, the error_metrics is incremented, and a specific error is +/// returned. +pub fn validate_fee_payer( + payer_address: &Pubkey, + payer_account: &mut AccountSharedData, + payer_index: IndexOfAccount, + error_metrics: &mut TransactionErrorMetrics, + rent: &Rent, + fee: u64, +) -> Result<()> { + if payer_account.lamports() == 0 { + error_metrics.account_not_found += 1; + return Err(TransactionError::AccountNotFound); + } + let system_account_kind = get_system_account_kind(payer_account).ok_or_else(|| { + error_metrics.invalid_account_for_fee += 1; + TransactionError::InvalidAccountForFee + })?; + let min_balance = match system_account_kind { + SystemAccountKind::System => 0, + SystemAccountKind::Nonce => { + // Should we ever allow a fees charge to zero a nonce account's + // balance. The state MUST be set to uninitialized in that case + rent.minimum_balance(NonceState::size()) + } + }; + + payer_account + .lamports() + .checked_sub(min_balance) + .and_then(|v| v.checked_sub(fee)) + .ok_or_else(|| { + error_metrics.insufficient_funds += 1; + TransactionError::InsufficientFundsForFee + })?; + + let payer_pre_rent_state = + get_account_rent_state(rent, payer_account.lamports(), payer_account.data().len()); + payer_account + .checked_sub_lamports(fee) + .map_err(|_| TransactionError::InsufficientFundsForFee)?; + + let payer_post_rent_state = + get_account_rent_state(rent, payer_account.lamports(), payer_account.data().len()); + check_rent_state_with_account( + &payer_pre_rent_state, + &payer_post_rent_state, + payer_address, + payer_index, + ) +} + +pub(crate) fn load_transaction( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + validation_result: TransactionValidationResult, + error_metrics: &mut TransactionErrorMetrics, + rent: &Rent, +) -> TransactionLoadResult { + match validation_result { + Err(e) => TransactionLoadResult::NotLoaded(e), + Ok(tx_details) => { + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(tx_details.loaded_accounts_bytes_limit); + + let load_result = load_transaction_accounts( + account_loader, + message, + tx_details.loaded_fee_payer_account, + &mut loaded_transaction_data_size, + error_metrics, + rent, + ); + + match load_result { + Ok(accounts) => TransactionLoadResult::Loaded(LoadedTransaction { + accounts, + fee_details: tx_details.fee_details, + rollback_accounts: tx_details.rollback_accounts, + compute_budget: tx_details.compute_budget, + loaded_accounts_data_size: loaded_transaction_data_size.into(), + }), + Err(err) => TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: err, + fee_details: tx_details.fee_details, + loaded_accounts_data_size: if account_loader + .feature_set + .define_ltds_fee_only_semantics + { + loaded_transaction_data_size.into() + } else { + tx_details.rollback_accounts.data_size() as u32 + }, + rollback_accounts: tx_details.rollback_accounts, + }), + } + } + } +} + +#[derive(PartialEq, Eq, Debug, Clone)] +struct LoadedTransactionDataSize { + loaded_accounts_data_size: u32, + requested_loaded_accounts_data_size_limit: u32, +} + +impl LoadedTransactionDataSize { + fn with_max_size(requested_loaded_accounts_data_size_limit: u32) -> Self { + Self { + loaded_accounts_data_size: 0, + requested_loaded_accounts_data_size_limit, + } + } + + fn increase_calculated_data_size( + &mut self, + data_size_delta: usize, + error_metrics: &mut TransactionErrorMetrics, + ) -> Result<()> { + // this branch is unreachable in practice (though not by construction), + // since it would imply an account >4gb in size + let Ok(data_size_delta) = u32::try_from(data_size_delta) else { + self.loaded_accounts_data_size = u32::MAX; + error_metrics.max_loaded_accounts_data_size_exceeded += 1; + return Err(TransactionError::MaxLoadedAccountsDataSizeExceeded); + }; + + self.loaded_accounts_data_size = self + .loaded_accounts_data_size + .saturating_add(data_size_delta); + + if self.loaded_accounts_data_size > self.requested_loaded_accounts_data_size_limit { + error_metrics.max_loaded_accounts_data_size_exceeded += 1; + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + } else { + Ok(()) + } + } +} + +impl From for u32 { + fn from(value: LoadedTransactionDataSize) -> Self { + value + .loaded_accounts_data_size + .min(value.requested_loaded_accounts_data_size_limit) + } +} + +fn load_transaction_accounts( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + loaded_fee_payer_account: LoadedTransactionAccount, + loaded_tx_data_size: &mut LoadedTransactionDataSize, + error_metrics: &mut TransactionErrorMetrics, + rent: &Rent, +) -> Result> { + let account_keys = message.account_keys(); + let mut loaded_transaction_accounts = Vec::with_capacity(account_keys.len()); + let mut additional_loaded_accounts: AHashSet = AHashSet::new(); + + // Transactions pay a base fee per address lookup table. + loaded_tx_data_size.increase_calculated_data_size( + message + .num_lookup_tables() + .saturating_mul(ADDRESS_LOOKUP_TABLE_BASE_SIZE), + error_metrics, + )?; + + let mut collect_loaded_account = + |account_loader: &mut AccountLoader, key: &Pubkey, loaded_account| -> Result<()> { + let LoadedTransactionAccount { + account, + loaded_size, + } = loaded_account; + + loaded_tx_data_size.increase_calculated_data_size(loaded_size, error_metrics)?; + + // This has been annotated branch-by-branch because collapsing the logic is infeasible. + // Its purpose is to ensure programdata accounts are counted once and *only* once per + // transaction. By checking account_keys, we never double-count a programdata account + // that was explicitly included in the transaction. We also use a hashset to gracefully + // handle cases that LoaderV3 presumably makes impossible, such as self-referential + // program accounts or multiply-referenced programdata accounts, for added safety. + // + // If in the future LoaderV3 programs are migrated to LoaderV4, this entire code block + // can be deleted. + // + // If this is a valid LoaderV3 program... + if bpf_loader_upgradeable::check_id(account.owner()) + && let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = account.state() + { + // ...its programdata was not already counted and will not later be counted... + if !account_keys.iter().any(|key| programdata_address == *key) + && !additional_loaded_accounts.contains(&programdata_address) + { + // ...and the programdata account exists (if it doesn't, it is *not* a load failure)... + if let Some(programdata_account) = + account_loader.load_account(&programdata_address) + { + // ...count programdata toward this transaction's total size. + loaded_tx_data_size.increase_calculated_data_size( + TRANSACTION_ACCOUNT_BASE_SIZE + .saturating_add(programdata_account.data().len()), + error_metrics, + )?; + additional_loaded_accounts.insert(programdata_address); + } + } + } + + loaded_transaction_accounts.push((*key, account)); + + Ok(()) + }; + + // Since the fee payer is always the first account, collect it first. + // We can use it directly because it was already loaded during validation. + collect_loaded_account( + account_loader, + message.fee_payer(), + loaded_fee_payer_account, + )?; + + // Attempt to load and collect remaining non-fee payer accounts. + for (account_index, account_key) in account_keys.iter().enumerate().skip(1) { + let loaded_account = + load_transaction_account(account_loader, message, account_key, account_index, rent); + collect_loaded_account(account_loader, account_key, loaded_account)?; + } + + for (program_id, _) in message.program_instructions_iter() { + let Some(program_account) = account_loader.load_account(program_id) else { + error_metrics.account_not_found += 1; + return Err(TransactionError::ProgramAccountNotFound); + }; + + let owner_id = program_account.owner(); + if !native_loader::check_id(owner_id) && !PROGRAM_OWNERS.contains(owner_id) { + error_metrics.invalid_program_for_execution += 1; + return Err(TransactionError::InvalidProgramForExecution); + } + } + + Ok(loaded_transaction_accounts) +} + +fn load_transaction_account( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + account_key: &Pubkey, + account_index: usize, + rent: &Rent, +) -> LoadedTransactionAccount { + let is_writable = message.is_writable(account_index); + if solana_sdk_ids::sysvar::instructions::check_id(account_key) { + // Since the instructions sysvar is constructed by the SVM and modified + // for each transaction instruction, it cannot be loaded. + LoadedTransactionAccount { + loaded_size: 0, + account: construct_instructions_account(message), + } + } else if let Some(mut loaded_account) = + account_loader.load_transaction_account(account_key, is_writable) + { + if is_writable { + update_rent_exempt_status_for_account(rent, &mut loaded_account.account); + } + loaded_account + } else { + let mut default_account = AccountSharedData::default(); + default_account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + LoadedTransactionAccount { + loaded_size: default_account.data().len(), + account: default_account, + } + } +} + +fn construct_instructions_account(message: &impl SVMMessage) -> AccountSharedData { + let account_keys = message.account_keys(); + let mut decompiled_instructions = Vec::with_capacity(message.num_instructions()); + for (program_id, instruction) in message.program_instructions_iter() { + let accounts = instruction + .accounts + .iter() + .map(|account_index| { + let account_index = usize::from(*account_index); + BorrowedAccountMeta { + is_signer: message.is_signer(account_index), + is_writable: message.is_writable(account_index), + pubkey: account_keys.get(account_index).unwrap(), + } + }) + .collect(); + + decompiled_instructions.push(BorrowedInstruction { + accounts, + data: instruction.data, + program_id, + }); + } + + AccountSharedData::from(Account { + data: construct_instructions_data(&decompiled_instructions), + owner: sysvar::id(), + ..Account::default() + }) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::transaction_account_state_info::TransactionAccountStateInfo, + rand::prelude::*, + solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction}, + solana_keypair::Keypair, + solana_loader_v3_interface::state::UpgradeableLoaderState, + solana_message::{ + LegacyMessage, Message, MessageHeader, SanitizedMessage, + compiled_instruction::CompiledInstruction, + v0::{LoadedAddresses, LoadedMessage}, + }, + solana_native_token::LAMPORTS_PER_SOL, + solana_nonce::{self as nonce, versions::Versions as NonceVersions}, + solana_program_runtime::execution_budget::{ + DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT, MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{ + bpf_loader, bpf_loader_upgradeable, native_loader, system_program, sysvar, + }, + solana_signature::Signature, + solana_signer::Signer, + solana_svm_callback::{InvokeContextCallback, TransactionProcessingCallback}, + solana_system_transaction::transfer, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::{ + transaction::TransactionContext, transaction_accounts::KeyedAccountSharedData, + }, + solana_transaction_error::{TransactionError, TransactionResult as Result}, + std::{ + borrow::Cow, + cell::RefCell, + collections::{HashMap, HashSet}, + sync::Arc, + }, + }; + + fn setup_test_logger() { + let _ = env_logger::Builder::from_env(env_logger::Env::new().default_filter_or("error")) + .format_timestamp_nanos() + .is_test(true) + .try_init(); + } + + #[derive(Clone)] + struct TestCallbacks { + accounts_map: HashMap, + #[allow(clippy::type_complexity)] + inspected_accounts: + RefCell, /* is_writable */ bool)>>>, + feature_set: SVMFeatureSet, + } + + impl Default for TestCallbacks { + fn default() -> Self { + Self { + accounts_map: HashMap::default(), + inspected_accounts: RefCell::default(), + feature_set: SVMFeatureSet::all_enabled(), + } + } + } + + impl InvokeContextCallback for TestCallbacks {} + + impl TransactionProcessingCallback for TestCallbacks { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.accounts_map + .get(pubkey) + .map(|(account, slot)| (account.clone(), *slot)) + } + + fn inspect_account( + &self, + address: &Pubkey, + account_state: AccountState, + is_writable: bool, + ) { + let account = match account_state { + AccountState::Dead => None, + AccountState::Alive(account) => Some(account.clone()), + }; + self.inspected_accounts + .borrow_mut() + .entry(*address) + .or_default() + .push((account, is_writable)); + } + } + + impl<'a> From<&'a TestCallbacks> for AccountLoader<'a, TestCallbacks> { + fn from(callbacks: &'a TestCallbacks) -> AccountLoader<'a, TestCallbacks> { + AccountLoader::new_with_loaded_accounts_capacity( + None, + callbacks, + &callbacks.feature_set, + 0, + ) + } + } + + fn load_accounts_with_features_and_rent( + tx: Transaction, + accounts: &[KeyedAccountSharedData], + rent: &Rent, + error_metrics: &mut TransactionErrorMetrics, + feature_set: SVMFeatureSet, + ) -> TransactionLoadResult { + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + let fee_payer_account = accounts[0].1.clone(); + let mut accounts_map = HashMap::new(); + for (pubkey, account) in accounts { + accounts_map.insert(*pubkey, (account.clone(), 1)); + } + let callbacks = TestCallbacks { + accounts_map, + ..Default::default() + }; + let mut account_loader: AccountLoader = (&callbacks).into(); + account_loader.feature_set = &feature_set; + load_transaction( + &mut account_loader, + &sanitized_tx, + Ok(ValidatedTransactionDetails { + loaded_fee_payer_account: LoadedTransactionAccount { + account: fee_payer_account, + ..LoadedTransactionAccount::default() + }, + ..ValidatedTransactionDetails::default() + }), + error_metrics, + rent, + ) + } + + fn new_unchecked_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) + } + + #[test] + fn test_load_accounts_unknown_program_id() { + let mut accounts: Vec = Vec::new(); + let mut error_metrics = TransactionErrorMetrics::default(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let account = AccountSharedData::new(2, 1, &Pubkey::default()); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![Pubkey::default()], + instructions, + ); + + let feature_set = SVMFeatureSet::all_enabled(); + let load_results = load_accounts_with_features_and_rent( + tx, + &accounts, + &Rent::default(), + &mut error_metrics, + feature_set, + ); + + assert_eq!(error_metrics.account_not_found.0, 1); + assert!(matches!( + load_results, + TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: TransactionError::ProgramAccountNotFound, + .. + }), + )); + } + + #[test] + fn test_load_accounts_no_loaders() { + let mut accounts: Vec = Vec::new(); + let mut error_metrics = TransactionErrorMetrics::default(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let mut account = AccountSharedData::new(1, 0, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(2, 1, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(2, &(), vec![0, 1])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[key1], + Hash::default(), + vec![native_loader::id()], + instructions, + ); + + let feature_set = SVMFeatureSet::all_enabled(); + let loaded_accounts = load_accounts_with_features_and_rent( + tx, + &accounts, + &Rent::default(), + &mut error_metrics, + feature_set, + ); + + match &loaded_accounts { + TransactionLoadResult::FeesOnly(fees_only_tx) => { + assert_eq!(error_metrics.account_not_found.0, 1); + assert_eq!( + fees_only_tx.load_error, + TransactionError::ProgramAccountNotFound, + ); + } + result => panic!("unexpected result: {result:?}"), + } + } + + #[test] + fn test_load_accounts_bad_owner() { + let mut accounts: Vec = Vec::new(); + let mut error_metrics = TransactionErrorMetrics::default(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(40, 1, &Pubkey::default()); + account.set_executable(true); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1], + instructions, + ); + + let feature_set = SVMFeatureSet::all_enabled(); + let load_results = load_accounts_with_features_and_rent( + tx, + &accounts, + &Rent::default(), + &mut error_metrics, + feature_set, + ); + + assert_eq!(error_metrics.invalid_program_for_execution.0, 1); + assert!(matches!( + load_results, + TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: TransactionError::InvalidProgramForExecution, + .. + }), + )); + } + + #[test] + fn test_load_accounts_not_executable() { + let mut accounts: Vec = Vec::new(); + let mut error_metrics = TransactionErrorMetrics::default(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = Pubkey::from([5u8; 32]); + + let account = AccountSharedData::new(1, 0, &Pubkey::default()); + accounts.push((key0, account)); + + let account = AccountSharedData::new(40, 0, &native_loader::id()); + accounts.push((key1, account)); + + let instructions = vec![CompiledInstruction::new(1, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1], + instructions, + ); + + let feature_set = SVMFeatureSet::all_enabled(); + let load_results = load_accounts_with_features_and_rent( + tx, + &accounts, + &Rent::default(), + &mut error_metrics, + feature_set, + ); + + assert_eq!(error_metrics.invalid_program_for_execution.0, 0); + match &load_results { + TransactionLoadResult::Loaded(loaded_transaction) => { + assert_eq!(loaded_transaction.accounts.len(), 2); + assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); + assert_eq!(loaded_transaction.accounts[1].1, accounts[1].1); + } + TransactionLoadResult::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), + TransactionLoadResult::NotLoaded(e) => panic!("{e}"), + } + } + + #[test] + fn test_load_accounts_multiple_loaders() { + let mut accounts: Vec = Vec::new(); + let mut error_metrics = TransactionErrorMetrics::default(); + + let keypair = Keypair::new(); + let key0 = keypair.pubkey(); + let key1 = bpf_loader_upgradeable::id(); + let key2 = Pubkey::from([6u8; 32]); + + let mut account = AccountSharedData::new(1, 0, &Pubkey::default()); + account.set_rent_epoch(1); + accounts.push((key0, account)); + + let mut account = AccountSharedData::new(40, 1, &Pubkey::default()); + account.set_executable(true); + account.set_rent_epoch(1); + account.set_owner(native_loader::id()); + accounts.push((key1, account)); + + let mut account = AccountSharedData::new(41, 1, &Pubkey::default()); + account.set_executable(true); + account.set_rent_epoch(1); + account.set_owner(key1); + accounts.push((key2, account)); + + let instructions = vec![ + CompiledInstruction::new(1, &(), vec![0]), + CompiledInstruction::new(2, &(), vec![0]), + ]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[], + Hash::default(), + vec![key1, key2], + instructions, + ); + + let feature_set = SVMFeatureSet::all_enabled(); + let loaded_accounts = load_accounts_with_features_and_rent( + tx, + &accounts, + &Rent::default(), + &mut error_metrics, + feature_set, + ); + + assert_eq!(error_metrics.account_not_found.0, 0); + match &loaded_accounts { + TransactionLoadResult::Loaded(loaded_transaction) => { + assert_eq!(loaded_transaction.accounts.len(), 3); + assert_eq!(loaded_transaction.accounts[0].1, accounts[0].1); + } + TransactionLoadResult::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), + TransactionLoadResult::NotLoaded(e) => panic!("{e}"), + } + } + + fn load_accounts_no_store( + accounts: &[KeyedAccountSharedData], + tx: Transaction, + account_overrides: Option<&AccountOverrides>, + ) -> TransactionLoadResult { + let tx = SanitizedTransaction::from_transaction_for_tests(tx); + + let mut error_metrics = TransactionErrorMetrics::default(); + let mut accounts_map = HashMap::new(); + for (pubkey, account) in accounts { + accounts_map.insert(*pubkey, (account.clone(), 1)); + } + let callbacks = TestCallbacks { + accounts_map, + ..Default::default() + }; + let feature_set = SVMFeatureSet::all_enabled(); + let mut account_loader = AccountLoader::new_with_loaded_accounts_capacity( + account_overrides, + &callbacks, + &feature_set, + 0, + ); + load_transaction( + &mut account_loader, + &tx, + Ok(ValidatedTransactionDetails::default()), + &mut error_metrics, + &Rent::default(), + ) + } + + #[test] + fn test_instructions() { + setup_test_logger(); + let instructions_key = solana_sdk_ids::sysvar::instructions::id(); + let keypair = Keypair::new(); + let instructions = vec![CompiledInstruction::new(1, &(), vec![0, 1])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[solana_pubkey::new_rand(), instructions_key], + Hash::default(), + vec![native_loader::id()], + instructions, + ); + + let load_results = load_accounts_no_store(&[], tx, None); + assert!(matches!( + load_results, + TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: TransactionError::ProgramAccountNotFound, + .. + }), + )); + } + + #[test] + fn test_overrides() { + setup_test_logger(); + let mut account_overrides = AccountOverrides::default(); + let slot_history_id = sysvar::slot_history::id(); + let account = AccountSharedData::new(42, 0, &Pubkey::default()); + account_overrides.set_slot_history(Some(account)); + + let keypair = Keypair::new(); + let account = AccountSharedData::new(1_000_000, 0, &Pubkey::default()); + + let mut program_account = AccountSharedData::default(); + program_account.set_lamports(1); + program_account.set_executable(true); + program_account.set_owner(native_loader::id()); + + let instructions = vec![CompiledInstruction::new(2, &(), vec![0])]; + let tx = Transaction::new_with_compiled_instructions( + &[&keypair], + &[slot_history_id], + Hash::default(), + vec![bpf_loader::id()], + instructions, + ); + + let loaded_accounts = load_accounts_no_store( + &[ + (keypair.pubkey(), account), + (bpf_loader::id(), program_account), + ], + tx, + Some(&account_overrides), + ); + match &loaded_accounts { + TransactionLoadResult::Loaded(loaded_transaction) => { + assert_eq!(loaded_transaction.accounts[0].0, keypair.pubkey()); + assert_eq!(loaded_transaction.accounts[1].0, slot_history_id); + assert_eq!(loaded_transaction.accounts[1].1.lamports(), 42); + } + TransactionLoadResult::FeesOnly(fees_only_tx) => panic!("{}", fees_only_tx.load_error), + TransactionLoadResult::NotLoaded(e) => panic!("{e}"), + } + } + + #[test] + fn test_increase_calculated_data_size() { + let mut error_metrics = TransactionErrorMetrics::default(); + let data_size: usize = 123; + let requested_data_size_limit = data_size as u32 + 1; + let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); + + // OK - loaded data size is under limit + assert!( + acc.increase_calculated_data_size(data_size, &mut error_metrics) + .is_ok() + ); + assert_eq!(data_size as u32, acc.clone().into()); + + // OK - loaded data size meets limit + assert!( + acc.increase_calculated_data_size(1, &mut error_metrics) + .is_ok() + ); + assert_eq!(requested_data_size_limit, acc.clone().into()); + + // fail - loading more data would exceed limit + // data size helper reports the limit only + assert_eq!( + acc.increase_calculated_data_size(1, &mut error_metrics), + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + ); + assert_eq!(requested_data_size_limit, acc.into()); + + let mut acc = LoadedTransactionDataSize::with_max_size(requested_data_size_limit); + + // fail - adding a huge number exceeds limit + // data size helper correctly reports we hit the limit + assert_eq!( + acc.increase_calculated_data_size(u32::MAX as usize + 1, &mut error_metrics), + Err(TransactionError::MaxLoadedAccountsDataSizeExceeded) + ); + assert_eq!(requested_data_size_limit, acc.into()); + } + + struct ValidateFeePayerTestParameter { + is_nonce: bool, + payer_init_balance: u64, + fee: u64, + expected_result: Result<()>, + payer_post_balance: u64, + } + fn validate_fee_payer_account(test_parameter: ValidateFeePayerTestParameter, rent: &Rent) { + let payer_account_keys = Keypair::new(); + let mut account = if test_parameter.is_nonce { + AccountSharedData::new_data( + test_parameter.payer_init_balance, + &NonceVersions::new(NonceState::Initialized(nonce::state::Data::default())), + &system_program::id(), + ) + .unwrap() + } else { + AccountSharedData::new(test_parameter.payer_init_balance, 0, &system_program::id()) + }; + let result = validate_fee_payer( + &payer_account_keys.pubkey(), + &mut account, + 0, + &mut TransactionErrorMetrics::default(), + rent, + test_parameter.fee, + ); + + assert_eq!(result, test_parameter.expected_result); + assert_eq!(account.lamports(), test_parameter.payer_post_balance); + } + + #[test] + fn test_validate_fee_payer() { + let rent = Rent { + lamports_per_byte: 1, + ..Rent::default() + }; + let min_balance = rent.minimum_balance(NonceState::size()); + let fee = 5_000; + + // If payer account has sufficient balance, expect successful fee deduction, + // regardless feature gate status, or if payer is nonce account. + { + for (is_nonce, min_balance) in [(true, min_balance), (false, 0)] { + validate_fee_payer_account( + ValidateFeePayerTestParameter { + is_nonce, + payer_init_balance: min_balance + fee, + fee, + expected_result: Ok(()), + payer_post_balance: min_balance, + }, + &rent, + ); + } + } + + // If payer account has no balance, expected AccountNotFound Error + // regardless feature gate status, or if payer is nonce account. + { + for is_nonce in [true, false] { + validate_fee_payer_account( + ValidateFeePayerTestParameter { + is_nonce, + payer_init_balance: 0, + fee, + expected_result: Err(TransactionError::AccountNotFound), + payer_post_balance: 0, + }, + &rent, + ); + } + } + + // If payer account has insufficient balance, expect InsufficientFundsForFee error + // regardless feature gate status, or if payer is nonce account. + { + for (is_nonce, min_balance) in [(true, min_balance), (false, 0)] { + validate_fee_payer_account( + ValidateFeePayerTestParameter { + is_nonce, + payer_init_balance: min_balance + fee - 1, + fee, + expected_result: Err(TransactionError::InsufficientFundsForFee), + payer_post_balance: min_balance + fee - 1, + }, + &rent, + ); + } + } + + // normal payer account has balance of u64::MAX, so does fee; since it does not require + // min_balance, expect successful fee deduction, regardless of feature gate status + { + validate_fee_payer_account( + ValidateFeePayerTestParameter { + is_nonce: false, + payer_init_balance: u64::MAX, + fee: u64::MAX, + expected_result: Ok(()), + payer_post_balance: 0, + }, + &rent, + ); + } + } + + #[test] + fn test_validate_nonce_fee_payer_with_checked_arithmetic() { + let rent = Rent { + lamports_per_byte: 1, + ..Rent::default() + }; + + // nonce payer account has balance of u64::MAX, so does fee; due to nonce account + // requires additional min_balance, expect InsufficientFundsForFee error if feature gate is + // enabled + validate_fee_payer_account( + ValidateFeePayerTestParameter { + is_nonce: true, + payer_init_balance: u64::MAX, + fee: u64::MAX, + expected_result: Err(TransactionError::InsufficientFundsForFee), + payer_post_balance: u64::MAX, + }, + &rent, + ); + } + + #[test] + fn test_construct_instructions_account() { + let loaded_message = LoadedMessage { + message: Cow::Owned(solana_message::v0::Message::default()), + loaded_addresses: Cow::Owned(LoadedAddresses::default()), + is_writable_account_cache: vec![false], + }; + let message = SanitizedMessage::V0(loaded_message); + let shared_data = construct_instructions_account(&message); + let expected = AccountSharedData::from(Account { + data: construct_instructions_data(&message.decompile_instructions()), + owner: sysvar::id(), + ..Account::default() + }); + assert_eq!(shared_data, expected); + } + + #[test] + fn test_load_transaction_accounts_fee_payer() { + let fee_payer_address = Pubkey::new_unique(); + let message = Message { + account_keys: vec![fee_payer_address], + header: MessageHeader::default(), + instructions: vec![], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + + let fee_payer_balance = 200; + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(fee_payer_balance); + mock_bank + .accounts_map + .insert(fee_payer_address, (fee_payer_account.clone(), 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount { + loaded_size: fee_payer_account.data().len(), + account: fee_payer_account.clone(), + }, + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + assert_eq!( + vec![(fee_payer_address, fee_payer_account)], + result.unwrap(), + ); + assert_eq!(0, loaded_transaction_data_size.loaded_accounts_data_size); + } + + #[test] + fn test_load_transaction_accounts_native_loader() { + let key1 = Keypair::new(); + let message = Message { + account_keys: vec![key1.pubkey(), native_loader::id()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + mock_bank + .accounts_map + .insert(native_loader::id(), (AccountSharedData::default(), 0)); + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank + .accounts_map + .insert(key1.pubkey(), (fee_payer_account.clone(), 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + assert_eq!( + result.unwrap_err(), + TransactionError::ProgramAccountNotFound + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_no_data() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount::default(), + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_invalid_program_for_execution() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key1.pubkey(), key2.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![0, 1], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount::default(), + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_native_loader_owner() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(native_loader::id()); + account_data.set_lamports(1); + account_data.set_executable(true); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + assert_eq!( + vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + ], + result.unwrap(), + ); + assert_eq!( + expected_loaded_accounts_data_size, + loaded_transaction_data_size.loaded_accounts_data_size + ); + } + + #[test] + fn test_load_transaction_accounts_program_account_not_found_after_all_checks() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_executable(true); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (account_data, 1)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount::default(), + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_load_transaction_accounts_program_account_invalid_program_for_execution_last_check() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(key3.pubkey()); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (account_data, 1)); + mock_bank + .accounts_map + .insert(key3.pubkey(), (AccountSharedData::default(), 0)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount::default(), + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + assert_eq!( + result.err(), + Some(TransactionError::InvalidProgramForExecution) + ); + } + + #[test] + fn test_load_transaction_accounts_program_success_complete() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey()], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 1)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank + .accounts_map + .insert(bpf_loader::id(), (account_data, 0)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + assert_eq!( + vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + ], + result.unwrap(), + ); + assert_eq!( + expected_loaded_accounts_data_size, + loaded_transaction_data_size.loaded_accounts_data_size + ); + } + + #[test] + fn test_load_transaction_accounts_program_builtin_saturating_add() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 0)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank + .accounts_map + .insert(bpf_loader::id(), (account_data, 0)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + let result = load_transaction_accounts( + &mut account_loader, + sanitized_transaction.message(), + LoadedTransactionAccount { + account: fee_payer_account.clone(), + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + &mut loaded_transaction_data_size, + &mut error_metrics, + &Rent::default(), + ); + + let expected_loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + assert_eq!( + vec![ + (key2.pubkey(), fee_payer_account), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + (key3.pubkey(), account_data), + ], + result.unwrap(), + ); + assert_eq!( + expected_loaded_accounts_data_size, + loaded_transaction_data_size.loaded_accounts_data_size + ); + } + + #[test] + fn test_rent_state_list_len() { + let mint_keypair = Keypair::new(); + let mut bank = TestCallbacks::default(); + let recipient = Pubkey::new_unique(); + let last_block_hash = Hash::new_unique(); + + let mut system_data = AccountSharedData::default(); + system_data.set_lamports(1); + system_data.set_executable(true); + system_data.set_owner(native_loader::id()); + bank.accounts_map + .insert(Pubkey::new_from_array([0u8; 32]), (system_data, 0)); + + let mut mint_data = AccountSharedData::default(); + mint_data.set_lamports(2); + bank.accounts_map + .insert(mint_keypair.pubkey(), (mint_data, 0)); + bank.accounts_map + .insert(recipient, (AccountSharedData::default(), 1)); + let mut account_loader = (&bank).into(); + + let tx = transfer(&mint_keypair, &recipient, LAMPORTS_PER_SOL, last_block_hash); + let num_accounts = tx.message().account_keys.len(); + let sanitized_tx = SanitizedTransaction::from_transaction_for_tests(tx); + let mut error_metrics = TransactionErrorMetrics::default(); + let load_result = load_transaction( + &mut account_loader, + &sanitized_tx, + Ok(ValidatedTransactionDetails::default()), + &mut error_metrics, + &Rent::default(), + ); + + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { + panic!("transaction loading failed"); + }; + + let compute_budget = SVMTransactionExecutionBudget { + compute_unit_limit: u64::from(DEFAULT_INSTRUCTION_COMPUTE_UNIT_LIMIT), + ..SVMTransactionExecutionBudget::default() + }; + let rent = Rent::default(); + let transaction_context = TransactionContext::new( + loaded_transaction.accounts, + rent.clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + 1, + ); + + assert_eq!( + TransactionAccountStateInfo::new(&transaction_context, sanitized_tx.message(), &rent,) + .len(), + num_accounts, + ); + } + + #[test] + fn test_load_accounts_success() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut mock_bank = TestCallbacks::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(bpf_loader::id()); + mock_bank + .accounts_map + .insert(key1.pubkey(), (account_data, 0)); + + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(200); + mock_bank + .accounts_map + .insert(key2.pubkey(), (fee_payer_account.clone(), 1)); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(1); + account_data.set_executable(true); + account_data.set_owner(native_loader::id()); + mock_bank + .accounts_map + .insert(bpf_loader::id(), (account_data, 0)); + let mut account_loader = (&mock_bank).into(); + + let mut error_metrics = TransactionErrorMetrics::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let validation_result = Ok(ValidatedTransactionDetails { + loaded_fee_payer_account: LoadedTransactionAccount { + account: fee_payer_account, + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE, + }, + ..ValidatedTransactionDetails::default() + }); + + let load_result = load_transaction( + &mut account_loader, + &sanitized_transaction, + validation_result, + &mut error_metrics, + &Rent::default(), + ); + + let loaded_accounts_data_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32 * 2; + + let mut account_data = AccountSharedData::default(); + account_data.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + + let TransactionLoadResult::Loaded(loaded_transaction) = load_result else { + panic!("transaction loading failed"); + }; + assert_eq!( + loaded_transaction, + LoadedTransaction { + accounts: vec![ + ( + key2.pubkey(), + mock_bank.accounts_map[&key2.pubkey()].0.clone() + ), + ( + key1.pubkey(), + mock_bank.accounts_map[&key1.pubkey()].0.clone() + ), + (key3.pubkey(), account_data), + ], + fee_details: FeeDetails::default(), + rollback_accounts: RollbackAccounts::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size, + } + ); + } + + #[test] + fn test_load_accounts_error() { + let mock_bank = TestCallbacks::default(); + let mut account_loader = (&mock_bank).into(); + let rent = Rent::default(); + + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let validation_result = Ok(ValidatedTransactionDetails::default()); + let load_result = load_transaction( + &mut account_loader, + &sanitized_transaction, + validation_result, + &mut TransactionErrorMetrics::default(), + &rent, + ); + + assert!(matches!( + load_result, + TransactionLoadResult::FeesOnly(FeesOnlyTransaction { + load_error: TransactionError::ProgramAccountNotFound, + .. + }), + )); + + let validation_result = Err(TransactionError::InvalidWritableAccount); + + let load_result = load_transaction( + &mut account_loader, + &sanitized_transaction, + validation_result, + &mut TransactionErrorMetrics::default(), + &rent, + ); + + assert!(matches!( + load_result, + TransactionLoadResult::NotLoaded(TransactionError::InvalidWritableAccount), + )); + } + + #[test] + fn test_update_rent_exempt_status_for_account() { + let rent = Rent::default(); + + let min_exempt_balance = rent.minimum_balance(0); + let mut account = AccountSharedData::from(Account { + lamports: min_exempt_balance, + ..Account::default() + }); + + update_rent_exempt_status_for_account(&rent, &mut account); + assert_eq!(account.rent_epoch(), RENT_EXEMPT_RENT_EPOCH); + } + + #[test] + fn test_update_rent_exempt_status_for_rent_paying_account() { + let rent = Rent::default(); + + let mut account = AccountSharedData::from(Account { + lamports: 1, + ..Account::default() + }); + + update_rent_exempt_status_for_account(&rent, &mut account); + assert_eq!(account.rent_epoch(), 0); + assert_eq!(account.lamports(), 1); + } + + // Ensure `TransactionProcessingCallback::inspect_account()` is called when + // loading accounts for transaction processing. + #[test] + fn test_inspect_account_non_fee_payer() { + let mut mock_bank = TestCallbacks::default(); + + let address0 = Pubkey::new_unique(); // <-- fee payer + let address1 = Pubkey::new_unique(); // <-- initially alive + let address2 = Pubkey::new_unique(); // <-- initially dead + let address3 = Pubkey::new_unique(); // <-- program + + let mut account0 = AccountSharedData::default(); + account0.set_lamports(1_000_000_000); + mock_bank + .accounts_map + .insert(address0, (account0.clone(), 1)); + + let mut account1 = AccountSharedData::default(); + account1.set_lamports(2_000_000_000); + mock_bank + .accounts_map + .insert(address1, (account1.clone(), 1)); + + // account2 *not* added to the bank's accounts_map + + let mut account3 = AccountSharedData::default(); + account3.set_lamports(4_000_000_000); + account3.set_executable(true); + account3.set_owner(bpf_loader::id()); + mock_bank + .accounts_map + .insert(address3, (account3.clone(), 0)); + let mut account_loader = (&mock_bank).into(); + + let message = Message { + account_keys: vec![address0, address1, address2, address3], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 3, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 3, + accounts: vec![1, 2], + data: vec![], + }, + CompiledInstruction { + program_id_index: 3, + accounts: vec![1], + data: vec![], + }, + ], + recent_blockhash: Hash::new_unique(), + }; + let sanitized_message = new_unchecked_sanitized_message(message); + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + let validation_result = Ok(ValidatedTransactionDetails { + loaded_fee_payer_account: LoadedTransactionAccount { + account: account0.clone(), + ..LoadedTransactionAccount::default() + }, + ..ValidatedTransactionDetails::default() + }); + let _load_results = load_transaction( + &mut account_loader, + &sanitized_transaction, + validation_result, + &mut TransactionErrorMetrics::default(), + &Rent::default(), + ); + + // ensure the loaded accounts are inspected + let mut actual_inspected_accounts: Vec<_> = mock_bank + .inspected_accounts + .borrow() + .iter() + .map(|(k, v)| (*k, v.clone())) + .collect(); + actual_inspected_accounts.sort_unstable_by_key(|a| a.0); + + let mut expected_inspected_accounts = vec![ + // *not* key0, since it is loaded during fee payer validation + (address1, vec![(Some(account1), true)]), + (address2, vec![(None, true)]), + (address3, vec![(Some(account3), false)]), + ]; + expected_inspected_accounts.sort_unstable_by_key(|a| a.0); + + assert_eq!(actual_inspected_accounts, expected_inspected_accounts,); + } + + #[test] + fn test_account_loader_wrappers() { + let fee_payer = Pubkey::new_unique(); + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_rent_epoch(u64::MAX); + fee_payer_account.set_lamports(5000); + + let mut mock_bank = TestCallbacks::default(); + mock_bank + .accounts_map + .insert(fee_payer, (fee_payer_account.clone(), 1)); + + // test without stored account + let mut account_loader: AccountLoader<_> = (&mock_bank).into(); + assert_eq!( + account_loader + .load_transaction_account(&fee_payer, false) + .unwrap() + .account, + fee_payer_account + ); + + let mut account_loader: AccountLoader<_> = (&mock_bank).into(); + assert_eq!( + account_loader + .load_transaction_account(&fee_payer, true) + .unwrap() + .account, + fee_payer_account + ); + + let mut account_loader: AccountLoader<_> = (&mock_bank).into(); + assert_eq!( + account_loader.load_account(&fee_payer).unwrap(), + fee_payer_account + ); + + let account_loader: AccountLoader<_> = (&mock_bank).into(); + assert_eq!( + account_loader + .get_account_shared_data(&fee_payer) + .unwrap() + .0, + fee_payer_account + ); + + // test with stored account + let mut account_loader: AccountLoader<_> = (&mock_bank).into(); + account_loader.load_account(&fee_payer).unwrap(); + + assert_eq!( + account_loader + .load_transaction_account(&fee_payer, false) + .unwrap() + .account, + fee_payer_account + ); + assert_eq!( + account_loader + .load_transaction_account(&fee_payer, true) + .unwrap() + .account, + fee_payer_account + ); + assert_eq!( + account_loader.load_account(&fee_payer).unwrap(), + fee_payer_account + ); + assert_eq!( + account_loader + .get_account_shared_data(&fee_payer) + .unwrap() + .0, + fee_payer_account + ); + + // drop the account and ensure all deliver the updated state + fee_payer_account.set_lamports(0); + account_loader.update_accounts_for_failed_tx( + &RollbackAccounts::FeePayerOnly { + fee_payer: (fee_payer, fee_payer_account), + }, + 0, + ); + + assert_eq!( + account_loader.load_transaction_account(&fee_payer, false), + None + ); + assert_eq!( + account_loader.load_transaction_account(&fee_payer, true), + None + ); + assert_eq!(account_loader.load_account(&fee_payer), None); + assert_eq!(account_loader.get_account_shared_data(&fee_payer), None); + } + + // note all magic numbers (how many accounts, how many instructions, how big to size buffers) are arbitrary + // other than trying not to swamp programs with blank accounts and keep transaction size below the 64mb limit + #[test] + fn test_load_transaction_accounts_data_sizes() { + let mut rng = rand::rng(); + let mut mock_bank = TestCallbacks::default(); + + // arbitrary accounts + for _ in 0..128 { + let account = AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..128)]), + Pubkey::new_unique(), + rng.random(), + u64::MAX, + ); + mock_bank + .accounts_map + .insert(Pubkey::new_unique(), (account, 1)); + } + + // fee-payers + let mut fee_payers = vec![]; + for _ in 0..8 { + let fee_payer = Pubkey::new_unique(); + let account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(vec![0; rng.random_range(0..32)]), + system_program::id(), + rng.random(), + u64::MAX, + ); + mock_bank.accounts_map.insert(fee_payer, (account, 1)); + fee_payers.push(fee_payer); + } + + // programs + let mut loader_owned_accounts = vec![]; + let mut programdata_tracker = AHashMap::new(); + for loader in PROGRAM_OWNERS { + for _ in 0..16 { + let program_id = Pubkey::new_unique(); + let mut account = AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..512)]), + *loader, + rng.random(), + u64::MAX, + ); + + // give half loaderv3 accounts (if they're long enough) a valid programdata + // a quarter a dead pointer and a quarter nothing + // we set executable like a program because after the flag is disabled... + // ...programdata and buffer accounts can be used as program ids without aborting loading + // this will always fail at execution but we are merely testing the data size accounting here + if *loader == bpf_loader_upgradeable::id() && account.data().len() >= 64 { + let programdata_address = Pubkey::new_unique(); + let has_programdata = rng.random(); + + if has_programdata { + let programdata_account = + AccountSharedData::create_from_existing_shared_data( + 1, + Arc::new(vec![0; rng.random_range(0..512)]), + *loader, + rng.random(), + u64::MAX, + ); + programdata_tracker.insert( + program_id, + (programdata_address, programdata_account.data().len()), + ); + mock_bank + .accounts_map + .insert(programdata_address, (programdata_account, 1)); + loader_owned_accounts.push(programdata_address); + } + + if has_programdata || rng.random() { + account + .set_state(&UpgradeableLoaderState::Program { + programdata_address, + }) + .unwrap(); + } + } + + mock_bank.accounts_map.insert(program_id, (account, 1)); + loader_owned_accounts.push(program_id); + } + } + + let mut all_accounts = mock_bank.accounts_map.keys().copied().collect::>(); + + // append some to-be-created accounts + // this is to test that their size is 0 rather than 64 + for _ in 0..32 { + all_accounts.push(Pubkey::new_unique()); + } + + let mut account_loader = (&mock_bank).into(); + + // now generate arbitrary transactions using this accounts + // we ensure valid fee-payers and that all program ids are loader-owned + // otherwise any account can appear anywhere + // some edge cases we hope to hit (not necessarily all in every run): + // * programs used multiple times as program ids and/or normal accounts are counted once + // * loaderv3 programdata used explicitly zero one or multiple times is counted once + // * loaderv3 programs with missing programdata are allowed through + // * loaderv3 programdata used as program id does nothing weird + // * loaderv3 programdata used as a regular account does nothing weird + // * the programdata conditions hold regardless of ordering + for _ in 0..1024 { + let mut instructions = vec![]; + for _ in 0..rng.random_range(1..8) { + let mut accounts = vec![]; + for _ in 0..rng.random_range(1..16) { + all_accounts.shuffle(&mut rng); + let pubkey = all_accounts[0]; + + accounts.push(AccountMeta { + pubkey, + is_writable: rng.random(), + is_signer: rng.random() && rng.random(), + }); + } + + loader_owned_accounts.shuffle(&mut rng); + let program_id = loader_owned_accounts[0]; + instructions.push(Instruction { + accounts, + program_id, + data: vec![], + }); + } + + fee_payers.shuffle(&mut rng); + let fee_payer = fee_payers[0]; + let fee_payer_account = mock_bank.accounts_map.get(&fee_payer).cloned().unwrap().0; + + let transaction = SanitizedTransaction::from_transaction_for_tests( + Transaction::new_with_payer(&instructions, Some(&fee_payer)), + ); + + let mut expected_size = 0; + let mut counted_programdatas = transaction + .account_keys() + .iter() + .copied() + .collect::>(); + + for pubkey in transaction.account_keys().iter() { + if let Some((account, _last_modification_slot)) = mock_bank.accounts_map.get(pubkey) + { + expected_size += TRANSACTION_ACCOUNT_BASE_SIZE + account.data().len(); + }; + + if let Some((programdata_address, programdata_size)) = + programdata_tracker.get(pubkey) + && counted_programdatas.get(programdata_address).is_none() + { + expected_size += TRANSACTION_ACCOUNT_BASE_SIZE + programdata_size; + counted_programdatas.insert(*programdata_address); + } + } + + assert!(expected_size <= MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get() as usize); + + let mut loaded_transaction_data_size = + LoadedTransactionDataSize::with_max_size(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get()); + + load_transaction_accounts( + &mut account_loader, + &transaction, + LoadedTransactionAccount { + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), + account: fee_payer_account, + }, + &mut loaded_transaction_data_size, + &mut TransactionErrorMetrics::default(), + &Rent::default(), + ) + .unwrap(); + + assert_eq!( + loaded_transaction_data_size.loaded_accounts_data_size, + expected_size as u32, + ); + } + } + + #[test] + fn test_loader_aliasing() { + let mut mock_bank = TestCallbacks::default(); + + let hit_address = Pubkey::new_unique(); + let miss_address = Pubkey::new_unique(); + + let expected_hit_account = AccountSharedData::default(); + mock_bank + .accounts_map + .insert(hit_address, (expected_hit_account.clone(), 1)); + + let mut account_loader: AccountLoader<_> = (&mock_bank).into(); + + // load hits accounts-db, same account is stored + account_loader.load_account(&hit_address); + let actual_hit_account = account_loader.loaded_accounts.get(&hit_address); + + assert_eq!(actual_hit_account.as_ref().unwrap().0, expected_hit_account); + assert_eq!(actual_hit_account.as_ref().unwrap().1, 1); + assert!(Arc::ptr_eq( + &actual_hit_account.unwrap().0.data_clone(), + &expected_hit_account.data_clone() + )); + + // reload doesn't affect this + account_loader.load_account(&hit_address); + let actual_hit_account = account_loader.loaded_accounts.get(&hit_address); + + assert_eq!(actual_hit_account.as_ref().unwrap().0, expected_hit_account); + assert_eq!(actual_hit_account.as_ref().unwrap().1, 1); + assert!(Arc::ptr_eq( + &actual_hit_account.unwrap().0.data_clone(), + &expected_hit_account.data_clone() + )); + + // load misses accounts-db, placeholder is inserted + account_loader.load_account(&miss_address); + let expected_miss_account = account_loader + .loaded_accounts + .get(&miss_address) + .unwrap() + .clone(); + + assert!(!Arc::ptr_eq( + &expected_miss_account.0.data_clone(), + &expected_hit_account.data_clone() + )); + + // reload keeps the same placeholder + account_loader.load_account(&miss_address); + let actual_miss_account = account_loader.loaded_accounts.get(&miss_address); + + assert_eq!(actual_miss_account, Some(&expected_miss_account)); + assert!(Arc::ptr_eq( + &actual_miss_account.unwrap().0.data_clone(), + &expected_miss_account.0.data_clone() + )); + } +} diff --git a/solana/svm/src/account_overrides.rs b/solana/svm/src/account_overrides.rs new file mode 100644 index 0000000..f548c1c --- /dev/null +++ b/solana/svm/src/account_overrides.rs @@ -0,0 +1,65 @@ +use { + solana_account::AccountSharedData, solana_pubkey::Pubkey, solana_sdk_ids::sysvar, + std::collections::HashMap, +}; + +/// Encapsulates overridden accounts, typically used for transaction +/// simulations. Account overrides are currently not used when loading the +/// durable nonce account or when constructing the instructions sysvar account. +#[derive(Default)] +pub struct AccountOverrides { + accounts: HashMap, +} + +impl AccountOverrides { + /// Insert or remove an account with a given pubkey to/from the list of overrides. + fn set_account(&mut self, pubkey: &Pubkey, account: Option) { + match account { + Some(account) => self.accounts.insert(*pubkey, account), + None => self.accounts.remove(pubkey), + }; + } + + /// Sets in the slot history + /// + /// Note: no checks are performed on the correctness of the contained data + pub fn set_slot_history(&mut self, slot_history: Option) { + self.set_account(&sysvar::slot_history::id(), slot_history); + } + + /// Gets the account if it's found in the list of overrides + pub(crate) fn get(&self, pubkey: &Pubkey) -> Option<&AccountSharedData> { + self.accounts.get(pubkey) + } +} + +#[cfg(test)] +mod test { + use { + crate::account_overrides::AccountOverrides, solana_account::AccountSharedData, + solana_pubkey::Pubkey, solana_sdk_ids::sysvar, + }; + + #[test] + fn test_set_account() { + let mut accounts = AccountOverrides::default(); + let data = AccountSharedData::default(); + let key = Pubkey::new_unique(); + accounts.set_account(&key, Some(data.clone())); + assert_eq!(accounts.get(&key), Some(&data)); + + accounts.set_account(&key, None); + assert!(accounts.get(&key).is_none()); + } + + #[test] + fn test_slot_history() { + let mut accounts = AccountOverrides::default(); + let data = AccountSharedData::default(); + + assert_eq!(accounts.get(&sysvar::slot_history::id()), None); + accounts.set_slot_history(Some(data.clone())); + + assert_eq!(accounts.get(&sysvar::slot_history::id()), Some(&data)); + } +} diff --git a/solana/svm/src/lib.rs b/solana/svm/src/lib.rs new file mode 100644 index 0000000..d837383 --- /dev/null +++ b/solana/svm/src/lib.rs @@ -0,0 +1,22 @@ +#![cfg(feature = "agave-unstable-api")] +#![allow(clippy::arithmetic_side_effects)] + +pub mod account_loader; +pub mod account_overrides; +pub mod message_processor; +pub mod nonce_info; +pub mod program_loader; +pub mod rent_calculator; +pub mod rollback_accounts; +pub mod transaction_account_state_info; +pub mod transaction_balances; +pub mod transaction_commit_result; +pub mod transaction_error_metrics; +pub mod transaction_execution_result; +pub mod transaction_processing_callback; +pub mod transaction_processing_result; +pub mod transaction_processor; + +#[cfg_attr(feature = "frozen-abi", macro_use)] +#[cfg(feature = "frozen-abi")] +extern crate solana_frozen_abi_macro; diff --git a/solana/svm/src/message_processor.rs b/solana/svm/src/message_processor.rs new file mode 100644 index 0000000..bef5d92 --- /dev/null +++ b/solana/svm/src/message_processor.rs @@ -0,0 +1,739 @@ +use { + solana_program_runtime::invoke_context::InvokeContext, + solana_svm_measure::measure_us, + solana_svm_timings::{ExecuteDetailsTimings, ExecuteTimings}, + solana_svm_transaction::svm_message::SVMMessage, + solana_transaction_error::TransactionError, +}; + +/// Process a message. +/// This method calls each instruction in the message over the set of loaded accounts. +/// For each instruction it calls the program entrypoint method and verifies that the result of +/// the call does not violate the bank's accounting rules. +/// The accounts are committed back to the bank only if every instruction succeeds. +pub(crate) fn process_message<'ix_data>( + message: &'ix_data impl SVMMessage, + invoke_context: &mut InvokeContext<'_, 'ix_data>, + execute_timings: &mut ExecuteTimings, + accumulated_consumed_units: &mut u64, +) -> Result<(), TransactionError> { + invoke_context + .prepare_top_level_instructions(message) + .map_err(|(ix_idx, err)| TransactionError::InstructionError(ix_idx, err))?; + + for (top_level_instruction_index, (program_id, instruction)) in + message.program_instructions_iter().enumerate() + { + let mut compute_units_consumed = 0; + let (result, process_instruction_us) = measure_us!({ + if invoke_context.is_precompile(program_id) { + invoke_context.process_precompile( + program_id, + instruction.data, + message.instructions_iter().map(|ix| ix.data), + ) + } else { + invoke_context.process_instruction(&mut compute_units_consumed, execute_timings) + } + }); + + *accumulated_consumed_units = + accumulated_consumed_units.saturating_add(compute_units_consumed); + // The per_program_timings are only used for metrics reporting at the trace + // level, so they should only be accumulated when trace level is enabled. + if log::log_enabled!(log::Level::Trace) { + execute_timings.details.accumulate_program( + program_id, + process_instruction_us, + compute_units_consumed, + result.is_err(), + ); + } + invoke_context.timings = { + execute_timings.details.accumulate(&invoke_context.timings); + ExecuteDetailsTimings::default() + }; + execute_timings + .execute_accessories + .process_instructions + .total_us += process_instruction_us; + + result.map_err(|err| { + TransactionError::InstructionError(top_level_instruction_index as u8, err) + })?; + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use { + super::*, + openssl::{ + ec::{EcGroup, EcKey}, + nid::Nid, + }, + solana_account::{ + Account, AccountSharedData, DUMMY_INHERITABLE_ACCOUNT_FIELDS, ReadableAccount, + WritableAccount, + }, + solana_ed25519_program::new_ed25519_instruction_with_signature, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction, error::InstructionError}, + solana_keypair::{Address, Keypair}, + solana_message::{AccountKeys, Message, SanitizedMessage}, + solana_precompile_error::PrecompileError, + solana_program_runtime::{ + declare_process_instruction, + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::EnvironmentConfig, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + program_cache_entry::ProgramCacheEntry, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sbpf::program::BuiltinFunctionDefinition, + solana_sdk_ids::{ed25519_program, native_loader, secp256k1_program, system_program}, + solana_secp256k1_program::{ + eth_address_from_pubkey, new_secp256k1_instruction_with_signature, + }, + solana_secp256r1_program::{new_secp256r1_instruction_with_signature, sign_message}, + solana_signer::Signer, + solana_svm_callback::InvokeContextCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_transaction_context::transaction::TransactionContext, + std::{ + collections::{HashMap, HashSet}, + sync::Arc, + }, + }; + + struct MockCallback {} + impl InvokeContextCallback for MockCallback {} + + fn create_loadable_account_for_test(name: &str) -> AccountSharedData { + let (lamports, rent_epoch) = DUMMY_INHERITABLE_ACCOUNT_FIELDS; + AccountSharedData::from(Account { + lamports, + owner: native_loader::id(), + data: name.as_bytes().to_vec(), + executable: true, + rent_epoch, + }) + } + + fn new_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::try_from_legacy_message(message, &HashSet::new()).unwrap() + } + + #[test] + fn test_process_message_readonly_handling() { + #[derive(serde::Serialize, serde::Deserialize)] + enum MockSystemInstruction { + Correct, + TransferLamports { lamports: u64 }, + ChangeData { data: u8 }, + } + + declare_process_instruction!(MockBuiltin, 1, |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockSystemInstruction::Correct => Ok(()), + MockSystemInstruction::TransferLamports { lamports } => { + instruction_context + .try_borrow_instruction_account(0)? + .checked_sub_lamports(lamports)?; + instruction_context + .try_borrow_instruction_account(1)? + .checked_add_lamports(lamports)?; + Ok(()) + } + MockSystemInstruction::ChangeData { data } => { + instruction_context + .try_borrow_instruction_account(1)? + .set_data_from_slice(&[data])?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + }); + + let writable_pubkey = Pubkey::new_unique(); + let readonly_pubkey = Pubkey::new_unique(); + let mock_system_program_id = Pubkey::new_unique(); + + let accounts = vec![ + ( + writable_pubkey, + AccountSharedData::new(100, 1, &mock_system_program_id), + ), + ( + readonly_pubkey, + AccountSharedData::new(0, 1, &mock_system_program_id), + ), + ( + mock_system_program_id, + create_loadable_account_for_test("mock_system_program"), + ), + ]; + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_system_program_id, + Arc::new(ProgramCacheEntry::new_builtin(0, 0, MockBuiltin::register)), + ); + let account_keys = (0..transaction_context.get_number_of_accounts()) + .map(|index| { + *transaction_context + .get_key_of_account_at_index(index) + .unwrap() + }) + .collect::>(); + let account_metas = vec![ + AccountMeta::new(writable_pubkey, true), + AccountMeta::new_readonly(readonly_pubkey, false), + ]; + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::Correct, + account_metas.clone(), + ), + ]), + )); + let sysvar_cache = SysvarCache::default(); + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert!(result.is_ok()); + assert_eq!( + transaction_context + .accounts() + .try_borrow(0) + .unwrap() + .lamports(), + 100 + ); + assert_eq!( + transaction_context + .accounts() + .try_borrow(1) + .unwrap() + .lamports(), + 0 + ); + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::TransferLamports { lamports: 50 }, + account_metas.clone(), + ), + ]), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::ReadonlyLamportChange + )) + ); + + let message = new_sanitized_message(Message::new_with_compiled_instructions( + 1, + 0, + 2, + account_keys.clone(), + Hash::default(), + AccountKeys::new(&account_keys, None).compile_instructions(&[ + Instruction::new_with_bincode( + mock_system_program_id, + &MockSystemInstruction::ChangeData { data: 50 }, + account_metas, + ), + ]), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::ReadonlyDataModified + )) + ); + } + + #[test] + fn test_process_message_duplicate_accounts() { + #[derive(serde::Serialize, serde::Deserialize)] + enum MockSystemInstruction { + BorrowFail, + MultiBorrowMut, + DoWork { lamports: u64, data: u8 }, + } + + declare_process_instruction!(MockBuiltin, 1, |invoke_context| { + let transaction_context = &invoke_context.transaction_context; + let instruction_context = transaction_context.get_current_instruction_context()?; + let instruction_data = instruction_context.get_instruction_data(); + let mut to_account = instruction_context.try_borrow_instruction_account(1)?; + if let Ok(instruction) = bincode::deserialize(instruction_data) { + match instruction { + MockSystemInstruction::BorrowFail => { + let from_account = instruction_context.try_borrow_instruction_account(0)?; + let dup_account = instruction_context.try_borrow_instruction_account(2)?; + if from_account.get_lamports() != dup_account.get_lamports() { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + MockSystemInstruction::MultiBorrowMut => { + let lamports_a = instruction_context + .try_borrow_instruction_account(0)? + .get_lamports(); + let lamports_b = instruction_context + .try_borrow_instruction_account(2)? + .get_lamports(); + if lamports_a != lamports_b { + return Err(InstructionError::InvalidArgument); + } + Ok(()) + } + MockSystemInstruction::DoWork { lamports, data } => { + let mut dup_account = + instruction_context.try_borrow_instruction_account(2)?; + dup_account.checked_sub_lamports(lamports)?; + to_account.checked_add_lamports(lamports)?; + dup_account.set_data_from_slice(&[data])?; + drop(dup_account); + let mut from_account = + instruction_context.try_borrow_instruction_account(0)?; + from_account.checked_sub_lamports(lamports)?; + to_account.checked_add_lamports(lamports)?; + Ok(()) + } + } + } else { + Err(InstructionError::InvalidInstructionData) + } + }); + let mock_program_id = Pubkey::from([2u8; 32]); + let accounts = vec![ + ( + solana_pubkey::new_rand(), + AccountSharedData::new(100, 1, &mock_program_id), + ), + ( + solana_pubkey::new_rand(), + AccountSharedData::new(0, 1, &mock_program_id), + ), + ( + mock_program_id, + create_loadable_account_for_test("mock_system_program"), + ), + ]; + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_program_id, + Arc::new(ProgramCacheEntry::new_builtin(0, 0, MockBuiltin::register)), + ); + let account_metas = vec![ + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(0).unwrap(), + true, + ), + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(1).unwrap(), + false, + ), + AccountMeta::new( + *transaction_context.get_key_of_account_at_index(0).unwrap(), + false, + ), + ]; + + // Try to borrow mut the same account + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::BorrowFail, + account_metas.clone(), + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let sysvar_cache = SysvarCache::default(); + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert_eq!( + result, + Err(TransactionError::InstructionError( + 0, + InstructionError::AccountBorrowFailed + )) + ); + + // Try to borrow mut the same account in a safe way + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::MultiBorrowMut, + account_metas.clone(), + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = + TransactionContext::new(accounts.clone(), Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert!(result.is_ok()); + + // Do work on the same transaction account but at different instruction accounts + let message = new_sanitized_message(Message::new( + &[Instruction::new_with_bincode( + mock_program_id, + &MockSystemInstruction::DoWork { + lamports: 10, + data: 42, + }, + account_metas, + )], + Some(transaction_context.get_key_of_account_at_index(0).unwrap()), + )); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 3, 1); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + assert!(result.is_ok()); + assert_eq!( + transaction_context + .accounts() + .try_borrow(0) + .unwrap() + .lamports(), + 80 + ); + assert_eq!( + transaction_context + .accounts() + .try_borrow(1) + .unwrap() + .lamports(), + 20 + ); + assert_eq!( + transaction_context.accounts().try_borrow(0).unwrap().data(), + &vec![42] + ); + } + + fn secp256k1_instruction_for_test() -> Instruction { + let message = b"hello"; + let bytes: [u8; 32] = rand::random(); + let secret_key = libsecp256k1::SecretKey::parse(&bytes).unwrap(); + let pubkey = libsecp256k1::PublicKey::from_secret_key(&secret_key); + let eth_address = eth_address_from_pubkey(&pubkey.serialize()[1..].try_into().unwrap()); + let (signature, recovery_id) = + solana_secp256k1_program::sign_message(&secret_key.serialize(), &message[..]).unwrap(); + new_secp256k1_instruction_with_signature( + &message[..], + &signature, + recovery_id, + ð_address, + ) + } + + fn ed25519_instruction_for_test() -> Instruction { + let keypair = Keypair::new(); + let signature = keypair.sign_message(b"hello"); + let pubkey = keypair.pubkey().to_bytes(); + new_ed25519_instruction_with_signature(b"hello", signature.as_array(), &pubkey) + } + + fn secp256r1_instruction_for_test() -> Instruction { + let group = EcGroup::from_curve_name(Nid::X9_62_PRIME256V1).unwrap(); + let secret_key = EcKey::generate(&group).unwrap(); + let signature = sign_message(b"hello", &secret_key.private_key_to_der().unwrap()).unwrap(); + let mut ctx = openssl::bn::BigNumContext::new().unwrap(); + let pubkey = secret_key + .public_key() + .to_bytes( + &group, + openssl::ec::PointConversionForm::COMPRESSED, + &mut ctx, + ) + .unwrap(); + new_secp256r1_instruction_with_signature(b"hello", &signature, &pubkey.try_into().unwrap()) + } + + #[test] + fn test_precompile() { + let mock_program_id = Pubkey::new_unique(); + declare_process_instruction!(MockBuiltin, 1, |_invoke_context| { + Err(InstructionError::Custom(0xbabb1e)) + }); + + let mut secp256k1_account = AccountSharedData::new(1, 0, &native_loader::id()); + secp256k1_account.set_executable(true); + let mut ed25519_account = AccountSharedData::new(1, 0, &native_loader::id()); + ed25519_account.set_executable(true); + let mut secp256r1_account = AccountSharedData::new(1, 0, &native_loader::id()); + secp256r1_account.set_executable(true); + let mut mock_program_account = AccountSharedData::new(1, 0, &native_loader::id()); + mock_program_account.set_executable(true); + + let fee_payer = Pubkey::new_unique(); + let accounts_map: HashMap = HashMap::from([ + ( + fee_payer, + AccountSharedData::new(1, 0, &system_program::id()), + ), + (secp256k1_program::id(), secp256k1_account), + (ed25519_program::id(), ed25519_account), + (solana_secp256r1_program::id(), secp256r1_account), + (mock_program_id, mock_program_account), + ]); + + let message = new_sanitized_message(Message::new( + &[ + secp256k1_instruction_for_test(), + ed25519_instruction_for_test(), + secp256r1_instruction_for_test(), + Instruction::new_with_bytes(mock_program_id, &[], vec![]), + ], + Some(&fee_payer), + )); + + let accounts = message + .account_keys() + .iter() + .map(|key| (*key, accounts_map.get(key).unwrap().clone())) + .collect(); + let mut transaction_context = TransactionContext::new(accounts, Rent::default(), 1, 4, 4); + + let sysvar_cache = SysvarCache::default(); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + program_cache_for_tx_batch.replenish( + mock_program_id, + Arc::new(ProgramCacheEntry::new_builtin(0, 0, MockBuiltin::register)), + ); + + struct MockCallback {} + impl InvokeContextCallback for MockCallback { + fn is_precompile(&self, program_id: &Pubkey) -> bool { + program_id == &secp256k1_program::id() + || program_id == &ed25519_program::id() + || program_id == &solana_secp256r1_program::id() + } + + fn process_precompile( + &self, + program_id: &Pubkey, + _data: &[u8], + _instruction_datas: Vec<&[u8]>, + ) -> std::result::Result<(), PrecompileError> { + if self.is_precompile(program_id) { + Ok(()) + } else { + Err(PrecompileError::InvalidPublicKey) + } + } + } + let feature_set = SVMFeatureSet::all_enabled(); + let program_runtime_environments = ProgramRuntimeEnvironments::mock(); + let environment_config = EnvironmentConfig::new( + Hash::default(), + 0, + false, + &MockCallback {}, + &feature_set, + &program_runtime_environments, + &sysvar_cache, + ); + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + &mut program_cache_for_tx_batch, + environment_config, + None, + SVMTransactionExecutionBudget::default(), + SVMTransactionExecutionCost::default(), + ); + let result = process_message( + &message, + &mut invoke_context, + &mut ExecuteTimings::default(), + &mut 0, + ); + + assert_eq!( + result, + Err(TransactionError::InstructionError( + 3, + InstructionError::Custom(0xbabb1e) + )) + ); + assert_eq!( + transaction_context.number_of_called_instructions_in_trace(), + 4 + ); + } +} diff --git a/solana/svm/src/nonce_info.rs b/solana/svm/src/nonce_info.rs new file mode 100644 index 0000000..3f0a788 --- /dev/null +++ b/solana/svm/src/nonce_info.rs @@ -0,0 +1,151 @@ +#[cfg(feature = "dev-context-only-utils")] +use { + qualifier_attr::qualifiers, + solana_account::state_traits::StateMut, + solana_nonce::{ + state::{DurableNonce, State as NonceState}, + versions::Versions as NonceVersions, + }, + thiserror::Error, +}; +use {solana_account::AccountSharedData, solana_pubkey::Pubkey}; + +/// Holds limited nonce info available during transaction checks +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct NonceInfo { + pub address: Pubkey, + pub account: AccountSharedData, +} + +#[derive(Error, Debug, PartialEq)] +#[cfg(feature = "dev-context-only-utils")] +#[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] +enum AdvanceNonceError { + #[error("Invalid account")] + Invalid, + #[error("Uninitialized nonce")] + Uninitialized, +} + +impl NonceInfo { + pub fn new(address: Pubkey, account: AccountSharedData) -> Self { + Self { address, account } + } + + // Advance the stored blockhash to prevent fee theft by someone + // replaying nonce transactions that have failed with an + // `InstructionError`. + #[cfg(feature = "dev-context-only-utils")] + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + fn try_advance_nonce( + &mut self, + durable_nonce: DurableNonce, + lamports_per_signature: u64, + ) -> Result<(), AdvanceNonceError> { + let nonce_versions = StateMut::::state(&self.account) + .map_err(|_| AdvanceNonceError::Invalid)?; + if let NonceState::Initialized(data) = nonce_versions.state() { + let nonce_state = + NonceState::new_initialized(&data.authority, durable_nonce, lamports_per_signature); + let nonce_versions = NonceVersions::new(nonce_state); + self.account.set_state(&nonce_versions).unwrap(); + Ok(()) + } else { + Err(AdvanceNonceError::Uninitialized) + } + } + + pub fn address(&self) -> &Pubkey { + &self.address + } + + pub fn account(&self) -> &AccountSharedData { + &self.account + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_hash::Hash, + solana_nonce::{ + state::{Data as NonceData, DurableNonce, State as NonceState}, + versions::Versions as NonceVersions, + }, + solana_sdk_ids::system_program, + }; + + fn create_nonce_account(state: NonceState) -> AccountSharedData { + AccountSharedData::new_data(1_000_000, &NonceVersions::new(state), &system_program::id()) + .unwrap() + } + + #[test] + fn test_nonce_info() { + let nonce_address = Pubkey::new_unique(); + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let lamports_per_signature = 42; + let nonce_account = create_nonce_account(NonceState::Initialized(NonceData::new( + Pubkey::default(), + durable_nonce, + lamports_per_signature, + ))); + + let nonce_info = NonceInfo::new(nonce_address, nonce_account.clone()); + assert_eq!(*nonce_info.address(), nonce_address); + assert_eq!(*nonce_info.account(), nonce_account); + } + + #[test] + fn test_try_advance_nonce_success() { + let authority = Pubkey::new_unique(); + let mut nonce_info = NonceInfo::new( + Pubkey::new_unique(), + create_nonce_account(NonceState::Initialized(NonceData::new( + authority, + DurableNonce::from_blockhash(&Hash::new_unique()), + 42, + ))), + ); + + let new_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let new_lamports_per_signature = 100; + let result = nonce_info.try_advance_nonce(new_nonce, new_lamports_per_signature); + assert_eq!(result, Ok(())); + + let nonce_versions = StateMut::::state(&nonce_info.account).unwrap(); + assert_eq!( + &NonceState::Initialized(NonceData::new( + authority, + new_nonce, + new_lamports_per_signature + )), + nonce_versions.state() + ); + } + + #[test] + fn test_try_advance_nonce_invalid() { + let mut nonce_info = NonceInfo::new( + Pubkey::new_unique(), + AccountSharedData::new(1_000_000, 0, &Pubkey::default()), + ); + + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let result = nonce_info.try_advance_nonce(durable_nonce, 5000); + assert_eq!(result, Err(AdvanceNonceError::Invalid)); + } + + #[test] + fn test_try_advance_nonce_uninitialized() { + let mut nonce_info = NonceInfo::new( + Pubkey::new_unique(), + create_nonce_account(NonceState::Uninitialized), + ); + + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let result = nonce_info.try_advance_nonce(durable_nonce, 5000); + assert_eq!(result, Err(AdvanceNonceError::Uninitialized)); + } +} diff --git a/solana/svm/src/program_loader.rs b/solana/svm/src/program_loader.rs new file mode 100644 index 0000000..edbba40 --- /dev/null +++ b/solana/svm/src/program_loader.rs @@ -0,0 +1,763 @@ +#[cfg(feature = "metrics")] +use solana_program_runtime::program_metrics::LoadProgramMetrics; +use { + solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, + solana_clock::Slot, + solana_instruction::error::InstructionError, + solana_loader_v3_interface::state::UpgradeableLoaderState, + solana_loader_v4_interface::state::{LoaderV4State, LoaderV4Status}, + solana_program_runtime::{ + loaded_programs::ProgramRuntimeEnvironment, + program_cache_entry::{ + DELAY_VISIBILITY_SLOT_OFFSET, ProgramCacheEntry, ProgramCacheEntryOwner, + ProgramCacheEntryType, + }, + }, + solana_pubkey::Pubkey, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, loader_v4}, + solana_svm_callback::TransactionProcessingCallback, + solana_svm_timings::ExecuteTimings, + solana_svm_type_overrides::sync::Arc, + solana_transaction_error::{TransactionError, TransactionResult}, +}; + +#[derive(Debug)] +pub(crate) enum ProgramAccountLoadResult { + InvalidAccountData(ProgramCacheEntryOwner), + ProgramOfLoaderV1(AccountSharedData), + ProgramOfLoaderV2(AccountSharedData), + ProgramOfLoaderV3(AccountSharedData, AccountSharedData, Slot), + ProgramOfLoaderV4(AccountSharedData, Slot), +} + +pub(crate) fn load_program_accounts( + callbacks: &CB, + pubkey: &Pubkey, +) -> Option<(ProgramAccountLoadResult, Slot)> { + let (program_account, last_modification_slot) = callbacks.get_account_shared_data(pubkey)?; + + let load_result = if loader_v4::check_id(program_account.owner()) { + loader_v4_get_state(program_account.data()) + .ok() + .and_then(|state| { + (!matches!(state.status, LoaderV4Status::Retracted)).then_some(state.slot) + }) + .map(|slot| ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, slot)) + .unwrap_or(ProgramAccountLoadResult::InvalidAccountData( + ProgramCacheEntryOwner::LoaderV4, + )) + } else if bpf_loader_upgradeable::check_id(program_account.owner()) { + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program_account.state() + { + if let Some((programdata_account, _slot)) = + callbacks.get_account_shared_data(&programdata_address) + { + if bpf_loader_upgradeable::check_id(programdata_account.owner()) { + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata_account.state() + { + ProgramAccountLoadResult::ProgramOfLoaderV3( + program_account, + programdata_account, + slot, + ) + } else { + ProgramAccountLoadResult::InvalidAccountData( + ProgramCacheEntryOwner::LoaderV3, + ) + } + } else { + ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) + } + } else { + ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) + } + } else { + ProgramAccountLoadResult::InvalidAccountData(ProgramCacheEntryOwner::LoaderV3) + } + } else if bpf_loader::check_id(program_account.owner()) { + ProgramAccountLoadResult::ProgramOfLoaderV2(program_account) + } else if bpf_loader_deprecated::check_id(program_account.owner()) { + ProgramAccountLoadResult::ProgramOfLoaderV1(program_account) + } else { + return None; + }; + + Some((load_result, last_modification_slot)) +} + +/// Loads the program with the given pubkey. +/// +/// If the account doesn't exist it returns `None`. If the account does exist, it must be a program +/// account (belong to one of the program loaders). Returns `Some(InvalidAccountData)` if the program +/// account is `Closed`, contains invalid data or any of the programdata accounts are invalid. +pub fn load_program_with_pubkey( + callbacks: &CB, + program_runtime_environment: &ProgramRuntimeEnvironment, + pubkey: &Pubkey, + current_slot: Slot, + execute_timings: &mut ExecuteTimings, +) -> Option<(Arc, Slot)> { + #[cfg(feature = "metrics")] + let mut load_program_metrics = LoadProgramMetrics { + program_id: pubkey.to_string(), + ..LoadProgramMetrics::default() + }; + #[cfg(not(feature = "metrics"))] + let _ = execute_timings; + + let (load_result, last_modification_slot) = load_program_accounts(callbacks, pubkey)?; + let loaded_program = match load_result { + ProgramAccountLoadResult::InvalidAccountData(owner) => Ok( + ProgramCacheEntry::new_tombstone(current_slot, owner, ProgramCacheEntryType::Closed), + ), + + ProgramAccountLoadResult::ProgramOfLoaderV1(program_account) => ProgramCacheEntry::new( + program_account.owner(), + ProgramRuntimeEnvironment::clone(program_runtime_environment), + 0, + DELAY_VISIBILITY_SLOT_OFFSET, + program_account.data(), + program_account.data().len(), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + ) + .map_err(|_| (0, ProgramCacheEntryOwner::LoaderV1)), + + ProgramAccountLoadResult::ProgramOfLoaderV2(program_account) => ProgramCacheEntry::new( + program_account.owner(), + ProgramRuntimeEnvironment::clone(program_runtime_environment), + 0, + DELAY_VISIBILITY_SLOT_OFFSET, + program_account.data(), + program_account.data().len(), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + ) + .map_err(|_| (0, ProgramCacheEntryOwner::LoaderV2)), + + ProgramAccountLoadResult::ProgramOfLoaderV3( + program_account, + programdata_account, + deployment_slot, + ) => programdata_account + .data() + .get(UpgradeableLoaderState::size_of_programdata_metadata()..) + .ok_or(()) + .and_then(|programdata| { + ProgramCacheEntry::new( + program_account.owner(), + ProgramRuntimeEnvironment::clone(program_runtime_environment), + deployment_slot, + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + programdata, + program_account + .data() + .len() + .saturating_add(programdata_account.data().len()), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + ) + .map_err(|_| ()) + }) + .map_err(|_| (deployment_slot, ProgramCacheEntryOwner::LoaderV3)), + + ProgramAccountLoadResult::ProgramOfLoaderV4(program_account, deployment_slot) => { + program_account + .data() + .get(LoaderV4State::program_data_offset()..) + .ok_or(()) + .and_then(|elf_bytes| { + ProgramCacheEntry::new( + &loader_v4::id(), + ProgramRuntimeEnvironment::clone(program_runtime_environment), + deployment_slot, + deployment_slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + elf_bytes, + program_account.data().len(), + #[cfg(feature = "metrics")] + &mut load_program_metrics, + ) + .map_err(|_| ()) + }) + .map_err(|_| (deployment_slot, ProgramCacheEntryOwner::LoaderV4)) + } + } + .unwrap_or_else(|(deployment_slot, owner)| { + let env = ProgramRuntimeEnvironment::clone(program_runtime_environment); + ProgramCacheEntry::new_tombstone( + deployment_slot, + owner, + ProgramCacheEntryType::FailedVerification(env), + ) + }); + + #[cfg(feature = "metrics")] + load_program_metrics.submit_datapoint(&mut execute_timings.details); + loaded_program.update_access_slot(current_slot); + Some((Arc::new(loaded_program), last_modification_slot)) +} + +/// Find the slot in which the program was most recently re-/deployed. +/// Returns slot 0 for programs deployed with v1/v2 loaders, since programs deployed +/// with those loaders do not retain deployment slot information. +/// Returns an error if the program's account state can not be found or parsed. +pub(crate) fn get_program_deployment_slot( + callbacks: &CB, + pubkey: &Pubkey, +) -> TransactionResult { + let (program, _slot) = callbacks + .get_account_shared_data(pubkey) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if bpf_loader_upgradeable::check_id(program.owner()) { + if let Ok(UpgradeableLoaderState::Program { + programdata_address, + }) = program.state() + { + let (programdata, _slot) = callbacks + .get_account_shared_data(&programdata_address) + .ok_or(TransactionError::ProgramAccountNotFound)?; + if let Ok(UpgradeableLoaderState::ProgramData { + slot, + upgrade_authority_address: _, + }) = programdata.state() + { + return Ok(slot); + } + } + Err(TransactionError::ProgramAccountNotFound) + } else if loader_v4::check_id(program.owner()) { + let state = loader_v4_get_state(program.data()) + .map_err(|_| TransactionError::ProgramAccountNotFound)?; + Ok(state.slot) + } else { + Ok(0) + } +} + +// Plucked from the now-removed Loader V4 program library. +fn loader_v4_get_state(data: &[u8]) -> Result<&LoaderV4State, InstructionError> { + unsafe { + let data = data + .get(0..LoaderV4State::program_data_offset()) + .ok_or(InstructionError::AccountDataTooSmall)? + .try_into() + .unwrap(); + Ok(std::mem::transmute::< + &[u8; LoaderV4State::program_data_offset()], + &LoaderV4State, + >(data)) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::transaction_processor::TransactionBatchProcessor, + solana_account::WritableAccount, + solana_program_runtime::{ + loaded_programs::{ + BlockRelation, ForkGraph, ProgramRuntimeEnvironment, + get_mock_program_runtime_environment, + }, + solana_sbpf::program::BuiltinProgram, + }, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable}, + solana_svm_callback::InvokeContextCallback, + std::{ + cell::RefCell, + collections::HashMap, + env, + fs::{self, File}, + io::Read, + }, + }; + + struct TestForkGraph {} + + impl ForkGraph for TestForkGraph { + fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { + BlockRelation::Unknown + } + } + + #[derive(Default, Clone)] + pub(crate) struct MockBankCallback { + pub(crate) account_shared_data: RefCell>, + } + + impl InvokeContextCallback for MockBankCallback {} + + impl TransactionProcessingCallback for MockBankCallback { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.account_shared_data.borrow().get(pubkey).cloned() + } + } + + #[test] + fn test_load_program_accounts_account_not_found() { + let mock_bank = MockBankCallback::default(); + let key = Pubkey::new_unique(); + + let result = load_program_accounts(&mock_bank, &key); + assert!(result.is_none()); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + let state = UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = load_program_accounts(&mock_bank, &key); + assert!(matches!( + result, + Some((ProgramAccountLoadResult::InvalidAccountData(_), _)) + )); + + account_data.set_data(Vec::new()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data, 0)); + + let result = load_program_accounts(&mock_bank, &key); + + assert!(matches!( + result, + Some((ProgramAccountLoadResult::InvalidAccountData(_), _)) + )); + } + + #[test] + fn test_load_program_accounts_loader_v1_or_v2() { + let key = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = load_program_accounts(&mock_bank, &key); + match result { + Some((ProgramAccountLoadResult::ProgramOfLoaderV1(data), last_modification_slot)) + | Some((ProgramAccountLoadResult::ProgramOfLoaderV2(data), last_modification_slot)) => { + assert_eq!(data, account_data); + assert_eq!(last_modification_slot, 0); + } + _ => panic!("Invalid result"), + } + } + + #[test] + fn test_load_program_accounts_success() { + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + + let state = UpgradeableLoaderState::Program { + programdata_address: key2, + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key1, (account_data.clone(), 25)); + + let state = UpgradeableLoaderState::ProgramData { + slot: 25, + upgrade_authority_address: None, + }; + let mut account_data2 = AccountSharedData::default(); + account_data2.set_owner(bpf_loader_upgradeable::id()); + account_data2.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key2, (account_data2.clone(), 25)); + + let result = load_program_accounts(&mock_bank, &key1); + + match result { + Some(( + ProgramAccountLoadResult::ProgramOfLoaderV3(data1, data2, deployment_slot), + last_modification_slot, + )) => { + assert_eq!(data1, account_data); + assert_eq!(data2, account_data2); + assert_eq!(deployment_slot, 25); + assert_eq!(last_modification_slot, 25); + } + + _ => panic!("Invalid result"), + } + } + + fn load_test_program() -> Vec { + let mut dir = env::current_dir().unwrap(); + dir.push("tests"); + dir.push("example-programs"); + dir.push("hello-solana"); + dir.push("hello_solana_program.so"); + let mut file = File::open(dir.clone()).expect("file not found"); + let metadata = fs::metadata(dir).expect("Unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + file.read_exact(&mut buffer).expect("Buffer overflow"); + buffer + } + + #[test] + fn test_load_program_from_bytes() { + let buffer = load_test_program(); + + #[cfg(feature = "metrics")] + let mut metrics = LoadProgramMetrics::default(); + let loader = bpf_loader_upgradeable::id(); + let size = buffer.len(); + let slot: Slot = 2; + let environment = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + + let result = ProgramCacheEntry::new( + &loader, + ProgramRuntimeEnvironment::clone(&environment), + slot, + slot.saturating_add(DELAY_VISIBILITY_SLOT_OFFSET), + &buffer, + size, + #[cfg(feature = "metrics")] + &mut metrics, + ); + + assert!(result.is_ok()); + } + + #[test] + fn test_load_program_not_found() { + let mock_bank = MockBankCallback::default(); + let key = Pubkey::new_unique(); + let batch_processor = TransactionBatchProcessor::::default(); + + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(50), + &key, + 500, + &mut ExecuteTimings::default(), + ); + assert!(result.is_none()); + } + + #[test] + fn test_load_program_invalid_account_data() { + let key = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(20), + &key, + 0, // Slot 0 + &mut ExecuteTimings::default(), + ); + + let loaded_program = ProgramCacheEntry::new_tombstone( + 0, // Slot 0 + ProgramCacheEntryOwner::LoaderV3, + ProgramCacheEntryType::FailedVerification( + batch_processor.program_runtime_environment_for_epoch(20), + ), + ); + assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); + } + + #[test] + fn test_load_program_program_loader_v1_or_v2() { + let key = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let batch_processor = TransactionBatchProcessor::::default(); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + // This should return an error + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(20), + &key, + 200, + &mut ExecuteTimings::default(), + ); + let loaded_program = ProgramCacheEntry::new_tombstone( + 0, + ProgramCacheEntryOwner::LoaderV2, + ProgramCacheEntryType::FailedVerification( + batch_processor.program_runtime_environment_for_epoch(20), + ), + ); + assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); + + let buffer = load_test_program(); + account_data.set_data(buffer); + + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(20), + &key, + 200, + &mut ExecuteTimings::default(), + ); + + let program_runtime_environment = get_mock_program_runtime_environment(); + let expected = ProgramCacheEntry::new( + account_data.owner(), + ProgramRuntimeEnvironment::clone(&program_runtime_environment), + 0, + DELAY_VISIBILITY_SLOT_OFFSET, + account_data.data(), + account_data.data().len(), + #[cfg(feature = "metrics")] + &mut LoadProgramMetrics::default(), + ); + + assert_eq!(result.unwrap(), (Arc::new(expected.unwrap()), 0)); + } + + #[test] + fn test_load_program_program_loader_v3() { + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader_upgradeable::id()); + + let state = UpgradeableLoaderState::Program { + programdata_address: key2, + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key1, (account_data.clone(), 0)); + + let state = UpgradeableLoaderState::ProgramData { + slot: 0, + upgrade_authority_address: None, + }; + let mut account_data2 = AccountSharedData::default(); + account_data2.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key2, (account_data2.clone(), 0)); + + // This should return an error + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(0), + &key1, + 0, + &mut ExecuteTimings::default(), + ); + let loaded_program = ProgramCacheEntry::new_tombstone( + 0, + ProgramCacheEntryOwner::LoaderV3, + ProgramCacheEntryType::FailedVerification( + batch_processor.program_runtime_environment_for_epoch(0), + ), + ); + assert_eq!(result.unwrap(), (Arc::new(loaded_program), 0)); + + let mut buffer = load_test_program(); + let mut header = bincode::serialize(&state).unwrap(); + let mut complement = vec![ + 0; + std::cmp::max( + 0, + UpgradeableLoaderState::size_of_programdata_metadata() - header.len() + ) + ]; + header.append(&mut complement); + header.append(&mut buffer); + account_data.set_data(header); + + mock_bank + .account_shared_data + .borrow_mut() + .insert(key2, (account_data.clone(), 0)); + + let result = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(20), + &key1, + 200, + &mut ExecuteTimings::default(), + ); + + let data = account_data.data(); + account_data + .set_data(data[UpgradeableLoaderState::size_of_programdata_metadata()..].to_vec()); + + let program_runtime_environment = get_mock_program_runtime_environment(); + let expected = ProgramCacheEntry::new( + account_data.owner(), + ProgramRuntimeEnvironment::clone(&program_runtime_environment), + 0, + DELAY_VISIBILITY_SLOT_OFFSET, + account_data.data(), + account_data.data().len(), + #[cfg(feature = "metrics")] + &mut LoadProgramMetrics::default(), + ); + assert_eq!(result.unwrap(), (Arc::new(expected.unwrap()), 0)); + } + + #[test] + fn test_load_program_environment() { + let key = Pubkey::new_unique(); + let mock_bank = MockBankCallback::default(); + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let batch_processor = TransactionBatchProcessor::::default(); + let upcoming_environment = get_mock_program_runtime_environment(); + let current_environment = + ProgramRuntimeEnvironment::clone(&batch_processor.program_runtime_environment); + { + let mut epoch_boundary_preparation = + batch_processor.epoch_boundary_preparation.write().unwrap(); + epoch_boundary_preparation.upcoming_epoch = 1; + epoch_boundary_preparation.upcoming_environment = Some(upcoming_environment.clone()); + } + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + for is_upcoming_env in [false, true] { + let (result, _last_modification_slot) = load_program_with_pubkey( + &mock_bank, + &batch_processor.program_runtime_environment_for_epoch(is_upcoming_env as u64), + &key, + 200, + &mut ExecuteTimings::default(), + ) + .unwrap(); + assert_ne!( + is_upcoming_env, + result.program.get_environment().unwrap() == ¤t_environment, + ); + assert_eq!( + is_upcoming_env, + result.program.get_environment().unwrap() == &upcoming_environment, + ); + } + } + + #[test] + fn test_program_modification_slot_account_not_found() { + let mock_bank = MockBankCallback::default(); + + let key = Pubkey::new_unique(); + + let result = get_program_deployment_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + + let mut account_data = AccountSharedData::new(100, 100, &bpf_loader_upgradeable::id()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = get_program_deployment_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + + let state = UpgradeableLoaderState::Program { + programdata_address: Pubkey::new_unique(), + }; + account_data.set_data(bincode::serialize(&state).unwrap()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key, (account_data.clone(), 0)); + + let result = get_program_deployment_slot(&mock_bank, &key); + assert_eq!(result.err(), Some(TransactionError::ProgramAccountNotFound)); + } + + #[test] + fn test_program_deployment_slot_success() { + let mock_bank = MockBankCallback::default(); + + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + + let account_data = AccountSharedData::new_data( + 100, + &UpgradeableLoaderState::Program { + programdata_address: key2, + }, + &bpf_loader_upgradeable::id(), + ) + .unwrap(); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key1, (account_data, 0)); + + let mut account_data = AccountSharedData::new_data( + 100, + &UpgradeableLoaderState::ProgramData { + slot: 77, + upgrade_authority_address: None, + }, + &bpf_loader_upgradeable::id(), + ) + .unwrap(); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key2, (account_data.clone(), 0)); + + let result = get_program_deployment_slot(&mock_bank, &key1); + assert_eq!(result.unwrap(), 77); + + account_data.set_owner(Pubkey::new_unique()); + mock_bank + .account_shared_data + .borrow_mut() + .insert(key2, (account_data, 0)); + + let result = get_program_deployment_slot(&mock_bank, &key2); + assert_eq!(result.unwrap(), 0); + } +} diff --git a/solana/svm/src/rent_calculator.rs b/solana/svm/src/rent_calculator.rs new file mode 100644 index 0000000..48bfb6b --- /dev/null +++ b/solana/svm/src/rent_calculator.rs @@ -0,0 +1,124 @@ +//! Solana SVM Rent Calculator. +//! +//! Rent management for SVM. + +use { + solana_clock::Epoch, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, + solana_transaction_error::{TransactionError, TransactionResult}, +}; + +/// When rent is collected from an exempt account, rent_epoch is set to this +/// value. The idea is to have a fixed, consistent value for rent_epoch for all accounts that do not collect rent. +/// This enables us to get rid of the field completely. +pub const RENT_EXEMPT_RENT_EPOCH: Epoch = Epoch::MAX; + +/// Rent state of a Solana account. +#[derive(Debug, PartialEq, Eq)] +pub enum RentState { + /// account.lamports == 0 + Uninitialized, + /// 0 < account.lamports < rent-exempt-minimum + RentPaying { + lamports: u64, // account.lamports() + data_size: usize, // account.data().len() + }, + /// account.lamports >= rent-exempt-minimum + RentExempt, +} + +/// Check rent state transition for an account in a transaction. +/// +/// This method has a default implementation that calls into +/// `check_rent_state_with_account`. +pub fn check_rent_state( + pre_rent_state: &RentState, + post_rent_state: &RentState, + transaction_context: &TransactionContext, + index: IndexOfAccount, +) -> TransactionResult<()> { + let expect_msg = "account must exist at TransactionContext index"; + check_rent_state_with_account( + pre_rent_state, + post_rent_state, + transaction_context + .get_key_of_account_at_index(index) + .expect(expect_msg), + index, + )?; + Ok(()) +} + +/// Check rent state transition for an account directly. +/// +/// This method has a default implementation that checks whether the +/// transition is allowed and returns an error if it is not. It also +/// verifies that the account is not the incinerator. +pub fn check_rent_state_with_account( + pre_rent_state: &RentState, + post_rent_state: &RentState, + address: &Pubkey, + account_index: IndexOfAccount, +) -> TransactionResult<()> { + if !solana_sdk_ids::incinerator::check_id(address) + && !transition_allowed(pre_rent_state, post_rent_state) + { + let account_index = account_index as u8; + Err(TransactionError::InsufficientFundsForRent { account_index }) + } else { + Ok(()) + } +} + +/// Determine the rent state of an account. +/// +/// This method has a default implementation that treats accounts with zero +/// lamports as uninitialized and uses the implemented `get_rent` to +/// determine whether an account is rent-exempt. +pub fn get_account_rent_state( + rent: &Rent, + account_lamports: u64, + account_size: usize, +) -> RentState { + if account_lamports == 0 { + RentState::Uninitialized + } else if rent.is_exempt(account_lamports, account_size) { + RentState::RentExempt + } else { + RentState::RentPaying { + data_size: account_size, + lamports: account_lamports, + } + } +} + +/// Check whether a transition from the pre_rent_state to the +/// post_rent_state is valid. +/// +/// This method has a default implementation that allows transitions from +/// any state to `RentState::Uninitialized` or `RentState::RentExempt`. +/// Pre-state `RentState::RentPaying` can only transition to +/// `RentState::RentPaying` if the data size remains the same and the +/// account is not credited. +pub fn transition_allowed(pre_rent_state: &RentState, post_rent_state: &RentState) -> bool { + match post_rent_state { + RentState::Uninitialized | RentState::RentExempt => true, + RentState::RentPaying { + data_size: post_data_size, + lamports: post_lamports, + } => { + match pre_rent_state { + RentState::Uninitialized | RentState::RentExempt => false, + RentState::RentPaying { + data_size: pre_data_size, + lamports: pre_lamports, + } => { + // Cannot remain RentPaying if resized or credited. + post_data_size == pre_data_size && post_lamports <= pre_lamports + } + } + } + } +} diff --git a/solana/svm/src/rollback_accounts.rs b/solana/svm/src/rollback_accounts.rs new file mode 100644 index 0000000..5e3418c --- /dev/null +++ b/solana/svm/src/rollback_accounts.rs @@ -0,0 +1,271 @@ +use { + crate::nonce_info::NonceInfo, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::Epoch, + solana_pubkey::Pubkey, + solana_transaction_context::transaction_accounts::KeyedAccountSharedData, +}; + +/// Captured account state used to rollback account state for nonce and fee +/// payer accounts after a failed executed transaction. +#[derive(PartialEq, Eq, Debug, Clone)] +pub enum RollbackAccounts { + FeePayerOnly { + fee_payer: KeyedAccountSharedData, + }, + SameNonceAndFeePayer { + nonce: KeyedAccountSharedData, + }, + SeparateNonceAndFeePayer { + nonce: KeyedAccountSharedData, + fee_payer: KeyedAccountSharedData, + }, +} + +#[cfg(feature = "dev-context-only-utils")] +impl Default for RollbackAccounts { + fn default() -> Self { + Self::FeePayerOnly { + fee_payer: KeyedAccountSharedData::default(), + } + } +} + +/// Rollback accounts iterator. +/// This struct is created by the `RollbackAccounts::iter`. +pub struct RollbackAccountsIter<'a> { + fee_payer: Option<&'a KeyedAccountSharedData>, + nonce: Option<&'a KeyedAccountSharedData>, +} + +impl<'a> Iterator for RollbackAccountsIter<'a> { + type Item = &'a KeyedAccountSharedData; + + fn next(&mut self) -> Option { + if let Some(fee_payer) = self.fee_payer.take() { + return Some(fee_payer); + } + if let Some(nonce) = self.nonce.take() { + return Some(nonce); + } + None + } +} + +impl<'a> IntoIterator for &'a RollbackAccounts { + type Item = &'a KeyedAccountSharedData; + type IntoIter = RollbackAccountsIter<'a>; + + fn into_iter(self) -> Self::IntoIter { + self.iter() + } +} + +impl RollbackAccounts { + pub(crate) fn new( + nonce: Option, + fee_payer_address: Pubkey, + mut fee_payer_account: AccountSharedData, + fee_payer_loaded_rent_epoch: Epoch, + ) -> Self { + if let Some(nonce) = nonce { + if &fee_payer_address == nonce.address() { + // `nonce` contains an AccountSharedData which has already been advanced to the current DurableNonce + // `fee_payer_account` is an AccountSharedData as it currently exists on-chain + // thus if the nonce account is being used as the fee payer, we need to update that data here + // so we capture both the data change for the nonce and the lamports/rent epoch change for the fee payer + fee_payer_account.set_data_from_slice(nonce.account().data()); + + RollbackAccounts::SameNonceAndFeePayer { + nonce: (fee_payer_address, fee_payer_account), + } + } else { + RollbackAccounts::SeparateNonceAndFeePayer { + nonce: (nonce.address, nonce.account), + fee_payer: (fee_payer_address, fee_payer_account), + } + } + } else { + // When rolling back failed transactions which don't use nonces, the + // runtime should not update the fee payer's rent epoch so reset the + // rollback fee payer account's rent epoch to its originally loaded + // rent epoch value. In the future, a feature gate could be used to + // alter this behavior such that rent epoch updates are handled the + // same for both nonce and non-nonce failed transactions. + fee_payer_account.set_rent_epoch(fee_payer_loaded_rent_epoch); + RollbackAccounts::FeePayerOnly { + fee_payer: (fee_payer_address, fee_payer_account), + } + } + } + + /// Return a reference to the fee payer account. + pub fn fee_payer(&self) -> &KeyedAccountSharedData { + match self { + Self::FeePayerOnly { fee_payer } => fee_payer, + Self::SameNonceAndFeePayer { nonce } => nonce, + Self::SeparateNonceAndFeePayer { fee_payer, .. } => fee_payer, + } + } + + /// Number of accounts tracked for rollback + pub fn count(&self) -> usize { + match self { + Self::FeePayerOnly { .. } | Self::SameNonceAndFeePayer { .. } => 1, + Self::SeparateNonceAndFeePayer { .. } => 2, + } + } + + /// Iterator over accounts tracked for rollback. + pub fn iter(&self) -> RollbackAccountsIter<'_> { + match self { + Self::FeePayerOnly { fee_payer } => RollbackAccountsIter { + fee_payer: Some(fee_payer), + nonce: None, + }, + Self::SameNonceAndFeePayer { nonce } => RollbackAccountsIter { + fee_payer: None, + nonce: Some(nonce), + }, + Self::SeparateNonceAndFeePayer { nonce, fee_payer } => RollbackAccountsIter { + fee_payer: Some(fee_payer), + nonce: Some(nonce), + }, + } + } + + // Size of accounts tracked for rollback, used internally when calculating the actual + // loaded transaction data size for the cost model. This function will be removed by + // the fee-payer data size amendment to SIMD-186. + pub(crate) fn data_size(&self) -> usize { + let mut total_size: usize = 0; + for (_, account) in self.iter() { + total_size = total_size.saturating_add(account.data().len()); + } + total_size + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_account::{ReadableAccount, WritableAccount}, + solana_hash::Hash, + solana_nonce::{ + state::{Data as NonceData, DurableNonce, State as NonceState}, + versions::Versions as NonceVersions, + }, + solana_sdk_ids::system_program, + }; + + #[test] + fn test_new_fee_payer_only() { + let fee_payer_address = Pubkey::new_unique(); + let fee_payer_account = AccountSharedData::new(100, 0, &Pubkey::default()); + let fee_payer_rent_epoch = fee_payer_account.rent_epoch(); + + let rent_epoch_updated_fee_payer_account = { + let mut account = fee_payer_account.clone(); + account.set_lamports(fee_payer_account.lamports()); + account.set_rent_epoch(fee_payer_rent_epoch + 1); + account + }; + + let rollback_accounts = RollbackAccounts::new( + None, + fee_payer_address, + rent_epoch_updated_fee_payer_account, + fee_payer_rent_epoch, + ); + + let expected_fee_payer = (fee_payer_address, fee_payer_account); + match rollback_accounts { + RollbackAccounts::FeePayerOnly { fee_payer } => { + assert_eq!(expected_fee_payer, fee_payer); + } + _ => panic!("Expected FeePayerOnly variant"), + } + } + + #[test] + fn test_new_same_nonce_and_fee_payer() { + let nonce_address = Pubkey::new_unique(); + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let lamports_per_signature = 42; + let nonce_account = AccountSharedData::new_data( + 43, + &NonceVersions::new(NonceState::Initialized(NonceData::new( + Pubkey::default(), + durable_nonce, + lamports_per_signature, + ))), + &system_program::id(), + ) + .unwrap(); + + let rent_epoch_updated_fee_payer_account = { + let mut account = nonce_account.clone(); + account.set_lamports(nonce_account.lamports()); + account + }; + + let nonce = NonceInfo::new(nonce_address, rent_epoch_updated_fee_payer_account.clone()); + let rollback_accounts = RollbackAccounts::new( + Some(nonce), + nonce_address, + rent_epoch_updated_fee_payer_account, + u64::MAX, // ignored + ); + + let expected_rollback_accounts = RollbackAccounts::SameNonceAndFeePayer { + nonce: (nonce_address, nonce_account), + }; + + assert_eq!(expected_rollback_accounts, rollback_accounts); + } + + #[test] + fn test_separate_nonce_and_fee_payer() { + let nonce_address = Pubkey::new_unique(); + let durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let lamports_per_signature = 42; + let nonce_account = AccountSharedData::new_data( + 43, + &NonceVersions::new(NonceState::Initialized(NonceData::new( + Pubkey::default(), + durable_nonce, + lamports_per_signature, + ))), + &system_program::id(), + ) + .unwrap(); + + let fee_payer_address = Pubkey::new_unique(); + let fee_payer_account = AccountSharedData::new(44, 0, &Pubkey::default()); + + let rent_epoch_updated_fee_payer_account = { + let mut account = fee_payer_account.clone(); + account.set_lamports(fee_payer_account.lamports()); + account + }; + + let nonce = NonceInfo::new(nonce_address, nonce_account.clone()); + let rollback_accounts = RollbackAccounts::new( + Some(nonce), + fee_payer_address, + rent_epoch_updated_fee_payer_account, + u64::MAX, // ignored + ); + + let expected_nonce = (nonce_address, nonce_account); + let expected_fee_payer = (fee_payer_address, fee_payer_account); + match rollback_accounts { + RollbackAccounts::SeparateNonceAndFeePayer { nonce, fee_payer } => { + assert_eq!(expected_nonce, nonce); + assert_eq!(expected_fee_payer, fee_payer); + } + _ => panic!("Expected SeparateNonceAndFeePayer variant"), + } + } +} diff --git a/solana/svm/src/transaction_account_state_info.rs b/solana/svm/src/transaction_account_state_info.rs new file mode 100644 index 0000000..bd8e4ef --- /dev/null +++ b/solana/svm/src/transaction_account_state_info.rs @@ -0,0 +1,309 @@ +use { + crate::rent_calculator::{RentState, check_rent_state, get_account_rent_state}, + solana_account::ReadableAccount, + solana_rent::Rent, + solana_svm_transaction::svm_message::SVMMessage, + solana_transaction_context::{IndexOfAccount, transaction::TransactionContext}, + solana_transaction_error::TransactionResult as Result, +}; + +#[derive(PartialEq, Debug)] +pub(crate) struct TransactionAccountStateInfo { + info: Option, // None: readonly account +} + +impl TransactionAccountStateInfo { + pub(crate) fn new( + transaction_context: &TransactionContext, + message: &impl SVMMessage, + rent: &Rent, + ) -> Vec { + (0..message.account_keys().len()) + .map(|i| { + let info = if message.is_writable(i) { + let state = if let Ok(account) = transaction_context + .accounts() + .try_borrow(i as IndexOfAccount) + { + let balance = account.lamports(); + let data_size = account.data().len(); + let rent_state = get_account_rent_state(rent, balance, data_size); + Some(WritableTransactionAccountStateInfo { + rent_state, + data_size, + }) + } else { + None + }; + debug_assert!( + state.is_some(), + "message and transaction context out of sync, fatal" + ); + state + } else { + None + }; + Self { info } + }) + .collect() + } + + pub(crate) fn verify_changes( + pre_state_infos: &[Self], + post_state_infos: &[Self], + transaction_context: &TransactionContext, + ) -> Result<()> { + for (i, (pre_state_info, post_state_info)) in + pre_state_infos.iter().zip(post_state_infos).enumerate() + { + if let (Some(pre_state_info), Some(post_state_info)) = + (pre_state_info.info.as_ref(), post_state_info.info.as_ref()) + { + check_rent_state( + &pre_state_info.rent_state, + &post_state_info.rent_state, + transaction_context, + i as IndexOfAccount, + )?; + } + } + Ok(()) + } +} + +#[derive(PartialEq, Debug)] +struct WritableTransactionAccountStateInfo { + rent_state: RentState, + data_size: usize, +} + +// Returns the cumulative size of all post-exec uninitialized accounts +pub(crate) fn get_uninitialized_accounts_size(post: &[TransactionAccountStateInfo]) -> u64 { + post.iter() + .filter_map(|post_info| post_info.info.as_ref()) + .filter_map(|post| { + matches!(&post.rent_state, RentState::Uninitialized).then_some(post.data_size as u64) + }) + .sum() +} + +#[cfg(test)] +mod test { + use { + super::*, + solana_account::AccountSharedData, + solana_hash::Hash, + solana_keypair::Keypair, + solana_message::{ + LegacyMessage, Message, MessageHeader, SanitizedMessage, + compiled_instruction::CompiledInstruction, + }, + solana_rent::Rent, + solana_signer::Signer, + solana_transaction_context::transaction::TransactionContext, + solana_transaction_error::TransactionError, + std::collections::HashSet, + }; + + #[test] + fn test_new() { + let rent = Rent::default(); + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + let key4 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key4.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20, 1); + let result = TransactionAccountStateInfo::new(&context, &sanitized_message, &rent); + assert_eq!( + result, + vec![ + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }) + }, + TransactionAccountStateInfo { info: None }, + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }) + } + ] + ); + } + + #[test] + #[should_panic(expected = "message and transaction context out of sync, fatal")] + fn test_new_panic() { + let rent = Rent::default(); + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let key3 = Keypair::new(); + let key4 = Keypair::new(); + + let message = Message { + account_keys: vec![key2.pubkey(), key1.pubkey(), key4.pubkey(), key3.pubkey()], + header: MessageHeader::default(), + instructions: vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![2], + data: vec![], + }, + ], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())); + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + (key3.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, rent.clone(), 20, 20, 1); + let _result = TransactionAccountStateInfo::new(&context, &sanitized_message, &rent); + } + + #[test] + fn test_verify_changes() { + let key1 = Keypair::new(); + let key2 = Keypair::new(); + let pre_rent_state = vec![ + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }), + }, + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }), + }, + ]; + let post_rent_state = vec![TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 1); + + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert!(result.is_ok()); + + let pre_rent_state = vec![TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 0, + }), + }]; + let post_rent_state = vec![TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::RentPaying { + data_size: 2, + lamports: 5, + }, + data_size: 2, + }), + }]; + + let transaction_accounts = vec![ + (key1.pubkey(), AccountSharedData::default()), + (key2.pubkey(), AccountSharedData::default()), + ]; + + let context = TransactionContext::new(transaction_accounts, Rent::default(), 20, 20, 1); + let result = TransactionAccountStateInfo::verify_changes( + &pre_rent_state, + &post_rent_state, + &context, + ); + assert_eq!( + result.err(), + Some(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + } + + #[test] + fn test_get_uninitialized_accounts_size_with_deleted_accounts() { + let post_state_infos = vec![ + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 50, + }), + }, + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 50, + }), + }, + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::Uninitialized, + data_size: 50, + }), + }, + TransactionAccountStateInfo { + info: Some(WritableTransactionAccountStateInfo { + rent_state: RentState::RentExempt, + data_size: 50, + }), + }, + ]; + + // 3 deleted accounts should contribute 3 * (50) = 150 to the count + assert_eq!(get_uninitialized_accounts_size(&post_state_infos), 150); + } +} diff --git a/solana/svm/src/transaction_balances.rs b/solana/svm/src/transaction_balances.rs new file mode 100644 index 0000000..97a54a9 --- /dev/null +++ b/solana/svm/src/transaction_balances.rs @@ -0,0 +1,204 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::field_qualifiers; +use { + crate::{ + account_loader::AccountLoader, + transaction_processing_callback::TransactionProcessingCallback, + }, + solana_account::{AccountSharedData, ReadableAccount}, + solana_pubkey::Pubkey, + solana_svm_transaction::svm_transaction::SVMTransaction, + spl_generic_token::{generic_token, is_known_spl_token_id}, +}; + +// we use internal aliases for clarity, the external type aliases are often confusing +type TxNativeBalances = Vec; +type TxTokenBalances = Vec; +type BatchNativeBalances = Vec; +type BatchTokenBalances = Vec; + +// to operate cleanly over Option we use a trait impled on the outer and inner type +pub(crate) trait BalanceCollectionRoutines { + fn collect_pre_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ); + + fn collect_post_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ); +} + +#[derive(Debug, Default)] +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(native_pre(pub), native_post(pub), token_pre(pub), token_post(pub),) +)] +pub struct BalanceCollector { + native_pre: BatchNativeBalances, + native_post: BatchNativeBalances, + token_pre: BatchTokenBalances, + token_post: BatchTokenBalances, +} + +impl BalanceCollector { + // we always provide one vec for every transaction, even if the vecs are empty + pub(crate) fn new_with_transaction_count(transaction_count: usize) -> Self { + Self { + native_pre: Vec::with_capacity(transaction_count), + native_post: Vec::with_capacity(transaction_count), + token_pre: Vec::with_capacity(transaction_count), + token_post: Vec::with_capacity(transaction_count), + } + } + + // we use this pattern to prevent anything outside svm mutating BalanceCollector internals + // with no public constructor, and only private fields, non-svm code can only disassemble the struct + pub fn into_vecs( + self, + ) -> ( + BatchNativeBalances, + BatchNativeBalances, + BatchTokenBalances, + BatchTokenBalances, + ) { + ( + self.native_pre, + self.native_post, + self.token_pre, + self.token_post, + ) + } + + // gather native lamport balances for all accounts + // and token balances for valid, initialized token accounts with valid, initialized mints + fn collect_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ) -> (TxNativeBalances, TxTokenBalances) { + let mut native_balances = Vec::with_capacity(transaction.account_keys().len()); + let mut token_balances = vec![]; + + let has_token_program = transaction.account_keys().iter().any(is_known_spl_token_id); + + for (index, key) in transaction.account_keys().iter().enumerate() { + let Some(account) = account_loader.load_account(key) else { + native_balances.push(0); + continue; + }; + + native_balances.push(account.lamports()); + + if has_token_program + && !transaction.is_invoked(index) + && !is_known_spl_token_id(key) + && is_known_spl_token_id(account.owner()) + && let Some(token_info) = + SvmTokenInfo::unpack_token_account(account_loader, &account, index) + { + token_balances.push(token_info); + } + } + + (native_balances, token_balances) + } + + pub(crate) fn lengths_match_expected(&self, expected_len: usize) -> bool { + self.native_pre.len() == expected_len + && self.native_post.len() == expected_len + && self.token_pre.len() == expected_len + && self.token_post.len() == expected_len + } +} + +impl BalanceCollectionRoutines for BalanceCollector { + fn collect_pre_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ) { + let (native_balances, token_balances) = self.collect_balances(account_loader, transaction); + self.native_pre.push(native_balances); + self.token_pre.push(token_balances); + } + + fn collect_post_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ) { + let (native_balances, token_balances) = self.collect_balances(account_loader, transaction); + self.native_post.push(native_balances); + self.token_post.push(token_balances); + } +} + +impl BalanceCollectionRoutines for Option { + fn collect_pre_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ) { + if let Some(inner) = self { + inner.collect_pre_balances(account_loader, transaction) + } + } + + fn collect_post_balances( + &mut self, + account_loader: &mut AccountLoader, + transaction: &impl SVMTransaction, + ) { + if let Some(inner) = self { + inner.collect_post_balances(account_loader, transaction) + } + } +} + +// this contains all the information we can provide to construct TransactionTokenBalance +// that type, in ledger, depends on UiTokenAmount from account-decoder, so we cannot build it here +#[derive(Debug, Clone, PartialEq)] +pub struct SvmTokenInfo { + pub account_index: u8, + pub mint: Pubkey, + pub amount: u64, + pub owner: Pubkey, + pub program_id: Pubkey, + pub decimals: u8, +} + +impl SvmTokenInfo { + fn unpack_token_account( + account_loader: &mut AccountLoader, + account: &AccountSharedData, + index: usize, + ) -> Option { + let program_id = *account.owner(); + let generic_token::Account { + mint, + owner, + amount, + } = generic_token::Account::unpack(account.data(), &program_id)?; + + let mint_account = account_loader.load_account(&mint)?; + if *mint_account.owner() != program_id { + return None; + } + + let generic_token::Mint { decimals, .. } = + generic_token::Mint::unpack(mint_account.data(), &program_id)?; + + Some(Self { + account_index: index.try_into().ok()?, + mint, + amount, + owner, + program_id, + decimals, + }) + } +} diff --git a/solana/svm/src/transaction_commit_result.rs b/solana/svm/src/transaction_commit_result.rs new file mode 100644 index 0000000..7fdc51a --- /dev/null +++ b/solana/svm/src/transaction_commit_result.rs @@ -0,0 +1,39 @@ +use { + crate::transaction_execution_result::TransactionLoadedAccountsStats, + solana_fee_structure::FeeDetails, solana_message::inner_instruction::InnerInstructionsList, + solana_transaction_context::transaction::TransactionReturnData, + solana_transaction_error::TransactionResult, +}; + +pub type TransactionCommitResult = TransactionResult; + +#[derive(Clone, Debug)] +#[cfg_attr(feature = "dev-context-only-utils", derive(PartialEq))] +pub struct CommittedTransaction { + pub status: TransactionResult<()>, + pub log_messages: Option>, + pub inner_instructions: Option, + pub return_data: Option, + pub executed_units: u64, + pub fee_details: FeeDetails, + pub loaded_account_stats: TransactionLoadedAccountsStats, + pub fee_payer_post_balance: u64, +} + +pub trait TransactionCommitResultExtensions { + fn was_committed(&self) -> bool; + fn was_executed_successfully(&self) -> bool; +} + +impl TransactionCommitResultExtensions for TransactionCommitResult { + fn was_committed(&self) -> bool { + self.is_ok() + } + + fn was_executed_successfully(&self) -> bool { + match self { + Ok(committed_tx) => committed_tx.status.is_ok(), + Err(_) => false, + } + } +} diff --git a/solana/svm/src/transaction_error_metrics.rs b/solana/svm/src/transaction_error_metrics.rs new file mode 100644 index 0000000..8f34539 --- /dev/null +++ b/solana/svm/src/transaction_error_metrics.rs @@ -0,0 +1,63 @@ +use std::num::Saturating; + +#[derive(Debug, Default)] +pub struct TransactionErrorMetrics { + pub total: Saturating, + pub account_in_use: Saturating, + pub too_many_account_locks: Saturating, + pub account_loaded_twice: Saturating, + pub account_not_found: Saturating, + pub blockhash_not_found: Saturating, + pub blockhash_too_old: Saturating, + pub call_chain_too_deep: Saturating, + pub already_processed: Saturating, + pub instruction_error: Saturating, + pub insufficient_funds: Saturating, + pub invalid_account_for_fee: Saturating, + pub invalid_account_index: Saturating, + pub invalid_program_for_execution: Saturating, + pub invalid_compute_budget: Saturating, + pub not_allowed_during_cluster_maintenance: Saturating, + pub invalid_writable_account: Saturating, + pub invalid_rent_paying_account: Saturating, + pub would_exceed_max_block_cost_limit: Saturating, + pub would_exceed_max_account_cost_limit: Saturating, + pub would_exceed_max_vote_cost_limit: Saturating, + pub would_exceed_account_data_block_limit: Saturating, + pub max_loaded_accounts_data_size_exceeded: Saturating, + pub program_execution_temporarily_restricted: Saturating, +} + +impl TransactionErrorMetrics { + pub fn new() -> Self { + Self::default() + } + + pub fn accumulate(&mut self, other: &TransactionErrorMetrics) { + self.total += other.total; + self.account_in_use += other.account_in_use; + self.too_many_account_locks += other.too_many_account_locks; + self.account_loaded_twice += other.account_loaded_twice; + self.account_not_found += other.account_not_found; + self.blockhash_not_found += other.blockhash_not_found; + self.blockhash_too_old += other.blockhash_too_old; + self.call_chain_too_deep += other.call_chain_too_deep; + self.already_processed += other.already_processed; + self.instruction_error += other.instruction_error; + self.insufficient_funds += other.insufficient_funds; + self.invalid_account_for_fee += other.invalid_account_for_fee; + self.invalid_account_index += other.invalid_account_index; + self.invalid_program_for_execution += other.invalid_program_for_execution; + self.invalid_compute_budget += other.invalid_compute_budget; + self.not_allowed_during_cluster_maintenance += other.not_allowed_during_cluster_maintenance; + self.invalid_writable_account += other.invalid_writable_account; + self.invalid_rent_paying_account += other.invalid_rent_paying_account; + self.would_exceed_max_block_cost_limit += other.would_exceed_max_block_cost_limit; + self.would_exceed_max_account_cost_limit += other.would_exceed_max_account_cost_limit; + self.would_exceed_max_vote_cost_limit += other.would_exceed_max_vote_cost_limit; + self.would_exceed_account_data_block_limit += other.would_exceed_account_data_block_limit; + self.max_loaded_accounts_data_size_exceeded += other.max_loaded_accounts_data_size_exceeded; + self.program_execution_temporarily_restricted += + other.program_execution_temporarily_restricted; + } +} diff --git a/solana/svm/src/transaction_execution_result.rs b/solana/svm/src/transaction_execution_result.rs new file mode 100644 index 0000000..dd4dd88 --- /dev/null +++ b/solana/svm/src/transaction_execution_result.rs @@ -0,0 +1,54 @@ +use { + crate::account_loader::LoadedTransaction, + solana_message::inner_instruction::InnerInstructionsList, + solana_program_runtime::program_cache_entry::ProgramCacheEntry, + solana_pubkey::Pubkey, + solana_transaction_context::transaction::TransactionReturnData, + solana_transaction_error::TransactionResult, + std::{collections::HashMap, sync::Arc}, +}; + +#[derive(Debug, Default, Clone, PartialEq)] +pub struct TransactionLoadedAccountsStats { + pub loaded_accounts_data_size: u32, + pub loaded_accounts_count: usize, +} + +#[derive(Debug, Clone)] +pub struct ExecutedTransaction { + pub loaded_transaction: LoadedTransaction, + pub execution_details: TransactionExecutionDetails, + pub programs_modified_by_tx: HashMap>, +} + +impl ExecutedTransaction { + pub fn was_successful(&self) -> bool { + self.execution_details.was_successful() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct TransactionExecutionDetails { + pub status: TransactionResult<()>, + pub log_messages: Option>, + pub inner_instructions: Option, + pub return_data: Option, + pub executed_units: u64, + /// deltas related to total account data size changes for this transaction. + /// NOTE: set to None IFF `status` is not `Ok`. + pub accounts_deltas: Option, +} + +impl TransactionExecutionDetails { + pub fn was_successful(&self) -> bool { + self.status.is_ok() + } +} + +#[derive(Clone, Debug, Eq, PartialEq)] +pub struct AccountsDeltas { + /// aggregate resize delta across all accounts touched by the transaction + pub accounts_resize_delta: i64, + /// aggregate size of all accounts that were uninitialized by this transaction + pub accounts_uninitialized_size: u64, +} diff --git a/solana/svm/src/transaction_processing_callback.rs b/solana/svm/src/transaction_processing_callback.rs new file mode 100644 index 0000000..ef1d7ca --- /dev/null +++ b/solana/svm/src/transaction_processing_callback.rs @@ -0,0 +1 @@ +pub use solana_svm_callback::{AccountState, TransactionProcessingCallback}; diff --git a/solana/svm/src/transaction_processing_result.rs b/solana/svm/src/transaction_processing_result.rs new file mode 100644 index 0000000..4805f48 --- /dev/null +++ b/solana/svm/src/transaction_processing_result.rs @@ -0,0 +1,100 @@ +use { + crate::{ + account_loader::FeesOnlyTransaction, + transaction_execution_result::{ExecutedTransaction, TransactionExecutionDetails}, + }, + solana_fee_structure::FeeDetails, + solana_transaction_error::{TransactionError, TransactionResult}, +}; + +pub type TransactionProcessingResult = TransactionResult; + +pub trait TransactionProcessingResultExtensions { + fn was_processed(&self) -> bool; + fn was_processed_with_successful_result(&self) -> bool; + fn processed_transaction(&self) -> Option<&ProcessedTransaction>; + fn flattened_result(&self) -> TransactionResult<()>; +} + +#[derive(Debug)] +pub enum ProcessedTransaction { + /// Transaction was executed, but if execution failed, all account state changes + /// will be rolled back except deducted fees and any advanced nonces + Executed(Box), + /// Transaction was not able to be executed but fees are able to be + /// collected and any nonces are advanceable + FeesOnly(Box), +} + +impl TransactionProcessingResultExtensions for TransactionProcessingResult { + fn was_processed(&self) -> bool { + self.is_ok() + } + + fn was_processed_with_successful_result(&self) -> bool { + match self { + Ok(processed_tx) => processed_tx.was_processed_with_successful_result(), + Err(_) => false, + } + } + + fn processed_transaction(&self) -> Option<&ProcessedTransaction> { + self.as_ref().ok() + } + + fn flattened_result(&self) -> TransactionResult<()> { + self.as_ref() + .map_err(|err| err.clone()) + .and_then(|processed_tx| processed_tx.status()) + } +} + +impl ProcessedTransaction { + fn was_processed_with_successful_result(&self) -> bool { + match self { + Self::Executed(executed_tx) => executed_tx.execution_details.status.is_ok(), + Self::FeesOnly(_) => false, + } + } + + pub fn status(&self) -> TransactionResult<()> { + match self { + Self::Executed(executed_tx) => executed_tx.execution_details.status.clone(), + Self::FeesOnly(details) => Err(TransactionError::clone(&details.load_error)), + } + } + + pub fn fee_details(&self) -> FeeDetails { + match self { + Self::Executed(executed_tx) => executed_tx.loaded_transaction.fee_details, + Self::FeesOnly(details) => details.fee_details, + } + } + + pub fn executed_transaction(&self) -> Option<&ExecutedTransaction> { + match self { + Self::Executed(context) => Some(context), + Self::FeesOnly { .. } => None, + } + } + + pub fn execution_details(&self) -> Option<&TransactionExecutionDetails> { + match self { + Self::Executed(context) => Some(&context.execution_details), + Self::FeesOnly { .. } => None, + } + } + + pub fn executed_units(&self) -> u64 { + self.execution_details() + .map(|detail| detail.executed_units) + .unwrap_or_default() + } + + pub fn loaded_accounts_data_size(&self) -> u32 { + match self { + Self::Executed(context) => context.loaded_transaction.loaded_accounts_data_size, + Self::FeesOnly(details) => details.loaded_accounts_data_size, + } + } +} diff --git a/solana/svm/src/transaction_processor.rs b/solana/svm/src/transaction_processor.rs new file mode 100644 index 0000000..235d6b9 --- /dev/null +++ b/solana/svm/src/transaction_processor.rs @@ -0,0 +1,2774 @@ +use { + crate::{ + account_loader::{ + AccountLoader, CheckedTransactionDetails, LoadedTransaction, PROGRAM_OWNERS, + TransactionCheckResult, TransactionLoadResult, ValidatedTransactionDetails, + load_transaction, update_rent_exempt_status_for_account, validate_fee_payer, + }, + account_overrides::AccountOverrides, + message_processor::process_message, + nonce_info::NonceInfo, + program_loader::{get_program_deployment_slot, load_program_with_pubkey}, + rollback_accounts::RollbackAccounts, + transaction_account_state_info::{ + TransactionAccountStateInfo, get_uninitialized_accounts_size, + }, + transaction_balances::{BalanceCollectionRoutines, BalanceCollector}, + transaction_error_metrics::TransactionErrorMetrics, + transaction_execution_result::{ + AccountsDeltas, ExecutedTransaction, TransactionExecutionDetails, + }, + transaction_processing_result::{ProcessedTransaction, TransactionProcessingResult}, + }, + log::debug, + percentage::Percentage, + solana_account::{AccountSharedData, ReadableAccount, state_traits::StateMut}, + solana_clock::{Epoch, Slot}, + solana_hash::Hash, + solana_instruction::TRANSACTION_LEVEL_STACK_HEIGHT, + solana_message::{ + compiled_instruction::CompiledInstruction, + inner_instruction::{InnerInstruction, InnerInstructionsList}, + }, + solana_nonce::{ + NONCED_TX_MARKER_IX_INDEX, + state::{DurableNonce, State as NonceState}, + versions::Versions as NonceVersions, + }, + solana_nonce_account::{SystemAccountKind, get_system_account_kind, verify_nonce_account}, + solana_program_runtime::{ + execution_budget::{ + SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionCost, + }, + invoke_context::{EnvironmentConfig, InvokeContext}, + loaded_programs::{ + EpochBoundaryPreparation, ForkGraph, ProgramCache, ProgramCacheForTxBatch, + ProgramCacheMatchCriteria, ProgramRuntimeEnvironment, ProgramRuntimeEnvironments, + }, + program_cache_entry::ProgramCacheEntry, + solana_sbpf::{program::BuiltinProgram, vm::Config as VmConfig}, + sysvar_cache::SysvarCache, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_svm_callback::TransactionProcessingCallback, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_log_collector::LogCollector, + solana_svm_measure::{measure::Measure, measure_us}, + solana_svm_timings::{ExecuteTimingType, ExecuteTimings}, + solana_svm_transaction::{svm_message::SVMMessage, svm_transaction::SVMTransaction}, + solana_svm_type_overrides::sync::{Arc, RwLock, RwLockReadGuard, atomic::Ordering}, + solana_transaction_context::transaction::{ExecutionRecord, TransactionContext}, + solana_transaction_error::{TransactionError, TransactionResult}, + std::{ + collections::{HashMap, HashSet}, + fmt::{Debug, Formatter}, + rc::Rc, + }, +}; +#[cfg(feature = "dev-context-only-utils")] +use { + qualifier_attr::{field_qualifiers, qualifiers}, + std::sync::Weak, +}; + +/// A list of log messages emitted during a transaction +pub type TransactionLogMessages = Vec; + +/// The output of the transaction batch processor's +/// `load_and_execute_sanitized_transactions` method. +pub struct LoadAndExecuteSanitizedTransactionsOutput { + /// Error metrics for transactions that were processed. + pub error_metrics: TransactionErrorMetrics, + /// Timings for transaction batch execution. + pub execute_timings: ExecuteTimings, + /// Vector of results indicating whether a transaction was processed or + /// could not be processed. Note processed transactions can still have a + /// failure result meaning that the transaction will be rolled back. + pub processing_results: Vec, + /// Balances accumulated for TransactionStatusSender when + /// transaction balance recording is enabled. + pub balance_collector: Option, +} + +/// Configuration of the recording capabilities for transaction execution +#[derive(Copy, Clone, Default)] +pub struct ExecutionRecordingConfig { + pub enable_cpi_recording: bool, + pub enable_log_recording: bool, + pub enable_return_data_recording: bool, + pub enable_transaction_balance_recording: bool, +} + +impl ExecutionRecordingConfig { + pub fn new_single_setting(option: bool) -> Self { + ExecutionRecordingConfig { + enable_return_data_recording: option, + enable_log_recording: option, + enable_cpi_recording: option, + enable_transaction_balance_recording: option, + } + } +} + +/// Configurations for processing transactions. +#[derive(Default)] +pub struct TransactionProcessingConfig<'a> { + /// Encapsulates overridden accounts, typically used for transaction + /// simulation. + pub account_overrides: Option<&'a AccountOverrides>, + /// Whether or not to check a program's deployment slot when replenishing + /// a program cache instance. + pub check_program_deployment_slot: bool, + /// The maximum number of bytes that log messages can consume. + pub log_messages_bytes_limit: Option, + /// Whether to limit the number of programs loaded for the transaction + /// batch. + pub limit_to_load_programs: bool, + /// Recording capabilities for transaction execution. + pub recording_config: ExecutionRecordingConfig, + /// Should failing transactions within the batch be dropped (no fee charged + /// & not committed). + pub drop_on_failure: bool, + /// If any transaction in the batch is not committed then the entire batch + /// should not be committed. + /// + /// # Note + /// + /// Without `drop_on_failure` this flag will still allow processed but + /// failing transactions to be committed. If both flags are set then any + /// failing transaction will cause all transactions to be aborted. + pub all_or_nothing: bool, + /// Strictly require durable nonce accounts to have the canonical nonce account size. + /// + /// This is a leader-side filtering policy. It must not be enabled for replay. + pub strict_nonce_size_check: bool, +} + +/// Runtime environment for transaction batch processing. +pub struct TransactionProcessingEnvironment { + /// The blockhash to use for the transaction batch. + pub blockhash: Hash, + /// Lamports per signature that corresponds to this blockhash. + /// + /// Note: This value is primarily used for nonce accounts. If set to zero, + /// it will disable transaction fees. However, any non-zero value will not + /// change transaction fees. For this reason, it is recommended to use the + /// `fee_per_signature` field to adjust transaction fees. + pub blockhash_lamports_per_signature: u64, + /// Whether the alpenglow migration has completed for this bank context. + pub alpenglow_migration_succeeded: bool, + /// The total stake for the current epoch. + pub epoch_total_stake: u64, + /// Runtime feature set to use for the transaction batch. + pub feature_set: SVMFeatureSet, + /// Program runtime environments for execution and deployment. + pub program_runtime_environments: ProgramRuntimeEnvironments, + /// Rent calculator to use for the transaction batch. + pub rent: Rent, +} + +#[cfg(feature = "dev-context-only-utils")] +pub fn get_mock_transaction_processing_environment() -> TransactionProcessingEnvironment { + TransactionProcessingEnvironment { + blockhash: Hash::default(), + blockhash_lamports_per_signature: 0, + alpenglow_migration_succeeded: false, + epoch_total_stake: 0, + feature_set: SVMFeatureSet::default(), + program_runtime_environments: ProgramRuntimeEnvironments::mock(), + rent: Rent::default(), + } +} + +#[cfg_attr(feature = "frozen-abi", derive(AbiExample))] +#[cfg_attr( + feature = "dev-context-only-utils", + field_qualifiers(slot(pub), epoch(pub), sysvar_cache(pub)) +)] +pub struct TransactionBatchProcessor { + /// Bank slot (i.e. block) + slot: Slot, + + /// Bank epoch + epoch: Epoch, + + /// SysvarCache is a collection of system variables that are + /// accessible from on chain programs. It is passed to SVM from + /// client code (e.g. Bank) and forwarded to process_message. + sysvar_cache: RwLock, + + /// Anticipates the environments of the upcoming epoch + pub epoch_boundary_preparation: Arc>, + + /// Programs required for transaction batch processing + pub global_program_cache: Arc>>, + + /// ProgramRuntimeEnvironment of the current epoch + pub program_runtime_environment: ProgramRuntimeEnvironment, + + /// Builtin program ids + pub builtin_program_ids: RwLock>, + + /// Cached ProgramCacheForTxBatch pre-populated with builtin entries. + /// Populated once per block in `new_from()` from the global program cache, + /// avoiding re-acquiring the lock and re-running extract() on every batch. + builtin_program_cache: RwLock, + + execution_cost: SVMTransactionExecutionCost, +} + +impl Debug for TransactionBatchProcessor { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("TransactionBatchProcessor") + .field("slot", &self.slot) + .field("epoch", &self.epoch) + .field("sysvar_cache", &self.sysvar_cache) + .field("global_program_cache", &self.global_program_cache) + .finish() + } +} + +impl Default for TransactionBatchProcessor { + fn default() -> Self { + Self { + slot: Slot::default(), + epoch: Epoch::default(), + sysvar_cache: RwLock::::default(), + epoch_boundary_preparation: Arc::new(RwLock::new(EpochBoundaryPreparation::default())), + global_program_cache: Arc::new(RwLock::new(ProgramCache::new(Slot::default()))), + program_runtime_environment: ProgramRuntimeEnvironment::from( + BuiltinProgram::new_loader(VmConfig::default()), + ), + builtin_program_ids: RwLock::new(HashSet::new()), + builtin_program_cache: RwLock::new(ProgramCacheForTxBatch::new(Slot::default())), + execution_cost: SVMTransactionExecutionCost::default(), + } + } +} + +impl TransactionBatchProcessor { + /// Create a new, uninitialized `TransactionBatchProcessor`. + /// + /// In this context, uninitialized means that the `TransactionBatchProcessor` + /// has been initialized with an empty program cache. The cache contains no + /// programs (including builtins) and has not been configured with a valid + /// fork graph. + /// + /// When using this method, it's advisable to call `set_fork_graph_in_program_cache` + /// as well as `add_builtin` to configure the cache before using the processor. + pub fn new_uninitialized(slot: Slot, epoch: Epoch) -> Self { + let epoch_boundary_preparation = + Arc::new(RwLock::new(EpochBoundaryPreparation::new(epoch))); + Self { + slot, + epoch, + epoch_boundary_preparation, + global_program_cache: Arc::new(RwLock::new(ProgramCache::new(slot))), + builtin_program_cache: RwLock::new(ProgramCacheForTxBatch::new(slot)), + ..Self::default() + } + } + + /// Create a new `TransactionBatchProcessor`. + /// + /// The created processor's program cache is initialized with the provided + /// fork graph and loaders. If any loaders are omitted, a default "empty" + /// loader (no syscalls) will be used. + /// + /// The cache will still not contain any builtin programs. It's advisable to + /// call `add_builtin` to add the required builtins before using the processor. + #[cfg(feature = "dev-context-only-utils")] + pub fn new( + slot: Slot, + epoch: Epoch, + fork_graph: Weak>, + program_runtime_environment: Option, + ) -> Self { + let mut processor = Self::new_uninitialized(slot, epoch); + processor + .global_program_cache + .write() + .unwrap() + .set_fork_graph(fork_graph); + let empty_loader = || ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + processor + .global_program_cache + .write() + .unwrap() + .latest_root_slot = processor.slot; + processor + .epoch_boundary_preparation + .write() + .unwrap() + .upcoming_epoch = processor.epoch; + processor.program_runtime_environment = + program_runtime_environment.unwrap_or(empty_loader()); + processor + } + + /// Create a new `TransactionBatchProcessor` from the current instance, but + /// with the provided slot and epoch. + /// + /// * Inherits the program cache and builtin program ids from the current + /// instance. + /// * Resets the sysvar cache. + pub fn new_from(&self, slot: Slot, epoch: Epoch) -> Self { + let builtin_program_ids = self.builtin_program_ids.read().unwrap().clone(); + let environments = self.program_runtime_environment.clone(); + + // Pre-populate the builtin program cache from the global cache. + // This is done once per block rather than once per batch. + let mut builtin_program_cache = ProgramCacheForTxBatch::new(slot); + let mut search_for: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = builtin_program_ids + .iter() + .map(|key| (*key, ProgramCacheMatchCriteria::NoCriteria, 0)) + .collect(); + self.global_program_cache.read().unwrap().extract( + &mut search_for, + &mut builtin_program_cache, + &environments, + false, + false, + ); + + Self { + slot, + epoch, + sysvar_cache: RwLock::::default(), + epoch_boundary_preparation: self.epoch_boundary_preparation.clone(), + global_program_cache: self.global_program_cache.clone(), + program_runtime_environment: environments, + builtin_program_ids: RwLock::new(builtin_program_ids), + builtin_program_cache: RwLock::new(builtin_program_cache), + execution_cost: self.execution_cost, + } + } + + /// Sets the base execution cost for the transactions that this instance of transaction processor + /// will execute. + pub fn set_execution_cost(&mut self, cost: SVMTransactionExecutionCost) { + self.execution_cost = cost; + } + + /// Updates the environments when entering a new Epoch. + pub fn set_program_runtime_environment(&mut self, new_environment: ProgramRuntimeEnvironment) { + // First update the environment only if it is different + if *self.program_runtime_environment != *new_environment { + self.program_runtime_environment = new_environment; + } + // Then try to consolidate with the upcoming environment (to reuse the address) + if let Some(upcoming_environment) = &self + .epoch_boundary_preparation + .read() + .unwrap() + .upcoming_environment + { + let upcoming_environment = ProgramRuntimeEnvironment::clone(upcoming_environment); + if self.program_runtime_environment != upcoming_environment + && *self.program_runtime_environment == *upcoming_environment + { + // Use the prediction if equal but not identical + self.program_runtime_environment = upcoming_environment; + } + } + } + + /// Returns the current environments depending on the given epoch + /// Returns None if the call could result in a deadlock + pub fn program_runtime_environment_for_epoch(&self, epoch: Epoch) -> ProgramRuntimeEnvironment { + self.epoch_boundary_preparation + .read() + .unwrap() + .get_upcoming_environment_for_epoch(epoch) + .unwrap_or_else(|| ProgramRuntimeEnvironment::clone(&self.program_runtime_environment)) + } + + pub fn sysvar_cache(&self) -> RwLockReadGuard<'_, SysvarCache> { + self.sysvar_cache.read().unwrap() + } + + /// Main entrypoint to the SVM. + pub fn load_and_execute_sanitized_transactions( + &self, + callbacks: &CB, + sanitized_txs: &[impl SVMTransaction], + check_results: Vec, + environment: &TransactionProcessingEnvironment, + config: &TransactionProcessingConfig, + ) -> LoadAndExecuteSanitizedTransactionsOutput { + // If `check_results` does not have the same length as `sanitized_txs`, + // transactions could be truncated as a result of `.iter().zip()` in + // many of the below methods. + // See . + debug_assert_eq!( + sanitized_txs.len(), + check_results.len(), + "Length of check_results does not match length of sanitized_txs" + ); + + // Initialize metrics. + let mut error_metrics = TransactionErrorMetrics::default(); + let mut execute_timings = ExecuteTimings::default(); + let mut processing_results = Vec::with_capacity(sanitized_txs.len()); + + // Determine a capacity for the internal account cache. This + // over-allocates but avoids ever reallocating, and spares us from + // deduplicating the account keys lists. + let account_keys_in_batch = sanitized_txs.iter().map(|tx| tx.account_keys().len()).sum(); + + // Create the account loader, which wraps all external account fetching. + let mut account_loader = AccountLoader::new_with_loaded_accounts_capacity( + config.account_overrides, + callbacks, + &environment.feature_set, + account_keys_in_batch, + ); + + // Create the transaction balance collector if recording is enabled. + let mut balance_collector = config + .recording_config + .enable_transaction_balance_recording + .then(|| BalanceCollector::new_with_transaction_count(sanitized_txs.len())); + + // Clone the batch-local program cache (builtins already populated in new_from()). + // User-deployed programs are loaded per-transaction via replenish_program_cache + // in the transaction loop below. + let mut program_cache_for_tx_batch = self.builtin_program_cache.read().unwrap().clone(); + + if program_cache_for_tx_batch.hit_max_limit { + return LoadAndExecuteSanitizedTransactionsOutput { + error_metrics, + execute_timings, + processing_results: (0..sanitized_txs.len()) + .map(|_| Err(TransactionError::ProgramCacheHitMaxLimit)) + .collect(), + // If we abort the batch and balance recording is enabled, no balances should be + // collected. If this is a leader thread, no batch will be committed. + balance_collector: None, + }; + } + + let (mut load_us, mut execution_us): (u64, u64) = (0, 0); + + // Validate, execute, and collect results from each transaction in order. + // With SIMD83, transactions must be executed in order, because transactions + // in the same batch may modify the same accounts. Transaction order is + // preserved within entries written to the ledger. + for (tx, check_result) in sanitized_txs.iter().zip(check_results) { + let (validate_result, validate_fees_us) = + measure_us!(check_result.and_then(|tx_details| { + Self::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + tx, + tx_details, + &environment.blockhash, + environment.blockhash_lamports_per_signature, + &environment.rent, + config.strict_nonce_size_check, + &mut error_metrics, + ) + })); + execute_timings + .saturating_add_in_place(ExecuteTimingType::ValidateFeesUs, validate_fees_us); + + let (load_result, single_load_us) = measure_us!(load_transaction( + &mut account_loader, + tx, + validate_result, + &mut error_metrics, + &environment.rent, + )); + load_us = load_us.saturating_add(single_load_us); + + let ((), collect_balances_us) = + measure_us!(balance_collector.collect_pre_balances(&mut account_loader, tx)); + execute_timings + .saturating_add_in_place(ExecuteTimingType::CollectBalancesUs, collect_balances_us); + + let (processing_result, single_execution_us) = measure_us!(match load_result { + TransactionLoadResult::NotLoaded(err) => Err(err), + TransactionLoadResult::FeesOnly(fees_only_tx) => match config.drop_on_failure { + true => Err(fees_only_tx.load_error), + false => { + // Update loaded accounts cache with nonce and fee-payer + account_loader.update_accounts_for_failed_tx( + &fees_only_tx.rollback_accounts, + self.slot, + ); + + Ok(ProcessedTransaction::FeesOnly(Box::new(fees_only_tx))) + } + }, + TransactionLoadResult::Loaded(loaded_transaction) => { + let (program_accounts_set, filter_executable_us) = + measure_us!(self.filter_executable_program_accounts( + &account_loader, + &mut program_cache_for_tx_batch, + tx, + )); + execute_timings.saturating_add_in_place( + ExecuteTimingType::FilterExecutableUs, + filter_executable_us, + ); + + let ((), program_cache_us) = measure_us!({ + self.replenish_program_cache( + &account_loader, + &program_accounts_set, + environment + .program_runtime_environments + .get_env_for_execution(), + &mut program_cache_for_tx_batch, + &mut execute_timings, + config.check_program_deployment_slot, + config.limit_to_load_programs, + true, // increment_usage_counter + ); + }); + execute_timings.saturating_add_in_place( + ExecuteTimingType::ProgramCacheUs, + program_cache_us, + ); + + if program_cache_for_tx_batch.hit_max_limit { + return LoadAndExecuteSanitizedTransactionsOutput { + error_metrics, + execute_timings, + processing_results: (0..sanitized_txs.len()) + .map(|_| Err(TransactionError::ProgramCacheHitMaxLimit)) + .collect(), + // If we abort the batch and balance recording is enabled, no balances should be + // collected. If this is a leader thread, no batch will be committed. + balance_collector: None, + }; + } + + let executed_tx = self.execute_loaded_transaction( + callbacks, + tx, + loaded_transaction, + &mut execute_timings, + &mut error_metrics, + &mut program_cache_for_tx_batch, + environment, + config, + ); + + match ( + &executed_tx.execution_details.status, + config.drop_on_failure, + ) { + // Successful transactions need to update the account loader cache as future + // transactions in the batch may depend on them. + (Ok(_), _) => { + account_loader.update_accounts_for_successful_tx( + tx, + &executed_tx.loaded_transaction.accounts, + self.slot, + ); + // Also update local program cache with modifications made by the + // transaction, if it executed successfully. + program_cache_for_tx_batch.merge(&executed_tx.programs_modified_by_tx); + + Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) + } + // If the transaction failed & drop on failure is set then we don't want to + // update the accounts as this transaction will be dropped from the batch. + (Err(err), true) => Err(err.clone()), + // Unsuccessful transactions will still update rollback accounts (fee payer, + // nonce, etc). + (Err(_), false) => { + account_loader.update_accounts_for_failed_tx( + &executed_tx.loaded_transaction.rollback_accounts, + self.slot, + ); + + Ok(ProcessedTransaction::Executed(Box::new(executed_tx))) + } + } + } + }); + execution_us = execution_us.saturating_add(single_execution_us); + + let ((), collect_balances_us) = + measure_us!(balance_collector.collect_post_balances(&mut account_loader, tx)); + execute_timings + .saturating_add_in_place(ExecuteTimingType::CollectBalancesUs, collect_balances_us); + + // If this is an all or nothing batch and we failed to process this transaction then we + // must abort all prior/remaining transactions. + if config.all_or_nothing && processing_result.is_err() { + // Abort prior transactions. + for res in processing_results.iter_mut() { + *res = Err(TransactionError::CommitCancelled); + } + + // Preserve the failure that triggered the batch to abort. + processing_results.push(processing_result); + + // Abort remaining transactions. + processing_results.extend( + (0..sanitized_txs.len() - processing_results.len()) + .map(|_| Err(TransactionError::CommitCancelled)), + ); + + return LoadAndExecuteSanitizedTransactionsOutput { + error_metrics, + execute_timings, + processing_results, + // If we abort the batch and balance recording is enabled, no balances should be + // collected. If this is a leader thread, no batch will be committed. + balance_collector: None, + }; + } + + processing_results.push(processing_result); + } + + // Skip eviction when there's no chance this particular tx batch has increased the size of + // ProgramCache entries. Note that loaded_missing is deliberately defined, so that there's + // still at least one other batch, which will evict the program cache, even after the + // occurrences of cooperative loading. + if program_cache_for_tx_batch.loaded_missing || program_cache_for_tx_batch.merged_modified { + const SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE: u8 = 90; + self.global_program_cache + .write() + .unwrap() + .evict_using_random_selection( + Percentage::from(SHRINK_LOADED_PROGRAMS_TO_PERCENTAGE), + self.slot, + ); + } + + debug!( + "load: {}us execute: {}us txs_len={}", + load_us, + execution_us, + sanitized_txs.len(), + ); + execute_timings.saturating_add_in_place(ExecuteTimingType::LoadUs, load_us); + execute_timings.saturating_add_in_place(ExecuteTimingType::ExecuteUs, execution_us); + + if let Some(ref balance_collector) = balance_collector { + debug_assert!(balance_collector.lengths_match_expected(sanitized_txs.len())); + } + + LoadAndExecuteSanitizedTransactionsOutput { + error_metrics, + execute_timings, + processing_results, + balance_collector, + } + } + + fn validate_transaction_nonce_and_fee_payer( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + checked_details: CheckedTransactionDetails, + environment_blockhash: &Hash, + next_lamports_per_signature: u64, + rent: &Rent, + strict_nonce_size_check: bool, + error_counters: &mut TransactionErrorMetrics, + ) -> TransactionResult { + let CheckedTransactionDetails { + nonce_address, + compute_budget_and_limits, + } = checked_details; + + // If this is a nonce transaction, validate the nonce info. + // This must be done for every transaction to support SIMD83 because + // it may have changed due to use, authorization, or deallocation. + let nonce_info = if let Some(ref nonce_address) = nonce_address { + let next_durable_nonce = DurableNonce::from_blockhash(environment_blockhash); + Some(Self::validate_transaction_nonce( + account_loader, + message, + nonce_address, + &next_durable_nonce, + next_lamports_per_signature, + strict_nonce_size_check, + error_counters, + )?) + } else { + None + }; + + // Now validate the fee-payer for the transaction unconditionally. + Self::validate_transaction_fee_payer( + account_loader, + message, + nonce_info, + compute_budget_and_limits, + rent, + error_counters, + ) + } + + // Loads transaction fee payer, collects rent if necessary, then calculates + // transaction fees, and deducts them from the fee payer balance. If the + // account is not found or has insufficient funds, an error is returned. + fn validate_transaction_fee_payer( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + nonce_info: Option, + compute_budget_and_limits: SVMTransactionExecutionAndFeeBudgetLimits, + rent: &Rent, + error_counters: &mut TransactionErrorMetrics, + ) -> TransactionResult { + let fee_payer_address = message.fee_payer(); + + // We *must* use load_transaction_account() here because *this* is when the fee-payer + // is loaded for the transaction. Transaction loading skips the first account and + // loads (and thus inspects) all others normally. + let Some(mut loaded_fee_payer) = + account_loader.load_transaction_account(fee_payer_address, true) + else { + error_counters.account_not_found += 1; + return Err(TransactionError::AccountNotFound); + }; + + let fee_payer_loaded_rent_epoch = loaded_fee_payer.account.rent_epoch(); + update_rent_exempt_status_for_account(rent, &mut loaded_fee_payer.account); + + let fee_payer_index = 0; + validate_fee_payer( + fee_payer_address, + &mut loaded_fee_payer.account, + fee_payer_index, + error_counters, + rent, + compute_budget_and_limits.fee_details.total_fee(), + )?; + + // Capture fee-subtracted fee payer account and next nonce account state + // to commit if transaction execution fails. + let rollback_accounts = RollbackAccounts::new( + nonce_info, + *fee_payer_address, + loaded_fee_payer.account.clone(), + fee_payer_loaded_rent_epoch, + ); + + Ok(ValidatedTransactionDetails { + fee_details: compute_budget_and_limits.fee_details, + rollback_accounts, + loaded_accounts_bytes_limit: compute_budget_and_limits.loaded_accounts_data_size_limit, + compute_budget: compute_budget_and_limits.budget, + loaded_fee_payer_account: loaded_fee_payer, + }) + } + + fn validate_transaction_nonce( + account_loader: &mut AccountLoader, + message: &impl SVMMessage, + nonce_address: &Pubkey, + next_durable_nonce: &DurableNonce, + next_lamports_per_signature: u64, + strict_nonce_size_check: bool, + error_counters: &mut TransactionErrorMetrics, + ) -> TransactionResult { + // When SIMD83 is enabled, if the nonce has been used in this batch already, we must drop + // the transaction. This is the same as if it was used in different batches in the same slot. + // It is possible that the nonce account was used, closed, closed and reopened, closed and + // spoofed by a non-system program, or had its authority changed. Such a transaction cannot + // be processed, even as fee-only. + + let Some(mut nonce_account) = account_loader + .load_transaction_account(nonce_address, true) + .map(|loaded| loaded.account) + else { + error_counters.account_not_found += 1; + return Err(TransactionError::AccountNotFound); + }; + + if strict_nonce_size_check + && get_system_account_kind(&nonce_account) != Some(SystemAccountKind::Nonce) + { + error_counters.blockhash_not_found += 1; + return Err(TransactionError::BlockhashNotFound); + } + + // This function verifies: + // * Nonce account owner is SystemProgram + // * Nonce account parses as State::Initialized + // * Stored durable nonce matches the message blockhash + let Some(nonce_data) = verify_nonce_account(&nonce_account, message.recent_blockhash()) + else { + error_counters.blockhash_not_found += 1; + return Err(TransactionError::BlockhashNotFound); + }; + + // We must still check that the nonce account is usable and that its authority has signed. + let nonce_can_be_advanced = &nonce_data.durable_nonce != next_durable_nonce; + let nonce_authority_is_valid = message + .get_ix_signers(NONCED_TX_MARKER_IX_INDEX as usize) + .any(|signer| signer == &nonce_data.authority); + + if nonce_can_be_advanced && nonce_authority_is_valid { + let next_nonce_state = NonceState::new_initialized( + &nonce_data.authority, + *next_durable_nonce, + next_lamports_per_signature, + ); + nonce_account + .set_state(&NonceVersions::new(next_nonce_state)) + .expect("Serializing into a validated nonce account cannot fail"); + + Ok(NonceInfo::new(*nonce_address, nonce_account)) + } else { + error_counters.blockhash_not_found += 1; + Err(TransactionError::BlockhashNotFound) + } + } + + /// Appends to a set of executable program accounts (all accounts owned by any loader) + /// for transactions with a valid blockhash or nonce. + fn filter_executable_program_accounts( + &self, + account_loader: &AccountLoader, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + tx: &impl SVMMessage, + ) -> HashMap { + let mut program_accounts_set = HashMap::default(); + for account_key in tx.account_keys().iter() { + if let Some(cache_entry) = program_cache_for_tx_batch.find(account_key) { + cache_entry.stats.uses.fetch_add(1, Ordering::Relaxed); + } else if let Some((account, last_modification_slot)) = + account_loader.get_account_shared_data(account_key) + && PROGRAM_OWNERS.contains(account.owner()) + { + program_accounts_set.insert(*account_key, last_modification_slot); + } + } + program_accounts_set + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + fn replenish_program_cache( + &self, + account_loader: &AccountLoader, + program_accounts_set: &HashMap, + program_runtime_environment_for_execution: &ProgramRuntimeEnvironment, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + execute_timings: &mut ExecuteTimings, + check_program_deployment_slot: bool, + limit_to_load_programs: bool, + increment_usage_counter: bool, + ) { + let mut missing_programs: Vec<(Pubkey, ProgramCacheMatchCriteria, Slot)> = + program_accounts_set + .iter() + .map(|(pubkey, last_modification_slot)| { + let match_criteria = if check_program_deployment_slot { + get_program_deployment_slot(account_loader, pubkey) + .map_or(ProgramCacheMatchCriteria::Tombstone, |slot| { + ProgramCacheMatchCriteria::DeployedOnOrAfterSlot(slot) + }) + } else { + ProgramCacheMatchCriteria::NoCriteria + }; + (*pubkey, match_criteria, *last_modification_slot) + }) + .collect(); + + let mut count_hits_and_misses = true; + loop { + // Lock the global cache. + let global_program_cache = self.global_program_cache.read().unwrap(); + // Figure out which program needs to be loaded next. + let program_to_load = global_program_cache.extract( + &mut missing_programs, + program_cache_for_tx_batch, + program_runtime_environment_for_execution, + increment_usage_counter, + count_hits_and_misses, + ); + count_hits_and_misses = false; + let task_waiter = Arc::clone(&global_program_cache.loading_task_waiter); + let task_cookie = task_waiter.cookie(); + // Unlock the global cache again. + drop(global_program_cache); + + let program_to_store = program_to_load.map(|key| { + // Load, verify and compile one program. + let (program, last_modification_slot) = load_program_with_pubkey( + account_loader, + program_runtime_environment_for_execution, + &key, + self.slot, + execute_timings, + ) + .expect("called load_program_with_pubkey() with nonexistent account"); + (key, program, last_modification_slot) + }); + + if let Some((key, program, last_modification_slot)) = program_to_store { + program_cache_for_tx_batch.loaded_missing = true; + let mut global_program_cache = self.global_program_cache.write().unwrap(); + // Submit our last completed loading task. + if global_program_cache.finish_cooperative_loading_task( + program_runtime_environment_for_execution, + self.slot, + key, + last_modification_slot, + program, + ) && limit_to_load_programs + { + // This branch is taken when there is an error in assigning a program to a + // cache slot. It is not possible to mock this error for SVM unit + // tests purposes. + *program_cache_for_tx_batch = ProgramCacheForTxBatch::new(self.slot); + program_cache_for_tx_batch.hit_max_limit = true; + return; + } + } else if missing_programs.is_empty() { + break; + } else { + // Remember: there are multiple transaction processor threads running concurrently + // and those other threads may be loading this or other programs. + // + // So, sleep until some other thread submits a program with their + // `finish_cooperative_loading_task` call. We'll then wake up and try to load the + // missing programs inside the tx batch again. + let _new_cookie = task_waiter.wait(task_cookie); + } + } + } + + /// Execute a transaction using the provided loaded accounts and update + /// the executors cache if the transaction was successful. + fn execute_loaded_transaction( + &self, + callback: &CB, + tx: &impl SVMTransaction, + mut loaded_transaction: LoadedTransaction, + execute_timings: &mut ExecuteTimings, + error_metrics: &mut TransactionErrorMetrics, + program_cache_for_tx_batch: &mut ProgramCacheForTxBatch, + environment: &TransactionProcessingEnvironment, + config: &TransactionProcessingConfig, + ) -> ExecutedTransaction { + let transaction_accounts = std::mem::take(&mut loaded_transaction.accounts); + + // Ensure the length of accounts matches the expected length from tx.account_keys(). + // This is a sanity check in case that someone starts adding some additional accounts + // since this has been done before. See discussion in PR #4497 for details + debug_assert!(transaction_accounts.len() == tx.account_keys().len()); + + fn transaction_accounts_lamports_sum( + accounts: &[(Pubkey, AccountSharedData)], + ) -> Option { + accounts.iter().try_fold(0u128, |sum, (_, account)| { + sum.checked_add(u128::from(account.lamports())) + }) + } + + let lamports_before_tx = + transaction_accounts_lamports_sum(&transaction_accounts).unwrap_or(0); + + let compute_budget = loaded_transaction.compute_budget; + + let mut transaction_context = TransactionContext::new( + transaction_accounts, + environment.rent.clone(), + compute_budget.max_instruction_stack_depth, + compute_budget.max_instruction_trace_length, + tx.num_instructions(), + ); + + let pre_account_state_info = + TransactionAccountStateInfo::new(&transaction_context, tx, &environment.rent); + + let log_collector = if config.recording_config.enable_log_recording { + match config.log_messages_bytes_limit { + None => Some(LogCollector::new_ref()), + Some(log_messages_bytes_limit) => Some(LogCollector::new_ref_with_limit(Some( + log_messages_bytes_limit, + ))), + } + } else { + None + }; + + let mut executed_units = 0u64; + let sysvar_cache = &self.sysvar_cache.read().unwrap(); + + let mut invoke_context = InvokeContext::new( + &mut transaction_context, + program_cache_for_tx_batch, + EnvironmentConfig::new( + environment.blockhash, + environment.blockhash_lamports_per_signature, + environment.alpenglow_migration_succeeded, + callback, + &environment.feature_set, + &environment.program_runtime_environments, + sysvar_cache, + ), + log_collector.clone(), + compute_budget, + self.execution_cost, + ); + + let mut process_message_time = Measure::start("process_message_time"); + let process_result = process_message( + tx, + &mut invoke_context, + execute_timings, + &mut executed_units, + ); + process_message_time.stop(); + + drop(invoke_context); + + execute_timings.execute_accessories.process_message_us += process_message_time.as_us(); + + let mut post_account_state_info_result = process_result + .and_then(|_| { + let post_account_state_info = + TransactionAccountStateInfo::new(&transaction_context, tx, &environment.rent); + TransactionAccountStateInfo::verify_changes( + &pre_account_state_info, + &post_account_state_info, + &transaction_context, + ) + .map(|_| post_account_state_info) + }) + .map_err(|err| { + match err { + TransactionError::InvalidRentPayingAccount + | TransactionError::InsufficientFundsForRent { .. } => { + error_metrics.invalid_rent_paying_account += 1; + } + TransactionError::InvalidAccountIndex => { + error_metrics.invalid_account_index += 1; + } + _ => { + error_metrics.instruction_error += 1; + } + } + err + }); + + let log_messages: Option = + log_collector.and_then(|log_collector| { + Rc::try_unwrap(log_collector) + .map(|log_collector| log_collector.into_inner().into_messages()) + .ok() + }); + + let (execution_record, inner_instructions) = Self::deconstruct_transaction( + transaction_context, + config.recording_config.enable_cpi_recording, + ); + + let ExecutionRecord { + accounts, + return_data, + touched_account_count, + accounts_resize_delta, + } = execution_record; + + if post_account_state_info_result.is_ok() + && transaction_accounts_lamports_sum(&accounts) + .filter(|lamports_after_tx| lamports_before_tx == *lamports_after_tx) + .is_none() + { + post_account_state_info_result = Err(TransactionError::UnbalancedTransaction); + } + + // accounts_resize_delta and accounts_uninitialized_size must be set to None + // in the result if status is an error + let (status, accounts_deltas) = post_account_state_info_result + .map(|post_state_info| { + ( + Ok(()), + Some(AccountsDeltas { + accounts_resize_delta, + accounts_uninitialized_size: get_uninitialized_accounts_size( + &post_state_info, + ), + }), + ) + }) + .unwrap_or_else(|err| (Err(err), None)); + + loaded_transaction.accounts = accounts; + execute_timings.details.total_account_count += loaded_transaction.accounts.len() as u64; + execute_timings.details.changed_account_count += touched_account_count; + + let return_data = if config.recording_config.enable_return_data_recording + && !return_data.data.is_empty() + { + Some(return_data) + } else { + None + }; + + ExecutedTransaction { + execution_details: TransactionExecutionDetails { + status, + log_messages, + inner_instructions, + return_data, + executed_units, + accounts_deltas, + }, + loaded_transaction, + programs_modified_by_tx: program_cache_for_tx_batch.drain_modified_entries(), + } + } + + /// Extract an ExecutionRecord and an InnerInstructionsList from a TransactionContext + fn deconstruct_transaction( + mut transaction_context: TransactionContext, + record_inner_instructions: bool, + ) -> (ExecutionRecord, Option) { + let inner_ix = if record_inner_instructions { + debug_assert!( + transaction_context + .get_instruction_context_at_index_in_trace(0) + .map(|instruction_context| instruction_context.get_stack_height() + == TRANSACTION_LEVEL_STACK_HEIGHT) + .unwrap_or(true) + ); + + let top_level_ixs_num = transaction_context + .get_instruction_trace_length() + .saturating_sub(transaction_context.number_of_cpis_in_trace()); + // This vector is a map between CPI number in trace (not counting top level + // instructions) and the top level caller index. + // In TransactionContext, caller instructions always precede callee instructions, so + // we can use it to avoid backtracking on instructions callers to + // find the top level instruction that started the call chain. + let mut parent_positions: Vec = + vec![usize::MAX; transaction_context.number_of_cpis_in_trace()]; + let (ix_trace, accounts, ix_data_trace) = transaction_context.take_instruction_trace(); + let mut outer_instructions: Vec> = + vec![Vec::new(); top_level_ixs_num]; + for (cpi_num, ((ix_in_trace, ix_data), ix_accounts)) in ix_trace + .into_iter() + .zip(ix_data_trace) + .zip(accounts) + .skip(top_level_ixs_num) + .enumerate() + { + let caller_ix = ix_in_trace.index_of_caller_instruction; + debug_assert_ne!(caller_ix, u16::MAX, "Instruction is not a CPI"); + + // If the caller index is less than the number of top level instructions, + // it directly represents a top level instruction index. + // Top level instructions precede all CPIs in the instruction trace. + let outer_index = if (caller_ix as usize) < top_level_ixs_num { + *parent_positions.get_mut(cpi_num).unwrap() = caller_ix as usize; + caller_ix as usize + // If the above condition was false, we are dealing with a nested CPI. + // The caller_ix represents the CPI index in the instruction trace. + // To calculate its cpi_number (i.e. the index in `parent_positions)` + // we subtract is from the number of top level instructions. + } else if let Some(caller_index) = parent_positions + .get((caller_ix as usize).saturating_sub(top_level_ixs_num)) + .copied() + && caller_index != usize::MAX + { + *parent_positions.get_mut(cpi_num).unwrap() = caller_index; + caller_index + } else { + // This case shall never happen. Program runtime always executes caller before + // callees, so the if-statement can only be broken into two different cases: + // 1. Top-level instructions doing a CPI + // 2. A nested CPI. + debug_assert!(false); + usize::MAX + }; + + if let Some(inner_instructions) = outer_instructions.get_mut(outer_index) { + let stack_height = ix_in_trace.nesting_level.saturating_add(1); + let stack_height = u8::try_from(stack_height).unwrap_or(u8::MAX); + inner_instructions.push(InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts( + ix_in_trace.program_account_index_in_tx as u8, + ix_data.into_owned(), + ix_accounts + .iter() + .map(|acc| acc.index_in_transaction as u8) + .collect(), + ), + stack_height, + }); + } else { + debug_assert!(false); + } + } + + Some(outer_instructions) + } else { + None + }; + + let record: ExecutionRecord = transaction_context.into(); + + (record, inner_ix) + } + + pub fn fill_missing_sysvar_cache_entries( + &self, + callbacks: &CB, + ) { + let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + sysvar_cache.fill_missing_entries(|pubkey, set_sysvar| { + if let Some((account, _slot)) = callbacks.get_account_shared_data(pubkey) { + set_sysvar(account.data()); + } + }); + } + + pub fn reset_sysvar_cache(&self) { + let mut sysvar_cache = self.sysvar_cache.write().unwrap(); + sysvar_cache.reset(); + } + + pub fn get_sysvar_cache_for_tests(&self) -> SysvarCache { + self.sysvar_cache.read().unwrap().clone() + } + + /// Add a built-in program + pub fn add_builtin(&self, program_id: Pubkey, builtin: ProgramCacheEntry) { + self.builtin_program_ids.write().unwrap().insert(program_id); + let entry = Arc::new(builtin); + self.global_program_cache.write().unwrap().assign_program( + &self.program_runtime_environment, + program_id, + 0, + Arc::clone(&entry), + ); + self.builtin_program_cache + .write() + .unwrap() + .replenish(program_id, entry); + } + + #[cfg(feature = "dev-context-only-utils")] + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + fn writable_sysvar_cache(&self) -> &RwLock { + &self.sysvar_cache + } +} + +#[cfg(test)] +mod tests { + #[allow(deprecated)] + use solana_sysvar::fees::Fees; + use { + super::*, + crate::{ + account_loader::{ + LoadedTransactionAccount, TRANSACTION_ACCOUNT_BASE_SIZE, + ValidatedTransactionDetails, + }, + nonce_info::NonceInfo, + rent_calculator::RENT_EXEMPT_RENT_EPOCH, + rollback_accounts::RollbackAccounts, + }, + solana_account::{WritableAccount, create_account_shared_data_for_test}, + solana_clock::Clock, + solana_compute_budget_interface::ComputeBudgetInstruction, + solana_epoch_schedule::EpochSchedule, + solana_fee_calculator::FeeCalculator, + solana_fee_structure::FeeDetails, + solana_hash::Hash, + solana_keypair::Keypair, + solana_message::{LegacyMessage, Message, MessageHeader, SanitizedMessage}, + solana_nonce as nonce, + solana_program_runtime::{ + execution_budget::{ + SVMTransactionExecutionAndFeeBudgetLimits, SVMTransactionExecutionBudget, + }, + invoke_context::BuiltinFunctionRegisterer, + loaded_programs::BlockRelation, + program_cache_entry::ProgramCacheEntryType, + }, + solana_rent::Rent, + solana_sbpf::vm, + solana_sdk_ids::{bpf_loader, bpf_loader_upgradeable, system_program, sysvar}, + solana_signature::Signature, + solana_svm_callback::{AccountState, InvokeContextCallback}, + solana_system_interface::instruction as system_instruction, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::transaction::TransactionContext, + solana_transaction_error::TransactionError, + std::{borrow::Cow, collections::HashMap}, + test_case::test_case, + }; + + fn new_unchecked_sanitized_message(message: Message) -> SanitizedMessage { + SanitizedMessage::Legacy(LegacyMessage::new(message, &HashSet::new())) + } + + struct TestForkGraph {} + + impl ForkGraph for TestForkGraph { + fn relationship(&self, _a: Slot, _b: Slot) -> BlockRelation { + BlockRelation::Unknown + } + } + + #[derive(Clone)] + struct MockBankCallback { + account_shared_data: Arc>>, + #[allow(clippy::type_complexity)] + inspected_accounts: + Arc, /* is_writable */ bool)>>>>, + feature_set: SVMFeatureSet, + } + + impl Default for MockBankCallback { + fn default() -> Self { + Self { + account_shared_data: Arc::default(), + inspected_accounts: Arc::default(), + feature_set: SVMFeatureSet::all_enabled(), + } + } + } + + impl InvokeContextCallback for MockBankCallback {} + + impl TransactionProcessingCallback for MockBankCallback { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.account_shared_data + .read() + .unwrap() + .get(pubkey) + .map(|account| (account.clone(), 0)) + } + + fn inspect_account( + &self, + address: &Pubkey, + account_state: AccountState, + is_writable: bool, + ) { + let account = match account_state { + AccountState::Dead => None, + AccountState::Alive(account) => Some(account.clone()), + }; + self.inspected_accounts + .write() + .unwrap() + .entry(*address) + .or_default() + .push((account, is_writable)); + } + } + + impl MockBankCallback { + pub fn calculate_fee_details( + message: &impl SVMMessage, + lamports_per_signature: u64, + prioritization_fee: u64, + ) -> FeeDetails { + let signature_count = message + .num_transaction_signatures() + .saturating_add(message.num_ed25519_signatures()) + .saturating_add(message.num_secp256k1_signatures()) + .saturating_add(message.num_secp256r1_signatures()); + + FeeDetails::new( + signature_count.saturating_mul(lamports_per_signature), + prioritization_fee, + ) + } + } + + impl<'a> From<&'a MockBankCallback> for AccountLoader<'a, MockBankCallback> { + fn from(callbacks: &'a MockBankCallback) -> AccountLoader<'a, MockBankCallback> { + AccountLoader::new_with_loaded_accounts_capacity( + None, + callbacks, + &callbacks.feature_set, + 0, + ) + } + } + + #[test_case(1; "Check results too small")] + #[test_case(3; "Check results too large")] + #[should_panic(expected = "Length of check_results does not match length of sanitized_txs")] + fn test_check_results_txs_length_mismatch(check_results_len: usize) { + let sanitized_message = new_unchecked_sanitized_message(Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }); + + // Transactions, length 2. + let sanitized_txs = vec![ + SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + 2 + ]; + + let check_results = vec![ + TransactionCheckResult::Ok(CheckedTransactionDetails::default()); + check_results_len + ]; + + let batch_processor = TransactionBatchProcessor::::default(); + let callback = MockBankCallback::default(); + + batch_processor.load_and_execute_sanitized_transactions( + &callback, + &sanitized_txs, + check_results, + &get_mock_transaction_processing_environment(), + &TransactionProcessingConfig::default(), + ); + } + + #[test] + fn test_inner_instructions_list_from_instruction_trace() { + let mut transaction_context = TransactionContext::new( + vec![( + Pubkey::new_unique(), + AccountSharedData::new(1, 1, &bpf_loader::ID), + )], + Rent::default(), + 4, + 11, + 4, + ); + + // Four top level instructions + for i in 0..4 { + transaction_context + .configure_instruction_at_index( + i, + 0, + vec![], + vec![u16::MAX; 256], + Cow::Owned(vec![i as u8]), + None, + ) + .unwrap(); + } + + // Execute ix #0 + transaction_context.push().unwrap(); + // ix #0 does a CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![0, 0]) + .unwrap(); + transaction_context.push().unwrap(); + // Returning from everything + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #1 + transaction_context.push().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #2 + transaction_context.push().unwrap(); + // ix #2 does a CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![2, 0]) + .unwrap(); + transaction_context.push().unwrap(); + // A nested CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![2, 1]) + .unwrap(); + transaction_context.push().unwrap(); + // Return from nested CPI + transaction_context.pop().unwrap(); + // Return from CPI + transaction_context.pop().unwrap(); + // ix #2 does another CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![2, 2]) + .unwrap(); + transaction_context.push().unwrap(); + // Return from everything related to ix #2 + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + // Execute ix #3 + transaction_context.push().unwrap(); + // ix #3 does a CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![3, 0]) + .unwrap(); + transaction_context.push().unwrap(); + // ix #3 does a nested CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![3, 1]) + .unwrap(); + transaction_context.push().unwrap(); + // ix #3 does a second nested CPI + transaction_context + .configure_next_cpi_for_tests(0, vec![], vec![3, 2]) + .unwrap(); + transaction_context.push().unwrap(); + // Return from everything related to ix #3 + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + transaction_context.pop().unwrap(); + + let inner_instructions = + TransactionBatchProcessor::::deconstruct_transaction( + transaction_context, + true, + ) + .1 + .unwrap(); + + assert_eq!( + inner_instructions, + vec![ + vec![InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![0, 0], vec![]), + stack_height: 2, + }], + vec![], + vec![ + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 0], vec![]), + stack_height: 2, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 1], vec![]), + stack_height: 3, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![2, 2], vec![]), + stack_height: 2, + }, + ], + vec![ + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 0], vec![]), + stack_height: 2, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 1], vec![]), + stack_height: 3, + }, + InnerInstruction { + instruction: CompiledInstruction::new_from_raw_parts(0, vec![3, 2], vec![]), + stack_height: 4, + }, + ] + ] + ); + } + + #[test] + fn test_execute_loaded_transaction_recordings() { + // Setting all the arguments correctly is too burdensome for testing + // execute_loaded_transaction separately.This function will be tested in an integration + // test with load_and_execute_sanitized_transactions + let message = Message { + account_keys: vec![Pubkey::new_from_array([0; 32])], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let loaded_transaction = LoadedTransaction { + accounts: vec![(Pubkey::new_unique(), AccountSharedData::default())], + fee_details: FeeDetails::default(), + rollback_accounts: RollbackAccounts::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size: 32, + }; + + let processing_environment = get_mock_transaction_processing_environment(); + + let mut processing_config = TransactionProcessingConfig::default(); + processing_config.recording_config.enable_log_recording = true; + + let mock_bank = MockBankCallback::default(); + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction.clone(), + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + assert!(executed_tx.execution_details.log_messages.is_some()); + + processing_config.log_messages_bytes_limit = Some(2); + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction.clone(), + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + assert!(executed_tx.execution_details.log_messages.is_some()); + assert!(executed_tx.execution_details.inner_instructions.is_none()); + + processing_config.recording_config.enable_log_recording = false; + processing_config.recording_config.enable_cpi_recording = true; + processing_config.log_messages_bytes_limit = None; + + let executed_tx = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction, + &mut ExecuteTimings::default(), + &mut TransactionErrorMetrics::default(), + &mut program_cache_for_tx_batch, + &processing_environment, + &processing_config, + ); + + assert!(executed_tx.execution_details.log_messages.is_none()); + assert!(executed_tx.execution_details.inner_instructions.is_some()); + } + + #[test] + fn test_execute_loaded_transaction_error_metrics() { + // Setting all the arguments correctly is too burdensome for testing + // execute_loaded_transaction separately.This function will be tested in an integration + // test with load_and_execute_sanitized_transactions + let key1 = Pubkey::new_unique(); + let key2 = Pubkey::new_unique(); + let message = Message { + account_keys: vec![key1, key2], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![2], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::default(); + let batch_processor = TransactionBatchProcessor::::default(); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + let loaded_transaction = LoadedTransaction { + accounts: vec![ + (key1, AccountSharedData::default()), + (key2, AccountSharedData::default()), + ], + fee_details: FeeDetails::default(), + rollback_accounts: RollbackAccounts::default(), + compute_budget: SVMTransactionExecutionBudget::default(), + loaded_accounts_data_size: 0, + }; + + let processing_config = TransactionProcessingConfig { + recording_config: ExecutionRecordingConfig::new_single_setting(false), + ..Default::default() + }; + let mut error_metrics = TransactionErrorMetrics::new(); + let mock_bank = MockBankCallback::default(); + + let _ = batch_processor.execute_loaded_transaction( + &mock_bank, + &sanitized_transaction, + loaded_transaction, + &mut ExecuteTimings::default(), + &mut error_metrics, + &mut program_cache_for_tx_batch, + &get_mock_transaction_processing_environment(), + &processing_config, + ); + + assert_eq!(error_metrics.instruction_error.0, 1); + } + + #[test] + #[should_panic = "called load_program_with_pubkey() with nonexistent account"] + fn test_replenish_program_cache_with_nonexistent_accounts() { + let mock_bank = MockBankCallback::default(); + let account_loader = (&mock_bank).into(); + let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); + let batch_processor = + TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); + let program_runtime_environment_for_execution = + batch_processor.program_runtime_environment_for_epoch(0); + let key = Pubkey::new_unique(); + + let mut account_set = HashMap::new(); + account_set.insert(key, 0); + + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); + + batch_processor.replenish_program_cache( + &account_loader, + &account_set, + &program_runtime_environment_for_execution, + &mut program_cache_for_tx_batch, + &mut ExecuteTimings::default(), + false, + true, + true, + ); + } + + #[test] + fn test_replenish_program_cache() { + let mock_bank = MockBankCallback::default(); + let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); + let batch_processor = + TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); + let program_runtime_environment_for_execution = + batch_processor.program_runtime_environment_for_epoch(0); + let key = Pubkey::new_unique(); + + let mut account_data = AccountSharedData::default(); + account_data.set_owner(bpf_loader::id()); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(key, account_data); + let account_loader = (&mock_bank).into(); + + let mut account_set = HashMap::new(); + account_set.insert(key, 0); + let mut loaded_missing = 0; + + for limit_to_load_programs in [false, true] { + let mut program_cache_for_tx_batch = ProgramCacheForTxBatch::new(batch_processor.slot); + + batch_processor.replenish_program_cache( + &account_loader, + &account_set, + &program_runtime_environment_for_execution, + &mut program_cache_for_tx_batch, + &mut ExecuteTimings::default(), + false, + limit_to_load_programs, + true, + ); + assert!(!program_cache_for_tx_batch.hit_max_limit); + if program_cache_for_tx_batch.loaded_missing { + loaded_missing += 1; + } + + let program = program_cache_for_tx_batch.find(&key).unwrap(); + assert!(matches!( + program.program, + ProgramCacheEntryType::FailedVerification(_) + )); + } + assert!(loaded_missing > 0); + } + + #[test] + fn test_filter_executable_program_accounts() { + let mock_bank = MockBankCallback::default(); + let key1 = Pubkey::new_unique(); + let owner1 = bpf_loader::id(); + let key2 = Pubkey::new_unique(); + let owner2 = bpf_loader_upgradeable::id(); + + let mut data1 = AccountSharedData::default(); + data1.set_owner(owner1); + data1.set_lamports(93); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(key1, data1); + + let mut data2 = AccountSharedData::default(); + data2.set_owner(owner2); + data2.set_lamports(90); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(key2, data2); + let account_loader = (&mock_bank).into(); + + let message = Message { + account_keys: vec![key1, key2], + header: MessageHeader::default(), + instructions: vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }], + recent_blockhash: Hash::default(), + }; + + let sanitized_message = new_unchecked_sanitized_message(message); + + let sanitized_transaction = SanitizedTransaction::new_for_tests( + sanitized_message, + vec![Signature::new_unique()], + false, + ); + + let batch_processor = TransactionBatchProcessor::::default(); + let program_accounts_set = batch_processor.filter_executable_program_accounts( + &account_loader, + &mut ProgramCacheForTxBatch::default(), + &sanitized_transaction, + ); + + assert_eq!(program_accounts_set.len(), 2); + assert!(program_accounts_set.contains_key(&key1)); + assert!(program_accounts_set.contains_key(&key2)); + } + + #[test] + fn test_filter_executable_program_accounts_no_errors() { + let keypair1 = Keypair::new(); + let keypair2 = Keypair::new(); + + let non_program_pubkey1 = Pubkey::new_unique(); + let non_program_pubkey2 = Pubkey::new_unique(); + let program1_pubkey = bpf_loader::id(); + let program2_pubkey = bpf_loader_upgradeable::id(); + let account1_pubkey = Pubkey::new_unique(); + let account2_pubkey = Pubkey::new_unique(); + let account3_pubkey = Pubkey::new_unique(); + let account4_pubkey = Pubkey::new_unique(); + + let account5_pubkey = Pubkey::new_unique(); + + let bank = MockBankCallback::default(); + bank.account_shared_data.write().unwrap().insert( + non_program_pubkey1, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.write().unwrap().insert( + non_program_pubkey2, + AccountSharedData::new(1, 10, &account5_pubkey), + ); + bank.account_shared_data.write().unwrap().insert( + program1_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.write().unwrap().insert( + program2_pubkey, + AccountSharedData::new(40, 1, &account5_pubkey), + ); + bank.account_shared_data.write().unwrap().insert( + account1_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey1), + ); + bank.account_shared_data.write().unwrap().insert( + account2_pubkey, + AccountSharedData::new(1, 10, &non_program_pubkey2), + ); + bank.account_shared_data.write().unwrap().insert( + account3_pubkey, + AccountSharedData::new(40, 1, &program1_pubkey), + ); + bank.account_shared_data.write().unwrap().insert( + account4_pubkey, + AccountSharedData::new(40, 1, &program2_pubkey), + ); + let account_loader = (&bank).into(); + + let tx1 = Transaction::new_with_compiled_instructions( + &[&keypair1], + &[non_program_pubkey1], + Hash::new_unique(), + vec![account1_pubkey, account2_pubkey, account3_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + let sanitized_tx1 = SanitizedTransaction::from_transaction_for_tests(tx1); + + let tx2 = Transaction::new_with_compiled_instructions( + &[&keypair2], + &[non_program_pubkey2], + Hash::new_unique(), + vec![account4_pubkey, account3_pubkey, account2_pubkey], + vec![CompiledInstruction::new(1, &(), vec![0])], + ); + let sanitized_tx2 = SanitizedTransaction::from_transaction_for_tests(tx2); + + let batch_processor = TransactionBatchProcessor::::default(); + + let tx1_programs = batch_processor.filter_executable_program_accounts( + &account_loader, + &mut ProgramCacheForTxBatch::default(), + &sanitized_tx1, + ); + + assert_eq!(tx1_programs.len(), 1); + assert!( + tx1_programs.contains_key(&account3_pubkey), + "failed to find the program account", + ); + + let tx2_programs = batch_processor.filter_executable_program_accounts( + &account_loader, + &mut ProgramCacheForTxBatch::default(), + &sanitized_tx2, + ); + + assert_eq!(tx2_programs.len(), 2); + assert!( + tx2_programs.contains_key(&account3_pubkey), + "failed to find the program account", + ); + assert!( + tx2_programs.contains_key(&account4_pubkey), + "failed to find the program account", + ); + } + + #[test] + #[allow(deprecated)] + fn test_sysvar_cache_initialization1() { + let mock_bank = MockBankCallback::default(); + + let clock = Clock { + slot: 1, + epoch_start_timestamp: 2, + epoch: 3, + leader_schedule_epoch: 4, + unix_timestamp: 5, + }; + let clock_account = create_account_shared_data_for_test(&clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), clock_account); + + let epoch_schedule = EpochSchedule::custom(64, 2, true); + let epoch_schedule_account = create_account_shared_data_for_test(&epoch_schedule); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); + + let fees = Fees { + fee_calculator: FeeCalculator { + lamports_per_signature: 123, + }, + }; + let fees_account = create_account_shared_data_for_test(&fees); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::fees::id(), fees_account); + + let rent = Rent::default(); + let rent_account = create_account_shared_data_for_test(&rent); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::rent::id(), rent_account); + + let transaction_processor = TransactionBatchProcessor::::default(); + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + let cached_clock = sysvar_cache.get_clock(); + let cached_epoch_schedule = sysvar_cache.get_epoch_schedule(); + let cached_fees = sysvar_cache.get_fees(); + let cached_rent = sysvar_cache.get_rent(); + + assert_eq!( + cached_clock.expect("clock sysvar missing in cache"), + clock.into() + ); + assert_eq!( + cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), + epoch_schedule.into() + ); + assert_eq!( + cached_fees.expect("fees sysvar missing in cache"), + fees.into() + ); + assert_eq!( + cached_rent.expect("rent sysvar missing in cache"), + rent.into() + ); + assert!(sysvar_cache.get_slot_hashes().is_err()); + assert!(sysvar_cache.get_epoch_rewards().is_err()); + } + + #[test] + #[allow(deprecated)] + fn test_reset_and_fill_sysvar_cache() { + let mock_bank = MockBankCallback::default(); + + let clock = Clock { + slot: 1, + epoch_start_timestamp: 2, + epoch: 3, + leader_schedule_epoch: 4, + unix_timestamp: 5, + }; + let clock_account = create_account_shared_data_for_test(&clock); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::clock::id(), clock_account); + + let epoch_schedule = EpochSchedule::custom(64, 2, true); + let epoch_schedule_account = create_account_shared_data_for_test(&epoch_schedule); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::epoch_schedule::id(), epoch_schedule_account); + + let fees = Fees { + fee_calculator: FeeCalculator { + lamports_per_signature: 123, + }, + }; + let fees_account = create_account_shared_data_for_test(&fees); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::fees::id(), fees_account); + + let rent = Rent::default(); + let rent_account = create_account_shared_data_for_test(&rent); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(sysvar::rent::id(), rent_account); + + let transaction_processor = TransactionBatchProcessor::::default(); + // Fill the sysvar cache + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + // Reset the sysvar cache + transaction_processor.reset_sysvar_cache(); + + { + let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + // Test that sysvar cache is empty and none of the values are found + assert!(sysvar_cache.get_clock().is_err()); + assert!(sysvar_cache.get_epoch_schedule().is_err()); + assert!(sysvar_cache.get_fees().is_err()); + assert!(sysvar_cache.get_epoch_rewards().is_err()); + assert!(sysvar_cache.get_rent().is_err()); + assert!(sysvar_cache.get_epoch_rewards().is_err()); + } + + // Refill the cache and test the values are available. + transaction_processor.fill_missing_sysvar_cache_entries(&mock_bank); + + let sysvar_cache = transaction_processor.sysvar_cache.read().unwrap(); + let cached_clock = sysvar_cache.get_clock(); + let cached_epoch_schedule = sysvar_cache.get_epoch_schedule(); + let cached_fees = sysvar_cache.get_fees(); + let cached_rent = sysvar_cache.get_rent(); + + assert_eq!( + cached_clock.expect("clock sysvar missing in cache"), + clock.into() + ); + assert_eq!( + cached_epoch_schedule.expect("epoch_schedule sysvar missing in cache"), + epoch_schedule.into() + ); + assert_eq!( + cached_fees.expect("fees sysvar missing in cache"), + fees.into() + ); + assert_eq!( + cached_rent.expect("rent sysvar missing in cache"), + rent.into() + ); + assert!(sysvar_cache.get_slot_hashes().is_err()); + assert!(sysvar_cache.get_epoch_rewards().is_err()); + } + + #[test] + fn test_add_builtin() { + let fork_graph = Arc::new(RwLock::new(TestForkGraph {})); + let batch_processor = + TransactionBatchProcessor::new(0, 0, Arc::downgrade(&fork_graph), None); + + let key = Pubkey::new_unique(); + let name = "a_builtin_name"; + let register_fn: BuiltinFunctionRegisterer = |p, n| { + p.register_function( + n, + ( + |_invoke_context, _param0, _param1, _param2, _param3, _param4| {}, + |_| {}, + ), + ) + }; + let program = ProgramCacheEntry::new_builtin(0, name.len(), register_fn); + batch_processor.add_builtin(key, program); + + let mut loaded_programs_for_tx_batch = ProgramCacheForTxBatch::new(0); + let program_runtime_environment = + batch_processor.program_runtime_environment_for_epoch(batch_processor.epoch); + batch_processor + .global_program_cache + .write() + .unwrap() + .extract( + &mut vec![(key, ProgramCacheMatchCriteria::NoCriteria, 0)], + &mut loaded_programs_for_tx_batch, + &program_runtime_environment, + true, + true, + ); + let entry = loaded_programs_for_tx_batch.find(&key).unwrap(); + + // Repeating code because ProgramCacheEntry does not implement clone. + let program = ProgramCacheEntry::new_builtin(0, name.len(), register_fn); + assert_eq!(entry, Arc::new(program)); + } + + #[test] + fn test_validate_transaction_fee_payer_exact_balance() { + let lamports_per_signature = 5000; + let message = new_unchecked_sanitized_message(Message::new_with_blockhash( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(2000u32), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000_000), + ], + Some(&Pubkey::new_unique()), + &Hash::new_unique(), + )); + let fee_payer_address = message.fee_payer(); + let current_epoch = 42; + let rent = Rent::default(); + let min_balance = rent.minimum_balance(nonce::state::State::size()); + let transaction_fee = lamports_per_signature; + let priority_fee = 2_000_000u64; + let starting_balance = transaction_fee + priority_fee; + assert!( + starting_balance > min_balance, + "we're testing that a rent exempt fee payer can be fully drained, so ensure that the \ + starting balance is more than the min balance" + ); + + let fee_payer_rent_epoch = current_epoch; + let fee_payer_account = AccountSharedData::new_rent_epoch( + starting_balance, + 0, + &Pubkey::default(), + fee_payer_rent_epoch, + ); + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + let compute_budget_and_limits = SVMTransactionExecutionAndFeeBudgetLimits { + budget: SVMTransactionExecutionBudget { + compute_unit_limit: 2000, + ..SVMTransactionExecutionBudget::default() + }, + fee_details: FeeDetails::new(transaction_fee, priority_fee), + ..SVMTransactionExecutionAndFeeBudgetLimits::default() + }; + let result = + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new(None, compute_budget_and_limits), + &Hash::default(), + lamports_per_signature, + &rent, + false, + &mut error_counters, + ); + + let post_validation_fee_payer_account = { + let mut account = fee_payer_account.clone(); + account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + account.set_lamports(0); + account + }; + + assert_eq!( + result, + Ok(ValidatedTransactionDetails { + rollback_accounts: RollbackAccounts::new( + None, // nonce + *fee_payer_address, + post_validation_fee_payer_account.clone(), + fee_payer_rent_epoch + ), + compute_budget: compute_budget_and_limits.budget, + loaded_accounts_bytes_limit: compute_budget_and_limits + .loaded_accounts_data_size_limit, + fee_details: FeeDetails::new(transaction_fee, priority_fee), + loaded_fee_payer_account: LoadedTransactionAccount { + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), + account: post_validation_fee_payer_account, + }, + }) + ); + } + + #[test] + fn test_validate_transaction_fee_payer_not_found() { + let lamports_per_signature = 5000; + let message = + new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); + + let mock_bank = MockBankCallback::default(); + let mut account_loader = (&mock_bank).into(); + let mut error_counters = TransactionErrorMetrics::default(); + let result = + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::default(), + ), + &Hash::default(), + lamports_per_signature, + &Rent::default(), + false, + &mut error_counters, + ); + + assert_eq!(error_counters.account_not_found.0, 1); + assert_eq!(result, Err(TransactionError::AccountNotFound)); + } + + #[test] + fn test_validate_transaction_fee_payer_insufficient_funds() { + let lamports_per_signature = 5000; + let message = + new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); + let fee_payer_address = message.fee_payer(); + let fee_payer_account = AccountSharedData::new(1, 0, &Pubkey::default()); + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + let result = + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::with_fee( + MockBankCallback::calculate_fee_details( + &message, + lamports_per_signature, + 0, + ), + ), + ), + &Hash::default(), + lamports_per_signature, + &Rent::default(), + false, + &mut error_counters, + ); + + assert_eq!(error_counters.insufficient_funds.0, 1); + assert_eq!(result, Err(TransactionError::InsufficientFundsForFee)); + } + + #[test] + fn test_validate_transaction_fee_payer_insufficient_rent() { + let lamports_per_signature = 5000; + let message = + new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); + let fee_payer_address = message.fee_payer(); + let transaction_fee = lamports_per_signature; + let rent = Rent::default(); + let min_balance = rent.minimum_balance(0); + let starting_balance = min_balance + transaction_fee - 1; + let fee_payer_account = AccountSharedData::new(starting_balance, 0, &Pubkey::default()); + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + let result = + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::with_fee( + MockBankCallback::calculate_fee_details( + &message, + lamports_per_signature, + 0, + ), + ), + ), + &Hash::default(), + lamports_per_signature, + &rent, + false, + &mut error_counters, + ); + + assert_eq!( + result, + Err(TransactionError::InsufficientFundsForRent { account_index: 0 }) + ); + } + + #[test] + fn test_validate_transaction_fee_payer_invalid() { + let lamports_per_signature = 5000; + let message = + new_unchecked_sanitized_message(Message::new(&[], Some(&Pubkey::new_unique()))); + let fee_payer_address = message.fee_payer(); + let fee_payer_account = AccountSharedData::new(1_000_000, 0, &Pubkey::new_unique()); + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + let result = + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::with_fee( + MockBankCallback::calculate_fee_details( + &message, + lamports_per_signature, + 0, + ), + ), + ), + &Hash::default(), + lamports_per_signature, + &Rent::default(), + false, + &mut error_counters, + ); + + assert_eq!(error_counters.invalid_account_for_fee.0, 1); + assert_eq!(result, Err(TransactionError::InvalidAccountForFee)); + } + + #[derive(Debug, PartialEq, Eq)] + enum ValidateNonce { + Success, + NoAccount, + BadOwner, + BlockhashMismatch, + AlreadyUsed, + BadSigner, + } + + #[test_case(ValidateNonce::Success)] + #[test_case(ValidateNonce::NoAccount)] + #[test_case(ValidateNonce::BadOwner)] + #[test_case(ValidateNonce::BlockhashMismatch)] + #[test_case(ValidateNonce::AlreadyUsed)] + #[test_case(ValidateNonce::BadSigner)] + fn test_validate_transaction_nonce(case: ValidateNonce) { + let lamports_per_signature = 5000; + let previous_durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let nonce_address = Pubkey::new_unique(); + let authority_address = Pubkey::new_unique(); + + let message_blockhash = if case == ValidateNonce::BlockhashMismatch { + Hash::new_unique() + } else { + *previous_durable_nonce.as_hash() + }; + + let message_authority = if case == ValidateNonce::BadSigner { + Pubkey::new_unique() + } else { + authority_address + }; + + let message = new_unchecked_sanitized_message(Message::new_with_blockhash( + &[system_instruction::advance_nonce_account( + &nonce_address, + &message_authority, + )], + Some(&Pubkey::new_unique()), + &message_blockhash, + )); + + let environment_blockhash = Hash::new_unique(); + let next_durable_nonce = DurableNonce::from_blockhash(&environment_blockhash); + + let stored_durable_nonce = if case == ValidateNonce::AlreadyUsed { + next_durable_nonce + } else { + previous_durable_nonce + }; + + let nonce_versions = nonce::versions::Versions::new(nonce::state::State::Initialized( + nonce::state::Data::new( + authority_address, + stored_durable_nonce, + lamports_per_signature, + ), + )); + + let nonce_owner = if case == ValidateNonce::BadOwner { + Pubkey::new_unique() + } else { + system_program::id() + }; + + let nonce_account = AccountSharedData::new_data(1, &nonce_versions, &nonce_owner).unwrap(); + + let mut mock_accounts = HashMap::new(); + + if case != ValidateNonce::NoAccount { + mock_accounts.insert(nonce_address, nonce_account.clone()); + } + + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + let result = TransactionBatchProcessor::::validate_transaction_nonce( + &mut account_loader, + &message, + &nonce_address, + &next_durable_nonce, + lamports_per_signature, + false, + &mut error_counters, + ); + + match case { + ValidateNonce::Success => { + let mut future_nonce_info = NonceInfo::new(nonce_address, nonce_account); + future_nonce_info + .try_advance_nonce(next_durable_nonce, lamports_per_signature) + .unwrap(); + + assert_eq!(result, Ok(future_nonce_info)); + } + ValidateNonce::NoAccount => { + assert_eq!(error_counters.account_not_found.0, 1); + assert_eq!(result, Err(TransactionError::AccountNotFound)); + } + _ => { + assert_eq!(error_counters.blockhash_not_found.0, 1); + assert_eq!(result, Err(TransactionError::BlockhashNotFound)); + } + } + } + + #[test] + fn test_validate_transaction_fee_payer_is_nonce() { + let lamports_per_signature = 5000; + let rent = Rent::default(); + let compute_unit_limit = 1000u64; + let previous_durable_nonce = DurableNonce::from_blockhash(&Hash::new_unique()); + let fee_payer_address = &Pubkey::new_unique(); + let message = new_unchecked_sanitized_message(Message::new_with_blockhash( + &[ + system_instruction::advance_nonce_account(fee_payer_address, fee_payer_address), + ComputeBudgetInstruction::set_compute_unit_limit(compute_unit_limit as u32), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000), + ], + Some(fee_payer_address), + previous_durable_nonce.as_hash(), + )); + let transaction_fee = lamports_per_signature; + let compute_budget_and_limits = SVMTransactionExecutionAndFeeBudgetLimits { + fee_details: FeeDetails::new(transaction_fee, compute_unit_limit), + ..SVMTransactionExecutionAndFeeBudgetLimits::default() + }; + let min_balance = Rent::default().minimum_balance(nonce::state::State::size()); + let priority_fee = compute_unit_limit; + + let nonce_versions = nonce::versions::Versions::new(nonce::state::State::Initialized( + nonce::state::Data::new( + *fee_payer_address, + previous_durable_nonce, + lamports_per_signature, + ), + )); + + let environment_blockhash = Hash::new_unique(); + let next_durable_nonce = DurableNonce::from_blockhash(&environment_blockhash); + + // Sufficient Fees + { + let fee_payer_account = AccountSharedData::new_data( + min_balance + transaction_fee + priority_fee, + &nonce_versions, + &system_program::id(), + ) + .unwrap(); + + let mut future_nonce = NonceInfo::new(*fee_payer_address, fee_payer_account.clone()); + future_nonce + .try_advance_nonce(next_durable_nonce, lamports_per_signature) + .unwrap(); + + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account.clone()); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + + let tx_details = + CheckedTransactionDetails::new(Some(*fee_payer_address), compute_budget_and_limits); + + let result = TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + tx_details, + &environment_blockhash, + lamports_per_signature, + &rent, + false, + &mut error_counters, + ); + + let post_validation_fee_payer_account = { + let mut account = fee_payer_account.clone(); + account.set_rent_epoch(RENT_EXEMPT_RENT_EPOCH); + account.set_lamports(min_balance); + account + }; + + assert_eq!( + result, + Ok(ValidatedTransactionDetails { + rollback_accounts: RollbackAccounts::new( + Some(future_nonce), + *fee_payer_address, + post_validation_fee_payer_account.clone(), + 0, // fee_payer_rent_epoch + ), + compute_budget: compute_budget_and_limits.budget, + loaded_accounts_bytes_limit: compute_budget_and_limits + .loaded_accounts_data_size_limit, + fee_details: FeeDetails::new(transaction_fee, priority_fee), + loaded_fee_payer_account: LoadedTransactionAccount { + loaded_size: TRANSACTION_ACCOUNT_BASE_SIZE + fee_payer_account.data().len(), + account: post_validation_fee_payer_account, + } + }) + ); + } + + // Insufficient Fees + { + let fee_payer_account = AccountSharedData::new_data( + transaction_fee + priority_fee, // no min_balance this time + &nonce_versions, + &system_program::id(), + ) + .unwrap(); + + let mut mock_accounts = HashMap::new(); + mock_accounts.insert(*fee_payer_address, fee_payer_account); + let mock_bank = MockBankCallback { + account_shared_data: Arc::new(RwLock::new(mock_accounts)), + ..Default::default() + }; + let mut account_loader = (&mock_bank).into(); + + let mut error_counters = TransactionErrorMetrics::default(); + + let tx_details = + CheckedTransactionDetails::new(Some(*fee_payer_address), compute_budget_and_limits); + + let result = TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + tx_details, + &environment_blockhash, + lamports_per_signature, + &rent, + false, + &mut error_counters, + ); + + assert_eq!(error_counters.insufficient_funds.0, 1); + assert_eq!(result, Err(TransactionError::InsufficientFundsForFee)); + } + } + + // Ensure `TransactionProcessingCallback::inspect_account()` is called when + // validating the fee payer, since that's when the fee payer account is loaded. + #[test] + fn test_inspect_account_fee_payer() { + let lamports_per_signature = 5000; + let fee_payer_address = Pubkey::new_unique(); + let fee_payer_account = AccountSharedData::new_rent_epoch( + 123_000_000_000, + 0, + &Pubkey::default(), + RENT_EXEMPT_RENT_EPOCH, + ); + let mock_bank = MockBankCallback::default(); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(fee_payer_address, fee_payer_account.clone()); + let mut account_loader = (&mock_bank).into(); + + let message = new_unchecked_sanitized_message(Message::new_with_blockhash( + &[ + ComputeBudgetInstruction::set_compute_unit_limit(2000u32), + ComputeBudgetInstruction::set_compute_unit_price(1_000_000_000), + ], + Some(&fee_payer_address), + &Hash::new_unique(), + )); + TransactionBatchProcessor::::validate_transaction_nonce_and_fee_payer( + &mut account_loader, + &message, + CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::with_fee( + MockBankCallback::calculate_fee_details(&message, 5000, 0), + ), + ), + &Hash::default(), + lamports_per_signature, + &Rent::default(), + false, + &mut TransactionErrorMetrics::default(), + ) + .unwrap(); + + // ensure the fee payer is an inspected account + let actual_inspected_accounts: Vec<_> = mock_bank + .inspected_accounts + .read() + .unwrap() + .iter() + .map(|(k, v)| (*k, v.clone())) + .collect(); + assert_eq!( + actual_inspected_accounts.as_slice(), + &[(fee_payer_address, vec![(Some(fee_payer_account), true)])], + ); + } + + #[test] + fn test_set_program_runtime_environment() { + let mut transaction_processor = TransactionBatchProcessor::::default(); + let current_environment = + ProgramRuntimeEnvironment::clone(&transaction_processor.program_runtime_environment); + let new_environment = ProgramRuntimeEnvironment::from(BuiltinProgram::new_mock()); + let config = vm::Config { + enable_symbol_and_section_labels: true, + ..vm::Config::default() + }; + let new_environment2 = ProgramRuntimeEnvironment::from(BuiltinProgram::new_loader(config)); + assert_ne!(current_environment, new_environment); + assert_ne!(current_environment, new_environment2); + assert_ne!(new_environment, new_environment2); + // Assign an equal and identical environment: No changes + transaction_processor.set_program_runtime_environment(ProgramRuntimeEnvironment::clone( + ¤t_environment, + )); + assert_eq!( + transaction_processor.program_runtime_environment, + current_environment, + ); + // Assign an equal but not identical environment: No changes + transaction_processor + .set_program_runtime_environment(ProgramRuntimeEnvironment::clone(&new_environment)); + assert_eq!( + transaction_processor.program_runtime_environment, + current_environment, + ); + // Assign a different and not identical environment: Overwritten + transaction_processor + .set_program_runtime_environment(ProgramRuntimeEnvironment::clone(&new_environment2)); + assert_eq!( + transaction_processor.program_runtime_environment, + new_environment2, + ); + // Assign an environment which is equal to the upcoming_environment: Overwritten + transaction_processor + .epoch_boundary_preparation + .write() + .unwrap() + .upcoming_environment = Some(ProgramRuntimeEnvironment::clone(&new_environment)); + transaction_processor.set_program_runtime_environment(ProgramRuntimeEnvironment::clone( + ¤t_environment, + )); + assert_eq!( + transaction_processor.program_runtime_environment, + new_environment, + ); + } +} diff --git a/solana/svm/tests/concurrent_tests.rs b/solana/svm/tests/concurrent_tests.rs new file mode 100644 index 0000000..ac4b1c4 --- /dev/null +++ b/solana/svm/tests/concurrent_tests.rs @@ -0,0 +1,311 @@ +#![cfg(feature = "shuttle-test")] + +use { + crate::mock_bank::{MockForkGraph, create_custom_loader, deploy_program, register_builtins}, + assert_matches::assert_matches, + mock_bank::MockBankCallback, + shuttle::{ + Runner, + sync::{Arc, RwLock}, + thread, + }, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::Slot, + solana_instruction::{AccountMeta, Instruction}, + solana_program_runtime::{ + execution_budget::SVMTransactionExecutionAndFeeBudgetLimits, + loaded_programs::{ProgramCacheForTxBatch, ProgramRuntimeEnvironments}, + program_cache_entry::ProgramCacheEntryType, + }, + solana_pubkey::Pubkey, + solana_svm::{ + account_loader::{AccountLoader, CheckedTransactionDetails, TransactionCheckResult}, + transaction_processing_result::{ + ProcessedTransaction, TransactionProcessingResultExtensions, + }, + transaction_processor::{ + ExecutionRecordingConfig, TransactionBatchProcessor, TransactionProcessingConfig, + TransactionProcessingEnvironment, get_mock_transaction_processing_environment, + }, + }, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_timings::ExecuteTimings, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + std::collections::{HashMap, HashSet}, +}; + +mod mock_bank; + +const MAX_ITERATIONS: usize = 10_000; + +fn program_cache_execution(threads: usize) { + let mut mock_bank = MockBankCallback::default(); + let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); + let batch_processor = TransactionBatchProcessor::new(5, 5, Arc::downgrade(&fork_graph), None); + + let programs = vec![ + deploy_program("hello-solana".to_string(), 0, &mut mock_bank), + deploy_program("simple-transfer".to_string(), 0, &mut mock_bank), + deploy_program("clock-sysvar".to_string(), 0, &mut mock_bank), + ]; + + let account_maps: HashMap = programs.iter().map(|key| (*key, 0)).collect(); + + let ths: Vec<_> = (0..threads) + .map(|_| { + let local_bank = mock_bank.clone(); + let processor = TransactionBatchProcessor::new_from( + &batch_processor, + batch_processor.slot, + batch_processor.epoch, + ); + let maps = account_maps.clone(); + let programs = programs.clone(); + thread::spawn(move || { + let feature_set = SVMFeatureSet::all_enabled(); + let account_loader = AccountLoader::new_with_loaded_accounts_capacity( + None, + &local_bank, + &feature_set, + 0, + ); + let mut result = ProgramCacheForTxBatch::new(processor.slot); + let program_runtime_environment_for_execution = + processor.program_runtime_environment_for_epoch(processor.epoch); + processor.replenish_program_cache( + &account_loader, + &maps, + &program_runtime_environment_for_execution, + &mut result, + &mut ExecuteTimings::default(), + false, + true, + true, + ); + for key in &programs { + let cache_entry = result.find(key); + assert!(matches!( + cache_entry.unwrap().program, + ProgramCacheEntryType::Loaded(_) + )); + } + }) + }) + .collect(); + + for th in ths { + th.join().unwrap(); + } +} + +// Shuttle has its own internal scheduler and the following tests change the way it operates to +// increase the efficiency in finding problems in the program cache's concurrent code. + +// This test leverages the probabilistic concurrency testing algorithm +// (https://www.microsoft.com/en-us/research/wp-content/uploads/2016/02/asplos277-pct.pdf). +// It bounds the numbers of preemptions to explore (five in this test) for the four +// threads we use. We run it for 300 iterations. +#[test] +fn test_program_cache_with_probabilistic_scheduler() { + shuttle::check_pct( + move || { + program_cache_execution(4); + }, + MAX_ITERATIONS, + 5, + ); +} + +// In this case, the scheduler is random and may preempt threads at any point and any time. +#[test] +fn test_program_cache_with_random_scheduler() { + shuttle::check_random(move || program_cache_execution(4), MAX_ITERATIONS); +} + +// This test explores all the possible thread scheduling patterns that might affect the program +// cache. There is a limitation to run only 500 iterations to avoid consuming too much CI time. +#[test] +fn test_program_cache_with_exhaustive_scheduler() { + // The DFS (shuttle::check_dfs) test is only complete when we do not generate random + // values in a thread. + // Since this is not the case for the execution of jitted program, we can still run the test + // but with decreased accuracy. + let scheduler = shuttle::scheduler::DfsScheduler::new(Some(MAX_ITERATIONS), true); + let runner = Runner::new(scheduler, Default::default()); + runner.run(move || program_cache_execution(4)); +} + +// This test executes multiple transactions in parallel where all read from the same data account, +// but write to different accounts. Given that there are no locks in this case, SVM must behave +// correctly. +fn svm_concurrent() { + let mock_bank = Arc::new(MockBankCallback::default()); + let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); + let batch_processor = Arc::new(TransactionBatchProcessor::new( + 5, + 2, + Arc::downgrade(&fork_graph), + Some(create_custom_loader()), + )); + + mock_bank.configure_sysvars(); + batch_processor.fill_missing_sysvar_cache_entries(&*mock_bank); + register_builtins(&mock_bank, &batch_processor); + + let program_id = deploy_program("transfer-from-account".to_string(), 0, &mock_bank); + + const THREADS: usize = 4; + const TRANSACTIONS_PER_THREAD: usize = 3; + const AMOUNT: u64 = 50; + const CAPACITY: usize = THREADS * TRANSACTIONS_PER_THREAD; + const BALANCE: u64 = 10_000_000; + + let mut transactions = vec![Vec::new(); THREADS]; + let mut check_data = vec![Vec::new(); THREADS]; + let read_account = Pubkey::new_unique(); + let mut account_data = AccountSharedData::default(); + account_data.set_data(AMOUNT.to_le_bytes().to_vec()); + account_data.set_rent_epoch(u64::MAX); + account_data.set_lamports(1); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(read_account, account_data); + + #[derive(Clone)] + struct CheckTxData { + sender: Pubkey, + recipient: Pubkey, + fee_payer: Pubkey, + } + + for idx in 0..CAPACITY { + let sender = Pubkey::new_unique(); + let recipient = Pubkey::new_unique(); + let fee_payer = Pubkey::new_unique(); + let system_account = Pubkey::from([0u8; 32]); + + let mut account_data = AccountSharedData::default(); + account_data.set_lamports(BALANCE); + + { + let shared_data = &mut mock_bank.account_shared_data.write().unwrap(); + shared_data.insert(sender, account_data.clone()); + shared_data.insert(recipient, account_data.clone()); + shared_data.insert(fee_payer, account_data); + } + + let accounts = vec![ + AccountMeta { + pubkey: sender, + is_signer: true, + is_writable: true, + }, + AccountMeta { + pubkey: recipient, + is_signer: false, + is_writable: true, + }, + AccountMeta { + pubkey: read_account, + is_signer: false, + is_writable: false, + }, + AccountMeta { + pubkey: system_account, + is_signer: false, + is_writable: false, + }, + ]; + + let instruction = Instruction::new_with_bytes(program_id, &[0], accounts); + let legacy_transaction = Transaction::new_with_payer(&[instruction], Some(&fee_payer)); + + let sanitized_transaction = + SanitizedTransaction::try_from_legacy_transaction(legacy_transaction, &HashSet::new()); + transactions[idx % THREADS].push(sanitized_transaction.unwrap()); + check_data[idx % THREADS].push(CheckTxData { + fee_payer, + recipient, + sender, + }); + } + + let ths: Vec<_> = (0..THREADS) + .map(|idx| { + let local_batch = batch_processor.clone(); + let local_bank = mock_bank.clone(); + let th_txs = std::mem::take(&mut transactions[idx]); + let check_results = th_txs + .iter() + .map(|tx| { + Ok(CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::with_fee( + MockBankCallback::calculate_fee_details(tx, 0), + ), + )) as TransactionCheckResult + }) + .collect(); + let processing_config = TransactionProcessingConfig { + recording_config: ExecutionRecordingConfig { + enable_log_recording: true, + enable_return_data_recording: false, + enable_cpi_recording: false, + enable_transaction_balance_recording: false, + }, + ..Default::default() + }; + let check_tx_data = std::mem::take(&mut check_data[idx]); + + thread::spawn(move || { + let result = local_batch.load_and_execute_sanitized_transactions( + &*local_bank, + &th_txs, + check_results, + &TransactionProcessingEnvironment { + program_runtime_environments: ProgramRuntimeEnvironments::new( + local_batch.program_runtime_environment.clone(), + local_batch.program_runtime_environment.clone(), + ), + ..get_mock_transaction_processing_environment() + }, + &processing_config, + ); + + for (idx, processing_result) in result.processing_results.iter().enumerate() { + assert!(processing_result.was_processed()); + let processed_tx = processing_result.processed_transaction().unwrap(); + assert_matches!(processed_tx, &ProcessedTransaction::Executed(_)); + let executed_tx = processed_tx.executed_transaction().unwrap(); + let inserted_accounts = &check_tx_data[idx]; + for (key, account_data) in &executed_tx.loaded_transaction.accounts { + if *key == inserted_accounts.fee_payer { + assert_eq!(account_data.lamports(), BALANCE - 10000); + } else if *key == inserted_accounts.sender { + assert_eq!(account_data.lamports(), BALANCE - AMOUNT); + } else if *key == inserted_accounts.recipient { + assert_eq!(account_data.lamports(), BALANCE + AMOUNT); + } + } + } + }) + }) + .collect(); + + for th in ths { + th.join().unwrap(); + } +} + +#[test] +fn test_svm_with_probabilistic_scheduler() { + shuttle::check_pct( + move || { + svm_concurrent(); + }, + MAX_ITERATIONS, + 5, + ); +} diff --git a/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml new file mode 100644 index 0000000..be59164 --- /dev/null +++ b/solana/svm/tests/example-programs/clock-sysvar/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "clock-sysvar-program" +version = "4.1.1" +edition = "2021" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so b/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so new file mode 100755 index 0000000..4c744a2 Binary files /dev/null and b/solana/svm/tests/example-programs/clock-sysvar/clock_sysvar_program.so differ diff --git a/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs b/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs new file mode 100644 index 0000000..b35d142 --- /dev/null +++ b/solana/svm/tests/example-programs/clock-sysvar/src/lib.rs @@ -0,0 +1,21 @@ +use { + solana_account_info::AccountInfo, + solana_program::program::set_return_data, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_sysvar::{clock::Clock, Sysvar}, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + let time_now = Clock::get().unwrap().unix_timestamp; + let return_data = time_now.to_be_bytes(); + set_return_data(&return_data); + Ok(()) +} diff --git a/solana/svm/tests/example-programs/hello-solana/Cargo.toml b/solana/svm/tests/example-programs/hello-solana/Cargo.toml new file mode 100644 index 0000000..b7c55eb --- /dev/null +++ b/solana/svm/tests/example-programs/hello-solana/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "hello-solana-program" +version = "4.1.1" +edition = "2021" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so b/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so new file mode 100755 index 0000000..f79f4f1 Binary files /dev/null and b/solana/svm/tests/example-programs/hello-solana/hello_solana_program.so differ diff --git a/solana/svm/tests/example-programs/hello-solana/src/lib.rs b/solana/svm/tests/example-programs/hello-solana/src/lib.rs new file mode 100644 index 0000000..3f6799c --- /dev/null +++ b/solana/svm/tests/example-programs/hello-solana/src/lib.rs @@ -0,0 +1,16 @@ +use { + solana_account_info::AccountInfo, solana_msg::msg, solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, solana_pubkey::Pubkey, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + msg!("Hello, Solana!"); + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/simple-transfer/Cargo.toml b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml new file mode 100644 index 0000000..6767871 --- /dev/null +++ b/solana/svm/tests/example-programs/simple-transfer/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "simple-transfer-program" +version = "4.1.1" +edition = "2021" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so b/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so new file mode 100755 index 0000000..b9041ec Binary files /dev/null and b/solana/svm/tests/example-programs/simple-transfer/simple_transfer_program.so differ diff --git a/solana/svm/tests/example-programs/simple-transfer/src/lib.rs b/solana/svm/tests/example-programs/simple-transfer/src/lib.rs new file mode 100644 index 0000000..1e922c2 --- /dev/null +++ b/solana/svm/tests/example-programs/simple-transfer/src/lib.rs @@ -0,0 +1,29 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_program::program::invoke, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_system_interface::instruction as system_instruction, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let amount = u64::from_be_bytes(data[0..8].try_into().unwrap()); + let accounts_iter = &mut accounts.iter(); + let payer = next_account_info(accounts_iter)?; + let recipient = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + invoke( + &system_instruction::transfer(payer.key, recipient.key, amount), + &[payer.clone(), recipient.clone(), system_program.clone()], + )?; + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml new file mode 100644 index 0000000..2484b5a --- /dev/null +++ b/solana/svm/tests/example-programs/transfer-from-account/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "transfer-from-account" +version = "4.1.1" +edition = "2021" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs b/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs new file mode 100644 index 0000000..4460873 --- /dev/null +++ b/solana/svm/tests/example-programs/transfer-from-account/src/lib.rs @@ -0,0 +1,31 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_program::program::invoke, + solana_program_entrypoint::entrypoint, + solana_program_error::ProgramResult, + solana_pubkey::Pubkey, + solana_system_interface::instruction as system_instruction, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + _data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let payer = next_account_info(accounts_iter)?; + let recipient = next_account_info(accounts_iter)?; + let data_account = next_account_info(accounts_iter)?; + let system_program = next_account_info(accounts_iter)?; + + let amount = u64::from_le_bytes(data_account.data.borrow()[0..8].try_into().unwrap()); + + invoke( + &system_instruction::transfer(payer.key, recipient.key, amount), + &[payer.clone(), recipient.clone(), system_program.clone()], + )?; + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so b/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so new file mode 100755 index 0000000..86e6859 Binary files /dev/null and b/solana/svm/tests/example-programs/transfer-from-account/transfer_from_account_program.so differ diff --git a/solana/svm/tests/example-programs/write-to-account/Cargo.toml b/solana/svm/tests/example-programs/write-to-account/Cargo.toml new file mode 100644 index 0000000..1875f3d --- /dev/null +++ b/solana/svm/tests/example-programs/write-to-account/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "write-to-account" +version = "4.1.1" +edition = "2021" + +[dependencies] + +[lib] +crate-type = ["cdylib", "rlib"] + +[workspace] diff --git a/solana/svm/tests/example-programs/write-to-account/src/lib.rs b/solana/svm/tests/example-programs/write-to-account/src/lib.rs new file mode 100644 index 0000000..0fde9fa --- /dev/null +++ b/solana/svm/tests/example-programs/write-to-account/src/lib.rs @@ -0,0 +1,62 @@ +use { + solana_account_info::{next_account_info, AccountInfo}, + solana_msg::msg, + solana_program_entrypoint::entrypoint, + solana_program_error::{ProgramError, ProgramResult}, + solana_pubkey::Pubkey, + solana_sdk_ids::incinerator, +}; + +entrypoint!(process_instruction); + +fn process_instruction( + _program_id: &Pubkey, + accounts: &[AccountInfo], + data: &[u8], +) -> ProgramResult { + let accounts_iter = &mut accounts.iter(); + let target_account_info = next_account_info(accounts_iter)?; + match data[0] { + // print account size + 0 => { + msg!( + "account size {}", + target_account_info.try_borrow_data()?.len() + ); + } + // set account data + 1 => { + let mut account_data = target_account_info.try_borrow_mut_data()?; + account_data[0] = 100; + } + // deallocate account + 2 => { + let incinerator_info = next_account_info(accounts_iter)?; + if !incinerator::check_id(incinerator_info.key) { + return Err(ProgramError::InvalidAccountData); + } + + let mut target_lamports = target_account_info.try_borrow_mut_lamports()?; + let mut incinerator_lamports = incinerator_info.try_borrow_mut_lamports()?; + + **incinerator_lamports = incinerator_lamports + .checked_add(**target_lamports) + .ok_or(ProgramError::ArithmeticOverflow)?; + + **target_lamports = target_lamports + .checked_sub(**target_lamports) + .ok_or(ProgramError::InsufficientFunds)?; + } + // reallocate account + 3 => { + let new_size = usize::from_le_bytes(data[1..9].try_into().unwrap()); + target_account_info.realloc(new_size, true)?; + } + // bad ixn + _ => { + return Err(ProgramError::InvalidArgument); + } + } + + Ok(()) +} diff --git a/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so b/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so new file mode 100755 index 0000000..a6a1472 Binary files /dev/null and b/solana/svm/tests/example-programs/write-to-account/write_to_account_program.so differ diff --git a/solana/svm/tests/integration_test.rs b/solana/svm/tests/integration_test.rs new file mode 100644 index 0000000..810a01d --- /dev/null +++ b/solana/svm/tests/integration_test.rs @@ -0,0 +1,4011 @@ +#![cfg(test)] +#![allow(clippy::arithmetic_side_effects)] + +use { + crate::mock_bank::{ + EXECUTION_EPOCH, EXECUTION_SLOT, MockBankCallback, MockForkGraph, WALLCLOCK_TIME, + create_custom_loader, deploy_program_with_upgrade_authority, load_program, program_address, + program_data_size, register_builtins, + }, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::Slot, + solana_compute_budget::compute_budget_limits::ComputeBudgetLimits, + solana_compute_budget_interface::ComputeBudgetInstruction, + solana_fee_structure::FeeDetails, + solana_hash::Hash, + solana_instruction::{AccountMeta, Instruction}, + solana_keypair::Keypair, + solana_loader_v3_interface::{ + get_program_data_address, instruction as loaderv3_instruction, + state::UpgradeableLoaderState, + }, + solana_native_token::LAMPORTS_PER_SOL, + solana_nonce::{self as nonce, state::DurableNonce}, + solana_program_entrypoint::MAX_PERMITTED_DATA_INCREASE, + solana_program_runtime::{ + execution_budget::{ + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, SVMTransactionExecutionAndFeeBudgetLimits, + }, + loaded_programs::ProgramRuntimeEnvironments, + }, + solana_pubkey::Pubkey, + solana_sdk_ids::{ + bpf_loader, bpf_loader_deprecated, bpf_loader_upgradeable, compute_budget, native_loader, + }, + solana_signer::Signer, + solana_svm::{ + account_loader::{ + CheckedTransactionDetails, TRANSACTION_ACCOUNT_BASE_SIZE, TransactionCheckResult, + }, + nonce_info::NonceInfo, + transaction_execution_result::TransactionExecutionDetails, + transaction_processing_result::{ + ProcessedTransaction, TransactionProcessingResult, + TransactionProcessingResultExtensions, + }, + transaction_processor::{ + ExecutionRecordingConfig, LoadAndExecuteSanitizedTransactionsOutput, + TransactionBatchProcessor, TransactionProcessingConfig, + TransactionProcessingEnvironment, + }, + }, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_transaction::{ + instruction::SVMInstruction, + svm_message::{SVMMessage, SVMStaticMessage}, + }, + solana_svm_type_overrides::sync::{Arc, RwLock}, + solana_system_interface::{instruction as system_instruction, program as system_program}, + solana_system_transaction as system_transaction, + solana_sysvar::rent::Rent, + solana_transaction::{Transaction, sanitized::SanitizedTransaction}, + solana_transaction_context::transaction::TransactionReturnData, + solana_transaction_error::TransactionError, + std::{collections::HashMap, num::NonZeroU32, slice, sync::atomic::Ordering}, + test_case::test_case, +}; + +// This module contains the implementation of TransactionProcessingCallback +mod mock_bank; + +// Local implementation of compute budget processing for tests. +fn process_test_compute_budget_instructions<'a>( + instructions: impl Iterator)> + Clone, +) -> Result { + let mut loaded_accounts_data_size_limit = None; + + // Scan for compute budget instructions. + // Only key on `SetLoadedAccountsDataSizeLimit`. + for (program_id, instruction) in instructions { + if *program_id == compute_budget::id() + && instruction.data.len() >= 5 + && instruction.data[0] == 4 + { + let size = u32::from_le_bytes([ + instruction.data[1], + instruction.data[2], + instruction.data[3], + instruction.data[4], + ]); + loaded_accounts_data_size_limit = Some(size); + } + } + + let loaded_accounts_bytes = + if let Some(requested_loaded_accounts_data_size_limit) = loaded_accounts_data_size_limit { + NonZeroU32::new(requested_loaded_accounts_data_size_limit) + .ok_or(TransactionError::InvalidLoadedAccountsDataSizeLimit)? + } else { + MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES + } + .min(MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES); + + Ok(ComputeBudgetLimits { + loaded_accounts_bytes, + ..Default::default() + }) +} + +const DEPLOYMENT_SLOT: u64 = 0; +const LAMPORTS_PER_SIGNATURE: u64 = 5000; +const LAST_BLOCKHASH: Hash = Hash::new_from_array([7; 32]); // Arbitrary constant hash for advancing nonces + +pub type AccountsMap = HashMap; + +// container for everything needed to execute a test entry +// care should be taken if reused, because we update bank account states, but otherwise leave it as-is +// the environment is made available for tests that check it after processing +pub struct SvmTestEnvironment<'a> { + pub mock_bank: MockBankCallback, + pub fork_graph: Arc>, + pub batch_processor: TransactionBatchProcessor, + pub processing_config: TransactionProcessingConfig<'a>, + pub processing_environment: TransactionProcessingEnvironment, + pub test_entry: SvmTestEntry, +} + +impl SvmTestEnvironment<'_> { + pub fn create(test_entry: SvmTestEntry) -> Self { + let mock_bank = MockBankCallback::default(); + + for (name, slot, authority) in &test_entry.initial_programs { + deploy_program_with_upgrade_authority(name.to_string(), *slot, &mock_bank, *authority); + } + + for (pubkey, account) in &test_entry.initial_accounts { + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(*pubkey, account.clone()); + } + + let fork_graph = Arc::new(RwLock::new(MockForkGraph {})); + let batch_processor = TransactionBatchProcessor::new( + EXECUTION_SLOT, + EXECUTION_EPOCH, + Arc::downgrade(&fork_graph), + Some(create_custom_loader()), + ); + + // The sysvars must be put in the cache + mock_bank.configure_sysvars(); + batch_processor.fill_missing_sysvar_cache_entries(&mock_bank); + register_builtins(&mock_bank, &batch_processor); + + let processing_config = TransactionProcessingConfig { + recording_config: ExecutionRecordingConfig { + enable_log_recording: true, + enable_return_data_recording: true, + enable_cpi_recording: false, + enable_transaction_balance_recording: false, + }, + drop_on_failure: test_entry.drop_on_failure, + all_or_nothing: test_entry.all_or_nothing, + ..Default::default() + }; + + let processing_environment = TransactionProcessingEnvironment { + blockhash: LAST_BLOCKHASH, + blockhash_lamports_per_signature: LAMPORTS_PER_SIGNATURE, + alpenglow_migration_succeeded: false, + epoch_total_stake: 0, + feature_set: test_entry.feature_set, + program_runtime_environments: ProgramRuntimeEnvironments::new( + batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH), + batch_processor.program_runtime_environment_for_epoch(EXECUTION_EPOCH), + ), + rent: test_entry.rent.clone(), + }; + + Self { + mock_bank, + fork_graph, + batch_processor, + processing_config, + processing_environment, + test_entry, + } + } + + pub fn execute(&self) -> LoadAndExecuteSanitizedTransactionsOutput { + let (transactions, check_results) = self.test_entry.prepare_transactions(); + let batch_output = self + .batch_processor + .load_and_execute_sanitized_transactions( + &self.mock_bank, + &transactions, + check_results, + &self.processing_environment, + &self.processing_config, + ); + + // build a hashmap of final account states incrementally + // starting with all initial states, updating to all final states + // with SIMD83, an account might change multiple times in the same batch + // but it might not exist on all transactions + let mut final_accounts_actual = self.test_entry.initial_accounts.clone(); + let update_or_dealloc_account = + |final_accounts: &mut AccountsMap, pubkey, account: AccountSharedData| { + if account.lamports() == 0 { + final_accounts.insert(pubkey, AccountSharedData::default()); + } else { + final_accounts.insert(pubkey, account); + } + }; + + for (tx_index, processed_transaction) in batch_output.processing_results.iter().enumerate() + { + let sanitized_transaction = &transactions[tx_index]; + + match processed_transaction { + Ok(ProcessedTransaction::Executed(executed_transaction)) => { + if executed_transaction.was_successful() { + for (index, (pubkey, account_data)) in executed_transaction + .loaded_transaction + .accounts + .iter() + .enumerate() + { + if sanitized_transaction.is_writable(index) { + update_or_dealloc_account( + &mut final_accounts_actual, + *pubkey, + account_data.clone(), + ); + } + } + } else { + for (pubkey, account_data) in + &executed_transaction.loaded_transaction.rollback_accounts + { + update_or_dealloc_account( + &mut final_accounts_actual, + *pubkey, + account_data.clone(), + ); + } + } + } + Ok(ProcessedTransaction::FeesOnly(fees_only_transaction)) => { + for (pubkey, account_data) in &fees_only_transaction.rollback_accounts { + update_or_dealloc_account( + &mut final_accounts_actual, + *pubkey, + account_data.clone(), + ); + } + } + Err(_) => {} + } + } + + // first assert all transaction states together, it makes test-driven development much less of a headache + let (actual_statuses, expected_statuses): (Vec<_>, Vec<_>) = batch_output + .processing_results + .iter() + .zip(self.test_entry.asserts()) + .map(|(processing_result, test_item_assert)| { + ( + ExecutionStatus::from(processing_result), + test_item_assert.status, + ) + }) + .unzip(); + assert_eq!( + expected_statuses, + actual_statuses, + "mismatch between expected and actual statuses. execution details:\n{}", + batch_output + .processing_results + .iter() + .enumerate() + .map(|(i, tx)| match tx { + Ok(ProcessedTransaction::Executed(executed)) => { + format!("{} (executed): {:#?}", i, executed.execution_details) + } + Ok(ProcessedTransaction::FeesOnly(fee_only)) => { + format!("{} (fee-only): {:?}", i, fee_only.load_error) + } + Err(e) => format!("{i} (discarded): {e:?}"), + }) + .collect::>() + .join("\n"), + ); + + // check that all the account states we care about are present and correct + for (pubkey, expected_account_data) in self.test_entry.final_accounts.iter() { + let actual_account_data = final_accounts_actual.get(pubkey); + assert_eq!( + Some(expected_account_data), + actual_account_data, + "mismatch on account {pubkey}" + ); + } + + // now run our transaction-by-transaction checks + for (processing_result, test_item_asserts) in batch_output + .processing_results + .iter() + .zip(self.test_entry.asserts()) + { + match processing_result { + Ok(ProcessedTransaction::Executed(executed_transaction)) => test_item_asserts + .check_executed_transaction(&executed_transaction.execution_details), + Ok(ProcessedTransaction::FeesOnly(_)) => { + assert!(test_item_asserts.processed()); + assert!(!test_item_asserts.executed()); + } + Err(_) => assert!(test_item_asserts.discarded()), + } + } + + // merge new account states into the bank for multi-batch tests + let mut mock_bank_accounts = self.mock_bank.account_shared_data.write().unwrap(); + mock_bank_accounts.extend(final_accounts_actual); + + // update global program cache + for processing_result in batch_output.processing_results.iter() { + if let Some(ProcessedTransaction::Executed(executed_tx)) = + processing_result.processed_transaction() + { + let programs_modified_by_tx = &executed_tx.programs_modified_by_tx; + if executed_tx.was_successful() && !programs_modified_by_tx.is_empty() { + self.batch_processor + .global_program_cache + .write() + .unwrap() + .merge( + &self.batch_processor.program_runtime_environment, + self.batch_processor.slot, + programs_modified_by_tx, + ); + } + } + } + + batch_output + } + + pub fn is_program_blocked(&self, program_id: &Pubkey) -> bool { + let (_, program_cache_entry) = self + .batch_processor + .global_program_cache + .read() + .unwrap() + .get_flattened_entries_for_tests() + .into_iter() + .rev() + .find(|(key, _)| key == program_id) + .unwrap(); + + // in the same batch, a new valid loaderv3 program may have a Loaded entry with a later execution slot + // in a later batch, the same loaderv3 program will have a DelayedVisibility tombstone + // a new loaderv1/v2 account will have a FailedVerification tombstone + // and a closed loaderv3 program or any loaderv3 buffer will have a Closed tombstone + program_cache_entry.effective_slot > EXECUTION_SLOT || program_cache_entry.is_tombstone() + } +} + +// container for a transaction batch and all data needed to run and verify it against svm +#[derive(Clone)] +pub struct SvmTestEntry { + // features configuration for this test + pub feature_set: SVMFeatureSet, + + // enables drop on failure processing (transactions without Ok status have no state effect) + pub drop_on_failure: bool, + + // enables all or nothing processing (if not all transactions can be committed then none are) + pub all_or_nothing: bool, + + // programs to deploy to the new svm + pub initial_programs: Vec<(String, Slot, Option)>, + + // accounts to deploy to the new svm before transaction execution + pub initial_accounts: AccountsMap, + + // transactions to execute and transaction-specific checks to perform on the results from svm + pub transaction_batch: Vec, + + // expected final account states, checked after transaction execution + pub final_accounts: AccountsMap, + + // rent parameters for the test + pub rent: Rent, +} + +impl Default for SvmTestEntry { + fn default() -> Self { + Self { + feature_set: SVMFeatureSet::all_enabled(), + all_or_nothing: false, + drop_on_failure: false, + initial_programs: Vec::new(), + initial_accounts: HashMap::new(), + transaction_batch: Vec::new(), + final_accounts: HashMap::new(), + rent: Rent::default(), + } + } +} + +impl SvmTestEntry { + pub fn set_rent_params(&mut self, rent: Rent) { + self.rent = rent; + } + + // add a new rent-exempt account that exists before the batch + // inserts it into both account maps, assuming it lives unchanged (except for svm fixing rent epoch) + // rent-paying accounts must be added by hand because svm will not set rent epoch to u64::MAX + pub fn add_initial_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) { + assert!( + self.initial_accounts + .insert(pubkey, account.clone()) + .is_none() + ); + + self.create_expected_account(pubkey, account); + } + + // add an immutable program that will have been deployed before the slot we execute transactions in + pub fn add_initial_program(&mut self, program_name: &str) { + self.initial_programs + .push((program_name.to_string(), DEPLOYMENT_SLOT, None)); + } + + // add a new rent-exempt account that is created by the transaction + // inserts it only into the post account map + pub fn create_expected_account(&mut self, pubkey: Pubkey, account: &AccountSharedData) { + let mut account = account.clone(); + account.set_rent_epoch(u64::MAX); + + assert!(self.final_accounts.insert(pubkey, account).is_none()); + } + + // edit an existing account to reflect changes you expect the transaction to make to it + pub fn update_expected_account_data(&mut self, pubkey: Pubkey, account: &AccountSharedData) { + let mut account = account.clone(); + account.set_rent_epoch(u64::MAX); + + assert!(self.final_accounts.insert(pubkey, account).is_some()); + } + + // indicate that an existing account is expected to be deallocated + pub fn drop_expected_account(&mut self, pubkey: Pubkey) { + assert!( + self.final_accounts + .insert(pubkey, AccountSharedData::default()) + .is_some() + ); + } + + // add lamports to an existing expected final account state + pub fn increase_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) { + self.final_accounts + .get_mut(pubkey) + .unwrap() + .checked_add_lamports(lamports) + .unwrap(); + } + + // subtract lamports from an existing expected final account state + pub fn decrease_expected_lamports(&mut self, pubkey: &Pubkey, lamports: u64) { + self.final_accounts + .get_mut(pubkey) + .unwrap() + .checked_sub_lamports(lamports) + .unwrap(); + } + + // convenience function that adds a transaction that is expected to succeed + pub fn push_transaction(&mut self, transaction: Transaction) { + self.push_transaction_with_status(transaction, ExecutionStatus::Succeeded) + } + + // convenience function that adds a transaction with an expected execution status + pub fn push_transaction_with_status( + &mut self, + transaction: Transaction, + status: ExecutionStatus, + ) { + self.transaction_batch.push(TransactionBatchItem { + transaction, + asserts: TransactionBatchItemAsserts { + status, + ..TransactionBatchItemAsserts::default() + }, + ..TransactionBatchItem::default() + }); + } + + // convenience function that adds a nonce transaction that is expected to succeed + pub fn push_nonce_transaction(&mut self, transaction: Transaction, nonce_address: Pubkey) { + self.push_nonce_transaction_with_status( + transaction, + nonce_address, + ExecutionStatus::Succeeded, + ) + } + + // convenience function that adds a nonce transaction with an expected execution status + pub fn push_nonce_transaction_with_status( + &mut self, + transaction: Transaction, + nonce_address: Pubkey, + status: ExecutionStatus, + ) { + self.transaction_batch.push(TransactionBatchItem { + transaction, + asserts: TransactionBatchItemAsserts { + status, + ..TransactionBatchItemAsserts::default() + }, + ..TransactionBatchItem::with_nonce(nonce_address) + }); + } + + // internal helper to gather SanitizedTransaction objects for execution + fn prepare_transactions(&self) -> (Vec, Vec) { + self.transaction_batch + .iter() + .cloned() + .map(|item| { + let message = SanitizedTransaction::from_transaction_for_tests(item.transaction); + let check_result = item.check_result.map(|tx_details| { + let compute_budget_limits = process_test_compute_budget_instructions( + SVMStaticMessage::program_instructions_iter(&message), + ); + let signature_count = message + .num_transaction_signatures() + .saturating_add(message.num_ed25519_signatures()) + .saturating_add(message.num_secp256k1_signatures()) + .saturating_add(message.num_secp256r1_signatures()); + + let compute_budget = compute_budget_limits + .map(|v| { + v.get_compute_budget_and_limits( + v.loaded_accounts_bytes, + FeeDetails::new( + signature_count.saturating_mul(LAMPORTS_PER_SIGNATURE), + v.get_prioritization_fee(), + ), + self.feature_set.raise_cpi_nesting_limit_to_8, + ) + }) + .unwrap(); + CheckedTransactionDetails::new(tx_details.nonce_address, compute_budget) + }); + + (message, check_result) + }) + .unzip() + } + + // internal helper to gather test items for post-execution checks + fn asserts(&self) -> Vec { + self.transaction_batch + .iter() + .cloned() + .map(|item| item.asserts) + .collect() + } +} + +// one transaction in a batch plus check results for svm and asserts for tests +#[derive(Clone, Debug)] +pub struct TransactionBatchItem { + pub transaction: Transaction, + pub check_result: TransactionCheckResult, + pub asserts: TransactionBatchItemAsserts, +} + +impl TransactionBatchItem { + fn with_nonce(nonce_address: Pubkey) -> Self { + Self { + check_result: Ok(CheckedTransactionDetails::new( + Some(nonce_address), + SVMTransactionExecutionAndFeeBudgetLimits::default(), + )), + ..Self::default() + } + } +} + +impl Default for TransactionBatchItem { + fn default() -> Self { + Self { + transaction: Transaction::default(), + check_result: Ok(CheckedTransactionDetails::new( + None, + SVMTransactionExecutionAndFeeBudgetLimits::default(), + )), + asserts: TransactionBatchItemAsserts::default(), + } + } +} + +// asserts for a given transaction in a batch +// we can automatically check whether it executed, whether it succeeded +// log items we expect to see (exact match only), and rodata +#[derive(Clone, Debug, Default)] +pub struct TransactionBatchItemAsserts { + pub status: ExecutionStatus, + pub logs: Vec, + pub return_data: ReturnDataAssert, +} + +impl TransactionBatchItemAsserts { + pub fn succeeded(&self) -> bool { + self.status.succeeded() + } + + pub fn executed(&self) -> bool { + self.status.executed() + } + + pub fn processed(&self) -> bool { + self.status.processed() + } + + pub fn discarded(&self) -> bool { + self.status.discarded() + } + + pub fn check_executed_transaction(&self, execution_details: &TransactionExecutionDetails) { + assert!(self.executed()); + assert_eq!(self.succeeded(), execution_details.status.is_ok()); + + if !self.logs.is_empty() { + let actual_logs = execution_details.log_messages.as_ref().unwrap(); + for expected_log in &self.logs { + assert!(actual_logs.contains(expected_log)); + } + } + + if self.return_data != ReturnDataAssert::Skip { + assert_eq!( + self.return_data, + execution_details.return_data.clone().into() + ); + } + } +} + +impl From for TransactionBatchItemAsserts { + fn from(status: ExecutionStatus) -> Self { + Self { + status, + ..Self::default() + } + } +} + +// states a transaction can end in after a trip through the batch processor: +// * discarded: no-op. not even processed. a flawed transaction excluded from the entry +// * processed-failed: aka fee (and nonce) only. charged and added to an entry but not executed, would have failed invariably +// * executed-failed: failed during execution. as above, fees charged and nonce advanced +// * succeeded: what we all aspire to be in our transaction processing lifecycles +#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, PartialOrd, Ord)] +pub enum ExecutionStatus { + Discarded, + ProcessedFailed, + ExecutedFailed, + #[default] + Succeeded, +} + +// note we avoid the word "failed" because it is confusing +// the batch processor uses it to mean "executed and not succeeded" +// but intuitively (and from the point of a user) it could just as likely mean "any state other than succeeded" +impl ExecutionStatus { + pub fn succeeded(self) -> bool { + self == Self::Succeeded + } + + pub fn executed(self) -> bool { + self > Self::ProcessedFailed + } + + pub fn processed(self) -> bool { + self != Self::Discarded + } + + pub fn discarded(self) -> bool { + self == Self::Discarded + } +} + +impl From<&TransactionProcessingResult> for ExecutionStatus { + fn from(processing_result: &TransactionProcessingResult) -> Self { + match processing_result { + Ok(ProcessedTransaction::Executed(executed_transaction)) => { + if executed_transaction.execution_details.status.is_ok() { + ExecutionStatus::Succeeded + } else { + ExecutionStatus::ExecutedFailed + } + } + Ok(ProcessedTransaction::FeesOnly(_)) => ExecutionStatus::ProcessedFailed, + Err(_) => ExecutionStatus::Discarded, + } + } +} + +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub enum ReturnDataAssert { + Some(TransactionReturnData), + None, + #[default] + Skip, +} + +impl From> for ReturnDataAssert { + fn from(option_ro_data: Option) -> Self { + match option_ro_data { + Some(ro_data) => Self::Some(ro_data), + None => Self::None, + } + } +} + +fn program_medley(drop_on_failure: bool) -> Vec { + let mut test_entry = SvmTestEntry { + drop_on_failure, + ..Default::default() + }; + + // 0: A transaction that works without any account + { + let program_name = "hello-solana"; + let program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + + test_entry.transaction_batch[0] + .asserts + .logs + .push("Program log: Hello, Solana!".to_string()); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + } + + // 1: A simple funds transfer between accounts + { + let program_name = "simple-transfer"; + let program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let sender_keypair = Keypair::new(); + + let fee_payer = fee_payer_keypair.pubkey(); + let sender = sender_keypair.pubkey(); + let recipient = Pubkey::new_unique(); + + let transfer_amount = 10; + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let mut sender_data = AccountSharedData::default(); + sender_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(sender, &sender_data); + + let mut recipient_data = AccountSharedData::default(); + recipient_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(recipient, &recipient_data); + + let instruction = Instruction::new_with_bytes( + program_id, + &u64::to_be_bytes(transfer_amount), + vec![ + AccountMeta::new(sender, true), + AccountMeta::new(recipient, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[instruction], + Some(&fee_payer), + &[&fee_payer_keypair, &sender_keypair], + Hash::default(), + )); + + test_entry.increase_expected_lamports(&recipient, transfer_amount); + test_entry.decrease_expected_lamports(&sender, transfer_amount); + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + } + + // 2: A program that utilizes a Sysvar + { + let program_name = "clock-sysvar"; + let program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + + let ro_data = TransactionReturnData { + program_id, + data: i64::to_be_bytes(WALLCLOCK_TIME).to_vec(), + }; + test_entry.transaction_batch[2].asserts.return_data = ReturnDataAssert::Some(ro_data); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + } + + // 3: A transaction that fails + { + let program_id = program_address("simple-transfer"); + + let fee_payer_keypair = Keypair::new(); + let sender_keypair = Keypair::new(); + + let fee_payer = fee_payer_keypair.pubkey(); + let sender = sender_keypair.pubkey(); + let recipient = Pubkey::new_unique(); + + let base_amount = 900_000; + let transfer_amount = base_amount + 50; + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + fee_payer_data.set_rent_epoch(u64::MAX); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + if drop_on_failure { + test_entry.final_accounts.insert(fee_payer, fee_payer_data); + } + + let mut sender_data = AccountSharedData::default(); + sender_data.set_lamports(base_amount); + sender_data.set_rent_epoch(u64::MAX); + test_entry.add_initial_account(sender, &sender_data); + if drop_on_failure { + test_entry.final_accounts.insert(sender, sender_data); + } + + let mut recipient_data = AccountSharedData::default(); + recipient_data.set_lamports(base_amount); + recipient_data.set_rent_epoch(u64::MAX); + test_entry.add_initial_account(recipient, &recipient_data); + if drop_on_failure { + test_entry.final_accounts.insert(recipient, recipient_data); + } + + let instruction = Instruction::new_with_bytes( + program_id, + &u64::to_be_bytes(transfer_amount), + vec![ + AccountMeta::new(sender, true), + AccountMeta::new(recipient, false), + AccountMeta::new_readonly(system_program::id(), false), + ], + ); + + test_entry.push_transaction_with_status( + Transaction::new_signed_with_payer( + &[instruction], + Some(&fee_payer), + &[&fee_payer_keypair, &sender_keypair], + Hash::default(), + ), + match drop_on_failure { + true => ExecutionStatus::Discarded, + false => ExecutionStatus::ExecutedFailed, + }, + ); + + if !drop_on_failure { + test_entry.transaction_batch[3] + .asserts + .logs + .push("Transfer: insufficient lamports 900000, need 900050".to_string()); + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + } + } + + // 4: A transaction whose verification has already failed + { + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + test_entry.transaction_batch.push(TransactionBatchItem { + transaction: Transaction::new_signed_with_payer( + &[], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ), + check_result: Err(TransactionError::BlockhashNotFound), + asserts: ExecutionStatus::Discarded.into(), + }); + } + + vec![test_entry] +} + +fn simple_transfer(drop_on_failure: bool) -> Vec { + let mut test_entry = SvmTestEntry { + drop_on_failure, + ..Default::default() + }; + let transfer_amount = LAMPORTS_PER_SOL; + let drop_on_failure_status = |status: ExecutionStatus| match (drop_on_failure, status) { + (true, ExecutionStatus::Succeeded) => ExecutionStatus::Succeeded, + (true, _) => ExecutionStatus::Discarded, + (false, status) => status, + }; + + // 0: a transfer that succeeds + { + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination = Pubkey::new_unique(); + + let mut source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); + + source_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(source, &source_data); + + test_entry.push_transaction(system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + )); + + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + test_entry.create_expected_account(destination, &destination_data); + + test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); + } + + // 1: an executable transfer that fails + { + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + + let mut source_data = AccountSharedData::default(); + + source_data.set_lamports(transfer_amount - 1); + source_data.set_rent_epoch(u64::MAX); + test_entry.add_initial_account(source, &source_data); + if drop_on_failure { + test_entry.final_accounts.insert(source, source_data); + } + + test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &Pubkey::new_unique(), + transfer_amount, + Hash::default(), + ), + drop_on_failure_status(ExecutionStatus::ExecutedFailed), + ); + + if !drop_on_failure { + test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE); + } + } + + // 2: a non-processable transfer that fails before loading + { + test_entry.transaction_batch.push(TransactionBatchItem { + transaction: system_transaction::transfer( + &Keypair::new(), + &Pubkey::new_unique(), + transfer_amount, + Hash::default(), + ), + check_result: Err(TransactionError::BlockhashNotFound), + asserts: ExecutionStatus::Discarded.into(), + }); + } + + // 3: a non-processable transfer that fails loading the fee-payer + { + test_entry.push_transaction_with_status( + system_transaction::transfer( + &Keypair::new(), + &Pubkey::new_unique(), + transfer_amount, + Hash::default(), + ), + ExecutionStatus::Discarded, + ); + } + + // 4: a processable non-executable transfer that fails loading the program + { + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + + let mut source_data = AccountSharedData::default(); + + source_data.set_lamports(transfer_amount * 10); + test_entry + .initial_accounts + .insert(source, source_data.clone()); + test_entry.final_accounts.insert(source, source_data); + + let mut instruction = + system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount); + instruction.program_id = Pubkey::new_unique(); + + if !drop_on_failure { + test_entry.decrease_expected_lamports(&source, LAMPORTS_PER_SIGNATURE); + } + + test_entry.push_transaction_with_status( + Transaction::new_signed_with_payer( + &[instruction], + Some(&source), + &[&source_keypair], + Hash::default(), + ), + drop_on_failure_status(ExecutionStatus::ProcessedFailed), + ); + } + + vec![test_entry] +} + +fn simple_nonce(fee_paying_nonce: bool) -> Vec { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "hello-solana"; + let real_program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + // create and return a transaction, fee payer, and nonce info + // sets up initial account states but not final ones + // there are four cases of fee_paying_nonce and fake_fee_payer: + // * false/false: normal nonce account with rent minimum, normal fee payer account with 1sol + // * true/false: normal nonce account used to pay fees with rent minimum plus 1sol + // * false/true: normal nonce account with rent minimum, fee payer doesn't exist + // * true/true: same account for both which does not exist + // we also provide a side door to bring a fee-paying nonce account below rent-exemption + let mk_nonce_transaction = |test_entry: &mut SvmTestEntry, + program_id, + fake_fee_payer: bool, + rent_paying_nonce: bool| { + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + let nonce_pubkey = if fee_paying_nonce { + fee_payer + } else { + Pubkey::new_unique() + }; + + let nonce_size = nonce::state::State::size(); + let mut nonce_balance = Rent::default().minimum_balance(nonce_size); + + if !fake_fee_payer && !fee_paying_nonce { + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + fee_payer_data.set_rent_epoch(u64::MAX); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + } else if rent_paying_nonce { + assert!(fee_paying_nonce); + nonce_balance += LAMPORTS_PER_SIGNATURE; + nonce_balance -= 1; + } else if fee_paying_nonce { + nonce_balance += LAMPORTS_PER_SOL; + } + + let nonce_initial_hash = DurableNonce::from_blockhash(&Hash::new_unique()); + let nonce_data = + nonce::state::Data::new(fee_payer, nonce_initial_hash, LAMPORTS_PER_SIGNATURE); + let mut nonce_account = AccountSharedData::new_data( + nonce_balance, + &nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())), + &system_program::id(), + ) + .unwrap(); + nonce_account.set_rent_epoch(u64::MAX); + let nonce_info = NonceInfo::new(nonce_pubkey, nonce_account.clone()); + + if !(fake_fee_payer && fee_paying_nonce) { + test_entry.add_initial_account(nonce_pubkey, &nonce_account); + } + + let instructions = vec![ + system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer), + Instruction::new_with_bytes(program_id, &[], vec![]), + ]; + + let transaction = Transaction::new_signed_with_payer( + &instructions, + Some(&fee_payer), + &[&fee_payer_keypair], + nonce_data.blockhash(), + ); + + (transaction, fee_payer, nonce_info) + }; + + // 0: successful nonce transaction, regardless of features + { + let (transaction, fee_payer, mut nonce_info) = + mk_nonce_transaction(&mut test_entry, real_program_id, false, false); + + test_entry.push_nonce_transaction(transaction, *nonce_info.address()); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + nonce_info + .try_advance_nonce( + DurableNonce::from_blockhash(&LAST_BLOCKHASH), + LAMPORTS_PER_SIGNATURE, + ) + .unwrap(); + + test_entry + .final_accounts + .get_mut(nonce_info.address()) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(nonce_info.account().data()); + } + + // 1: non-executing nonce transaction (fee payer doesn't exist) regardless of features + { + let (transaction, _fee_payer, nonce_info) = + mk_nonce_transaction(&mut test_entry, real_program_id, true, false); + + test_entry.push_nonce_transaction_with_status( + transaction, + *nonce_info.address(), + ExecutionStatus::Discarded, + ); + } + + // 2: failing nonce transaction (bad system instruction) regardless of features + { + let (transaction, fee_payer, mut nonce_info) = + mk_nonce_transaction(&mut test_entry, system_program::id(), false, false); + + test_entry.push_nonce_transaction_with_status( + transaction, + *nonce_info.address(), + ExecutionStatus::ExecutedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + nonce_info + .try_advance_nonce( + DurableNonce::from_blockhash(&LAST_BLOCKHASH), + LAMPORTS_PER_SIGNATURE, + ) + .unwrap(); + + test_entry + .final_accounts + .get_mut(nonce_info.address()) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(nonce_info.account().data()); + } + + // 3: processable non-executable nonce transaction with fee-only enabled, otherwise discarded + { + let (transaction, fee_payer, mut nonce_info) = + mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, false); + + test_entry.push_nonce_transaction_with_status( + transaction, + *nonce_info.address(), + ExecutionStatus::ProcessedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + nonce_info + .try_advance_nonce( + DurableNonce::from_blockhash(&LAST_BLOCKHASH), + LAMPORTS_PER_SIGNATURE, + ) + .unwrap(); + + test_entry + .final_accounts + .get_mut(nonce_info.address()) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(nonce_info.account().data()); + } + + // 4: safety check that nonce fee-payers are required to be rent-exempt (blockhash fee-payers may be below rent-exemption) + // if this situation is ever allowed in the future, the nonce account MUST be hidden for fee-only transactions + // as an aside, nonce accounts closed by WithdrawNonceAccount are safe because they are ordinary executed transactions + // we also dont care whether a non-fee nonce (or any account) pays rent because rent is charged on executed transactions + if fee_paying_nonce { + let (transaction, _, nonce_info) = + mk_nonce_transaction(&mut test_entry, real_program_id, false, true); + + test_entry.push_nonce_transaction_with_status( + transaction, + *nonce_info.address(), + ExecutionStatus::Discarded, + ); + } + + // 5: rent-paying nonce fee-payers are also not charged for fee-only transactions + if fee_paying_nonce { + let (transaction, _, nonce_info) = + mk_nonce_transaction(&mut test_entry, Pubkey::new_unique(), false, true); + + test_entry.push_nonce_transaction_with_status( + transaction, + *nonce_info.address(), + ExecutionStatus::Discarded, + ); + } + + vec![test_entry] +} + +fn simd83_intrabatch_account_reuse() -> Vec { + let mut test_entries = vec![]; + let transfer_amount = LAMPORTS_PER_SOL; + let wallet_rent = Rent::default().minimum_balance(0); + + // batch 0: two successful transfers from the same source + { + let mut test_entry = SvmTestEntry::default(); + + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination1 = Pubkey::new_unique(); + let destination2 = Pubkey::new_unique(); + + let mut source_data = AccountSharedData::default(); + let destination1_data = AccountSharedData::default(); + let destination2_data = AccountSharedData::default(); + + source_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(source, &source_data); + + for (destination, mut destination_data) in [ + (destination1, destination1_data), + (destination2, destination2_data), + ] { + test_entry.push_transaction(system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + )); + + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + test_entry.create_expected_account(destination, &destination_data); + + test_entry + .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); + } + + test_entries.push(test_entry); + } + + // batch 1: + // * successful transfer, source left with rent-exempt minimum + // * non-processable transfer due to underfunded fee-payer + { + let mut test_entry = SvmTestEntry::default(); + + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination = Pubkey::new_unique(); + + let mut source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); + + source_data.set_lamports(transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent); + test_entry.add_initial_account(source, &source_data); + + test_entry.push_transaction(system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + )); + + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + test_entry.create_expected_account(destination, &destination_data); + + test_entry.decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); + + test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + ), + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 2: + // * successful transfer to a previously unfunded account + // * successful transfer using the new account as a fee-payer in the same batch + { + let mut test_entry = SvmTestEntry::default(); + let first_transfer_amount = transfer_amount + LAMPORTS_PER_SIGNATURE + wallet_rent; + let second_transfer_amount = transfer_amount; + + let grandparent_keypair = Keypair::new(); + let grandparent = grandparent_keypair.pubkey(); + let parent_keypair = Keypair::new(); + let parent = parent_keypair.pubkey(); + let child = Pubkey::new_unique(); + + let mut grandparent_data = AccountSharedData::default(); + let mut parent_data = AccountSharedData::default(); + let mut child_data = AccountSharedData::default(); + + grandparent_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(grandparent, &grandparent_data); + + test_entry.push_transaction(system_transaction::transfer( + &grandparent_keypair, + &parent, + first_transfer_amount, + Hash::default(), + )); + + parent_data + .checked_add_lamports(first_transfer_amount) + .unwrap(); + test_entry.create_expected_account(parent, &parent_data); + + test_entry.decrease_expected_lamports( + &grandparent, + first_transfer_amount + LAMPORTS_PER_SIGNATURE, + ); + + test_entry.push_transaction(system_transaction::transfer( + &parent_keypair, + &child, + second_transfer_amount, + Hash::default(), + )); + + child_data + .checked_add_lamports(second_transfer_amount) + .unwrap(); + test_entry.create_expected_account(child, &child_data); + + test_entry + .decrease_expected_lamports(&parent, second_transfer_amount + LAMPORTS_PER_SIGNATURE); + + test_entries.push(test_entry); + } + + // batch 3: + // * non-processable transfer due to underfunded fee-payer (two signatures) + // * successful transfer with the same fee-payer (one signature) + { + let mut test_entry = SvmTestEntry::default(); + + let feepayer_keypair = Keypair::new(); + let feepayer = feepayer_keypair.pubkey(); + let separate_source_keypair = Keypair::new(); + let separate_source = separate_source_keypair.pubkey(); + let destination = Pubkey::new_unique(); + + let mut feepayer_data = AccountSharedData::default(); + let mut separate_source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); + + feepayer_data.set_lamports(1 + LAMPORTS_PER_SIGNATURE + wallet_rent); + test_entry.add_initial_account(feepayer, &feepayer_data); + + separate_source_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(separate_source, &separate_source_data); + + test_entry.push_transaction_with_status( + Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &separate_source, + &destination, + 1, + )], + Some(&feepayer), + &[&feepayer_keypair, &separate_source_keypair], + Hash::default(), + ), + ExecutionStatus::Discarded, + ); + + test_entry.push_transaction(system_transaction::transfer( + &feepayer_keypair, + &destination, + 1, + Hash::default(), + )); + + destination_data.checked_add_lamports(1).unwrap(); + test_entry.create_expected_account(destination, &destination_data); + + test_entry.decrease_expected_lamports(&feepayer, 1 + LAMPORTS_PER_SIGNATURE); + } + + // batch 4: + // * processable non-executable transaction + // * successful transfer + // this confirms we update the AccountsMap from RollbackAccounts intrabatch + { + let mut test_entry = SvmTestEntry::default(); + + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination = Pubkey::new_unique(); + + let mut source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); + + source_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(source, &source_data); + + let mut load_program_fail_instruction = + system_instruction::transfer(&source, &Pubkey::new_unique(), transfer_amount); + load_program_fail_instruction.program_id = Pubkey::new_unique(); + + test_entry.push_transaction_with_status( + Transaction::new_signed_with_payer( + &[load_program_fail_instruction], + Some(&source), + &[&source_keypair], + Hash::default(), + ), + ExecutionStatus::ProcessedFailed, + ); + + test_entry.push_transaction(system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + )); + + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + test_entry.create_expected_account(destination, &destination_data); + + test_entry + .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE * 2); + + test_entries.push(test_entry); + } + + test_entries +} + +fn simd83_nonce_reuse(fee_paying_nonce: bool) -> Vec { + let mut test_entries = vec![]; + + let program_name = "hello-solana"; + let program_id = program_address(program_name); + + let fee_payer_keypair = Keypair::new(); + let non_fee_nonce_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + let nonce_pubkey = if fee_paying_nonce { + fee_payer + } else { + non_fee_nonce_keypair.pubkey() + }; + + let nonce_size = nonce::state::State::size(); + let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); + let initial_nonce_data = + nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); + let mut initial_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), + &system_program::id(), + ) + .unwrap(); + initial_nonce_account.set_rent_epoch(u64::MAX); + let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); + + let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); + let mut advanced_nonce_info = initial_nonce_info; + advanced_nonce_info + .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) + .unwrap(); + + let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer); + let withdraw_instruction = system_instruction::withdraw_nonce_account( + &nonce_pubkey, + &fee_payer, + &fee_payer, + LAMPORTS_PER_SOL, + ); + + let successful_noop_instruction = Instruction::new_with_bytes(program_id, &[], vec![]); + let failing_noop_instruction = Instruction::new_with_bytes(system_program::id(), &[], vec![]); + let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + + let second_transaction = Transaction::new_signed_with_payer( + &[ + advance_instruction.clone(), + successful_noop_instruction.clone(), + ], + Some(&fee_payer), + &[&fee_payer_keypair], + *advanced_durable.as_hash(), + ); + + let mut common_test_entry = SvmTestEntry::default(); + + common_test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); + + if !fee_paying_nonce { + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + common_test_entry.add_initial_account(fee_payer, &fee_payer_data); + } + + common_test_entry + .final_accounts + .get_mut(&nonce_pubkey) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(advanced_nonce_info.account().data()); + + common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + let common_test_entry = common_test_entry; + + // batch 0: one transaction that advances the nonce twice + { + let mut test_entry = common_test_entry.clone(); + + let transaction = Transaction::new_signed_with_payer( + &[advance_instruction.clone(), advance_instruction.clone()], + Some(&fee_payer), + &[&fee_payer_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_nonce_transaction_with_status( + transaction, + nonce_pubkey, + ExecutionStatus::ExecutedFailed, + ); + + test_entries.push(test_entry); + } + + // batch 1: + // * a successful nonce transaction + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[ + advance_instruction.clone(), + successful_noop_instruction.clone(), + ], + Some(&fee_payer), + &[&fee_payer_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_nonce_transaction(first_transaction, nonce_pubkey); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 2: + // * an executable failed nonce transaction + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[advance_instruction.clone(), failing_noop_instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_nonce_transaction_with_status( + first_transaction, + nonce_pubkey, + ExecutionStatus::ExecutedFailed, + ); + + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 3: + // * a processable non-executable nonce transaction, if fee-only transactions are enabled + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[advance_instruction.clone(), fee_only_noop_instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_nonce_transaction_with_status( + first_transaction, + nonce_pubkey, + ExecutionStatus::ProcessedFailed, + ); + + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 4: + // * a successful blockhash transaction that also advances the nonce + // * a nonce transaction that reuses the same nonce; this transaction must be dropped + { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + &[successful_noop_instruction.clone(), advance_instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 5: + // * a successful blockhash transaction that closes the nonce + // * a nonce transaction that uses the nonce; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + slice::from_ref(&withdraw_instruction), + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entry.increase_expected_lamports(&fee_payer, LAMPORTS_PER_SOL); + + test_entry.drop_expected_account(nonce_pubkey); + + test_entries.push(test_entry); + } + + // batch 6: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that funds the closed account + // * a nonce transaction that uses the account; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + slice::from_ref(&withdraw_instruction), + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + let middle_transaction = system_transaction::transfer( + &fee_payer_keypair, + &nonce_pubkey, + LAMPORTS_PER_SOL, + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_transaction(middle_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + let mut new_nonce_state = AccountSharedData::default(); + new_nonce_state.set_lamports(LAMPORTS_PER_SOL); + + test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state); + + test_entries.push(test_entry); + } + + // batch 7: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that reopens the account with proper nonce size + // * a nonce transaction that uses the account; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + slice::from_ref(&withdraw_instruction), + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + let middle_transaction = system_transaction::create_account( + &fee_payer_keypair, + &non_fee_nonce_keypair, + Hash::default(), + LAMPORTS_PER_SOL, + nonce_size as u64, + &system_program::id(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_transaction(middle_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + let new_nonce_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(vec![0; nonce_size]), + system_program::id(), + false, + u64::MAX, + ); + + test_entry.update_expected_account_data(nonce_pubkey, &new_nonce_state); + + test_entries.push(test_entry); + } + + // batch 8: + // * a successful blockhash transaction that closes the nonce + // * a successful blockhash transaction that reopens the nonce + // * a nonce transaction that uses the nonce; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let first_transaction = Transaction::new_signed_with_payer( + slice::from_ref(&withdraw_instruction), + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + let create_instructions = system_instruction::create_nonce_account( + &fee_payer, + &nonce_pubkey, + &fee_payer, + LAMPORTS_PER_SOL, + ); + + let middle_transaction = Transaction::new_signed_with_payer( + &create_instructions, + Some(&fee_payer), + &[&fee_payer_keypair, &non_fee_nonce_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_transaction(middle_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + test_entries.push(test_entry); + } + + // batch 9: + // * a successful blockhash noop transaction + // * a nonce transaction that uses a spoofed nonce account; this transaction must be dropped + // check_age would never let such a transaction through validation + // this simulates the case where someone closes a nonce account, then reuses the address in the same batch + // but as a non-system account that parses as an initialized nonce account + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + test_entry.initial_accounts.remove(&nonce_pubkey); + test_entry.final_accounts.remove(&nonce_pubkey); + + let mut fake_nonce_account = initial_nonce_account.clone(); + fake_nonce_account.set_rent_epoch(u64::MAX); + fake_nonce_account.set_owner(Pubkey::new_unique()); + test_entry.add_initial_account(nonce_pubkey, &fake_nonce_account); + + let first_transaction = Transaction::new_signed_with_payer( + slice::from_ref(&successful_noop_instruction), + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction.clone(), + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + test_entries.push(test_entry); + } + + // batch 10: + // * a successful blockhash transaction that changes the nonce authority + // * a nonce transaction that uses the nonce with the old authority; this transaction must be dropped + if !fee_paying_nonce { + let mut test_entry = common_test_entry.clone(); + + let new_authority = Pubkey::new_unique(); + + let first_transaction = Transaction::new_signed_with_payer( + &[system_instruction::authorize_nonce_account( + &nonce_pubkey, + &fee_payer, + &new_authority, + )], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction_with_status( + second_transaction, + nonce_pubkey, + ExecutionStatus::Discarded, + ); + + let final_nonce_data = + nonce::state::Data::new(new_authority, initial_durable, LAMPORTS_PER_SIGNATURE); + let final_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)), + &system_program::id(), + ) + .unwrap(); + + test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account); + + test_entries.push(test_entry); + } + + // batch 11: + // * a successful blockhash transaction that changes the nonce authority + // * a nonce transaction that uses the nonce with the new authority; this transaction succeeds + if !fee_paying_nonce { + let mut test_entry = common_test_entry; + + let new_authority_keypair = Keypair::new(); + let new_authority = new_authority_keypair.pubkey(); + + let first_transaction = Transaction::new_signed_with_payer( + &[system_instruction::authorize_nonce_account( + &nonce_pubkey, + &fee_payer, + &new_authority, + )], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + let second_transaction = Transaction::new_signed_with_payer( + &[ + system_instruction::advance_nonce_account(&nonce_pubkey, &new_authority), + successful_noop_instruction, + ], + Some(&fee_payer), + &[&fee_payer_keypair, &new_authority_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_transaction(first_transaction); + test_entry.push_nonce_transaction(second_transaction, nonce_pubkey); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + let final_nonce_data = + nonce::state::Data::new(new_authority, advanced_durable, LAMPORTS_PER_SIGNATURE); + let final_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::versions::Versions::new(nonce::state::State::Initialized(final_nonce_data)), + &system_program::id(), + ) + .unwrap(); + + test_entry.update_expected_account_data(nonce_pubkey, &final_nonce_account); + + test_entries.push(test_entry); + } + + for test_entry in &mut test_entries { + test_entry.add_initial_program(program_name); + } + + test_entries +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +enum WriteProgramInstruction { + Print, + Set, + Dealloc, + Realloc(usize), +} +impl WriteProgramInstruction { + fn create_transaction( + self, + program_id: Pubkey, + fee_payer: &Keypair, + target: Pubkey, + clamp_data_size: Option, + ) -> Transaction { + let (instruction_data, account_metas) = match self { + Self::Print => (vec![0], vec![AccountMeta::new_readonly(target, false)]), + Self::Set => (vec![1], vec![AccountMeta::new(target, false)]), + Self::Dealloc => ( + vec![2], + vec![ + AccountMeta::new(target, false), + AccountMeta::new(solana_sdk_ids::incinerator::id(), false), + ], + ), + Self::Realloc(new_size) => { + let mut instruction_data = vec![3]; + instruction_data.extend_from_slice(&new_size.to_le_bytes()); + (instruction_data, vec![AccountMeta::new(target, false)]) + } + }; + + let mut instructions = vec![]; + + if let Some(size) = clamp_data_size { + instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size)); + } + + instructions.push(Instruction::new_with_bytes( + program_id, + &instruction_data, + account_metas, + )); + + Transaction::new_signed_with_payer( + &instructions, + Some(&fee_payer.pubkey()), + &[fee_payer], + Hash::default(), + ) + } +} + +fn simd83_account_deallocate() -> Vec { + let mut test_entries = vec![]; + + // batch 0: sanity check, the program actually sets data + // batch 1: removing lamports from account hides it from subsequent in-batch transactions + for remove_lamports in [false, true] { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "write-to-account"; + let program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let target = Pubkey::new_unique(); + + let mut target_data = AccountSharedData::create_from_existing_shared_data( + Rent::default().minimum_balance(1), + Arc::new(vec![0]), + program_id, + false, + u64::MAX, + ); + test_entry.add_initial_account(target, &target_data); + + let set_data_transaction = WriteProgramInstruction::Set.create_transaction( + program_id, + &fee_payer_keypair, + target, + None, + ); + test_entry.push_transaction(set_data_transaction); + + target_data.data_as_mut_slice()[0] = 100; + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + test_entry.update_expected_account_data(target, &target_data); + + if remove_lamports { + let dealloc_transaction = WriteProgramInstruction::Dealloc.create_transaction( + program_id, + &fee_payer_keypair, + target, + None, + ); + test_entry.push_transaction(dealloc_transaction); + + let print_transaction = WriteProgramInstruction::Print.create_transaction( + program_id, + &fee_payer_keypair, + target, + None, + ); + test_entry.push_transaction(print_transaction); + test_entry.transaction_batch[2] + .asserts + .logs + .push("Program log: account size 0".to_string()); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + test_entry.drop_expected_account(target); + } + + test_entries.push(test_entry); + } + + test_entries +} + +fn simd83_fee_payer_deallocate() -> Vec { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "hello-solana"; + let real_program_id = program_address(program_name); + test_entry.add_initial_program(program_name); + + // rent minimum needs to be adjusted so fee payer can be deallocated + let rent = Rent { + lamports_per_byte: LAMPORTS_PER_SIGNATURE / solana_rent::ACCOUNT_STORAGE_OVERHEAD, + ..Rent::default() + }; + test_entry.set_rent_params(rent); + + // 0/1: a fee-payer balance goes to zero lamports on an executed transaction, the batch sees it as deallocated + // 2/3: the same, except if fee-only transactions are enabled, it goes to zero lamports from a fee-only transaction + for do_fee_only_transaction in [false, true] { + let dealloc_fee_payer_keypair = Keypair::new(); + let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey(); + + let mut dealloc_fee_payer_data = AccountSharedData::default(); + dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE); + test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data); + + let stable_fee_payer_keypair = Keypair::new(); + let stable_fee_payer = stable_fee_payer_keypair.pubkey(); + + let mut stable_fee_payer_data = AccountSharedData::default(); + stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data); + + // transaction which drains a fee-payer + let instruction = Instruction::new_with_bytes( + if do_fee_only_transaction { + Pubkey::new_unique() + } else { + real_program_id + }, + &[], + vec![], + ); + + let transaction = Transaction::new_signed_with_payer( + &[instruction], + Some(&dealloc_fee_payer), + &[&dealloc_fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction_with_status( + transaction, + if do_fee_only_transaction { + ExecutionStatus::ProcessedFailed + } else { + ExecutionStatus::Succeeded + }, + ); + + test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE); + + // as noted in `account_deallocate()` we must touch the account to see if anything actually happened + let instruction = Instruction::new_with_bytes( + real_program_id, + &[], + vec![AccountMeta::new_readonly(dealloc_fee_payer, false)], + ); + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[instruction], + Some(&stable_fee_payer), + &[&stable_fee_payer_keypair], + Hash::default(), + )); + + test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE); + + test_entry.drop_expected_account(dealloc_fee_payer); + } + + // 4: a non-nonce fee-payer balance goes to zero on a fee-only nonce transaction, the batch sees it as deallocated + // we test in `simple_nonce()` that nonce fee-payers cannot as a rule be brought below rent-exemption + { + let dealloc_fee_payer_keypair = Keypair::new(); + let dealloc_fee_payer = dealloc_fee_payer_keypair.pubkey(); + + let mut dealloc_fee_payer_data = AccountSharedData::default(); + dealloc_fee_payer_data.set_lamports(LAMPORTS_PER_SIGNATURE); + dealloc_fee_payer_data.set_rent_epoch(u64::MAX - 1); + test_entry.add_initial_account(dealloc_fee_payer, &dealloc_fee_payer_data); + + let stable_fee_payer_keypair = Keypair::new(); + let stable_fee_payer = stable_fee_payer_keypair.pubkey(); + + let mut stable_fee_payer_data = AccountSharedData::default(); + stable_fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(stable_fee_payer, &stable_fee_payer_data); + + let nonce_pubkey = Pubkey::new_unique(); + let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); + let initial_nonce_data = + nonce::state::Data::new(dealloc_fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); + let mut initial_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), + &system_program::id(), + ) + .unwrap(); + initial_nonce_account.set_rent_epoch(u64::MAX); + let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); + + let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); + let mut advanced_nonce_info = initial_nonce_info; + advanced_nonce_info + .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) + .unwrap(); + + test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); + + let advance_instruction = + system_instruction::advance_nonce_account(&nonce_pubkey, &dealloc_fee_payer); + let fee_only_noop_instruction = + Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + + // fee-only nonce transaction which drains a fee-payer + let transaction = Transaction::new_signed_with_payer( + &[advance_instruction, fee_only_noop_instruction], + Some(&dealloc_fee_payer), + &[&dealloc_fee_payer_keypair], + *initial_durable.as_hash(), + ); + test_entry.push_nonce_transaction_with_status( + transaction, + nonce_pubkey, + ExecutionStatus::ProcessedFailed, + ); + + test_entry + .final_accounts + .get_mut(&nonce_pubkey) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(advanced_nonce_info.account().data()); + + test_entry.decrease_expected_lamports(&dealloc_fee_payer, LAMPORTS_PER_SIGNATURE); + + // as noted in `account_deallocate()` we must touch the account to see if anything actually happened + let instruction = Instruction::new_with_bytes( + real_program_id, + &[], + vec![AccountMeta::new_readonly(dealloc_fee_payer, false)], + ); + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[instruction], + Some(&stable_fee_payer), + &[&stable_fee_payer_keypair], + Hash::default(), + )); + + test_entry.decrease_expected_lamports(&stable_fee_payer, LAMPORTS_PER_SIGNATURE); + + test_entry.drop_expected_account(dealloc_fee_payer); + } + + vec![test_entry] +} + +fn simd83_account_reallocate() -> Vec { + let mut test_entries = vec![]; + + let program_name = "write-to-account"; + let program_id = program_address(program_name); + let program_size = program_data_size(program_name); + + let mut common_test_entry = SvmTestEntry::default(); + common_test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + common_test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let mk_target = |size| { + AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL * 10, + Arc::new(vec![0; size]), + program_id, + false, + u64::MAX, + ) + }; + + let target = Pubkey::new_unique(); + let target_start_size = 100; + common_test_entry.add_initial_account(target, &mk_target(target_start_size)); + + // we set a budget that is enough pre-large-realloc but not enough post-large-realloc + // we must add program size because programdata buffers are counted + let size_budget = Some((program_size + MAX_PERMITTED_DATA_INCREASE) as u32); + + let print_transaction = WriteProgramInstruction::Print.create_transaction( + program_id, + &fee_payer_keypair, + target, + size_budget, + ); + + common_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + let common_test_entry = common_test_entry; + + // batch 0/1: + // * successful realloc up/down + // * change reflected in same batch + for new_target_size in [target_start_size + 1, target_start_size - 1] { + let mut test_entry = common_test_entry.clone(); + + let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size) + .create_transaction(program_id, &fee_payer_keypair, target, None); + test_entry.push_transaction(realloc_transaction); + + test_entry.push_transaction(print_transaction.clone()); + test_entry.transaction_batch[1] + .asserts + .logs + .push(format!("Program log: account size {new_target_size}")); + + test_entry.update_expected_account_data(target, &mk_target(new_target_size)); + + test_entries.push(test_entry); + } + + // batch 2: + // * successful large realloc up + // * transaction is aborted based on the new transaction data size post-realloc + { + let mut test_entry = common_test_entry; + + let new_target_size = target_start_size + MAX_PERMITTED_DATA_INCREASE; + + let realloc_transaction = WriteProgramInstruction::Realloc(new_target_size) + .create_transaction(program_id, &fee_payer_keypair, target, None); + test_entry.push_transaction(realloc_transaction); + + test_entry + .push_transaction_with_status(print_transaction, ExecutionStatus::ProcessedFailed); + + test_entry.update_expected_account_data(target, &mk_target(new_target_size)); + + test_entries.push(test_entry); + } + + test_entries +} + +enum AbortReason { + None, + Unprocessable, + DropOnFailure, +} + +fn all_or_nothing(abort: AbortReason) -> Vec { + let mut test_entry = SvmTestEntry { + all_or_nothing: true, + drop_on_failure: matches!(abort, AbortReason::DropOnFailure), + ..Default::default() + }; + let transfer_amount = LAMPORTS_PER_SOL; + + // 0: a transfer that succeeds + { + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let destination = Pubkey::new_unique(); + + let mut source_data = AccountSharedData::default(); + let mut destination_data = AccountSharedData::default(); + + source_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(source, &source_data); + + let status = match abort { + AbortReason::None => { + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + test_entry.create_expected_account(destination, &destination_data); + test_entry + .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); + + ExecutionStatus::Succeeded + } + AbortReason::Unprocessable | AbortReason::DropOnFailure => { + test_entry.final_accounts.insert(source, source_data); + + ExecutionStatus::Discarded + } + }; + + test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + ), + status, + ); + } + + // 1: an executable transfer that fails + if matches!(abort, AbortReason::DropOnFailure) { + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + + let mut source_data = AccountSharedData::default(); + + source_data.set_lamports(transfer_amount - 1); + test_entry.add_initial_account(source, &source_data); + test_entry.final_accounts.insert(source, source_data); + + test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &Pubkey::new_unique(), + transfer_amount, + Hash::default(), + ), + ExecutionStatus::Discarded, + ); + } + + // 2: a non-processable transfer that fails before loading + if matches!(abort, AbortReason::Unprocessable) { + test_entry.transaction_batch.push(TransactionBatchItem { + transaction: system_transaction::transfer( + &Keypair::new(), + &Pubkey::new_unique(), + transfer_amount, + Hash::default(), + ), + check_result: Err(TransactionError::BlockhashNotFound), + asserts: ExecutionStatus::Discarded.into(), + }); + } + + vec![test_entry] +} + +fn drop_on_failure_batch(statuses: &[bool]) -> Vec { + let mut test_entry = SvmTestEntry { + drop_on_failure: true, + ..Default::default() + }; + let transfer_amount = LAMPORTS_PER_SOL; + + // Shared source account to fund all transfers. + let source_keypair = Keypair::new(); + let source = source_keypair.pubkey(); + let mut source_data = AccountSharedData::default(); + source_data.set_lamports(LAMPORTS_PER_SOL * 100); + test_entry.add_initial_account(source, &source_data); + + // Shared destination account to receive all transfers. + let destination = Pubkey::new_unique(); + let mut destination_data = AccountSharedData::default(); + + println!("source: {source}"); + println!("destination: {destination}"); + + for success in statuses { + match success { + true => { + test_entry + .decrease_expected_lamports(&source, transfer_amount + LAMPORTS_PER_SIGNATURE); + destination_data + .checked_add_lamports(transfer_amount) + .unwrap(); + destination_data.set_rent_epoch(u64::MAX); + + test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &destination, + transfer_amount, + Hash::default(), + ), + ExecutionStatus::Succeeded, + ); + } + false => test_entry.push_transaction_with_status( + system_transaction::transfer( + &source_keypair, + &destination, + source_data.lamports() + 1, + Hash::default(), + ), + ExecutionStatus::Discarded, + ), + } + } + + // Set the final expected source state. + if statuses.iter().all(|success| !*success) { + test_entry + .final_accounts + .get_mut(&source) + .unwrap() + .set_rent_epoch(0); + } + + // Set the final expected destination state. + if statuses.iter().any(|success| *success) { + assert!( + test_entry + .final_accounts + .insert(destination, destination_data) + .is_none() + ); + } + + vec![test_entry] +} + +#[test_case(program_medley(false))] +#[test_case(program_medley(true))] +#[test_case(simple_transfer(false))] +#[test_case(simple_transfer(true))] +#[test_case(simple_nonce(false))] +#[test_case(simple_nonce(true))] +#[test_case(simd83_intrabatch_account_reuse())] +#[test_case(simd83_nonce_reuse(false))] +#[test_case(simd83_nonce_reuse(true))] +#[test_case(simd83_account_deallocate())] +#[test_case(simd83_fee_payer_deallocate())] +#[test_case(simd83_account_reallocate())] +#[test_case(all_or_nothing(AbortReason::None))] +#[test_case(all_or_nothing(AbortReason::Unprocessable))] +#[test_case(all_or_nothing(AbortReason::DropOnFailure))] +#[test_case(drop_on_failure_batch(&[false]))] +#[test_case(drop_on_failure_batch(&[true]))] +#[test_case(drop_on_failure_batch(&[false, false]))] +#[test_case(drop_on_failure_batch(&[true, true]))] +#[test_case(drop_on_failure_batch(&[false, false, true]))] +#[test_case(drop_on_failure_batch(&[true, true, false]))] +#[test_case(drop_on_failure_batch(&[false, true, false]))] +#[test_case(drop_on_failure_batch(&[true, false, true]))] +fn svm_integration(test_entries: Vec) { + for test_entry in test_entries { + let env = SvmTestEnvironment::create(test_entry); + env.execute(); + } +} + +#[test] +fn program_cache_create_account() { + let supported_loaders = [ + bpf_loader_upgradeable::id(), + bpf_loader::id(), + bpf_loader_deprecated::id(), + ]; + for loader_id in &supported_loaders { + let mut test_entry = SvmTestEntry::default(); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let new_account_keypair = Keypair::new(); + let program_id = new_account_keypair.pubkey(); + + // create an account owned by a loader + let create_transaction = system_transaction::create_account( + &fee_payer_keypair, + &new_account_keypair, + Hash::default(), + LAMPORTS_PER_SOL, + 0, + loader_id, + ); + + test_entry.push_transaction(create_transaction); + + test_entry + .decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SOL + LAMPORTS_PER_SIGNATURE * 2); + + // attempt to invoke the new account + let invoke_transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes(program_id, &[], vec![])], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + test_entry.push_transaction_with_status( + invoke_transaction.clone(), + ExecutionStatus::ExecutedFailed, + ); + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + let mut env = SvmTestEnvironment::create(test_entry); + + // test in same entry as account creation + env.execute(); + + let mut test_entry = SvmTestEntry { + initial_accounts: env.test_entry.final_accounts.clone(), + final_accounts: env.test_entry.final_accounts.clone(), + ..SvmTestEntry::default() + }; + + test_entry + .push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + // test in different entry same slot + env.test_entry = test_entry; + env.execute(); + } +} + +#[test_case(false, false; "close::scan_only")] +#[test_case(false, true; "close::invoke")] +#[test_case(true, false; "upgrade::scan_only")] +#[test_case(true, true; "upgrade::invoke")] +fn program_cache_loaderv3_update_tombstone(upgrade_program: bool, invoke_changed_program: bool) { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "hello-solana"; + let program_id = program_address(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + test_entry + .initial_programs + .push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer))); + + let buffer_address = Pubkey::new_unique(); + + // upgrade or close a deployed program + let change_instruction = if upgrade_program { + let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer { + authority_address: Some(fee_payer), + }) + .unwrap(); + let mut program_bytecode = load_program(program_name.to_string()); + data.append(&mut program_bytecode); + + let buffer_account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(data), + bpf_loader_upgradeable::id(), + true, + u64::MAX, + ); + + test_entry.add_initial_account(buffer_address, &buffer_account); + test_entry.drop_expected_account(buffer_address); + + loaderv3_instruction::upgrade( + &program_id, + &buffer_address, + &fee_payer, + &Pubkey::new_unique(), + ) + } else { + loaderv3_instruction::close_any( + &get_program_data_address(&program_id), + &Pubkey::new_unique(), + Some(&fee_payer), + Some(&program_id), + ) + }; + + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[change_instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + let invoke_transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes(program_id, &[], vec![])], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + // attempt to invoke the program, which must fail + // this ensures the local program cache reflects the change of state + // we have cases without this so we can assert the cache *before* the invoke contains the tombstone + if invoke_changed_program { + test_entry.push_transaction_with_status( + invoke_transaction.clone(), + ExecutionStatus::ExecutedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + } + + let mut env = SvmTestEnvironment::create(test_entry); + + // test in same entry as program change + env.execute(); + assert!(env.is_program_blocked(&program_id)); + + let mut test_entry = SvmTestEntry { + initial_accounts: env.test_entry.final_accounts.clone(), + final_accounts: env.test_entry.final_accounts.clone(), + ..SvmTestEntry::default() + }; + + test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + // test in different entry same slot + env.test_entry = test_entry; + env.execute(); + assert!(env.is_program_blocked(&program_id)); +} + +#[test_case(false; "upgrade::scan_only")] +#[test_case(true; "upgrade::invoke")] +fn program_cache_loaderv3_buffer_swap(invoke_changed_program: bool) { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "hello-solana"; + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 10); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + // this account will start as a buffer and then become a program + // buffers make their way into the program cache + // so we test that pathological address reuse is not a problem + let target_keypair = Keypair::new(); + let target = target_keypair.pubkey(); + let programdata_address = get_program_data_address(&target); + + // we have the same buffer ready at a different address to deploy from + let deploy_keypair = Keypair::new(); + let deploy = deploy_keypair.pubkey(); + + let mut buffer_data = bincode::serialize(&UpgradeableLoaderState::Buffer { + authority_address: Some(fee_payer), + }) + .unwrap(); + let mut program_bytecode = load_program(program_name.to_string()); + buffer_data.append(&mut program_bytecode); + + let buffer_account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(buffer_data.clone()), + bpf_loader_upgradeable::id(), + true, + u64::MAX, + ); + + test_entry.add_initial_account(target, &buffer_account); + test_entry.add_initial_account(deploy, &buffer_account); + + let program_data = bincode::serialize(&UpgradeableLoaderState::Program { + programdata_address, + }) + .unwrap(); + let program_account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(program_data), + bpf_loader_upgradeable::id(), + true, + u64::MAX, + ); + test_entry.update_expected_account_data(target, &program_account); + test_entry.drop_expected_account(deploy); + + // close the buffer + let close_instruction = + loaderv3_instruction::close_any(&target, &Pubkey::new_unique(), Some(&fee_payer), None); + + // reopen as a program + #[allow(deprecated)] + let deploy_instruction = loaderv3_instruction::deploy_with_max_program_len( + &fee_payer, + &target, + &deploy, + &fee_payer, + LAMPORTS_PER_SOL, + buffer_data.len(), + ) + .unwrap(); + + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[close_instruction], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + + test_entry.push_transaction(Transaction::new_signed_with_payer( + &deploy_instruction, + Some(&fee_payer), + &[&fee_payer_keypair, &target_keypair], + Hash::default(), + )); + + test_entry.decrease_expected_lamports( + &fee_payer, + Rent::default().minimum_balance( + UpgradeableLoaderState::size_of_programdata_metadata() + buffer_data.len(), + ) + LAMPORTS_PER_SIGNATURE * 3, + ); + + let invoke_transaction = Transaction::new_signed_with_payer( + &[Instruction::new_with_bytes(target, &[], vec![])], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ); + + if invoke_changed_program { + test_entry.push_transaction_with_status( + invoke_transaction.clone(), + ExecutionStatus::ExecutedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + } + + let mut env = SvmTestEnvironment::create(test_entry); + + // test in same entry as program change + env.execute(); + assert!(env.is_program_blocked(&target)); + + let mut test_entry = SvmTestEntry { + initial_accounts: env.test_entry.final_accounts.clone(), + final_accounts: env.test_entry.final_accounts.clone(), + ..SvmTestEntry::default() + }; + + test_entry.push_transaction_with_status(invoke_transaction, ExecutionStatus::ExecutedFailed); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + // test in different entry same slot + env.test_entry = test_entry; + env.execute(); + assert!(env.is_program_blocked(&target)); +} + +#[test] +fn program_cache_stats() { + let mut test_entry = SvmTestEntry::default(); + + let program_name = "hello-solana"; + let noop_program = program_address(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + + let mut fee_payer_data = AccountSharedData::default(); + fee_payer_data.set_lamports(LAMPORTS_PER_SOL * 100); + test_entry.add_initial_account(fee_payer, &fee_payer_data); + + test_entry + .initial_programs + .push((program_name.to_string(), DEPLOYMENT_SLOT, Some(fee_payer))); + + let missing_program = Pubkey::new_unique(); + + // set up a future upgrade after the first batch + let buffer_address = Pubkey::new_unique(); + { + let mut data = bincode::serialize(&UpgradeableLoaderState::Buffer { + authority_address: Some(fee_payer), + }) + .unwrap(); + let mut program_bytecode = load_program(program_name.to_string()); + data.append(&mut program_bytecode); + + let buffer_account = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(data), + bpf_loader_upgradeable::id(), + true, + u64::MAX, + ); + + test_entry.add_initial_account(buffer_address, &buffer_account); + } + + let make_transaction = |instructions: &[Instruction]| { + Transaction::new_signed_with_payer( + instructions, + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ) + }; + + let successful_noop_instruction = Instruction::new_with_bytes(noop_program, &[], vec![]); + let successful_transfer_instruction = + system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL); + let failing_transfer_instruction = + system_instruction::transfer(&fee_payer, &Pubkey::new_unique(), LAMPORTS_PER_SOL * 1000); + let fee_only_noop_instruction = Instruction::new_with_bytes(missing_program, &[], vec![]); + + let mut noop_tx_usage = 0; + let mut system_tx_usage = 0; + let mut successful_transfers = 0; + + test_entry.push_transaction(make_transaction(slice::from_ref( + &successful_noop_instruction, + ))); + noop_tx_usage += 1; + + test_entry.push_transaction(make_transaction(slice::from_ref( + &successful_transfer_instruction, + ))); + system_tx_usage += 1; + successful_transfers += 1; + + test_entry.push_transaction_with_status( + make_transaction(slice::from_ref(&failing_transfer_instruction)), + ExecutionStatus::ExecutedFailed, + ); + system_tx_usage += 1; + + test_entry.push_transaction(make_transaction(&[ + successful_noop_instruction.clone(), + successful_noop_instruction.clone(), + successful_transfer_instruction.clone(), + successful_transfer_instruction.clone(), + successful_noop_instruction.clone(), + ])); + noop_tx_usage += 1; + system_tx_usage += 1; + successful_transfers += 2; + + test_entry.push_transaction_with_status( + make_transaction(&[ + failing_transfer_instruction, + successful_noop_instruction.clone(), + successful_transfer_instruction.clone(), + ]), + ExecutionStatus::ExecutedFailed, + ); + noop_tx_usage += 1; + system_tx_usage += 1; + + // load failure/fee-only does not touch the program cache + test_entry.push_transaction_with_status( + make_transaction(&[ + successful_noop_instruction.clone(), + fee_only_noop_instruction, + ]), + ExecutionStatus::ProcessedFailed, + ); + + test_entry.decrease_expected_lamports( + &fee_payer, + LAMPORTS_PER_SIGNATURE * test_entry.transaction_batch.len() as u64 + + LAMPORTS_PER_SOL * successful_transfers, + ); + + // nor does discard + test_entry.transaction_batch.push(TransactionBatchItem { + transaction: make_transaction(slice::from_ref(&successful_transfer_instruction)), + check_result: Err(TransactionError::BlockhashNotFound), + asserts: ExecutionStatus::Discarded.into(), + }); + + let mut env = SvmTestEnvironment::create(test_entry); + env.execute(); + + // check all usage stats are as we expect + let global_program_cache = env + .batch_processor + .global_program_cache + .read() + .unwrap() + .get_flattened_entries_for_tests() + .into_iter() + .rev() + .collect::>(); + + let (_, noop_entry) = global_program_cache + .iter() + .find(|(pubkey, _)| *pubkey == noop_program) + .unwrap(); + + assert_eq!( + noop_entry.stats.uses.load(Ordering::Relaxed), + noop_tx_usage, + "noop_tx_usage matches" + ); + + let (_, system_entry) = global_program_cache + .iter() + .find(|(pubkey, _)| *pubkey == system_program::id()) + .unwrap(); + + assert_eq!( + system_entry.stats.uses.load(Ordering::Relaxed), + system_tx_usage, + "system_tx_usage matches" + ); + + assert!( + !global_program_cache + .iter() + .any(|(pubkey, _)| *pubkey == missing_program), + "missing_program is missing" + ); + + // set up the second batch + let mut test_entry = SvmTestEntry { + initial_accounts: env.test_entry.final_accounts.clone(), + final_accounts: env.test_entry.final_accounts.clone(), + ..SvmTestEntry::default() + }; + + // upgrade the program. this blocks execution but does not create a tombstone + // the main thing we are testing is the tx counter is ported across upgrades + // + // note the upgrade transaction actually counts as a usage, per the existing rules + // the program cache must load the program because it has no idea if it will be used for cpi + test_entry.push_transaction(Transaction::new_signed_with_payer( + &[loaderv3_instruction::upgrade( + &noop_program, + &buffer_address, + &fee_payer, + &Pubkey::new_unique(), + )], + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + )); + noop_tx_usage += 1; + + test_entry.drop_expected_account(buffer_address); + + test_entry.push_transaction_with_status( + make_transaction(slice::from_ref(&successful_noop_instruction)), + ExecutionStatus::ExecutedFailed, + ); + noop_tx_usage += 1; + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + env.test_entry = test_entry; + env.execute(); + + let (_, noop_entry) = env + .batch_processor + .global_program_cache + .read() + .unwrap() + .get_flattened_entries_for_tests() + .into_iter() + .rev() + .find(|(pubkey, _)| *pubkey == noop_program) + .unwrap(); + + assert_eq!( + noop_entry.stats.uses.load(Ordering::Relaxed), + noop_tx_usage, + "noop_tx_usage matches" + ); + + // third batch, this creates a delayed visibility tombstone + let mut test_entry = SvmTestEntry { + initial_accounts: env.test_entry.final_accounts.clone(), + final_accounts: env.test_entry.final_accounts.clone(), + ..SvmTestEntry::default() + }; + + test_entry.push_transaction_with_status( + make_transaction(slice::from_ref(&successful_noop_instruction)), + ExecutionStatus::ExecutedFailed, + ); + noop_tx_usage += 1; + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + + env.test_entry = test_entry; + env.execute(); + + let (_, noop_entry) = env + .batch_processor + .global_program_cache + .read() + .unwrap() + .get_flattened_entries_for_tests() + .into_iter() + .rev() + .find(|(pubkey, _)| *pubkey == noop_program) + .unwrap(); + + assert_eq!( + noop_entry.stats.uses.load(Ordering::Relaxed), + noop_tx_usage, + "noop_tx_usage matches" + ); +} + +#[derive(Clone, PartialEq, Eq)] +enum Inspect<'a> { + LiveRead(&'a AccountSharedData), + LiveWrite(&'a AccountSharedData), + #[allow(dead_code)] + DeadRead, + DeadWrite, +} +impl From> for (Option, bool) { + fn from(inspect: Inspect) -> Self { + match inspect { + Inspect::LiveRead(account) => (Some(account.clone()), false), + Inspect::LiveWrite(account) => (Some(account.clone()), true), + Inspect::DeadRead => (None, false), + Inspect::DeadWrite => (None, true), + } + } +} + +#[derive(Clone, Default)] +struct InspectedAccounts(pub HashMap, bool)>>); +impl InspectedAccounts { + fn inspect(&mut self, pubkey: Pubkey, inspect: Inspect) { + self.0.entry(pubkey).or_default().push(inspect.into()) + } +} + +#[test_case(false; "separate_nonce::old")] +#[test_case(true; "fee_paying_nonce::old")] +fn svm_inspect_nonce_load_failure(fee_paying_nonce: bool) { + let mut test_entry = SvmTestEntry::default(); + let mut expected_inspected_accounts = InspectedAccounts::default(); + + let fee_payer_keypair = Keypair::new(); + let separate_nonce_keypair = Keypair::new(); + + let fee_payer = fee_payer_keypair.pubkey(); + let nonce_pubkey = if fee_paying_nonce { + fee_payer + } else { + separate_nonce_keypair.pubkey() + }; + + let initial_durable = DurableNonce::from_blockhash(&Hash::new_unique()); + let initial_nonce_data = + nonce::state::Data::new(fee_payer, initial_durable, LAMPORTS_PER_SIGNATURE); + let mut initial_nonce_account = AccountSharedData::new_data( + LAMPORTS_PER_SOL, + &nonce::versions::Versions::new(nonce::state::State::Initialized(initial_nonce_data)), + &system_program::id(), + ) + .unwrap(); + initial_nonce_account.set_rent_epoch(u64::MAX); + let initial_nonce_account = initial_nonce_account; + let initial_nonce_info = NonceInfo::new(nonce_pubkey, initial_nonce_account.clone()); + + let advanced_durable = DurableNonce::from_blockhash(&LAST_BLOCKHASH); + let mut advanced_nonce_info = initial_nonce_info; + advanced_nonce_info + .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE) + .unwrap(); + + let compute_instruction = ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(1); + let advance_instruction = system_instruction::advance_nonce_account(&nonce_pubkey, &fee_payer); + let fee_only_noop_instruction = Instruction::new_with_bytes(Pubkey::new_unique(), &[], vec![]); + + test_entry.add_initial_account(nonce_pubkey, &initial_nonce_account); + + let mut separate_fee_payer_account = AccountSharedData::default(); + separate_fee_payer_account.set_lamports(LAMPORTS_PER_SOL); + let separate_fee_payer_account = separate_fee_payer_account; + + // we always inspect the nonce at least once + expected_inspected_accounts.inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account)); + + // if we have a fee-paying nonce, we happen to inspect it again + // this is an unimportant implementation detail and also means these cases are trivial + // the true test is a separate nonce, to ensure we inspect it in pre-checks + if fee_paying_nonce { + expected_inspected_accounts + .inspect(nonce_pubkey, Inspect::LiveWrite(&initial_nonce_account)); + } else { + test_entry.add_initial_account(fee_payer, &separate_fee_payer_account); + expected_inspected_accounts + .inspect(fee_payer, Inspect::LiveWrite(&separate_fee_payer_account)); + } + + let transaction = Transaction::new_signed_with_payer( + &[ + advance_instruction, + compute_instruction, + fee_only_noop_instruction, + ], + Some(&fee_payer), + &[&fee_payer_keypair], + *initial_durable.as_hash(), + ); + + test_entry.push_nonce_transaction_with_status( + transaction, + nonce_pubkey, + ExecutionStatus::ProcessedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE); + test_entry + .final_accounts + .get_mut(&nonce_pubkey) + .unwrap() + .data_as_mut_slice() + .copy_from_slice(advanced_nonce_info.account().data()); + + let env = SvmTestEnvironment::create(test_entry.clone()); + env.execute(); + + let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone(); + for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 { + let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap(); + assert_eq!( + expected_account, actual_account, + "pubkey: {expected_pubkey}", + ); + } +} + +#[test] +fn svm_inspect_account() { + let mut initial_test_entry = SvmTestEntry::default(); + let mut expected_inspected_accounts = InspectedAccounts::default(); + + let fee_payer_keypair = Keypair::new(); + let sender_keypair = Keypair::new(); + + let fee_payer = fee_payer_keypair.pubkey(); + let sender = sender_keypair.pubkey(); + let recipient = Pubkey::new_unique(); + + // Setting up the accounts for the transfer + + // fee payer + let mut fee_payer_account = AccountSharedData::default(); + fee_payer_account.set_lamports(10_000_000); + fee_payer_account.set_rent_epoch(u64::MAX); + initial_test_entry.add_initial_account(fee_payer, &fee_payer_account); + expected_inspected_accounts.inspect(fee_payer, Inspect::LiveWrite(&fee_payer_account)); + + // sender + let mut sender_account = AccountSharedData::default(); + sender_account.set_lamports(11_000_000); + sender_account.set_rent_epoch(u64::MAX); + initial_test_entry.add_initial_account(sender, &sender_account); + expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&sender_account)); + + // recipient -- initially dead + expected_inspected_accounts.inspect(recipient, Inspect::DeadWrite); + + // system program + let system_account = AccountSharedData::create_from_existing_shared_data( + 5000, + Arc::new("system_program".as_bytes().to_vec()), + native_loader::id(), + true, + 0, + ); + expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account)); + + let transfer_amount = 1_000_000; + let transaction = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &sender, + &recipient, + transfer_amount, + )], + Some(&fee_payer), + &[&fee_payer_keypair, &sender_keypair], + Hash::default(), + ); + + initial_test_entry.push_transaction(transaction); + + let mut recipient_account = AccountSharedData::default(); + recipient_account.set_lamports(transfer_amount); + + initial_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + initial_test_entry.decrease_expected_lamports(&sender, transfer_amount); + initial_test_entry.create_expected_account(recipient, &recipient_account); + + let initial_test_entry = initial_test_entry; + + // Load and execute the transaction + let mut env = SvmTestEnvironment::create(initial_test_entry.clone()); + env.execute(); + + // do another transfer; recipient should be alive now + + // fee payer + let intermediate_fee_payer_account = initial_test_entry + .final_accounts + .get(&fee_payer) + .cloned() + .unwrap(); + expected_inspected_accounts.inspect( + fee_payer, + Inspect::LiveWrite(&intermediate_fee_payer_account), + ); + + // sender + let intermediate_sender_account = initial_test_entry + .final_accounts + .get(&sender) + .cloned() + .unwrap(); + expected_inspected_accounts.inspect(sender, Inspect::LiveWrite(&intermediate_sender_account)); + + // recipient -- now alive + let intermediate_recipient_account = initial_test_entry + .final_accounts + .get(&recipient) + .cloned() + .unwrap(); + expected_inspected_accounts.inspect( + recipient, + Inspect::LiveWrite(&intermediate_recipient_account), + ); + + // system program + expected_inspected_accounts.inspect(system_program::id(), Inspect::LiveRead(&system_account)); + + let mut final_test_entry = SvmTestEntry { + initial_accounts: initial_test_entry.final_accounts.clone(), + final_accounts: initial_test_entry.final_accounts, + ..SvmTestEntry::default() + }; + + let transfer_amount = 456; + let transaction = Transaction::new_signed_with_payer( + &[system_instruction::transfer( + &sender, + &recipient, + transfer_amount, + )], + Some(&fee_payer), + &[&fee_payer_keypair, &sender_keypair], + Hash::default(), + ); + + final_test_entry.push_transaction(transaction); + + final_test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + final_test_entry.decrease_expected_lamports(&sender, transfer_amount); + final_test_entry.increase_expected_lamports(&recipient, transfer_amount); + + // Load and execute the second transaction + env.test_entry = final_test_entry; + env.execute(); + + // Ensure all the expected inspected accounts were inspected + let actual_inspected_accounts = env.mock_bank.inspected_accounts.read().unwrap().clone(); + for (expected_pubkey, expected_account) in &expected_inspected_accounts.0 { + let actual_account = actual_inspected_accounts.get(expected_pubkey).unwrap(); + assert_eq!( + expected_account, actual_account, + "pubkey: {expected_pubkey}", + ); + } + + let num_expected_inspected_accounts: usize = + expected_inspected_accounts.0.values().map(Vec::len).sum(); + let num_actual_inspected_accounts: usize = + actual_inspected_accounts.values().map(Vec::len).sum(); + + assert_eq!( + num_expected_inspected_accounts, + num_actual_inspected_accounts, + ); +} + +#[test_case(false; "old_fee_only")] +#[test_case(true; "simd186_fee_only")] +fn fee_only_loaded_transaction_data_size(define_ltds_fee_only_semantics: bool) { + let mut common_test_entry = SvmTestEntry::default(); + common_test_entry.feature_set.define_ltds_fee_only_semantics = define_ltds_fee_only_semantics; + + let program_name = "hello-solana"; + let program_id = program_address(program_name); + let loaded_program_size = (UpgradeableLoaderState::size_of_program() + + program_data_size(program_name) + + TRANSACTION_ACCOUNT_BASE_SIZE * 2) as u32; + + common_test_entry.add_initial_program(program_name); + + let fee_payer_keypair = Keypair::new(); + let fee_payer = fee_payer_keypair.pubkey(); + let loaded_fee_payer_size = TRANSACTION_ACCOUNT_BASE_SIZE as u32; + + let fee_payer_data = + AccountSharedData::new_rent_epoch(LAMPORTS_PER_SOL, 0, &Pubkey::default(), u64::MAX); + + common_test_entry.add_initial_account(fee_payer, &fee_payer_data); + + let mut loaded_account_sizes = vec![]; + + // make accounts of base size 512..=8192 + for i in 9..=13 { + let base_size = 2_usize.pow(i); + + let pubkey = Pubkey::new_unique(); + let account_data = AccountSharedData::new_rent_epoch( + LAMPORTS_PER_SOL, + base_size, + &Pubkey::default(), + u64::MAX, + ); + + common_test_entry.add_initial_account(pubkey, &account_data); + loaded_account_sizes.push((pubkey, base_size + TRANSACTION_ACCOUNT_BASE_SIZE)); + } + + let common_test_entry = common_test_entry; + + let transaction = |program_id: Pubkey, accounts: &[Pubkey], loaded_data_limit: Option| { + let account_metas = accounts + .iter() + .map(|pubkey| AccountMeta { + pubkey: *pubkey, + ..AccountMeta::default() + }) + .collect::>(); + + let mut instructions = vec![]; + + if let Some(size) = loaded_data_limit { + instructions.push(ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(size)); + } + + instructions.push(Instruction::new_with_bytes(program_id, &[], account_metas)); + + Transaction::new_signed_with_payer( + &instructions, + Some(&fee_payer), + &[&fee_payer_keypair], + Hash::default(), + ) + }; + + // for increasing sets of accounts, run: + // * success: loaded size is total size + // * fail due to limit: loaded size is limit with feature, 0 without + // * fail due to program id: loaded size is total size with feature, 0 without + for count in 0..loaded_account_sizes.len() { + let mut test_entry = common_test_entry.clone(); + + let (account_keys, other_accounts_size) = + &loaded_account_sizes[..count] + .iter() + .fold((vec![], 0), |mut acc, (pubkey, size)| { + acc.0.push(*pubkey); + acc.1 += *size as u32; + acc + }); + + let success_transaction = transaction(program_id, account_keys, None); + test_entry.push_transaction_with_status(success_transaction, ExecutionStatus::Succeeded); + + let size_limit = (other_accounts_size / 2).max(1); + let fail_limit_transaction = transaction(program_id, account_keys, Some(size_limit)); + test_entry + .push_transaction_with_status(fail_limit_transaction, ExecutionStatus::ProcessedFailed); + + let fail_program_id_transaction = transaction(Pubkey::new_unique(), account_keys, None); + test_entry.push_transaction_with_status( + fail_program_id_transaction, + ExecutionStatus::ProcessedFailed, + ); + + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 3); + + let env = SvmTestEnvironment::create(test_entry); + let output = env.execute(); + + let success_loaded_size = output.processing_results[0] + .as_ref() + .unwrap() + .loaded_accounts_data_size(); + + // success is always computed size + assert_eq!( + loaded_fee_payer_size + loaded_program_size + other_accounts_size, + success_loaded_size, + ); + + let fail_limit_loaded_size = output.processing_results[1] + .as_ref() + .unwrap() + .loaded_accounts_data_size(); + + // blowing limit with define_ltds_fee_only_semantics sets the size to the limit + // otherwise it is the raw sum of rollback sizes which here is zero + assert_eq!( + if define_ltds_fee_only_semantics { + size_limit + } else { + 0 + }, + fail_limit_loaded_size, + ); + + let fail_program_id_loaded_size = output.processing_results[2] + .as_ref() + .unwrap() + .loaded_accounts_data_size(); + + // violating constraints *after* passing size with define_ltds_fee_only_semantics uses the size + // otherwise as above it is the raw sum of rollback sizes which here is zero + assert_eq!( + if define_ltds_fee_only_semantics { + loaded_fee_payer_size + other_accounts_size + } else { + 0 + }, + fail_program_id_loaded_size, + ); + } +} + +// Tests for proper accumulation of metrics across loaded programs in a batch. +#[test] +fn svm_metrics_accumulation() { + for test_entry in program_medley(false) { + let env = SvmTestEnvironment::create(test_entry); + + let (transactions, check_results) = env.test_entry.prepare_transactions(); + + let result = env.batch_processor.load_and_execute_sanitized_transactions( + &env.mock_bank, + &transactions, + check_results, + &env.processing_environment, + &env.processing_config, + ); + + // jit compilation only happens on non-windows && x86_64 + #[cfg(all(not(target_os = "windows"), target_arch = "x86_64"))] + { + assert_ne!( + result + .execute_timings + .details + .create_executor_jit_compile_us + .0, + 0 + ); + } + assert_ne!( + result.execute_timings.details.create_executor_load_elf_us.0, + 0 + ); + assert_ne!( + result + .execute_timings + .details + .create_executor_verify_code_us + .0, + 0 + ); + } +} + +// NOTE this could be moved to its own file in the future, but it requires a total refactor of the test runner +mod balance_collector { + use { + super::*, + rand::prelude::*, + solana_program_pack::Pack, + spl_generic_token::token_2022, + spl_token_interface::state::{ + Account as TokenAccount, AccountState as TokenAccountState, Mint, + }, + test_case::test_case, + }; + + const STARTING_BALANCE: u64 = LAMPORTS_PER_SOL * 100; + + // a helper for constructing a transfer instruction, agnostic over system/token + // it also pulls double duty as a record of what the *result* of a transfer should be + // so we can instantiate a Transfer, gen the instruction, change it to fail, change the record to amount 0 + // and then the final test confirms the pre/post balances are unchanged with no special casing + #[derive(Debug, Default)] + struct Transfer { + from: Pubkey, + to: Pubkey, + amount: u64, + } + + impl Transfer { + // given a set of users, picks two randomly and does a random transfer between them + fn new_rand(users: &[Pubkey]) -> Self { + let mut rng = rand::rng(); + let [from_idx, to_idx] = (0..users.len()).choose_multiple(&mut rng, 2)[..] else { + unreachable!() + }; + let from = users[from_idx]; + let to = users[to_idx]; + let amount = rng.random_range(1..STARTING_BALANCE / 100); + + Self { from, to, amount } + } + + fn to_system_instruction(&self) -> Instruction { + system_instruction::transfer(&self.from, &self.to, self.amount) + } + + fn to_token_instruction(&self, fee_payer: &Pubkey) -> Instruction { + // true tokenkeg connoisseurs will note we shouldnt have to sign the sender + // we use a common account owner, the fee-payer, to conveniently reuse account state + // so why do we sign? to force the sender and receiver to be in a consistent order in account keys + // which means we can grab them by index in our final test instead of searching by key + let mut instruction = spl_token_interface::instruction::transfer( + &spl_token_interface::id(), + &self.from, + &self.to, + fee_payer, + &[], + self.amount, + ) + .unwrap(); + instruction.accounts[0].is_signer = true; + + instruction + } + + fn to_instruction(&self, fee_payer: &Pubkey, use_tokens: bool) -> Instruction { + if use_tokens { + self.to_token_instruction(fee_payer) + } else { + self.to_system_instruction() + } + } + } + + #[test_case(false; "native")] + #[test_case(true; "token")] + fn svm_collect_balances(use_tokens: bool) { + let mut rng = rand::rng(); + + let fee_payer_keypair = Keypair::new(); + let fake_fee_payer_keypair = Keypair::new(); + let alice_keypair = Keypair::new(); + let bob_keypair = Keypair::new(); + let charlie_keypair = Keypair::new(); + + let fee_payer = fee_payer_keypair.pubkey(); + let fake_fee_payer = fake_fee_payer_keypair.pubkey(); + let mint = Pubkey::new_unique(); + let alice = alice_keypair.pubkey(); + let bob = bob_keypair.pubkey(); + let charlie = charlie_keypair.pubkey(); + + let native_state = AccountSharedData::create_from_existing_shared_data( + STARTING_BALANCE, + Arc::new(vec![]), + system_program::id(), + false, + u64::MAX, + ); + + let mut mint_buf = vec![0; Mint::get_packed_len()]; + Mint { + decimals: 9, + is_initialized: true, + ..Mint::default() + } + .pack_into_slice(&mut mint_buf); + + let mint_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(mint_buf), + spl_token_interface::id(), + false, + u64::MAX, + ); + + let token_account_for_tests = || TokenAccount { + mint, + owner: fee_payer, + amount: STARTING_BALANCE, + state: TokenAccountState::Initialized, + ..TokenAccount::default() + }; + + let mut token_buf = vec![0; TokenAccount::get_packed_len()]; + token_account_for_tests().pack_into_slice(&mut token_buf); + + let token_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(token_buf), + spl_token_interface::id(), + false, + u64::MAX, + ); + + let mut program_accounts = + solana_program_binaries::by_id(&spl_token_interface::id(), &Rent::default()).unwrap(); + + let (_, spl_token) = program_accounts.swap_remove(0); + let (program_data_key, program_data) = program_accounts.swap_remove(0); + + for _ in 0..100 { + let mut test_entry = SvmTestEntry::default(); + test_entry.add_initial_account(fee_payer, &native_state.clone()); + + if use_tokens { + test_entry.add_initial_account(spl_token_interface::id(), &spl_token); + test_entry.add_initial_account(program_data_key, &program_data); + + test_entry.add_initial_account(mint, &mint_state); + test_entry.add_initial_account(alice, &token_state); + test_entry.add_initial_account(bob, &token_state); + test_entry.add_initial_account(charlie, &token_state); + } else { + test_entry.add_initial_account(alice, &native_state); + test_entry.add_initial_account(bob, &native_state); + test_entry.add_initial_account(charlie, &native_state); + } + + // test that fee-payer balances are reported correctly + // all we need to know is whether the transaction is processed or dropped + let mut transaction_discards = vec![]; + + // every time we perform a transfer, we mutate user_balances + // and then clone and push it into user_balance_history + // this lets us go through every svm balance record and confirm correctness + let mut user_balances = HashMap::new(); + user_balances.insert(alice, STARTING_BALANCE); + user_balances.insert(bob, STARTING_BALANCE); + user_balances.insert(charlie, STARTING_BALANCE); + let mut user_balance_history = vec![(Transfer::default(), user_balances.clone())]; + + for _ in 0..50 { + // failures result in no balance changes (note we use a separate fee-payer) + // we mix some in with the successes to test that we never record changes for failures + let expected_status = match rng.random::() { + n if n < 0.85 => ExecutionStatus::Succeeded, + n if n < 0.90 => ExecutionStatus::ExecutedFailed, + n if n < 0.95 => ExecutionStatus::ProcessedFailed, + _ => ExecutionStatus::Discarded, + }; + transaction_discards.push(expected_status == ExecutionStatus::Discarded); + + let mut transfer = Transfer::new_rand(&[alice, bob, charlie]); + let from_signer = vec![&alice_keypair, &bob_keypair, &charlie_keypair] + .into_iter() + .find(|k| k.pubkey() == transfer.from) + .unwrap(); + + let instructions = match expected_status { + // a success results in balance changes and is a normal transaction + ExecutionStatus::Succeeded => { + user_balances + .entry(transfer.from) + .and_modify(|v| *v -= transfer.amount); + user_balances + .entry(transfer.to) + .and_modify(|v| *v += transfer.amount); + + vec![transfer.to_instruction(&fee_payer, use_tokens)] + } + // transfer an unreasonable amount to fail execution + ExecutionStatus::ExecutedFailed => { + transfer.amount = u64::MAX / 2; + let instruction = transfer.to_instruction(&fee_payer, use_tokens); + transfer.amount = 0; + + vec![instruction] + } + // use a non-existent program to fail loading + // token22 is very convenient because its presence ensures token bals are recorded + // if we had to use a random program id we would need to push a token program onto account keys + ExecutionStatus::ProcessedFailed => { + let mut instruction = transfer.to_instruction(&fee_payer, use_tokens); + instruction.program_id = token_2022::id(); + transfer.amount = 0; + + vec![instruction] + } + // use a non-existent fee-payer to trigger a discard + ExecutionStatus::Discarded => { + let mut instruction = transfer.to_instruction(&fee_payer, use_tokens); + if use_tokens { + instruction.accounts[2].pubkey = fake_fee_payer; + } + transfer.amount = 0; + + vec![instruction] + } + }; + + let transaction = if expected_status.discarded() { + Transaction::new_signed_with_payer( + &instructions, + Some(&fake_fee_payer), + &[&fake_fee_payer_keypair, from_signer], + Hash::default(), + ) + } else { + test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2); + + Transaction::new_signed_with_payer( + &instructions, + Some(&fee_payer), + &[&fee_payer_keypair, from_signer], + Hash::default(), + ) + }; + + test_entry.push_transaction_with_status(transaction, expected_status); + user_balance_history.push((transfer, user_balances.clone())); + } + + // this block just updates the SvmTestEntry final account states to be accurate + // doing this instead of skipping it, we validate that user_balances is definitely correct + // because env.execute() will assert all these states match the final bank state + if use_tokens { + let mut token_account = token_account_for_tests(); + let mut token_buf = vec![0; TokenAccount::get_packed_len()]; + + token_account.amount = *user_balances.get(&alice).unwrap(); + token_account.pack_into_slice(&mut token_buf); + let final_token_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(token_buf.clone()), + spl_token_interface::id(), + false, + u64::MAX, + ); + test_entry.update_expected_account_data(alice, &final_token_state); + + token_account.amount = *user_balances.get(&bob).unwrap(); + token_account.pack_into_slice(&mut token_buf); + let final_token_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(token_buf.clone()), + spl_token_interface::id(), + false, + u64::MAX, + ); + test_entry.update_expected_account_data(bob, &final_token_state); + + token_account.amount = *user_balances.get(&charlie).unwrap(); + token_account.pack_into_slice(&mut token_buf); + let final_token_state = AccountSharedData::create_from_existing_shared_data( + LAMPORTS_PER_SOL, + Arc::new(token_buf.clone()), + spl_token_interface::id(), + false, + u64::MAX, + ); + test_entry.update_expected_account_data(charlie, &final_token_state); + } else { + let mut alice_final_state = native_state.clone(); + alice_final_state.set_lamports(*user_balances.get(&alice).unwrap()); + test_entry.update_expected_account_data(alice, &alice_final_state); + + let mut bob_final_state = native_state.clone(); + bob_final_state.set_lamports(*user_balances.get(&bob).unwrap()); + test_entry.update_expected_account_data(bob, &bob_final_state); + + let mut charlie_final_state = native_state.clone(); + charlie_final_state.set_lamports(*user_balances.get(&charlie).unwrap()); + test_entry.update_expected_account_data(charlie, &charlie_final_state); + } + + // turn on balance recording and run the batch + let mut env = SvmTestEnvironment::create(test_entry); + env.processing_config + .recording_config + .enable_transaction_balance_recording = true; + + let batch_output = env.execute(); + let (pre_lamport_vecs, post_lamport_vecs, pre_token_vecs, post_token_vecs) = + batch_output.balance_collector.unwrap().into_vecs(); + + // first test the fee-payer balances + let mut running_fee_payer_balance = STARTING_BALANCE; + for (pre_bal, post_bal, was_discarded) in pre_lamport_vecs + .iter() + .zip(post_lamport_vecs.clone()) + .zip(transaction_discards) + .map(|((pres, posts), discard)| (pres[0], posts[0], discard)) + { + // we trigger discards with a non-existent fee-payer + if was_discarded { + assert_eq!(pre_bal, 0); + assert_eq!(post_bal, 0); + continue; + } + + let expected_post_balance = running_fee_payer_balance - LAMPORTS_PER_SIGNATURE * 2; + + assert_eq!(pre_bal, running_fee_payer_balance); + assert_eq!(post_bal, expected_post_balance); + + running_fee_payer_balance = expected_post_balance; + } + + // thanks to execute() we know user_balances is correct + // now we test that every step in user_balance_history matches the svm recorded balances + // in other words, the test effectively has three balance trackers and we can test they *all* agree + // first get the collected balances in a manner that is system/token agnostic + let (batch_pre, batch_post) = if use_tokens { + let pre_tupls: Vec<_> = pre_token_vecs + .iter() + .map(|bals| (bals[0].amount, bals[1].amount)) + .collect(); + + let post_tupls: Vec<_> = post_token_vecs + .iter() + .map(|bals| (bals[0].amount, bals[1].amount)) + .collect(); + + (pre_tupls, post_tupls) + } else { + let pre_tupls: Vec<_> = pre_lamport_vecs + .iter() + .map(|bals| (bals[1], bals[2])) + .collect(); + + let post_tupls: Vec<_> = post_lamport_vecs + .iter() + .map(|bals| (bals[1], bals[2])) + .collect(); + + (pre_tupls, post_tupls) + }; + + // these two asserts are trivially true. we include them just to make it clearer what these vecs are + // for n transactions, we have n pre-balance sets and n post-balance sets from svm + // but we have *n+1* test balance sets: we push initial state, and then push post-tx bals once per tx + // this mismatch is not strange at all. we also only have n+1 distinct svm timesteps despite 2n records + // pre-balances: (0 1 2 3) + // post-balances: (1 2 3 4) + // this does not mean time-overlapping svm records are equal. svm only captures the two accounts used by transfer + // whereas our test balances capture all three accounts at every timestep, so we require no pre/post separation + assert_eq!(user_balance_history.len(), batch_pre.len() + 1); + assert_eq!(user_balance_history.len(), batch_post.len() + 1); + + // these are the real tests + for (i, (svm_pre_balances, svm_post_balances)) in + batch_pre.into_iter().zip(batch_post).enumerate() + { + let (_, ref expected_pre_balances) = user_balance_history[i]; + let (ref transfer, ref expected_post_balances) = user_balance_history[i + 1]; + + assert_eq!( + svm_pre_balances.0, + *expected_pre_balances.get(&transfer.from).unwrap() + ); + assert_eq!( + svm_pre_balances.1, + *expected_pre_balances.get(&transfer.to).unwrap() + ); + + assert_eq!( + svm_post_balances.0, + *expected_post_balances.get(&transfer.from).unwrap() + ); + assert_eq!( + svm_post_balances.1, + *expected_post_balances.get(&transfer.to).unwrap() + ); + } + } + } +} diff --git a/solana/svm/tests/mock_bank.rs b/solana/svm/tests/mock_bank.rs new file mode 100644 index 0000000..ec89a0e --- /dev/null +++ b/solana/svm/tests/mock_bank.rs @@ -0,0 +1,387 @@ +#![allow(unused)] + +#[allow(deprecated)] +use solana_sysvar::recent_blockhashes::{Entry as BlockhashesEntry, RecentBlockhashes}; +use { + solana_account::{Account, AccountSharedData, ReadableAccount, WritableAccount}, + solana_clock::{Clock, Slot, UnixTimestamp}, + solana_epoch_schedule::EpochSchedule, + solana_fee_structure::{FeeDetails, FeeStructure}, + solana_loader_v3_interface::{self as bpf_loader_upgradeable, state::UpgradeableLoaderState}, + solana_program_runtime::{ + execution_budget::{SVMTransactionExecutionBudget, SVMTransactionExecutionCost}, + invoke_context::InvokeContext, + loaded_programs::{BlockRelation, ForkGraph, ProgramRuntimeEnvironment}, + program_cache_entry::ProgramCacheEntry, + solana_sbpf::{ + program::{BuiltinFunctionDefinition, BuiltinProgram, SBPFVersion}, + vm::Config, + }, + }, + solana_pubkey::Pubkey, + solana_rent::Rent, + solana_sdk_ids::{bpf_loader, bpf_loader_deprecated, compute_budget}, + solana_svm::transaction_processor::TransactionBatchProcessor, + solana_svm_callback::{AccountState, InvokeContextCallback, TransactionProcessingCallback}, + solana_svm_feature_set::SVMFeatureSet, + solana_svm_transaction::svm_message::SVMMessage, + solana_svm_type_overrides::sync::{Arc, RwLock}, + solana_syscalls::{ + SyscallAbort, SyscallGetClockSysvar, SyscallGetEpochScheduleSysvar, SyscallGetRentSysvar, + SyscallGetSysvar, SyscallInvokeSignedRust, SyscallLog, SyscallMemcmp, SyscallMemcpy, + SyscallMemmove, SyscallMemset, SyscallPanic, SyscallSetReturnData, + }, + solana_sysvar_id::SysvarId, + std::{ + cmp::Ordering, + collections::HashMap, + env, + fs::{self, File}, + io::Read, + }, +}; + +pub const EXECUTION_SLOT: u64 = 5; // The execution slot must be greater than the deployment slot +pub const EXECUTION_EPOCH: u64 = 2; // The execution epoch must be greater than the deployment epoch +pub const WALLCLOCK_TIME: i64 = 1704067200; // Arbitrarily Jan 1, 2024 + +pub struct MockForkGraph {} + +impl ForkGraph for MockForkGraph { + fn relationship(&self, a: Slot, b: Slot) -> BlockRelation { + match a.cmp(&b) { + Ordering::Less => BlockRelation::Ancestor, + Ordering::Equal => BlockRelation::Equal, + Ordering::Greater => BlockRelation::Descendant, + } + } +} + +#[derive(Default, Clone)] +pub struct MockBankCallback { + pub feature_set: SVMFeatureSet, + pub account_shared_data: Arc>>, + #[allow(clippy::type_complexity)] + pub inspected_accounts: + Arc, /* is_writable */ bool)>>>>, +} + +impl InvokeContextCallback for MockBankCallback {} + +impl TransactionProcessingCallback for MockBankCallback { + fn get_account_shared_data(&self, pubkey: &Pubkey) -> Option<(AccountSharedData, Slot)> { + self.account_shared_data + .read() + .unwrap() + .get(pubkey) + .map(|account| (account.clone(), 0)) + } + + fn inspect_account(&self, address: &Pubkey, account_state: AccountState, is_writable: bool) { + let account = match account_state { + AccountState::Dead => None, + AccountState::Alive(account) => Some(account.clone()), + }; + self.inspected_accounts + .write() + .unwrap() + .entry(*address) + .or_default() + .push((account, is_writable)); + } +} + +impl MockBankCallback { + pub fn calculate_fee_details(message: &impl SVMMessage, prioritization_fee: u64) -> FeeDetails { + let signature_count = message + .num_transaction_signatures() + .saturating_add(message.num_ed25519_signatures()) + .saturating_add(message.num_secp256k1_signatures()) + .saturating_add(message.num_secp256r1_signatures()); + + FeeDetails::new( + signature_count.saturating_mul(FeeStructure::default().lamports_per_signature), + prioritization_fee, + ) + } + + pub fn add_builtin( + &self, + batch_processor: &TransactionBatchProcessor, + program_id: Pubkey, + name: &str, + builtin: ProgramCacheEntry, + ) { + let account_data = AccountSharedData::from(Account { + lamports: 5000, + data: name.as_bytes().to_vec(), + owner: solana_sdk_ids::native_loader::id(), + executable: true, + rent_epoch: 0, + }); + + self.account_shared_data + .write() + .unwrap() + .insert(program_id, account_data); + + batch_processor.add_builtin(program_id, builtin); + } + + #[allow(unused)] + pub fn override_feature_set(&mut self, new_set: SVMFeatureSet) { + self.feature_set = new_set + } + + pub fn configure_sysvars(&self) { + // We must fill in the sysvar cache entries + + // clock contents are important because we use them for a sysvar loading test + let clock = Clock { + slot: EXECUTION_SLOT, + epoch_start_timestamp: WALLCLOCK_TIME.saturating_sub(10) as UnixTimestamp, + epoch: EXECUTION_EPOCH, + leader_schedule_epoch: EXECUTION_EPOCH, + unix_timestamp: WALLCLOCK_TIME as UnixTimestamp, + }; + + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&clock).unwrap()); + self.account_shared_data + .write() + .unwrap() + .insert(Clock::id(), account_data); + + // default rent is fine + let rent = Rent::default(); + + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&rent).unwrap()); + self.account_shared_data + .write() + .unwrap() + .insert(Rent::id(), account_data); + + // SystemInstruction::AdvanceNonceAccount asserts RecentBlockhashes is + // non-empty but then just gets the blockhash from InvokeContext. So, + // the sysvar doesn't need real entries + #[allow(deprecated)] + let recent_blockhashes = vec![BlockhashesEntry::default()]; + + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&recent_blockhashes).unwrap()); + #[allow(deprecated)] + self.account_shared_data + .write() + .unwrap() + .insert(RecentBlockhashes::id(), account_data); + + // EpochSchedule is required for non-mocked LoaderV3 deploy + let epoch_schedule = EpochSchedule::without_warmup(); + + let mut account_data = AccountSharedData::default(); + account_data.set_data(bincode::serialize(&epoch_schedule).unwrap()); + self.account_shared_data + .write() + .unwrap() + .insert(EpochSchedule::id(), account_data); + } +} + +pub fn load_program(name: String) -> Vec { + // Loading the program file + let mut dir = env::current_dir().unwrap(); + dir.push("tests"); + dir.push("example-programs"); + dir.push(name.as_str()); + let name = name.replace('-', "_"); + dir.push(name + "_program.so"); + let mut file = File::open(dir.clone()).expect("file not found"); + let metadata = fs::metadata(dir).expect("Unable to read metadata"); + let mut buffer = vec![0; metadata.len() as usize]; + file.read_exact(&mut buffer).expect("Buffer overflow"); + buffer +} + +pub fn program_address(program_name: &str) -> Pubkey { + Pubkey::create_with_seed(&Pubkey::default(), program_name, &Pubkey::default()).unwrap() +} + +pub fn program_data_size(program_name: &str) -> usize { + UpgradeableLoaderState::size_of_programdata_metadata() + .saturating_add(load_program(program_name.to_string()).len()) +} + +pub fn deploy_program(name: String, deployment_slot: Slot, mock_bank: &MockBankCallback) -> Pubkey { + deploy_program_with_upgrade_authority(name, deployment_slot, mock_bank, None) +} + +pub fn deploy_program_with_upgrade_authority( + name: String, + deployment_slot: Slot, + mock_bank: &MockBankCallback, + upgrade_authority_address: Option, +) -> Pubkey { + let rent = Rent::default(); + let program_account = program_address(&name); + let program_data_account = bpf_loader_upgradeable::get_program_data_address(&program_account); + + let state = UpgradeableLoaderState::Program { + programdata_address: program_data_account, + }; + + // The program account must have funds and hold the executable binary + let mut account_data = AccountSharedData::default(); + let buffer = bincode::serialize(&state).unwrap(); + account_data.set_lamports(rent.minimum_balance(buffer.len())); + account_data.set_owner(solana_sdk_ids::bpf_loader_upgradeable::id()); + account_data.set_executable(true); + account_data.set_data(buffer); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(program_account, account_data); + + let mut account_data = AccountSharedData::default(); + let state = UpgradeableLoaderState::ProgramData { + slot: deployment_slot, + upgrade_authority_address, + }; + let mut header = bincode::serialize(&state).unwrap(); + let mut complement = vec![ + 0; + std::cmp::max( + 0, + UpgradeableLoaderState::size_of_programdata_metadata().saturating_sub(header.len()) + ) + ]; + let mut buffer = load_program(name); + header.append(&mut complement); + header.append(&mut buffer); + account_data.set_lamports(rent.minimum_balance(header.len())); + account_data.set_owner(solana_sdk_ids::bpf_loader_upgradeable::id()); + account_data.set_data(header); + mock_bank + .account_shared_data + .write() + .unwrap() + .insert(program_data_account, account_data); + + program_account +} + +pub fn register_builtins( + mock_bank: &MockBankCallback, + batch_processor: &TransactionBatchProcessor, +) { + const DEPLOYMENT_SLOT: u64 = 0; + // We must register LoaderV3 as a loadable account, otherwise programs won't execute. + let loader_v3_name = "solana_bpf_loader_upgradeable_program"; + mock_bank.add_builtin( + batch_processor, + solana_sdk_ids::bpf_loader_upgradeable::id(), + loader_v3_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + loader_v3_name.len(), + solana_bpf_loader_program::Entrypoint::register, + ), + ); + + // Other loaders are needed for testing program cache behavior. + let loader_v1_name = "solana_bpf_loader_deprecated_program"; + mock_bank.add_builtin( + batch_processor, + bpf_loader_deprecated::id(), + loader_v1_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + loader_v1_name.len(), + solana_bpf_loader_program::Entrypoint::register, + ), + ); + + let loader_v2_name = "solana_bpf_loader_program"; + mock_bank.add_builtin( + batch_processor, + bpf_loader::id(), + loader_v2_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + loader_v2_name.len(), + solana_bpf_loader_program::Entrypoint::register, + ), + ); + + // In order to perform a transference of native tokens using the system instruction, + // the system program builtin must be registered. + let system_program_name = "system_program"; + mock_bank.add_builtin( + batch_processor, + solana_system_program::id(), + system_program_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + system_program_name.len(), + solana_system_program::system_processor::Entrypoint::register, + ), + ); + + // For testing realloc, we need the compute budget program + let compute_budget_program_name = "compute_budget_program"; + mock_bank.add_builtin( + batch_processor, + compute_budget::id(), + compute_budget_program_name, + ProgramCacheEntry::new_builtin( + DEPLOYMENT_SLOT, + compute_budget_program_name.len(), + solana_compute_budget_program::Entrypoint::register, + ), + ); +} + +pub fn create_custom_loader() -> ProgramRuntimeEnvironment { + let compute_budget = SVMTransactionExecutionBudget::default(); + let vm_config = Config { + max_call_depth: compute_budget.max_call_depth, + stack_frame_size: compute_budget.stack_frame_size, + enable_address_translation: true, + enable_stack_frame_gaps: true, + instruction_meter_checkpoint_distance: 10000, + enable_instruction_meter: true, + enable_register_tracing: true, + enable_symbol_and_section_labels: false, + reject_broken_elfs: true, + noop_instruction_rate: 256, + sanitize_user_provided_values: true, + enabled_sbpf_versions: SBPFVersion::V0..=SBPFVersion::V3, + optimize_rodata: false, + aligned_memory_mapping: false, + allow_memory_region_zero: true, + }; + + // These functions are system calls the compile contract calls during execution, so they + // need to be registered. + let mut loader = BuiltinProgram::new_loader(vm_config); + SyscallAbort::register(&mut loader, "abort").expect("Registration failed"); + SyscallLog::register(&mut loader, "sol_log_").expect("Registration failed"); + SyscallMemcpy::register(&mut loader, "sol_memcpy_").expect("Registration failed"); + SyscallMemset::register(&mut loader, "sol_memset_").expect("Registration failed"); + SyscallMemcmp::register(&mut loader, "sol_memcmp_").expect("Registration failed"); + SyscallMemmove::register(&mut loader, "sol_memmove_").expect("Registration failed"); + SyscallInvokeSignedRust::register(&mut loader, "sol_invoke_signed_rust") + .expect("Registration failed"); + SyscallSetReturnData::register(&mut loader, "sol_set_return_data") + .expect("Registration failed"); + SyscallGetClockSysvar::register(&mut loader, "sol_get_clock_sysvar") + .expect("Registration failed"); + SyscallGetRentSysvar::register(&mut loader, "sol_get_rent_sysvar") + .expect("Registration failed"); + SyscallGetEpochScheduleSysvar::register(&mut loader, "sol_get_epoch_schedule_sysvar") + .expect("Registration failed"); + SyscallPanic::register(&mut loader, "sol_panic_").expect("Registration failed"); + SyscallGetSysvar::register(&mut loader, "sol_get_sysvar").expect("Registration failed"); + ProgramRuntimeEnvironment::from(loader) +} diff --git a/solana/transaction-context/Cargo.toml b/solana/transaction-context/Cargo.toml new file mode 100644 index 0000000..f9cc89e --- /dev/null +++ b/solana/transaction-context/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "solana-transaction-context" +description = "Solana data shared between program runtime and built-in programs as well as SBF programs." +documentation = "https://docs.rs/solana-transaction-context" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] +all-features = true +rustdoc-args = ["--cfg=docsrs"] + +[features] +agave-unstable-api = [] +bincode = ["dep:bincode", "serde", "solana-account/bincode"] +dev-context-only-utils = ["bincode", "solana-account/dev-context-only-utils", "dep:qualifier_attr"] +serde = ["serde/derive", "solana-pubkey/serde"] +wincode = ["dep:wincode", "solana-pubkey/wincode"] + +[dependencies] +solana-account = { workspace = true } +solana-instruction = { workspace = true, features = ["std"] } +solana-instructions-sysvar = { workspace = true } +solana-pubkey = { workspace = true } +wincode = { workspace = true, optional = true } + +[target.'cfg(not(any(target_arch = "sbf", target_arch = "bpf")))'.dependencies] +bincode = { workspace = true, optional = true } +serde = { workspace = true, optional = true } +solana-rent = { workspace = true } +solana-sbpf = { workspace = true } +solana-sdk-ids = { workspace = true } + +[dev-dependencies] +solana-account-info = { workspace = true } +solana-program-entrypoint = { workspace = true } +solana-system-interface = { workspace = true } +solana-transaction-context = { path = ".", features = [ + "agave-unstable-api", + "dev-context-only-utils", +] } +static_assertions = { workspace = true } + +[lints] +workspace = true diff --git a/solana/transaction-context/src/instruction.rs b/solana/transaction-context/src/instruction.rs new file mode 100644 index 0000000..c90ab5b --- /dev/null +++ b/solana/transaction-context/src/instruction.rs @@ -0,0 +1,278 @@ +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..00ac411 --- /dev/null +++ b/solana/transaction-context/src/instruction_accounts.rs @@ -0,0 +1,387 @@ +use { + crate::{ + IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_INSTRUCTION, transaction::TransactionContext, + transaction_accounts::AccountRefMut, + }, + solana_account::{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 the underlying AccountSharedData is shared. + /// + /// The data is shared if the account has been loaded from the accounts database and has never + /// been written to. Writing to an account unshares it. + /// + /// During account serialization, if an account is shared it'll get mapped as CoW, else it'll + /// get mapped directly as writable. + pub fn is_shared(&self) -> bool { + self.account.is_shared() + } + + 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 { + 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 + #[expect(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(); + // Only the owner can change the length of the data + if new_len != old_len && !self.is_owned_by_current_program() { + return Err(InstructionError::AccountDataSizeChanged); + } + 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); + + #[expect(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..537d4a5 --- /dev/null +++ b/solana/transaction-context/src/lib.rs @@ -0,0 +1,50 @@ +#![cfg(feature = "agave-unstable-api")] +//! Data shared between program runtime and built-in programs as well as SBF programs. +#![deny(clippy::indexing_slicing)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +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: With virtual_address_space_adjustments programs can grow accounts +// faster than they intend to, because the AccessViolationHandler 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..0ea8f81 --- /dev/null +++ b/solana/transaction-context/src/transaction.rs @@ -0,0 +1,1302 @@ +use { + crate::{ + IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION, 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( + GUEST_INSTRUCTION_DATA_BASE_ADDRESS.saturating_add( + GUEST_REGION_SIZE.saturating_mul(number_of_top_level_instructions as u64), + ), + 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, + }; + + // We need an extra space for the placeholder, so we avoid relocations. + let mut instruction_trace = + Vec::with_capacity(instruction_trace_capacity.saturating_add(1)); + instruction_trace.resize_with( + number_of_top_level_instructions.saturating_add(1), + InstructionFrame::default, + ); + + Self { + accounts: Rc::new(TransactionAccounts::new(transaction_accounts)), + instruction_stack_capacity, + instruction_trace_capacity, + instruction_stack: Vec::with_capacity(instruction_stack_capacity), + instruction_trace, + 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 = if self.instruction_stack.is_empty() { + self.next_top_level_instruction_index + } else { + 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; + let next_ptr = self + .transaction_frame + .cpi_scratchpad + .ptr() + .saturating_add(GUEST_REGION_SIZE); + self.transaction_frame.cpi_scratchpad = VmSlice::new(next_ptr, 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.next_top_level_instruction_index, + 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; + } + + if self.number_of_called_instructions_in_trace() >= self.instruction_trace_capacity { + return Err(InstructionError::MaxInstructionTraceLengthExceeded); + } + + let (index_in_trace, 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, index) + } else { + let index = self.get_instruction_trace_length(); + self.transaction_frame.number_of_cpis_in_trace = self + .transaction_frame + .number_of_cpis_in_trace + .saturating_add(1); + self.instruction_trace.push(InstructionFrame::default()); + ( + index, + self.next_top_level_instruction_index.saturating_sub(1), + ) + }; + + 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, + virtual_address_space_adjustments: bool, + account_data_direct_mapping: bool, + ) -> 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| { + 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; + } + + let remaining_allowed_growth = MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION + .saturating_sub(accounts.resize_delta()) + .max(0) as usize; + + if requested_length > region.len as usize { + // Realloc immediately here to fit the requested access, + // then later in CPI or deserialization realloc again to the + // account length the program stored in AccountInfo. + let old_len = account.data().len(); + let new_len = (address_space_reserved_for_account as usize) + .min(MAX_ACCOUNT_DATA_LEN as usize) + .min(old_len.saturating_add(remaining_allowed_growth)); + // The last two min operations ensure the following: + debug_assert!(accounts.can_data_be_resized(old_len, new_len).is_ok()); + if accounts + .update_accounts_resize_delta(old_len, new_len) + .is_err() + { + return; + } + account.resize(new_len, 0); + region.len = new_len as u64; + } + + // Potentially unshare / make the account shared data unique (CoW logic). + if virtual_address_space_adjustments && account_data_direct_mapping { + region.host_addr = account.data_as_mut_slice().as_mut_ptr() 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), + ) + } + + /// Called instruction are those that the program runtime has already called into. It + /// encompasses instructions under execution (e.g. all nested CPIs are already called) and + /// finished ones. + /// + /// Top level instructions that have not yet been executed aren't considered called. + pub fn number_of_called_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))] +#[cfg_attr(feature = "wincode", derive(wincode::SchemaRead, wincode::SchemaWrite))] +#[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(); + + // Instruction #1 + transaction_context + .configure_instruction_at_index( + 1, + 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.number_of_called_instructions_in_trace(), + 1 + ); + + 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_called_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, + 2 + ); + + 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_called_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, + 3 + ); + + 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_called_instructions_in_trace(), + 3 + ); + // Return from nested CPI + transaction_context.pop().unwrap(); + assert_eq!( + transaction_context.number_of_called_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, + 2 + ); + + // 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, + 4 + ); + + 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_called_instructions_in_trace(), + 4 + ); + + // Return from second nested CPI + transaction_context.pop().unwrap(); + + assert_eq!( + transaction_context + .transaction_frame + .current_executing_instruction, + 2 + ); + + 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_called_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(); + transaction_context.push().unwrap(); + assert_eq!( + transaction_context + .transaction_frame + .current_executing_instruction, + 1, + ); + 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_called_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, + 1, + ); + + 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(); + + // 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(), + 0 + ); + + transaction_context.pop().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..a2dc8d8 --- /dev/null +++ b/solana/transaction-context/src/transaction_accounts.rs @@ -0,0 +1,738 @@ +#[cfg(feature = "dev-context-only-utils")] +use qualifier_attr::qualifiers; +use { + crate::{ + IndexOfAccount, MAX_ACCOUNT_DATA_GROWTH_PER_TRANSACTION, MAX_ACCOUNT_DATA_LEN, + vm_addresses::{GUEST_ACCOUNT_PAYLOAD_BASE_ADDRESS, GUEST_REGION_SIZE}, + vm_slice::VmSlice, + }, + solana_account::{AccountSharedData, ReadableAccount, WritableAccount}, + solana_instruction::error::InstructionError, + solana_pubkey::Pubkey, + std::{ + cell::{Cell, UnsafeCell}, + ops::{Deref, DerefMut}, + ptr, + sync::Arc, + }, +}; + +/// This struct is shared with programs. Do not alter its fields. +#[repr(C)] +#[derive(Debug, PartialEq)] +struct AccountSharedFields { + key: Pubkey, + owner: Pubkey, + lamports: u64, + // The payload is going to be filled with the guest virtual address of the account payload + // vector. + payload: VmSlice, +} + +#[derive(Debug, PartialEq)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +struct AccountPrivateFields { + rent_epoch: u64, + executable: bool, + payload: Arc>, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl AccountPrivateFields { + fn payload_len(&self) -> usize { + self.payload.len() + } +} + +#[derive(Debug, PartialEq)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionAccountView<'a> { + abi_account: &'a AccountSharedFields, + private_fields: &'a AccountPrivateFields, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl ReadableAccount for TransactionAccountView<'_> { + fn lamports(&self) -> u64 { + self.abi_account.lamports + } + + fn data(&self) -> &[u8] { + self.private_fields.payload.as_slice() + } + + fn owner(&self) -> &Pubkey { + &self.abi_account.owner + } + + fn executable(&self) -> bool { + self.private_fields.executable + } + + fn rent_epoch(&self) -> u64 { + self.private_fields.rent_epoch + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl PartialEq for TransactionAccountView<'_> { + fn eq(&self, other: &AccountSharedData) -> bool { + other.lamports() == self.lamports() + && other.data() == self.data() + && other.owner() == self.owner() + && other.executable() == self.executable() + && other.rent_epoch() == self.rent_epoch() + } +} + +#[derive(Debug)] +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +pub struct TransactionAccountViewMut<'a> { + abi_account: &'a mut AccountSharedFields, + private_fields: &'a mut AccountPrivateFields, +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl TransactionAccountViewMut<'_> { + fn data_mut(&mut self) -> &mut Vec { + Arc::make_mut(&mut self.private_fields.payload) + } + + pub(crate) fn resize(&mut self, new_len: usize, value: u8) { + self.data_mut().resize(new_len, value); + // SAFETY: We are synchronizing the lengths. + unsafe { + self.abi_account.payload.set_len(new_len as u64); + } + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + pub(crate) fn set_data_from_slice(&mut self, new_data: &[u8]) { + // If the buffer isn't shared, we're going to memcpy in place. + let Some(data) = Arc::get_mut(&mut self.private_fields.payload) else { + // If the buffer is shared, the cheapest thing to do is to clone the + // incoming slice and replace the buffer. + self.private_fields.payload = Arc::new(new_data.to_vec()); + // SAFETY: We are synchronizing the lengths. + unsafe { + self.abi_account.payload.set_len(new_data.len() as u64); + } + return; + }; + + let new_len = new_data.len(); + + // Reserve additional capacity if needed. Here we make the assumption + // that growing the current buffer is cheaper than doing a whole new + // allocation to make `new_data` owned. + // + // This assumption holds true during CPI, especially when the account + // size doesn't change but the account is only changed in place. And + // it's also true when the account is grown by a small margin (the + // realloc limit is quite low), in which case the allocator can just + // update the allocation metadata without moving. + // + // Shrinking and copying in place is always faster than making + // `new_data` owned, since shrinking boils down to updating the Vec's + // length. + + data.reserve(new_len.saturating_sub(data.len())); + + // Safety: + // We just reserved enough capacity. We set data::len to 0 to avoid + // possible UB on panic (dropping uninitialized elements), do the copy, + // finally set the new length once everything is initialized. + unsafe { + data.set_len(0); + ptr::copy_nonoverlapping(new_data.as_ptr(), data.as_mut_ptr(), new_len); + data.set_len(new_len); + self.abi_account.payload.set_len(new_len as u64); + }; + } + + pub(crate) fn extend_from_slice(&mut self, data: &[u8]) { + self.data_mut().extend_from_slice(data); + // SAFETY: We are synchronizing the lengths. + unsafe { + self.abi_account + .payload + .set_len(self.private_fields.payload_len() as u64); + } + } + + pub(crate) fn reserve(&mut self, additional: usize) { + if let Some(data) = Arc::get_mut(&mut self.private_fields.payload) { + data.reserve(additional) + } else { + let mut data = + Vec::with_capacity(self.private_fields.payload_len().saturating_add(additional)); + data.extend_from_slice(self.private_fields.payload.as_slice()); + self.private_fields.payload = Arc::new(data); + } + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + pub(crate) fn is_shared(&self) -> bool { + Arc::strong_count(&self.private_fields.payload) > 1 + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl ReadableAccount for TransactionAccountViewMut<'_> { + fn lamports(&self) -> u64 { + self.abi_account.lamports + } + + fn data(&self) -> &[u8] { + self.private_fields.payload.as_slice() + } + + fn owner(&self) -> &Pubkey { + &self.abi_account.owner + } + + fn executable(&self) -> bool { + self.private_fields.executable + } + + fn rent_epoch(&self) -> u64 { + self.private_fields.rent_epoch + } +} + +#[cfg(not(any(target_arch = "bpf", target_arch = "sbf")))] +impl WritableAccount for TransactionAccountViewMut<'_> { + fn set_lamports(&mut self, lamports: u64) { + self.abi_account.lamports = lamports; + } + + fn data_as_mut_slice(&mut self) -> &mut [u8] { + Arc::make_mut(&mut self.private_fields.payload).as_mut_slice() + } + + fn set_owner(&mut self, owner: Pubkey) { + self.abi_account.owner = owner; + } + + fn copy_into_owner_from_slice(&mut self, source: &[u8]) { + self.abi_account.owner.as_mut().copy_from_slice(source); + } + + fn set_executable(&mut self, executable: bool) { + self.private_fields.executable = executable; + } + + fn set_rent_epoch(&mut self, epoch: u64) { + self.private_fields.rent_epoch = epoch; + } +} + +/// 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 { + shared_account_fields: Box<[UnsafeCell]>, + private_account_fields: 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 (shared_accounts, private_fields) = accounts + .into_iter() + .enumerate() + .map(|(idx, item)| { + ( + UnsafeCell::new(AccountSharedFields { + key: item.0, + owner: *item.1.owner(), + lamports: item.1.lamports(), + payload: VmSlice::new( + GUEST_ACCOUNT_PAYLOAD_BASE_ADDRESS + .saturating_add(GUEST_REGION_SIZE.saturating_mul(idx as u64)), + item.1.data().len() as u64, + ), + }), + UnsafeCell::new(AccountPrivateFields { + rent_epoch: item.1.rent_epoch(), + executable: item.1.executable(), + payload: item.1.data_clone(), + }), + ) + }) + .collect::<( + Vec>, + Vec>, + )>(); + + TransactionAccounts { + shared_account_fields: shared_accounts.into_boxed_slice(), + private_account_fields: private_fields.into_boxed_slice(), + borrow_counters, + touched_flags, + resize_delta: Cell::new(0), + lamports_delta: Cell::new(0), + } + } + + pub(crate) fn len(&self) -> usize { + self.shared_account_fields.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(()) + } + + #[cfg_attr(feature = "dev-context-only-utils", qualifiers(pub))] + pub(crate) 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 svm_account = unsafe { + &mut *self + .shared_account_fields + .get(index as usize) + .unwrap() + .get() + }; + + let private_fields = unsafe { + &mut *self + .private_account_fields + .get(index as usize) + .unwrap() + .get() + }; + + let account = TransactionAccountViewMut { + abi_account: svm_account, + private_fields, + }; + + 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 svm_account = unsafe { + &*self + .shared_account_fields + .get(index as usize) + .unwrap() + .get() + }; + + let private_fields = unsafe { + &*self + .private_account_fields + .get(index as usize) + .unwrap() + .get() + }; + + let account = TransactionAccountView { + abi_account: svm_account, + private_fields, + }; + + 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 { + let shared_account_fields = std::mem::take(&mut self.shared_account_fields); + let private_account_fields = std::mem::take(&mut self.private_account_fields); + shared_account_fields + .into_iter() + .zip(private_account_fields) + .map(|(shared_fields_cell, private_fields_cell)| { + let shared_fields = shared_fields_cell.into_inner(); + let private_fields = private_fields_cell.into_inner(); + ( + shared_fields.key, + AccountSharedData::create_from_existing_shared_data( + shared_fields.lamports, + private_fields.payload.clone(), + shared_fields.owner, + private_fields.executable, + private_fields.rent_epoch, + ), + ) + }) + .collect() + } + + pub(crate) fn deconstruct_into_account_shared_data(&mut self) -> Vec { + let shared_account_fields = std::mem::take(&mut self.shared_account_fields); + let private_account_fields = std::mem::take(&mut self.private_account_fields); + shared_account_fields + .into_iter() + .zip(private_account_fields) + .map(|(shared_fields_cell, private_fields_cell)| { + let shared_fields = shared_fields_cell.into_inner(); + let private_fields = private_fields_cell.into_inner(); + AccountSharedData::create_from_existing_shared_data( + shared_fields.lamports, + private_fields.payload.clone(), + shared_fields.owner, + private_fields.executable, + private_fields.rent_epoch, + ) + }) + .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.shared_account_fields + .get(index as usize) + .map(|acc| &(*acc.get()).key) + } + } + + 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.shared_account_fields + .iter() + .map(|item| &(*item.get()).key) + } + } +} + +#[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) { + // SAFETY: We are synchronizing the lengths. + unsafe { + self.account + .abi_account + .payload + .set_len(self.account.private_fields.payload_len() as u64); + } + 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::AccountSharedData, + 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)); + } + } + } +} diff --git a/solana/transaction-context/src/vm_addresses.rs b/solana/transaction-context/src/vm_addresses.rs new file mode 100644 index 0000000..a28a36b --- /dev/null +++ b/solana/transaction-context/src/vm_addresses.rs @@ -0,0 +1,5 @@ +pub(crate) const GUEST_REGION_SIZE: u64 = 1 << 32; +pub(crate) const RETURN_DATA_SCRATCHPAD: u64 = 7 * GUEST_REGION_SIZE; +pub(crate) const GUEST_ACCOUNT_PAYLOAD_BASE_ADDRESS: u64 = 8 * 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..b973c5c --- /dev/null +++ b/solana/transaction-context/src/vm_slice.rs @@ -0,0 +1,55 @@ +// 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; + } +} diff --git a/solana/transaction-view/Cargo.toml b/solana/transaction-view/Cargo.toml new file mode 100644 index 0000000..68e3c49 --- /dev/null +++ b/solana/transaction-view/Cargo.toml @@ -0,0 +1,49 @@ +[package] +name = "agave-transaction-view" +description = "Zero-copy parser and sanitizer for serialized Solana transactions" +documentation = "https://docs.rs/agave-transaction-view" +version = { workspace = true } +authors = { workspace = true } +repository = { workspace = true } +homepage = { workspace = true } +license = { workspace = true } +edition = "2024" + +[features] +agave-unstable-api = [] +dev-context-only-utils = [] + +[dependencies] +solana-hash = { workspace = true } +solana-message = { workspace = true } +solana-packet = { workspace = true } +solana-program-runtime = { workspace = true } +solana-pubkey = { workspace = true } +solana-sdk-ids = { workspace = true } +solana-short-vec = { workspace = true } +solana-signature = { workspace = true } +solana-svm-transaction = { workspace = true } +solana-transaction = { workspace = true } +solana-transaction-context = { workspace = true } + +[dev-dependencies] +# See order-crates-for-publishing.py for using this unusual `path = "."` +agave-transaction-view = { path = ".", features = ["agave-unstable-api", "dev-context-only-utils"] } +bincode = { workspace = true } +criterion = { workspace = true } +solana-instruction = { workspace = true } +solana-keypair = { workspace = true } +solana-message = { workspace = true, features = ["serde"] } +solana-signature = { workspace = true, features = ["serde"] } +solana-signer = { workspace = true } +solana-system-interface = { workspace = true, features = ["wincode"] } +solana-transaction = { workspace = true, features = ["serde", "wincode"] } +wincode = { workspace = true } + +[[bench]] +name = "bytes" +harness = false + +[[bench]] +name = "transaction_view" +harness = false diff --git a/solana/transaction-view/benches/bytes.rs b/solana/transaction-view/benches/bytes.rs new file mode 100644 index 0000000..a64e85d --- /dev/null +++ b/solana/transaction-view/benches/bytes.rs @@ -0,0 +1,68 @@ +use { + agave_transaction_view::bytes::read_compressed_u16, + bincode::{DefaultOptions, Options, serialize_into}, + criterion::{Criterion, Throughput, criterion_group, criterion_main}, + solana_packet::PACKET_DATA_SIZE, + solana_short_vec::{ShortU16, decode_shortu16_len}, + std::hint::black_box, +}; + +fn setup() -> Vec<(u16, usize, Vec)> { + let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding + + // Create a vector of all valid u16 values serialized into 16-byte buffers. + let mut values = Vec::with_capacity(PACKET_DATA_SIZE); + for value in 0..PACKET_DATA_SIZE as u16 { + let short_u16 = ShortU16(value); + let mut buffer = vec![0u8; 16]; + let serialized_len = options + .serialized_size(&short_u16) + .expect("Failed to get serialized size"); + serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); + values.push((value, serialized_len as usize, buffer)); + } + + values +} + +fn bench_u16_parsing(c: &mut Criterion) { + let values_serialized_lengths_and_buffers = setup(); + let mut group = c.benchmark_group("compressed_u16_parsing"); + group.throughput(Throughput::Elements( + values_serialized_lengths_and_buffers.len() as u64, + )); + + // Benchmark the decode_shortu16_len function from `solana-sdk` + group.bench_function("short_u16_decode", |c| { + c.iter(|| { + decode_shortu16_len_iter(&values_serialized_lengths_and_buffers); + }) + }); + + // Benchmark `read_compressed_u16` + group.bench_function("read_compressed_u16", |c| { + c.iter(|| { + read_compressed_u16_iter(&values_serialized_lengths_and_buffers); + }) + }); +} + +fn decode_shortu16_len_iter(values_serialized_lengths_and_buffers: &[(u16, usize, Vec)]) { + for (value, serialized_len, buffer) in values_serialized_lengths_and_buffers.iter() { + let (read_value, bytes_read) = decode_shortu16_len(black_box(buffer)).unwrap(); + assert_eq!(read_value, *value as usize, "Value mismatch for: {value}"); + assert_eq!(bytes_read, *serialized_len, "Offset mismatch for: {value}"); + } +} + +fn read_compressed_u16_iter(values_serialized_lengths_and_buffers: &[(u16, usize, Vec)]) { + for (value, serialized_len, buffer) in values_serialized_lengths_and_buffers.iter() { + let mut offset = 0; + let read_value = read_compressed_u16(black_box(buffer), &mut offset).unwrap(); + assert_eq!(read_value, *value, "Value mismatch for: {value}"); + assert_eq!(offset, *serialized_len, "Offset mismatch for: {value}"); + } +} + +criterion_group!(benches, bench_u16_parsing); +criterion_main!(benches); diff --git a/solana/transaction-view/benches/transaction_view.rs b/solana/transaction-view/benches/transaction_view.rs new file mode 100644 index 0000000..262c5ee --- /dev/null +++ b/solana/transaction-view/benches/transaction_view.rs @@ -0,0 +1,234 @@ +use { + agave_transaction_view::transaction_view::TransactionView, + criterion::{ + BenchmarkGroup, Criterion, Throughput, criterion_group, criterion_main, + measurement::Measurement, + }, + solana_hash::Hash, + solana_instruction::Instruction, + solana_keypair::Keypair, + solana_message::{ + Message, MessageHeader, VersionedMessage, + v0::{self, MessageAddressTableLookup}, + }, + solana_pubkey::Pubkey, + solana_signer::Signer, + solana_system_interface::instruction as system_instruction, + solana_transaction::versioned::{ + VersionedTransaction, sanitized::SanitizedVersionedTransaction, + }, + std::hint::black_box, +}; + +const NUM_TRANSACTIONS: usize = 1024; + +fn serialize_transactions(transactions: Vec) -> Vec> { + transactions + .into_iter() + .map(|transaction| wincode::serialize(&transaction).unwrap()) + .collect() +} + +fn bench_transactions_parsing( + group: &mut BenchmarkGroup, + serialized_transactions: Vec>, +) { + // Legacy Transaction Parsing + group.bench_function("VersionedTransaction", |c| { + c.iter(|| { + for bytes in serialized_transactions.iter() { + let _ = wincode::deserialize::(black_box(bytes)).unwrap(); + } + }); + }); + + // Legacy Transaction Parsing and Sanitize checks + group.bench_function("SanitizedVersionedTransaction", |c| { + c.iter(|| { + for bytes in serialized_transactions.iter() { + let tx = wincode::deserialize::(black_box(bytes)).unwrap(); + let _ = SanitizedVersionedTransaction::try_new(tx).unwrap(); + } + }); + }); + + // New Transaction Parsing + group.bench_function("TransactionView", |c| { + c.iter(|| { + for bytes in serialized_transactions.iter() { + let _ = TransactionView::try_new_unsanitized(black_box(bytes.as_ref())).unwrap(); + } + }); + }); + + // New Transaction Parsing and Sanitize checks + group.bench_function("TransactionView (Sanitized)", |c| { + c.iter(|| { + for bytes in serialized_transactions.iter() { + let _ = + TransactionView::try_new_sanitized(black_box(bytes.as_ref()), true).unwrap(); + } + }); + }); +} + +fn minimum_sized_transactions() -> Vec { + (0..NUM_TRANSACTIONS) + .map(|_| { + let keypair = Keypair::new(); + VersionedTransaction::try_new( + VersionedMessage::Legacy(Message::new_with_blockhash( + &[], + Some(&keypair.pubkey()), + &Hash::default(), + )), + &[&keypair], + ) + .unwrap() + }) + .collect() +} + +fn simple_transfers() -> Vec { + (0..NUM_TRANSACTIONS) + .map(|_| { + let keypair = Keypair::new(); + VersionedTransaction::try_new( + VersionedMessage::Legacy(Message::new_with_blockhash( + &[system_instruction::transfer( + &keypair.pubkey(), + &Pubkey::new_unique(), + 1, + )], + Some(&keypair.pubkey()), + &Hash::default(), + )), + &[&keypair], + ) + .unwrap() + }) + .collect() +} + +fn packed_transfers() -> Vec { + // Creating transfer instructions between same keys to maximize the number + // of transfers per transaction. We can fit up to 60 transfers. + const MAX_TRANSFERS_PER_TX: usize = 60; + + (0..NUM_TRANSACTIONS) + .map(|_| { + let keypair = Keypair::new(); + let to_pubkey = Pubkey::new_unique(); + let ixs = system_instruction::transfer_many( + &keypair.pubkey(), + &vec![(to_pubkey, 1); MAX_TRANSFERS_PER_TX], + ); + VersionedTransaction::try_new( + VersionedMessage::Legacy(Message::new(&ixs, Some(&keypair.pubkey()))), + &[&keypair], + ) + .unwrap() + }) + .collect() +} + +fn packed_noops() -> Vec { + // Creating noop instructions to maximize the number of instructions per + // transaction. We are allowed to fit up to 64 instructions per transaction. + const MAX_INSTRUCTIONS_PER_TRANSACTION: usize = 64; + + (0..NUM_TRANSACTIONS) + .map(|_| { + let keypair = Keypair::new(); + let program_id = Pubkey::new_unique(); + let ixs = (0..MAX_INSTRUCTIONS_PER_TRANSACTION) + .map(|_| Instruction::new_with_bytes(program_id, &[], vec![])); + VersionedTransaction::try_new( + VersionedMessage::Legacy(Message::new( + &ixs.collect::>(), + Some(&keypair.pubkey()), + )), + &[&keypair], + ) + .unwrap() + }) + .collect() +} + +fn packed_atls() -> Vec { + // Creating ATLs to maximize the number of ATLS per transaction. We can fit + // up to 31. + const MAX_ATLS_PER_TRANSACTION: usize = 31; + + (0..NUM_TRANSACTIONS) + .map(|_| { + let keypair = Keypair::new(); + VersionedTransaction::try_new( + VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + account_keys: vec![keypair.pubkey()], + recent_blockhash: Hash::default(), + instructions: vec![], + address_table_lookups: Vec::from_iter((0..MAX_ATLS_PER_TRANSACTION).map( + |_| MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![], + }, + )), + }), + &[&keypair], + ) + .unwrap() + }) + .collect() +} + +fn bench_parse_min_sized_transactions(c: &mut Criterion) { + let serialized_transactions = serialize_transactions(minimum_sized_transactions()); + let mut group = c.benchmark_group("min sized transactions"); + group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); + bench_transactions_parsing(&mut group, serialized_transactions); +} + +fn bench_parse_simple_transfers(c: &mut Criterion) { + let serialized_transactions = serialize_transactions(simple_transfers()); + let mut group = c.benchmark_group("simple transfers"); + group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); + bench_transactions_parsing(&mut group, serialized_transactions); +} + +fn bench_parse_packed_transfers(c: &mut Criterion) { + let serialized_transactions = serialize_transactions(packed_transfers()); + let mut group = c.benchmark_group("packed transfers"); + group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); + bench_transactions_parsing(&mut group, serialized_transactions); +} + +fn bench_parse_packed_noops(c: &mut Criterion) { + let serialized_transactions = serialize_transactions(packed_noops()); + let mut group = c.benchmark_group("packed noops"); + group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); + bench_transactions_parsing(&mut group, serialized_transactions); +} + +fn bench_parse_packed_atls(c: &mut Criterion) { + let serialized_transactions = serialize_transactions(packed_atls()); + let mut group = c.benchmark_group("packed atls"); + group.throughput(Throughput::Elements(serialized_transactions.len() as u64)); + bench_transactions_parsing(&mut group, serialized_transactions); +} + +criterion_group!( + benches, + bench_parse_min_sized_transactions, + bench_parse_simple_transfers, + bench_parse_packed_transfers, + bench_parse_packed_noops, + bench_parse_packed_atls +); +criterion_main!(benches); diff --git a/solana/transaction-view/src/address_table_lookup_frame.rs b/solana/transaction-view/src/address_table_lookup_frame.rs new file mode 100644 index 0000000..e274bbd --- /dev/null +++ b/solana/transaction-view/src/address_table_lookup_frame.rs @@ -0,0 +1,314 @@ +use { + crate::{ + bytes::{ + advance_offset_for_array, advance_offset_for_type, check_remaining, read_byte, + read_compressed_u16, read_slice_data, read_type, try_u32_offset, + }, + result::{Result, TransactionViewError}, + }, + core::fmt::{Debug, Formatter}, + solana_hash::Hash, + solana_packet::PACKET_DATA_SIZE, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_svm_transaction::message_address_table_lookup::SVMMessageAddressTableLookup, +}; + +// Each ATL has at least a Pubkey, one byte for the number of write indexes, +// and one byte for the number of read indexes. Additionally, for validity +// the ATL must have at least one write or read index giving a minimum size +// of 35 bytes. +const MIN_SIZED_ATL: usize = { + core::mem::size_of::() // account key + + 1 // writable indexes length + + 1 // readonly indexes length + + 1 // single account (either write or read) +}; + +// A valid packet with ATLs has: +// 1. At least 1 signature +// 2. 1 message prefix byte +// 3. 3 bytes for the message header +// 4. 1 static account key +// 5. 1 recent blockhash +// 6. 1 byte for the number of instructions (0) +// 7. 1 byte for the number of ATLS +const MIN_SIZED_PACKET_WITH_ATLS: usize = { + 1 // signatures count + + core::mem::size_of::() // signature + + 1 // message prefix + + 3 // message header + + 1 // static account keys count + + core::mem::size_of::() // static account key + + core::mem::size_of::() // recent blockhash + + 1 // number of instructions + + 1 // number of ATLS +}; + +/// The maximum number of ATLS that can fit in a valid packet. +const MAX_ATLS_PER_PACKET: u8 = + ((PACKET_DATA_SIZE - MIN_SIZED_PACKET_WITH_ATLS) / MIN_SIZED_ATL) as u8; + +/// Contains metadata about the address table lookups in a transaction packet. +#[derive(Debug)] +pub(crate) struct AddressTableLookupFrame { + /// The number of address table lookups in the transaction. + pub(crate) num_address_table_lookups: u8, + /// The offset to the first address table lookup in the transaction. + pub(crate) offset: u32, + /// The total number of writable lookup accounts in the transaction. + pub(crate) total_writable_lookup_accounts: u16, + /// The total number of readonly lookup accounts in the transaction. + pub(crate) total_readonly_lookup_accounts: u16, +} + +impl AddressTableLookupFrame { + /// Get the number of address table lookups (ATL) and offset to the first. + /// The offset will be updated to point to the first byte after the last + /// ATL. + /// This function will parse each ATL to ensure the data is well-formed, + /// but will not cache data related to these ATLs. + #[inline(always)] + pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { + // Maximum number of ATLs should be represented by a single byte, + // thus the MSB should not be set. + const _: () = assert!(MAX_ATLS_PER_PACKET & 0b1000_0000 == 0); + let num_address_table_lookups = read_byte(bytes, offset)?; + if num_address_table_lookups > MAX_ATLS_PER_PACKET { + return Err(TransactionViewError::ParseError); + } + + // Check that the remaining bytes are enough to hold the ATLs. + check_remaining( + bytes, + *offset, + MIN_SIZED_ATL + .checked_mul(usize::from(num_address_table_lookups)) + .ok_or(TransactionViewError::ParseError)?, + )?; + + let address_table_lookups_offset = try_u32_offset(*offset)?; + + // Check that there is no chance of overflow when calculating the total + // number of writable and readonly lookup accounts using a u32. + const _: () = + assert!(u16::MAX as usize * MAX_ATLS_PER_PACKET as usize <= u32::MAX as usize); + let mut total_writable_lookup_accounts: u32 = 0; + let mut total_readonly_lookup_accounts: u32 = 0; + + // The ATLs do not have a fixed size. So we must iterate over + // each ATL to find the total size of the ATLs in the packet, + // and check for any malformed ATLs or buffer overflows. + for _index in 0..num_address_table_lookups { + // Each ATL has 3 pieces: + // 1. Address (Pubkey) + // 2. write indexes ([u8]) + // 3. read indexes ([u8]) + + // Advance offset for address of the lookup table. + advance_offset_for_type::(bytes, offset)?; + + // Read the number of write indexes, and then update the offset. + let num_write_accounts = read_compressed_u16(bytes, offset)?; + total_writable_lookup_accounts = total_writable_lookup_accounts + .checked_add(u32::from(num_write_accounts)) + .ok_or(TransactionViewError::ParseError)?; + advance_offset_for_array::(bytes, offset, num_write_accounts)?; + + // Read the number of read indexes, and then update the offset. + let num_read_accounts = read_compressed_u16(bytes, offset)?; + total_readonly_lookup_accounts = total_readonly_lookup_accounts + .checked_add(u32::from(num_read_accounts)) + .ok_or(TransactionViewError::ParseError)?; + advance_offset_for_array::(bytes, offset, num_read_accounts)?; + } + + Ok(Self { + num_address_table_lookups, + offset: address_table_lookups_offset, + total_writable_lookup_accounts: u16::try_from(total_writable_lookup_accounts) + .map_err(|_| TransactionViewError::SanitizeError)?, + total_readonly_lookup_accounts: u16::try_from(total_readonly_lookup_accounts) + .map_err(|_| TransactionViewError::SanitizeError)?, + }) + } +} + +#[derive(Clone)] +pub struct AddressTableLookupIterator<'a> { + pub(crate) bytes: &'a [u8], + pub(crate) offset: usize, + pub(crate) num_address_table_lookups: u8, + pub(crate) index: u8, +} + +impl<'a> Iterator for AddressTableLookupIterator<'a> { + type Item = SVMMessageAddressTableLookup<'a>; + + #[inline] + fn next(&mut self) -> Option { + if self.index < self.num_address_table_lookups { + self.index = self.index.wrapping_add(1); + + // Each ATL has 3 pieces: + // 1. Address (Pubkey) + // 2. write indexes ([u8]) + // 3. read indexes ([u8]) + + // Advance offset for address of the lookup table. + const _: () = assert!(core::mem::align_of::() == 1, "Pubkey alignment"); + // SAFETY: + // - The offset is checked to be valid in the slice. + // - The alignment of Pubkey is 1. + // - `Pubkey` is a byte array, it cannot be improperly initialized. + let account_key = unsafe { read_type::(self.bytes, &mut self.offset) }.ok()?; + + // Read the number of write indexes, and then update the offset. + let num_write_accounts = read_compressed_u16(self.bytes, &mut self.offset).ok()?; + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let writable_indexes = + unsafe { read_slice_data::(self.bytes, &mut self.offset, num_write_accounts) } + .ok()?; + + // Read the number of read indexes, and then update the offset. + let num_read_accounts = read_compressed_u16(self.bytes, &mut self.offset).ok()?; + + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + let readonly_indexes = + unsafe { read_slice_data::(self.bytes, &mut self.offset, num_read_accounts) } + .ok()?; + + Some(SVMMessageAddressTableLookup { + account_key, + writable_indexes, + readonly_indexes, + }) + } else { + None + } + } +} + +impl ExactSizeIterator for AddressTableLookupIterator<'_> { + fn len(&self) -> usize { + usize::from(self.num_address_table_lookups.wrapping_sub(self.index)) + } +} + +impl Debug for AddressTableLookupIterator<'_> { + fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { + f.debug_list().entries(self.clone()).finish() + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_message::v0::MessageAddressTableLookup, solana_short_vec::ShortVec}; + + #[test] + fn test_zero_atls() { + let bytes = bincode::serialize(&ShortVec::(vec![])).unwrap(); + let mut offset = 0; + let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_address_table_lookups, 0); + assert_eq!(frame.offset, 1); + assert_eq!(offset, bytes.len()); + assert_eq!(frame.total_writable_lookup_accounts, 0); + assert_eq!(frame.total_readonly_lookup_accounts, 0); + } + + #[test] + fn test_length_too_high() { + let mut bytes = bincode::serialize(&ShortVec::(vec![])).unwrap(); + let mut offset = 0; + // modify the number of atls to be too high + bytes[0] = 5; + assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_single_atl() { + let bytes = bincode::serialize(&ShortVec::(vec![ + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![1, 2, 3], + readonly_indexes: vec![4, 5, 6], + }, + ])) + .unwrap(); + let mut offset = 0; + let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_address_table_lookups, 1); + assert_eq!(frame.offset, 1); + assert_eq!(offset, bytes.len()); + assert_eq!(frame.total_writable_lookup_accounts, 3); + assert_eq!(frame.total_readonly_lookup_accounts, 3); + } + + #[test] + fn test_multiple_atls() { + let bytes = bincode::serialize(&ShortVec::(vec![ + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![1, 2, 3], + readonly_indexes: vec![4, 5, 6], + }, + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![1, 2, 3], + readonly_indexes: vec![4, 5], + }, + ])) + .unwrap(); + let mut offset = 0; + let frame = AddressTableLookupFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_address_table_lookups, 2); + assert_eq!(frame.offset, 1); + assert_eq!(offset, bytes.len()); + assert_eq!(frame.total_writable_lookup_accounts, 6); + assert_eq!(frame.total_readonly_lookup_accounts, 5); + } + + #[test] + fn test_invalid_writable_indexes_vec() { + let mut bytes = bincode::serialize(&ShortVec(vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![1, 2, 3], + readonly_indexes: vec![4, 5, 6], + }])) + .unwrap(); + + // modify the number of accounts to be too high + bytes[33] = 127; + + let mut offset = 0; + assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_invalid_readonly_indexes_vec() { + let mut bytes = bincode::serialize(&ShortVec(vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![1, 2, 3], + readonly_indexes: vec![4, 5, 6], + }])) + .unwrap(); + + // modify the number of accounts to be too high + bytes[37] = 127; + + let mut offset = 0; + assert!(AddressTableLookupFrame::try_new(&bytes, &mut offset).is_err()); + } +} diff --git a/solana/transaction-view/src/bytes.rs b/solana/transaction-view/src/bytes.rs new file mode 100644 index 0000000..ece04b8 --- /dev/null +++ b/solana/transaction-view/src/bytes.rs @@ -0,0 +1,436 @@ +use crate::result::{Result, TransactionViewError}; + +#[inline(always)] +pub(crate) fn try_u32_offset(offset: usize) -> Result { + u32::try_from(offset).map_err(|_| TransactionViewError::ParseError) +} + +/// Check that the buffer has at least `len` bytes remaining starting at +/// `offset`. Returns Err if the buffer is too short. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// * `num_bytes` - Number of bytes that must be remaining. +/// +#[inline(always)] +pub fn check_remaining(bytes: &[u8], offset: usize, num_bytes: usize) -> Result<()> { + let end = offset.checked_add(num_bytes).ok_or(TransactionViewError::ParseError)?; + (end <= bytes.len()).then_some(()).ok_or(TransactionViewError::ParseError) +} + +/// Check that the buffer has at least 1 byte remaining starting at `offset`. +/// Returns Err if the buffer is too short. +#[inline(always)] +pub fn read_byte(bytes: &[u8], offset: &mut usize) -> Result { + // Implicitly checks that the offset is within bounds, no need + // to call `check_remaining` explicitly here. + let value = bytes + .get(*offset) + .copied() + .ok_or(TransactionViewError::ParseError); + *offset = offset.wrapping_add(1); + value +} + +/// Read a byte and advance the offset without any bounds checks. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +#[inline(always)] +pub unsafe fn unchecked_read_byte(bytes: &[u8], offset: &mut usize) -> u8 { + let value = unsafe { *bytes.get_unchecked(*offset) }; + *offset += 1; + value +} + +/// Read a compressed u16 from `bytes` starting at `offset`. +/// If the buffer is too short or the encoding is invalid, return Err. +/// `offset` is updated to point to the byte after the compressed u16. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// +/// Assumptions: +/// - The current offset is not greater than `bytes.len()`. +#[allow(dead_code)] +#[inline(always)] +pub fn read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { + let mut result = 0u16; + let mut shift = 0u16; + + for i in 0..3 { + // Implicitly checks that the offset is within bounds, no need + // to call check_remaining explicitly here. + let byte = *bytes + .get(offset.wrapping_add(i)) + .ok_or(TransactionViewError::ParseError)?; + // non-minimal encoding or overflow + if (i > 0 && byte == 0) || (i == 2 && byte > 3) { + return Err(TransactionViewError::ParseError); + } + result |= ((byte & 0x7F) as u16) << shift; + shift += 7; + if byte & 0x80 == 0 { + *offset = index.checked_add(1).ok_or(TransactionViewError::ParseError)?; + return Ok(result); + } + } + + // if we reach here, it means that all 3 bytes were used + *offset = offset.wrapping_add(3); + Ok(result) +} + +/// Domain-specific optimization for reading a compressed u16. +/// +/// The compressed u16's are only used for array-lengths in our transaction +/// format. The transaction packet has a maximum size of 1232 bytes. +/// This means that the maximum array length within a **valid** transaction is +/// 1232. This has a minimally encoded length of 2 bytes. +/// Although the encoding scheme allows for more, any arrays with this length +/// would be too large to fit in a packet. This function optimizes for this +/// case, and reads a maximum of 2 bytes. +/// If the buffer is too short or the encoding is invalid, return Err. +/// `offset` is updated to point to the byte after the compressed u16. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +#[inline(always)] +pub fn optimized_read_compressed_u16(bytes: &[u8], offset: &mut usize) -> Result { + let mut result = 0u16; + + // First byte + let byte1 = *bytes.get(*offset).ok_or(TransactionViewError::ParseError)?; + result |= (byte1 & 0x7F) as u16; + if byte1 & 0x80 == 0 { + *offset = offset.wrapping_add(1); + return Ok(result); + } + + // Second byte + let byte2 = *bytes + .get(offset.wrapping_add(1)) + .ok_or(TransactionViewError::ParseError)?; + if byte2 == 0 || byte2 & 0x80 != 0 { + return Err(TransactionViewError::ParseError); // non-minimal encoding or overflow + } + result |= ((byte2 & 0x7F) as u16) << 7; + *offset = offset.wrapping_add(2); + + Ok(result) +} + +/// Update the `offset` to point to the byte after an array of length `len` and +/// of type `T`. If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// * `num_elements` - Number of `T` elements in the array. +/// +/// Assumptions: +/// 1. The current offset is not greater than `bytes.len()`. +/// 2. The size of `T` is small enough such that a usize will not overflow if +/// given the maximum array size (u16::MAX). +#[inline(always)] +pub fn advance_offset_for_array( + bytes: &[u8], + offset: &mut usize, + num_elements: u16, +) -> Result<()> { + let array_len_bytes = usize::from(num_elements) + .checked_mul(core::mem::size_of::()) + .ok_or(TransactionViewError::ParseError)?; + check_remaining(bytes, *offset, array_len_bytes)?; + *offset = offset.checked_add(array_len_bytes).ok_or(TransactionViewError::ParseError)?; + Ok(()) +} + +/// Update the `offset` to point t the byte after the `T`. +/// If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// +/// Assumptions: +/// 1. The current offset is not greater than `bytes.len()`. +/// 2. The size of `T` is small enough such that a usize will not overflow. +#[inline(always)] +pub fn advance_offset_for_type(bytes: &[u8], offset: &mut usize) -> Result<()> { + let type_size = core::mem::size_of::(); + check_remaining(bytes, *offset, type_size)?; + *offset = offset.checked_add(type_size).ok_or(TransactionViewError::ParseError)?; + Ok(()) +} + +/// Return a reference to the next slice of `T` in the buffer, checking bounds +/// and advancing the offset. +/// If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// * `num_elements` - Number of `T` elements in the slice. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 3. `bytes + offset` must be properly aligned for `T`. +/// 4. `T` slice must be validly initialized. +/// 5. The size of `T` is small enough such that a usize will not overflow if +/// given the maximum slice size (u16::MAX). +#[inline(always)] +pub unsafe fn read_slice_data<'a, T: Sized>( + bytes: &'a [u8], + offset: &mut usize, + num_elements: u16, +) -> Result<&'a [T]> { + let start = *offset; + advance_offset_for_array::(bytes, offset, num_elements)?; + let current_ptr = unsafe { bytes.as_ptr().add(start) }; + Ok(unsafe { core::slice::from_raw_parts(current_ptr as *const T, usize::from(num_elements)) }) +} + +/// Return a reference to the next slice of `T` in the buffer, +/// and advancing the offset. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// * `num_elements` - Number of `T` elements in the slice. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 3. `bytes + offset` must be properly aligned for `T`. +/// 4. `T` slice must be validly initialized. +/// 5. The size of `T` is small enough such that a usize will not overflow if +/// given the maximum slice size (u16::MAX). +#[inline(always)] +pub unsafe fn unchecked_read_slice_data<'a, T: Sized>( + bytes: &'a [u8], + offset: &mut usize, + num_elements: u16, +) -> &'a [T] { + let current_ptr = unsafe { bytes.as_ptr().add(*offset) }; + let array_len_bytes = usize::from(num_elements) * core::mem::size_of::(); + *offset += array_len_bytes; + unsafe { core::slice::from_raw_parts(current_ptr as *const T, usize::from(num_elements)) } +} + +/// Return a reference to the next `T` in the buffer, checking bounds and +/// advancing the offset. +/// If the buffer is too short, return Err. +/// +/// * `bytes` - Slice of bytes to read from. +/// * `offset` - Current offset into `bytes`. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 3. `bytes + offset` must be properly aligned for `T`. +/// 4. `T` must be validly initialized. +#[inline(always)] +pub unsafe fn read_type<'a, T: Sized>(bytes: &'a [u8], offset: &mut usize) -> Result<&'a T> { + let start = *offset; + advance_offset_for_type::(bytes, offset)?; + let current_ptr = unsafe { bytes.as_ptr().add(start) }; + Ok(unsafe { &*(current_ptr as *const T) }) +} + +/// Copy a `T` in the buffer without checking bounds or advancing offset. +/// +/// # Safety +/// 1. `bytes` must be a valid slice of bytes. +/// 2. `offset` must be a valid offset into `bytes`. +/// 4. `T` must be validly initialized. +#[inline(always)] +pub unsafe fn unchecked_copy_value(bytes: &[u8], offset: usize) -> T { + let current_ptr = unsafe { bytes.as_ptr().add(offset) }.cast::(); + unsafe { current_ptr.read_unaligned() } +} + +#[cfg(test)] +mod tests { + use { + super::*, + bincode::{DefaultOptions, Options, serialize_into}, + solana_short_vec::ShortU16, + }; + + #[test] + fn test_check_remaining() { + // Empty buffer checks + assert!(check_remaining(&[], 0, 0).is_ok()); + assert!(check_remaining(&[], 0, 1).is_err()); + + // Buffer with data checks + assert!(check_remaining(&[1, 2, 3], 0, 0).is_ok()); + assert!(check_remaining(&[1, 2, 3], 0, 1).is_ok()); + assert!(check_remaining(&[1, 2, 3], 0, 3).is_ok()); + assert!(check_remaining(&[1, 2, 3], 0, 4).is_err()); + + // Non-zero offset. + assert!(check_remaining(&[1, 2, 3], 1, 0).is_ok()); + assert!(check_remaining(&[1, 2, 3], 1, 1).is_ok()); + assert!(check_remaining(&[1, 2, 3], 1, 2).is_ok()); + assert!(check_remaining(&[1, 2, 3], 1, usize::MAX).is_err()); + } + + #[test] + fn test_read_byte() { + let bytes = [5, 6, 7]; + let mut offset = 0; + assert_eq!(read_byte(&bytes, &mut offset), Ok(5)); + assert_eq!(offset, 1); + assert_eq!(read_byte(&bytes, &mut offset), Ok(6)); + assert_eq!(offset, 2); + assert_eq!(read_byte(&bytes, &mut offset), Ok(7)); + assert_eq!(offset, 3); + assert!(read_byte(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_read_compressed_u16() { + let mut buffer = [0u8; 1024]; + let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding + + // Test all possible u16 values + for value in 0..=u16::MAX { + let mut offset; + let short_u16 = ShortU16(value); + + // Serialize the value into the buffer + serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); + + // Use bincode's size calculation to determine the length of the serialized data + let serialized_len = options + .serialized_size(&short_u16) + .expect("Failed to get serialized size"); + + // Reset offset + offset = 0; + + // Read the value back using unchecked_read_u16_compressed + let read_value = read_compressed_u16(&buffer, &mut offset); + + // Assert that the read value matches the original value + assert_eq!(read_value, Ok(value), "Value mismatch for: {value}"); + + // Assert that the offset matches the serialized length + assert_eq!( + offset, serialized_len as usize, + "Offset mismatch for: {value}" + ); + } + + // Test bounds. + // All 0s => 0 + assert_eq!(Ok(0), read_compressed_u16(&[0; 3], &mut 0)); + // Overflow + assert!(read_compressed_u16(&[0xFF, 0xFF, 0x04], &mut 0).is_err()); + assert_eq!( + read_compressed_u16(&[0xFF, 0xFF, 0x03], &mut 0), + Ok(u16::MAX) + ); + + // overflow errors + assert!(read_compressed_u16(&[u8::MAX; 1], &mut 0).is_err()); + assert!(read_compressed_u16(&[u8::MAX; 2], &mut 0).is_err()); + + // Minimal encoding checks + assert!(read_compressed_u16(&[0x81, 0x80, 0x00], &mut 0).is_err()); + } + + #[test] + fn test_optimized_read_compressed_u16() { + let mut buffer = [0u8; 1024]; + let options = DefaultOptions::new().with_fixint_encoding(); // Ensure fixed-int encoding + + // Test all possible u16 values under the packet length + for value in 0..=PACKET_DATA_SIZE as u16 { + let mut offset; + let short_u16 = ShortU16(value); + + // Serialize the value into the buffer + serialize_into(&mut buffer[..], &short_u16).expect("Serialization failed"); + + // Use bincode's size calculation to determine the length of the serialized data + let serialized_len = options + .serialized_size(&short_u16) + .expect("Failed to get serialized size"); + + // Reset offset + offset = 0; + + // Read the value back using unchecked_read_u16_compressed + let read_value = optimized_read_compressed_u16(&buffer, &mut offset); + + // Assert that the read value matches the original value + assert_eq!(read_value, Ok(value), "Value mismatch for: {value}"); + + // Assert that the offset matches the serialized length + assert_eq!( + offset, serialized_len as usize, + "Offset mismatch for: {value}" + ); + } + + // Test bounds. + // All 0s => 0 + assert_eq!(Ok(0), optimized_read_compressed_u16(&[0; 3], &mut 0)); + // Overflow + assert!(optimized_read_compressed_u16(&[0xFF, 0xFF, 0x04], &mut 0).is_err()); + assert!(optimized_read_compressed_u16(&[0xFF, 0x80], &mut 0).is_err()); + + // overflow errors + assert!(optimized_read_compressed_u16(&[u8::MAX; 1], &mut 0).is_err()); + assert!(optimized_read_compressed_u16(&[u8::MAX; 2], &mut 0).is_err()); + + // Minimal encoding checks + assert!(optimized_read_compressed_u16(&[0x81, 0x00], &mut 0).is_err()); + } + + #[test] + fn test_advance_offset_for_array() { + #[repr(C)] + struct MyStruct { + _a: u8, + _b: u8, + } + const _: () = assert!(core::mem::size_of::() == 2); + + // Test with a buffer that is too short + let bytes = [0u8; 1]; + let mut offset = 0; + assert!(advance_offset_for_array::(&bytes, &mut offset, 1).is_err()); + + // Test with a buffer that is long enough + let bytes = [0u8; 4]; + let mut offset = 0; + assert!(advance_offset_for_array::(&bytes, &mut offset, 2).is_ok()); + assert_eq!(offset, 4); + } + + #[test] + fn test_advance_offset_for_type() { + #[repr(C)] + struct MyStruct { + _a: u8, + _b: u8, + } + const _: () = assert!(core::mem::size_of::() == 2); + + // Test with a buffer that is too short + let bytes = [0u8; 1]; + let mut offset = 0; + assert!(advance_offset_for_type::(&bytes, &mut offset).is_err()); + + // Test with a buffer that is long enough + let bytes = [0u8; 4]; + let mut offset = 0; + assert!(advance_offset_for_type::(&bytes, &mut offset).is_ok()); + assert_eq!(offset, 2); + } +} diff --git a/solana/transaction-view/src/instructions_frame.rs b/solana/transaction-view/src/instructions_frame.rs new file mode 100644 index 0000000..721de14 --- /dev/null +++ b/solana/transaction-view/src/instructions_frame.rs @@ -0,0 +1,863 @@ +use { + crate::{ + bytes::{ + advance_offset_for_array, check_remaining, read_byte, read_compressed_u16, + try_u32_offset, unchecked_copy_value, unchecked_read_byte, unchecked_read_slice_data, + }, + result::{Result, TransactionViewError}, + }, + core::fmt::{Debug, Formatter}, + solana_svm_transaction::instruction::SVMInstruction, +}; + +/// Contains metadata about the instructions in a transaction packet. +#[derive(Debug)] +pub(crate) enum InstructionsFrame { + LegacyAndV0 { + /// The number of instructions in the transaction. + num_instructions: u16, + /// The offset to the first instruction in the transaction. + offset: u32, + frames: Vec, + }, + V1 { + num_instructions: u16, + headers_offset: u32, + payloads_offset: u32, + }, +} + +#[derive(Debug)] +pub struct LegacyAndV0InstructionFrame { + num_accounts: u16, + data_len: u16, + num_accounts_len: u8, // either 1 or 2 + data_len_len: u8, // either 1 or 2 +} + +#[allow(dead_code)] +#[repr(C)] +#[derive(Debug)] +struct V1InstructionHeader { + program_id_index: u8, + num_accounts: u8, + data_len: u16, +} + +impl InstructionsFrame { + /// Get the number of instructions and offset to the first instruction. + /// The offset will be updated to point to the first byte after the last + /// instruction. + /// This function will parse each individual instruction to ensure the + /// instruction data is well-formed, but will not cache data related to + /// these instructions. + #[inline(always)] + pub(crate) fn try_new_for_legacy_and_v0(bytes: &[u8], offset: &mut usize) -> Result { + // Read the number of instructions at the current offset. + // Each instruction needs at least 3 bytes, so do a sanity check here to + // ensure we have enough bytes to read the number of instructions. + let num_instructions = read_compressed_u16(bytes, offset)?; + let minimum_instructions_len = 3usize + .checked_mul(usize::from(num_instructions)) + .ok_or(TransactionViewError::ParseError)?; + check_remaining(bytes, *offset, minimum_instructions_len)?; + + let instructions_offset = try_u32_offset(*offset)?; + + // Pre-allocate buffer for frames. + let mut frames = Vec::with_capacity(usize::from(num_instructions)); + + // The instructions do not have a fixed size. So we must iterate over + // each instruction to find the total size of the instructions, + // and check for any malformed instructions or buffer overflows. + for _index in 0..num_instructions { + // Each instruction has 3 pieces: + // 1. Program ID index (u8) + // 2. Accounts indexes ([u8]) + // 3. Data ([u8]) + + // Read the program ID index. + let _program_id_index = read_byte(bytes, offset)?; + + // Read the number of account indexes, and then update the offset + // to skip over the account indexes. + let num_accounts_offset = *offset; + let num_accounts = read_compressed_u16(bytes, offset)?; + let num_accounts_len = u8::try_from( + offset + .checked_sub(num_accounts_offset) + .ok_or(TransactionViewError::ParseError)?, + ) + .map_err(|_| TransactionViewError::ParseError)?; + advance_offset_for_array::(bytes, offset, num_accounts)?; + + // Read the length of the data, and then update the offset to skip + // over the data. + let data_len_offset = *offset; + let data_len = read_compressed_u16(bytes, offset)?; + let data_len_len = u8::try_from( + offset.checked_sub(data_len_offset).ok_or(TransactionViewError::ParseError)?, + ) + .map_err(|_| TransactionViewError::ParseError)?; + advance_offset_for_array::(bytes, offset, data_len)?; + + frames.push(LegacyAndV0InstructionFrame { + num_accounts, + num_accounts_len, + data_len, + data_len_len, + }); + } + + Ok(Self::LegacyAndV0 { + num_instructions, + offset: instructions_offset, + frames, + }) + } + + #[allow(dead_code)] + #[inline(always)] + pub(crate) fn try_new_for_v1( + bytes: &[u8], + offset: &mut usize, + num_instructions: u8, + ) -> Result { + let headers_offset = try_u32_offset(*offset)?; + let headers_len = core::mem::size_of::() + .checked_mul(usize::from(num_instructions)) + .ok_or(TransactionViewError::ParseError)?; + + check_remaining(bytes, *offset, headers_len)?; + + let mut header_offset = *offset; + *offset = offset.checked_add(headers_len).ok_or(TransactionViewError::ParseError)?; + + let payloads_offset = try_u32_offset(*offset)?; + + // Tx v1 stores all instruction payloads contiguously after the header block. + // We validate headers first, accumulate the total payload size across all + // instructions, and then do a single bounds check for the whole payload region + // instead of one bounds check per instruction. + let mut total_payload_len: usize = 0; + for _ in 0..num_instructions { + // SAFETY: we have already verified bytes contains enough space for `num_instruction` headers. + let header = unsafe { Self::read_v1_header(bytes, &mut header_offset) }; + + let payload_len = usize::from(header.num_accounts) + .checked_add(usize::from(header.data_len)) + .ok_or(TransactionViewError::ParseError)?; + + total_payload_len = total_payload_len + .checked_add(payload_len) + .ok_or(TransactionViewError::ParseError)?; + } + + check_remaining(bytes, *offset, total_payload_len)?; + *offset = offset.checked_add(total_payload_len).ok_or(TransactionViewError::ParseError)?; + + Ok(Self::V1 { + num_instructions: u16::from(num_instructions), + headers_offset, + payloads_offset, + }) + } + + /// # Safety + /// `bytes[*offset..*offset + size_of::()]` must be valid. + #[inline(always)] + unsafe fn read_v1_header(bytes: &[u8], offset: &mut usize) -> V1InstructionHeader { + let mut header: V1InstructionHeader = unsafe { unchecked_copy_value(bytes, *offset) }; + *offset += core::mem::size_of::(); + header.data_len = u16::from_le(header.data_len); + header + } + + #[inline(always)] + pub(crate) fn num_instructions(&self) -> u16 { + match self { + Self::LegacyAndV0 { + num_instructions, .. + } => *num_instructions, + Self::V1 { + num_instructions, .. + } => *num_instructions, + } + } + + #[inline(always)] + pub(crate) fn iter<'a>(&'a self, bytes: &'a [u8]) -> InstructionsIterator<'a> { + match self { + Self::LegacyAndV0 { + num_instructions, + offset, + frames, + } => InstructionsIterator::LegacyAndV0 { + bytes, + offset: *offset as usize, + index: 0, + num_instructions: *num_instructions, + frames, + }, + Self::V1 { + num_instructions, + headers_offset, + payloads_offset, + } => InstructionsIterator::V1 { + bytes, + index: 0, + num_instructions: *num_instructions, + headers_offset: *headers_offset as usize, + payloads_offset: *payloads_offset as usize, + }, + } + } +} + +#[derive(Clone)] +pub enum InstructionsIterator<'a> { + LegacyAndV0 { + bytes: &'a [u8], + offset: usize, + num_instructions: u16, + index: u16, + frames: &'a [LegacyAndV0InstructionFrame], + }, + V1 { + bytes: &'a [u8], + index: u16, + num_instructions: u16, + headers_offset: usize, + payloads_offset: usize, + }, +} + +impl<'a> Iterator for InstructionsIterator<'a> { + type Item = SVMInstruction<'a>; + + #[inline] + fn next(&mut self) -> Option { + match self { + Self::LegacyAndV0 { + bytes, + offset, + index, + num_instructions, + frames, + } => { + if *index >= *num_instructions { + return None; + } + + let LegacyAndV0InstructionFrame { + num_accounts, + num_accounts_len, + data_len, + data_len_len, + } = frames[usize::from(*index)]; + + *index = index.wrapping_add(1); + + Some(unsafe { + for_legacy_and_v0( + bytes, + offset, + num_accounts, + num_accounts_len, + data_len, + data_len_len, + ) + }) + } + Self::V1 { + bytes, + index, + num_instructions, + headers_offset, + payloads_offset, + } => { + if *index >= *num_instructions { + return None; + } + + let header = unsafe { InstructionsFrame::read_v1_header(bytes, headers_offset) }; + *index = index.wrapping_add(1); + + Some(unsafe { + for_v1( + bytes, + payloads_offset, + header.program_id_index, + u16::from(header.num_accounts), + header.data_len, + ) + }) + } + } + } +} + +/// Builds SNVInstruction from legacy/v0 pre-validated frame metadata. +/// +/// # Safety +/// The caller must ensure that: +/// - `offset` points to the beginning of a serialized legacy/v0 instruction +/// in `bytes`. +/// - `num_accounts_len` and `data_len_len` are the exact encoded lengths of the +/// compact-u16 account-count and data-length fields for that instruction. +/// - `num_accounts` and `data_len` exactly match the serialized instruction at +/// `offset`. +/// - The byte ranges implied by those values are fully in bounds of `bytes`. +/// +/// These invariants are expected to have been established by the initial +/// instruction frame parsing. Violating them may cause out-of-bounds unchecked +/// reads and undefined behavior. +#[inline(always)] +unsafe fn for_legacy_and_v0<'a>( + bytes: &'a [u8], + offset: &mut usize, + num_accounts: u16, + num_accounts_len: u8, + data_len: u16, + data_len_len: u8, +) -> SVMInstruction<'a> { + // Each instruction has 3 pieces: + // 1. Program ID index (u8) + // 2. Accounts indexes ([u8]) + // 3. Data ([u8]) + + // Read the program ID index. + // SAFETY: Offset and length checks have been done in the initial parsing. + let program_id_index = unsafe { unchecked_read_byte(bytes, offset) }; + + // Move offset to accounts offset - do not re-parse u16. + *offset += usize::from(num_accounts_len); + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + // - Offset and length checks have been done in the initial parsing. + let accounts = unsafe { unchecked_read_slice_data::(bytes, offset, num_accounts) }; + + // Move offset to accounts offset - do not re-parse u16. + *offset += usize::from(data_len_len); + const _: () = assert!(core::mem::align_of::() == 1, "u8 alignment"); + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + // - Offset and length checks have been done in the initial parsing. + let data = unsafe { unchecked_read_slice_data::(bytes, offset, data_len) }; + + SVMInstruction { + program_id_index, + accounts, + data, + } +} + +/// Builds SMVInstruction from v1 pre-validated frame metadata. +/// +/// # Safety +/// The caller must ensure that: +/// +/// - `payload_offset` points to the beginning of this instruction’s payload +/// (i.e. the first account index byte) within `bytes`. +/// - `num_accounts` and `data_len` exactly match the instruction header that +/// was previously parsed for this instruction. +/// - The byte range +/// `payload_offset .. payload_offset + num_accounts + data_len` +/// lies entirely within `bytes`. +/// - `bytes` has not been mutated since the initial parsing that produced +/// the instruction frames. +/// +/// These invariants are expected to have been established during the initial +/// tx-v1 instruction parsing phase, where header and payload bounds were +/// validated together. +/// +/// Violating any of these conditions may result in out-of-bounds unchecked +/// reads and thus undefined behavior. +#[inline(always)] +unsafe fn for_v1<'a>( + bytes: &'a [u8], + payloads_offset: &mut usize, + program_id_index: u8, + num_accounts: u16, + data_len: u16, +) -> SVMInstruction<'a> { + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + // - Offset and length checks have been done in the initial parsing. + let accounts = unsafe { unchecked_read_slice_data::(bytes, payloads_offset, num_accounts) }; + // SAFETY: + // - The offset is checked to be valid in the byte slice. + // - The alignment of u8 is 1. + // - The slice length is checked to be valid. + // - `u8` cannot be improperly initialized. + // - Offset and length checks have been done in the initial parsing. + let data = unsafe { unchecked_read_slice_data::(bytes, payloads_offset, data_len) }; + + SVMInstruction { + program_id_index, + accounts, + data, + } +} + +impl ExactSizeIterator for InstructionsIterator<'_> { + fn len(&self) -> usize { + match self { + Self::LegacyAndV0 { + num_instructions, + index, + .. + } => usize::from(num_instructions.wrapping_sub(*index)), + Self::V1 { + num_instructions, + index, + .. + } => usize::from(num_instructions.wrapping_sub(*index)), + } + } +} + +impl Debug for InstructionsIterator<'_> { + fn fmt(&self, f: &mut Formatter) -> core::fmt::Result { + f.debug_list().entries(self.clone()).finish() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, solana_message::compiled_instruction::CompiledInstruction, + solana_short_vec::ShortVec, + }; + + impl InstructionsFrame { + fn offset(&self) -> u32 { + match self { + Self::LegacyAndV0 { offset, .. } => *offset, + Self::V1 { headers_offset, .. } => *headers_offset, + } + } + } + + #[test] + fn test_zero_instructions() { + let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); + let mut offset = 0; + let instructions_frame = + InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + + assert_eq!(instructions_frame.num_instructions(), 0); + assert_eq!(instructions_frame.offset(), 1); + assert_eq!(offset, bytes.len()); + } + + #[test] + fn test_num_instructions_too_high() { + let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![], + data: vec![], + }])) + .unwrap(); + // modify the number of instructions to be too high + bytes[0] = 0x02; + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_single_instruction() { + let bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3], + data: vec![4, 5, 6, 7, 8, 9, 10], + }])) + .unwrap(); + let mut offset = 0; + let instructions_frame = + InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + assert_eq!(instructions_frame.num_instructions(), 1); + assert_eq!(instructions_frame.offset(), 1); + assert_eq!(offset, bytes.len()); + } + + #[test] + fn test_multiple_instructions() { + let bytes = bincode::serialize(&ShortVec(vec![ + CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3], + data: vec![4, 5, 6, 7, 8, 9, 10], + }, + CompiledInstruction { + program_id_index: 1, + accounts: vec![4, 5, 6], + data: vec![7, 8, 9, 10, 11, 12, 13], + }, + ])) + .unwrap(); + let mut offset = 0; + let instructions_frame = + InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + assert_eq!(instructions_frame.num_instructions(), 2); + assert_eq!(instructions_frame.offset(), 1); + assert_eq!(offset, bytes.len()); + } + + #[test] + fn test_invalid_instruction_accounts_vec() { + let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3], + data: vec![4, 5, 6, 7, 8, 9, 10], + }])) + .unwrap(); + + // modify the number of accounts to be too high + bytes[2] = 127; + + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_invalid_instruction_data_vec() { + let mut bytes = bincode::serialize(&ShortVec(vec![CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3], + data: vec![4, 5, 6, 7, 8, 9, 10], + }])) + .unwrap(); + + // modify the number of data bytes to be too high + bytes[6] = 127; + + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_txv1_instructions_iterator() { + let message = solana_message::v1::Message { + instructions: vec![ + CompiledInstruction { + program_id_index: 0, + accounts: vec![1, 2, 3], + data: vec![4, 5, 6, 7, 8, 9, 10], + }, + CompiledInstruction { + program_id_index: 10, + accounts: vec![11, 12], + data: vec![13, 14, 15, 16, 17, 18, 19, 20], + }, + ], + ..solana_message::v1::Message::default() + }; + + let serialized = solana_message::v1::serialize(&message); + + let mut offset = 41; // instruction headers starts at offset 41 for no config, 0 addresses, per spec + let instructions_frame = + InstructionsFrame::try_new_for_v1(&serialized, &mut offset, 2).unwrap(); + + let mut iter = instructions_frame.iter(&serialized); + assert_eq!( + iter.next(), + Some(SVMInstruction { + program_id_index: 0, + accounts: &[1, 2, 3], + data: &[4, 5, 6, 7, 8, 9, 10] + }) + ); + assert_eq!( + iter.next(), + Some(SVMInstruction { + program_id_index: 10, + accounts: &[11, 12], + data: &[13, 14, 15, 16, 17, 18, 19, 20] + }) + ); + assert_eq!(iter.next(), None); + } + + fn short_u16_1(x: u8) -> Vec { + vec![x] + } + + // short_vec / compact-u16 encoding for 128..=16383 style values + fn short_u16_2(x: u16) -> Vec { + assert!(x >= 128); + vec![((x & 0x7f) as u8) | 0x80, (x >> 7) as u8] + } + + #[test] + fn test_try_new_legacy_single_instruction() { + // num_instructions = 1 + // instruction: + // program_id_index = 7 + // num_accounts = 2 + // accounts = [3, 4] + // data_len = 3 + // data = [9, 8, 7] + let bytes = vec![ + 1, // num_instructions + 7, // program_id_index + 2, // num_accounts + 3, 4, // account indexes + 3, // data_len + 9, 8, 7, // data + ]; + + let mut offset = 0; + let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + + assert_eq!(offset, bytes.len()); + + match frame { + InstructionsFrame::LegacyAndV0 { + num_instructions, + offset, + frames, + } => { + assert_eq!(num_instructions, 1); + assert_eq!(offset, 1); + assert_eq!(frames.len(), 1); + let ix = &frames[0]; + assert_eq!(ix.num_accounts, 2); + assert_eq!(ix.data_len, 3); + assert_eq!(ix.num_accounts_len, 1); + assert_eq!(ix.data_len_len, 1); + } + _ => panic!("expected legacy/v0 repr"), + } + } + + #[test] + fn test_try_new_legacy_two_byte_lengths() { + let num_accounts = 128u16; + let data_len = 130u16; + + let mut bytes = Vec::new(); + bytes.push(1); // num_instructions + bytes.push(42); // program_id_index + bytes.extend_from_slice(&short_u16_2(num_accounts)); + bytes.extend(std::iter::repeat_n(5u8, num_accounts as usize)); + bytes.extend_from_slice(&short_u16_2(data_len)); + bytes.extend(std::iter::repeat_n(9u8, data_len as usize)); + + let mut offset = 0; + let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + + assert_eq!(offset, bytes.len()); + + match frame { + InstructionsFrame::LegacyAndV0 { + num_instructions, + offset, + frames, + } => { + assert_eq!(num_instructions, 1); + assert_eq!(offset, 1); + assert_eq!(frames.len(), 1); + let ix = &frames[0]; + assert_eq!(ix.num_accounts, num_accounts); + assert_eq!(ix.data_len, data_len); + + assert_eq!(ix.num_accounts_len, 2); + assert_eq!(ix.data_len_len, 2); + } + _ => panic!("expected legacy/v0 repr"), + } + } + + #[test] + fn test_try_new_for_v1_single_instruction() { + // one v1 instruction + // header: + // program_id_index = 9 + // num_accounts = 2 + // data_len = 3 + // payload: + // accounts = [10, 11] + // data = [1, 2, 3] + let bytes = vec![ + 9, // program_id_index + 2, // num_accounts + 3, 0, // data_len (u16 LE) + 10, 11, // payload accounts + 1, 2, 3, // payload data + ]; + + let mut offset = 0; + let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).unwrap(); + + assert_eq!(offset, bytes.len()); + + match frame { + InstructionsFrame::V1 { + num_instructions, + headers_offset, + payloads_offset, + } => { + assert_eq!(num_instructions, 1); + assert_eq!(headers_offset, 0); + assert_eq!(payloads_offset, 4); + let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 0) }; + assert_eq!(hdr.program_id_index, 9); + assert_eq!(hdr.num_accounts, 2); + assert_eq!(hdr.data_len, 3); + } + _ => panic!("expected v1 repr"), + } + } + + #[test] + fn test_try_new_for_v1_two_instructions() { + // headers: + // ix0: pid=1, accounts=2, data_len=1 + // ix1: pid=7, accounts=1, data_len=2 + // + // payloads: + // ix0: [20, 21] [99] + // ix1: [42] [5, 6] + let bytes = vec![ + // header 0 + 1, 2, 1, 0, // header 1 + 7, 1, 2, 0, // payload 0 + 20, 21, 99, // payload 1 + 42, 5, 6, + ]; + + let mut offset = 0; + let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 2).unwrap(); + + assert_eq!(offset, bytes.len()); + match frame { + InstructionsFrame::V1 { + num_instructions, + headers_offset, + payloads_offset, + } => { + assert_eq!(num_instructions, 2); + assert_eq!(headers_offset, 0); + assert_eq!(payloads_offset, 8); + let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 0) }; + assert_eq!(hdr.program_id_index, 1); + assert_eq!(hdr.num_accounts, 2); + assert_eq!(hdr.data_len, 1); + let hdr = unsafe { InstructionsFrame::read_v1_header(&bytes, &mut 4) }; + assert_eq!(hdr.program_id_index, 7); + assert_eq!(hdr.num_accounts, 1); + assert_eq!(hdr.data_len, 2); + } + _ => panic!("expected v1 repr"), + } + } + + #[test] + fn test_try_new_for_v1_truncated_header_fails() { + // num_instructions = 1, but only 3 header bytes instead of 4 + let bytes = vec![ + 9, // program_id_index + 2, // num_accounts + 3, // incomplete data_len + ]; + + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).is_err()); + } + + #[test] + fn test_try_new_for_v1_truncated_payload_fails() { + // header says payload len = 2 + 3 = 5, but only 4 bytes provided + let bytes = vec![ + 9, 2, 3, 0, // header + 10, 11, 1, 2, // truncated payload + ]; + + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1).is_err()); + } + + #[test] + fn test_try_new_legacy_truncated_payload_fails() { + // data_len says 3, only 2 bytes provided + let bytes = vec![ + 1, // num_instructions + 7, // program_id_index + 1, // num_accounts + 9, // account idx + 3, // data_len + 1, 2, // truncated data + ]; + + let mut offset = 0; + assert!(InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_try_new_for_v1_zero_instructions() { + let bytes = vec![]; + let mut offset = 0; + + let frame = InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 0).unwrap(); + assert_eq!(offset, 0); + match frame { + InstructionsFrame::V1 { + num_instructions, + headers_offset, + payloads_offset, + } => { + assert_eq!(num_instructions, 0); + assert_eq!(headers_offset, 0); + assert_eq!(payloads_offset, 0); + } + _ => panic!("expected v1 repr"), + } + } + + #[test] + fn data_len_max_header_fails_parse() { + // header: pid=1, accounts=1, data_len=65535 + let bytes = vec![1, 1, 0xff, 0xff]; + let mut offset = 0; + assert_eq!( + InstructionsFrame::try_new_for_v1(&bytes, &mut offset, 1) + .err() + .unwrap(), + TransactionViewError::ParseError + ); + } + + #[test] + fn test_try_new_legacy_zero_instructions() { + let bytes = short_u16_1(0); + let mut offset = 0; + + let frame = InstructionsFrame::try_new_for_legacy_and_v0(&bytes, &mut offset).unwrap(); + assert_eq!(offset, 1); + + match frame { + InstructionsFrame::LegacyAndV0 { + num_instructions, + offset, + frames, + } => { + assert_eq!(num_instructions, 0); + assert_eq!(offset, 1); + assert!(frames.is_empty()); + } + _ => panic!("expected legacy/v0 repr"), + } + } +} diff --git a/solana/transaction-view/src/lib.rs b/solana/transaction-view/src/lib.rs new file mode 100644 index 0000000..b2794a4 --- /dev/null +++ b/solana/transaction-view/src/lib.rs @@ -0,0 +1,26 @@ +#![cfg(feature = "agave-unstable-api")] +#![doc = include_str!("../README.md")] +// Parsing helpers only need to be public for benchmarks. +#[cfg(feature = "dev-context-only-utils")] +pub mod bytes; +#[cfg(not(feature = "dev-context-only-utils"))] +mod bytes; + +mod address_table_lookup_frame; +mod instructions_frame; +mod message_header_frame; +pub mod resolved_transaction_view; +pub mod result; +mod sanitize; +mod signature_frame; +mod static_account_keys_frame; +mod transaction_config_frame; +pub mod transaction_data; +mod transaction_frame; +pub mod transaction_version; +pub mod transaction_view; + +pub use sanitize::{ + MAGICBLOCK_INSTRUCTION_TRACE_LENGTH, MAX_MAGICBLOCK_TRANSACTION_SIZE, + MAX_STANDARD_TRANSACTION_SIZE, +}; diff --git a/solana/transaction-view/src/message_header_frame.rs b/solana/transaction-view/src/message_header_frame.rs new file mode 100644 index 0000000..b37c201 --- /dev/null +++ b/solana/transaction-view/src/message_header_frame.rs @@ -0,0 +1,110 @@ +use { + crate::{ + bytes::{read_byte, try_u32_offset}, + result::{Result, TransactionViewError}, + transaction_version::TransactionVersion, + }, + solana_message::MESSAGE_VERSION_PREFIX, +}; + +/// Metadata for accessing message header fields in a transaction view. +#[derive(Debug)] +pub(crate) struct MessageHeaderFrame { + /// The offset to the first byte of the message in the transaction packet. + pub(crate) offset: u32, + /// The version of the transaction. + pub(crate) version: TransactionVersion, + /// The number of signatures required for this message to be considered + /// valid. + pub(crate) num_required_signatures: u8, + /// The last `num_readonly_signed_accounts` of the signed keys are + /// read-only. + pub(crate) num_readonly_signed_accounts: u8, + /// The last `num_readonly_unsigned_accounts` of the unsigned keys are + /// read-only accounts. + pub(crate) num_readonly_unsigned_accounts: u8, +} + +impl MessageHeaderFrame { + #[inline(always)] + pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { + // Get the message offset. + let message_offset = try_u32_offset(*offset)?; + + // Read the message prefix byte if present. This byte is present in V0 + // transactions but not in legacy transactions. + // The message header begins immediately after the message prefix byte + // if present. + let message_prefix = read_byte(bytes, offset)?; + let (version, num_required_signatures) = if message_prefix & MESSAGE_VERSION_PREFIX != 0 { + let version = message_prefix & !MESSAGE_VERSION_PREFIX; + match version { + 0 => (TransactionVersion::V0, read_byte(bytes, offset)?), + _ => return Err(TransactionViewError::ParseError), + } + } else { + // Legacy transaction. The `message_prefix` that was just read is + // actually the number of required signatures. + (TransactionVersion::Legacy, message_prefix) + }; + + let num_readonly_signed_accounts = read_byte(bytes, offset)?; + let num_readonly_unsigned_accounts = read_byte(bytes, offset)?; + + Ok(Self { + offset: message_offset, + version, + num_required_signatures, + num_readonly_signed_accounts, + num_readonly_unsigned_accounts, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_version() { + let bytes = [0b1000_0001]; + let mut offset = 0; + assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_legacy_transaction_missing_header_byte() { + let bytes = [5, 0]; + let mut offset = 0; + assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_legacy_transaction_valid() { + let bytes = [5, 1, 2]; + let mut offset = 0; + let header = MessageHeaderFrame::try_new(&bytes, &mut offset).unwrap(); + assert!(matches!(header.version, TransactionVersion::Legacy)); + assert_eq!(header.num_required_signatures, 5); + assert_eq!(header.num_readonly_signed_accounts, 1); + assert_eq!(header.num_readonly_unsigned_accounts, 2); + } + + #[test] + fn test_v0_transaction_missing_header_byte() { + let bytes = [MESSAGE_VERSION_PREFIX, 5, 1]; + let mut offset = 0; + assert!(MessageHeaderFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_v0_transaction_valid() { + let bytes = [MESSAGE_VERSION_PREFIX, 5, 1, 2]; + let mut offset = 0; + let header = MessageHeaderFrame::try_new(&bytes, &mut offset).unwrap(); + assert!(matches!(header.version, TransactionVersion::V0)); + assert_eq!(header.num_required_signatures, 5); + assert_eq!(header.num_readonly_signed_accounts, 1); + assert_eq!(header.num_readonly_unsigned_accounts, 2); + } +} diff --git a/solana/transaction-view/src/resolved_transaction_view.rs b/solana/transaction-view/src/resolved_transaction_view.rs new file mode 100644 index 0000000..b16202a --- /dev/null +++ b/solana/transaction-view/src/resolved_transaction_view.rs @@ -0,0 +1,338 @@ +use { + crate::{ + result::{Result, TransactionViewError}, + transaction_data::TransactionData, + transaction_view::TransactionView, + }, + core::{ + fmt::{Debug, Formatter}, + ops::Deref, + }, + solana_hash::Hash, + solana_message::{AccountKeys, v0::LoadedAddresses}, + solana_pubkey::Pubkey, + solana_sdk_ids::bpf_loader_upgradeable, + solana_signature::Signature, + solana_svm_transaction::{ + instruction::SVMInstruction, + message_address_table_lookup::SVMMessageAddressTableLookup, + svm_message::{SVMMessage, SVMStaticMessage}, + svm_transaction::SVMTransaction, + }, + std::collections::HashSet, +}; + +/// A parsed and sanitized transaction view with validated loaded-address state. +pub struct ResolvedTransactionView { + /// The parsed and sanitized transaction view. + view: TransactionView, + /// The resolved address lookups. + resolved_addresses: Option, + /// A cache for whether an address is writable. + // Sanitized transactions are guaranteed to have a maximum of 256 keys, + // because account indexing is done with a u8. + writable_cache: [bool; 256], +} + +impl Deref for ResolvedTransactionView { + type Target = TransactionView; + + fn deref(&self) -> &Self::Target { + &self.view + } +} + +impl ResolvedTransactionView { + /// Creates a resolved view after validating any supplied loaded addresses. + /// + /// Address lookup tables are rejected during sanitization, so loaded + /// addresses may only be absent or empty. + pub fn try_new( + view: TransactionView, + resolved_addresses: Option, + reserved_account_keys: &HashSet, + ) -> Result { + let resolved_addresses_ref = resolved_addresses.as_ref(); + + // Reject unexpected loaded addresses while retaining the upstream API. + if let Some(loaded_addresses) = resolved_addresses_ref { + if loaded_addresses.writable.len() != usize::from(view.total_writable_lookup_accounts()) + || loaded_addresses.readonly.len() + != usize::from(view.total_readonly_lookup_accounts()) + { + return Err(TransactionViewError::AddressLookupMismatch); + } + } else if view.total_writable_lookup_accounts() != 0 + || view.total_readonly_lookup_accounts() != 0 + { + return Err(TransactionViewError::AddressLookupMismatch); + } + + let writable_cache = + Self::cache_is_writable(&view, resolved_addresses_ref, reserved_account_keys); + Ok(Self { + view, + resolved_addresses, + writable_cache, + }) + } + + /// Helper function to check if an address is writable, + /// and cache the result. + /// This is done so we avoid recomputing the expensive checks each time we call + /// `is_writable` - since there is more to it than just checking index. + fn cache_is_writable( + view: &TransactionView, + resolved_addresses: Option<&LoadedAddresses>, + reserved_account_keys: &HashSet, + ) -> [bool; 256] { + // Build account keys so that we can iterate over and check if + // an address is writable. + let account_keys = AccountKeys::new(view.static_account_keys(), resolved_addresses); + + let mut is_writable_cache = [false; 256]; + let num_static_account_keys = usize::from(view.num_static_account_keys()); + let num_writable_lookup_accounts = usize::from(view.total_writable_lookup_accounts()); + let num_signed_accounts = usize::from(view.num_required_signatures()); + let num_writable_unsigned_static_accounts = + usize::from(view.num_writable_unsigned_static_accounts()); + let num_writable_signed_static_accounts = + usize::from(view.num_writable_signed_static_accounts()); + + for (index, key) in account_keys.iter().enumerate() { + let is_requested_write = { + // If the account is a resolved address, check if it is writable. + if index >= num_static_account_keys { + let loaded_address_index = index.wrapping_sub(num_static_account_keys); + loaded_address_index < num_writable_lookup_accounts + } else if index >= num_signed_accounts { + let unsigned_account_index = index.wrapping_sub(num_signed_accounts); + unsigned_account_index < num_writable_unsigned_static_accounts + } else { + index < num_writable_signed_static_accounts + } + }; + + // If the key is reserved it cannot be writable. + is_writable_cache[index] = is_requested_write && !reserved_account_keys.contains(key); + } + + // If a program account is locked, it cannot be writable unless the + // upgradable loader is present. + // However, checking for the upgradable loader is somewhat expensive, so + // we only do it if we find a writable program id. + let mut is_upgradable_loader_present = None; + for ix in view.instructions_iter() { + let program_id_index = usize::from(ix.program_id_index); + if is_writable_cache[program_id_index] + && !*is_upgradable_loader_present.get_or_insert_with(|| { + for key in account_keys.iter() { + if key == &bpf_loader_upgradeable::ID { + return true; + } + } + false + }) + { + is_writable_cache[program_id_index] = false; + } + } + + is_writable_cache + } + + pub fn loaded_addresses(&self) -> Option<&LoadedAddresses> { + self.resolved_addresses.as_ref() + } + + pub fn into_view(self) -> TransactionView { + self.view + } +} + +impl SVMStaticMessage for ResolvedTransactionView { + fn version(&self) -> solana_transaction::versioned::TransactionVersion { + self.view.version().into() + } + + fn num_transaction_signatures(&self) -> u64 { + u64::from(self.view.num_required_signatures()) + } + + fn num_write_locks(&self) -> u64 { + self.view.num_requested_write_locks() + } + + fn recent_blockhash(&self) -> &Hash { + self.view.recent_blockhash() + } + + fn num_instructions(&self) -> usize { + usize::from(self.view.num_instructions()) + } + + fn instructions_iter(&self) -> impl Iterator> { + self.view.instructions_iter() + } + + fn program_instructions_iter( + &self, + ) -> impl Iterator< + Item = ( + &solana_pubkey::Pubkey, + solana_svm_transaction::instruction::SVMInstruction<'_>, + ), + > + Clone { + self.view.program_instructions_iter() + } + + fn static_account_keys(&self) -> &[Pubkey] { + self.view.static_account_keys() + } + + fn fee_payer(&self) -> &Pubkey { + &self.view.static_account_keys()[0] + } + + fn num_lookup_tables(&self) -> usize { + usize::from(self.view.num_address_table_lookups()) + } + + fn message_address_table_lookups( + &self, + ) -> impl Iterator> { + self.view.address_table_lookup_iter() + } +} + +impl SVMMessage for ResolvedTransactionView { + fn account_keys(&self) -> AccountKeys<'_> { + AccountKeys::new( + self.view.static_account_keys(), + self.resolved_addresses.as_ref(), + ) + } + + fn is_writable(&self, index: usize) -> bool { + self.writable_cache.get(index).copied().unwrap_or(false) + } + + fn is_signer(&self, index: usize) -> bool { + index < usize::from(self.view.num_required_signatures()) + } + + fn is_invoked(&self, key_index: usize) -> bool { + let Ok(index) = u8::try_from(key_index) else { + return false; + }; + self.view + .instructions_iter() + .any(|ix| ix.program_id_index == index) + } +} + +impl SVMTransaction for ResolvedTransactionView { + fn signature(&self) -> &Signature { + &self.view.signatures()[0] + } + + fn signatures(&self) -> &[Signature] { + self.view.signatures() + } +} + +impl Debug for ResolvedTransactionView { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ResolvedTransactionView") + .field("view", &self.view) + .finish() + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::transaction_view::SanitizedTransactionView, + solana_message::{ + MessageHeader, VersionedMessage, + v0::{self, MessageAddressTableLookup}, + }, + solana_signature::Signature, + solana_transaction::versioned::VersionedTransaction, + }; + + fn v0_transaction( + address_table_lookups: Vec, + ) -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V0(v0::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + instructions: vec![], + account_keys: vec![Pubkey::new_unique(), Pubkey::new_unique()], + address_table_lookups, + recent_blockhash: Hash::default(), + }), + } + } + + #[test] + fn test_address_lookup_tables_are_rejected() { + let lookups = [ + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![], + }, + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![], + readonly_indexes: vec![0], + }, + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![1], + }, + ]; + + for lookup in lookups { + let transaction = v0_transaction(vec![lookup]); + let bytes = wincode::serialize(&transaction).unwrap(); + let result = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true); + assert!(matches!( + result, + Err(TransactionViewError::AddressLookupMismatch) + )); + } + } + + #[test] + fn test_v0_without_lookups_needs_no_loaded_addresses() { + let bytes = wincode::serialize(&v0_transaction(vec![])).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let resolved = ResolvedTransactionView::try_new(view, None, &HashSet::default()).unwrap(); + assert!(resolved.loaded_addresses().is_none()); + } + + #[test] + fn test_unexpected_loaded_addresses() { + let loaded_addresses = LoadedAddresses { + writable: vec![Pubkey::new_unique()], + readonly: vec![], + }; + let bytes = wincode::serialize(&v0_transaction(vec![])).unwrap(); + let view = SanitizedTransactionView::try_new_sanitized(bytes.as_ref(), true).unwrap(); + let result = + ResolvedTransactionView::try_new(view, Some(loaded_addresses), &HashSet::default()); + assert!(matches!( + result, + Err(TransactionViewError::AddressLookupMismatch) + )); + } +} diff --git a/solana/transaction-view/src/result.rs b/solana/transaction-view/src/result.rs new file mode 100644 index 0000000..028a7f1 --- /dev/null +++ b/solana/transaction-view/src/result.rs @@ -0,0 +1,9 @@ +#[derive(Debug, PartialEq, Eq)] +#[repr(u8)] // repr(u8) is used to ensure that the enum is represented as a single byte in memory. +pub enum TransactionViewError { + ParseError, + SanitizeError, + AddressLookupMismatch, +} + +pub type Result = core::result::Result; diff --git a/solana/transaction-view/src/sanitize.rs b/solana/transaction-view/src/sanitize.rs new file mode 100644 index 0000000..ada57af --- /dev/null +++ b/solana/transaction-view/src/sanitize.rs @@ -0,0 +1,1030 @@ +use { + crate::{ + result::{Result, TransactionViewError}, + signature_frame::MAX_SIGNATURES_PER_PACKET, + transaction_data::TransactionData, + transaction_version::TransactionVersion, + transaction_view::UnsanitizedTransactionView, + }, + solana_program_runtime::execution_budget::{MAX_HEAP_FRAME_BYTES, MIN_HEAP_FRAME_BYTES}, +}; + +/// Maximum instruction trace length for an Engine-private transaction. +pub const MAGICBLOCK_INSTRUCTION_TRACE_LENGTH: usize = 255; +/// Maximum serialized size accepted for standard transaction versions. +pub const MAX_STANDARD_TRANSACTION_SIZE: usize = u16::MAX as usize; +/// Maximum serialized size accepted for an Engine-private transaction. +pub const MAX_MAGICBLOCK_TRANSACTION_SIZE: usize = 16 * 1024 * 1024; + +pub(crate) fn sanitize( + view: &UnsanitizedTransactionView, + enable_instruction_accounts_limit: bool, +) -> Result<()> { + sanitize_transaction_size(view)?; + sanitize_message_header(view)?; + sanitize_config(view)?; + sanitize_signatures(view)?; + sanitize_account_access(view)?; + sanitize_instructions(view, enable_instruction_accounts_limit)?; + sanitize_address_table_lookups(view) +} + +/// Transaction size constraints are version-specific. +fn sanitize_transaction_size( + view: &UnsanitizedTransactionView, +) -> Result<()> { + let max_transaction_size = match view.version() { + TransactionVersion::Legacy | TransactionVersion::V0 | TransactionVersion::V1 => { + MAX_STANDARD_TRANSACTION_SIZE + } + TransactionVersion::Magicblock => MAX_MAGICBLOCK_TRANSACTION_SIZE, + }; + + if view.data().len() > max_transaction_size { + return Err(TransactionViewError::SanitizeError); + } + Ok(()) +} + +/// message header constraints: +/// * num_required_signatures >= 1 +/// * num_readonly_signed_accounts < num_required_signatures (fee payer must be writable) +/// * num_readonly_unsigned_accounts <= (num_addresses - num_required_signatures) +fn sanitize_message_header(view: &UnsanitizedTransactionView) -> Result<()> { + if view.num_required_signatures() < 1 { + return Err(TransactionViewError::SanitizeError); + } + + if view.num_readonly_signed_static_accounts() >= view.num_required_signatures() { + return Err(TransactionViewError::SanitizeError); + } + + // Check there is no overlap of signing area and readonly non-signing area. + // We have already checked that `num_required_signatures` is less than or equal to `num_static_account_keys`, + // so it is safe to use wrapping arithmetic. + if view.num_readonly_unsigned_static_accounts() + > view + .num_static_account_keys() + .wrapping_sub(view.num_required_signatures()) + { + return Err(TransactionViewError::SanitizeError); + } + + Ok(()) +} + +/// Config Constraints: +/// * heap_size must be multiples of 1024, if specified +fn sanitize_config(view: &UnsanitizedTransactionView) -> Result<()> { + #[allow(clippy::collapsible_if)] + if let Some(requested_heap_bytes) = view + .transaction_config() + .and_then(|config| config.requested_heap_size()) + { + if !(MIN_HEAP_FRAME_BYTES..=MAX_HEAP_FRAME_BYTES).contains(&requested_heap_bytes) + || !requested_heap_bytes.is_multiple_of(1024) + { + return Err(TransactionViewError::SanitizeError); + } + } + + Ok(()) +} + +/// Sigantures Constraint: +/// * Number of signatures must equal: num_required_signatures +/// * Max signatures <= 12 +fn sanitize_signatures(view: &UnsanitizedTransactionView) -> Result<()> { + // Check the required number of signatures matches the number of signatures. + if view.num_signatures() != view.num_required_signatures() { + return Err(TransactionViewError::SanitizeError); + } + + if view.num_signatures() > MAX_SIGNATURES_PER_PACKET { + return Err(TransactionViewError::SanitizeError); + } + + // Each signature is associated with a unique static public key. + // Check that there are at least as many static account keys as signatures. + if view.num_static_account_keys() < view.num_signatures() { + return Err(TransactionViewError::SanitizeError); + } + + Ok(()) +} + +/// Accounts (aka Addresses) Constraints: +/// * for v1: 1 <= NumAddresses <= 64 +/// * legacy/v0 uses current limits of: num_accounts <= 256 (u8 bound) +/// * No duplicate addresses +fn sanitize_account_access(view: &UnsanitizedTransactionView) -> Result<()> { + let addresses_limit = match view.version() { + TransactionVersion::Legacy | TransactionVersion::V0 => 256, + TransactionVersion::V1 => 64, + TransactionVersion::Magicblock => 256, + }; + + if total_number_of_accounts(view) > addresses_limit { + return Err(TransactionViewError::SanitizeError); + } + + // No duplicated accounts + // Note: This check is performed downstream in `validate_account_locks()`. + // It is skipped here to avoid redundant work on the hot path. + + Ok(()) +} + +/// Instructions Constraints +/// * NumInstructions <= 64 +/// * Per instruction: +/// * 0 < program_id_index < MaxProgramIdIndex +/// * all account indices < MaxAccountIndex +fn sanitize_instructions( + view: &UnsanitizedTransactionView, + enable_instruction_accounts_limit: bool, +) -> Result<()> { + let instructions_limit = match view.version() { + TransactionVersion::Magicblock => MAGICBLOCK_INSTRUCTION_TRACE_LENGTH, + TransactionVersion::V0 | TransactionVersion::V1 | TransactionVersion::Legacy => { + solana_transaction_context::MAX_INSTRUCTION_TRACE_LENGTH + } + }; + let num_instructions = usize::from(view.num_instructions()); + // Standard transactions retain the SIMD-160 top-level instruction limit. + if num_instructions > instructions_limit { + return Err(TransactionViewError::SanitizeError); + } + + // already verified there is at least one static account. + let max_program_id_index = view.num_static_account_keys().wrapping_sub(1); + // verified that there are no more than 256 accounts in `sanitize_account_access` + let max_account_index = total_number_of_accounts(view).wrapping_sub(1) as u8; + + for instruction in view.instructions_iter() { + // Check that program indexes are static account keys. + if instruction.program_id_index > max_program_id_index { + return Err(TransactionViewError::SanitizeError); + } + + // Check that the program index is not the fee-payer. + if instruction.program_id_index == 0 { + return Err(TransactionViewError::SanitizeError); + } + + // Check that all account indexes are valid. + for account_index in instruction.accounts.iter().copied() { + if account_index > max_account_index { + return Err(TransactionViewError::SanitizeError); + } + } + + if enable_instruction_accounts_limit + && instruction.accounts.len() > solana_transaction_context::MAX_ACCOUNTS_PER_INSTRUCTION + { + return Err(TransactionViewError::SanitizeError); + } + } + + Ok(()) +} + +fn sanitize_address_table_lookups( + view: &UnsanitizedTransactionView, +) -> Result<()> { + if view.num_address_table_lookups() != 0 { + // Preserve the resolution-specific error at the earlier ingress + // boundary where lookup tables are now rejected. + return Err(TransactionViewError::AddressLookupMismatch); + } + Ok(()) +} + +fn total_number_of_accounts(view: &UnsanitizedTransactionView) -> u16 { + u16::from(view.num_static_account_keys()) + .saturating_add(view.total_writable_lookup_accounts()) + .saturating_add(view.total_readonly_lookup_accounts()) +} + +#[cfg(test)] +mod tests { + use { + super::*, + crate::transaction_view::TransactionView, + solana_hash::Hash, + solana_message::{ + Message, MessageHeader, VersionedMessage, + compiled_instruction::CompiledInstruction, + v0::{self, MessageAddressTableLookup}, + v1::{self, TransactionConfig}, + }, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_system_interface::instruction as system_instruction, + solana_transaction::versioned::VersionedTransaction, + }; + + fn create_legacy_transaction( + num_signatures: u8, + header: MessageHeader, + account_keys: Vec, + instructions: Vec, + ) -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default(); num_signatures as usize], + message: VersionedMessage::Legacy(Message { + header, + account_keys, + recent_blockhash: Hash::default(), + instructions, + }), + } + } + + fn create_v0_transaction( + num_signatures: u8, + header: MessageHeader, + account_keys: Vec, + instructions: Vec, + address_table_lookups: Vec, + ) -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default(); num_signatures as usize], + message: VersionedMessage::V0(v0::Message { + header, + account_keys, + recent_blockhash: Hash::default(), + instructions, + address_table_lookups, + }), + } + } + + fn create_v1_transaction( + num_signatures: u8, + header: MessageHeader, + account_keys: Vec, + instructions: Vec, + config: TransactionConfig, + ) -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default(); num_signatures as usize], + message: VersionedMessage::V1(v1::Message { + header, + account_keys, + lifetime_specifier: Hash::default(), + instructions, + config, + }), + } + } + + fn multiple_transfers() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[ + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + ], + Some(&payer), + )), + } + } + + #[test] + fn test_sanitize_multiple_transfers() { + let transaction = multiple_transfers(); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(view.sanitize(true).is_ok()); + } + + #[test] + fn test_sanitize_standard_transaction_size_boundaries() { + let account_keys = vec![Pubkey::new_unique(), Pubkey::new_unique()]; + let header = MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }; + let instruction = CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: Vec::new(), + }; + let transactions = [ + create_legacy_transaction(1, header, account_keys.clone(), vec![instruction.clone()]), + create_v0_transaction( + 1, + header, + account_keys.clone(), + vec![instruction.clone()], + vec![], + ), + create_v1_transaction( + 1, + header, + account_keys, + vec![instruction], + TransactionConfig::empty(), + ), + ]; + + for mut transaction in transactions { + resize_transaction(&mut transaction, MAX_STANDARD_TRANSACTION_SIZE); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(view.sanitize(true).is_ok()); + + resize_transaction(&mut transaction, MAX_STANDARD_TRANSACTION_SIZE + 1); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_transaction_size(&view), + Err(TransactionViewError::SanitizeError) + ); + } + } + + fn resize_transaction(transaction: &mut VersionedTransaction, target: usize) { + const SAMPLE_DATA_LEN: usize = 65_000; + + instruction_data(transaction).resize(SAMPLE_DATA_LEN, 0); + let overhead = wincode::serialize(&*transaction).unwrap().len() - SAMPLE_DATA_LEN; + instruction_data(transaction).resize(target - overhead, 0); + assert_eq!(wincode::serialize(&*transaction).unwrap().len(), target); + } + + fn instruction_data(transaction: &mut VersionedTransaction) -> &mut Vec { + match &mut transaction.message { + VersionedMessage::Legacy(message) => &mut message.instructions[0].data, + VersionedMessage::V0(message) => &mut message.instructions[0].data, + VersionedMessage::V1(message) => &mut message.instructions[0].data, + } + } + + #[test] + fn test_sanitize_signatures() { + // Too few signatures. + { + let transaction = create_legacy_transaction( + 1, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..3).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Too many signatures. + { + let transaction = create_legacy_transaction( + 2, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..3).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Not enough static accounts. + { + let transaction = create_legacy_transaction( + 2, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..1).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // More than 12 signatures. + { + let transaction = create_legacy_transaction( + 13, + MessageHeader { + num_required_signatures: 13, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..13).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()); + // SignatureFrame validates number of signatures, it throw ParseError if + // it is less than 12 + assert!(matches!(view, Err(TransactionViewError::ParseError))); + } + + { + let transaction = create_v1_transaction( + 13, + MessageHeader { + num_required_signatures: 13, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..13).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Not enough static accounts. + { + let transaction = create_legacy_transaction( + 2, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..1).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Not enough static accounts - with look up accounts + { + let transaction = create_v0_transaction( + 2, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..1).map(|_| Pubkey::new_unique()).collect(), + vec![], + vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0, 1, 2, 3, 4, 5], + readonly_indexes: vec![6, 7, 8], + }], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_signatures(&view), + Err(TransactionViewError::SanitizeError) + ); + } + } + + #[test] + fn test_sanitize_account_access() { + // num_required_signatures must be >= 1. + { + let transaction = create_legacy_transaction( + 0, + MessageHeader { + num_required_signatures: 0, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + vec![Pubkey::new_unique()], + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()); + // SignatureFrame validates number of signatures, it throw ParseError if + // it is less than 1 + assert!(matches!(view, Err(TransactionViewError::ParseError))); + } + { + let transaction = create_v1_transaction( + 0, + MessageHeader { + num_required_signatures: 0, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + vec![Pubkey::new_unique()], + vec![], + TransactionConfig::empty(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_message_header(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Overlap of signing and readonly non-signing accounts. + { + let transaction = create_legacy_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 2, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_message_header(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Not enough writable accounts. + { + let transaction = create_legacy_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 1, + num_readonly_unsigned_accounts: 0, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_message_header(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Too many accounts in legacy/v0 + { + let transaction = create_v0_transaction( + 2, + MessageHeader { + num_required_signatures: 2, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + (0..1).map(|_| Pubkey::new_unique()).collect(), + vec![], + vec![ + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: (0..100).collect(), + readonly_indexes: (100..200).collect(), + }, + MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: (100..200).collect(), + readonly_indexes: (0..100).collect(), + }, + ], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_account_access(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // V1: too many static accounts. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 63, + }, + (0..65).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_account_access(&view), + Err(TransactionViewError::SanitizeError) + ); + } + } + + #[test] + fn test_sanitize_instructions() { + let num_signatures = 1; + let header = MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }; + let account_keys = vec![ + Pubkey::new_unique(), + Pubkey::new_unique(), + Pubkey::new_unique(), + ]; + let valid_instructions = vec![ + CompiledInstruction { + program_id_index: 1, + accounts: vec![0, 1], + data: vec![1, 2, 3], + }, + CompiledInstruction { + program_id_index: 2, + accounts: vec![1, 0], + data: vec![3, 2, 1, 4], + }, + ]; + let atls = vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0, 1], + readonly_indexes: vec![2], + }]; + + // Verify that the unmodified transaction(s) are valid/sanitized. + { + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + valid_instructions.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_instructions(&view, true).is_ok()); + + let transaction = create_v0_transaction( + num_signatures, + header, + account_keys.clone(), + valid_instructions.clone(), + atls.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_instructions(&view, true).is_ok()); + } + + for instruction_index in 0..valid_instructions.len() { + // Invalid program index. + { + let mut instructions = valid_instructions.clone(); + instructions[instruction_index].program_id_index = account_keys.len() as u8; + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + instructions, + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // Invalid program index with lookups. + { + let mut instructions = valid_instructions.clone(); + instructions[instruction_index].program_id_index = account_keys.len() as u8; + let transaction = create_v0_transaction( + num_signatures, + header, + account_keys.clone(), + instructions, + atls.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // Program index is fee-payer. + { + let mut instructions = valid_instructions.clone(); + instructions[instruction_index].program_id_index = 0; + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + instructions, + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // Invalid account index. + { + let mut instructions = valid_instructions.clone(); + instructions[instruction_index] + .accounts + .push(account_keys.len() as u8); + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + instructions, + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // Invalid account index with v0. + { + let num_lookup_accounts = + atls[0].writable_indexes.len() + atls[0].readonly_indexes.len(); + let total_accounts = (account_keys.len() + num_lookup_accounts) as u8; + let mut instructions = valid_instructions.clone(); + instructions[instruction_index] + .accounts + .push(total_accounts); + let transaction = create_v0_transaction( + num_signatures, + header, + account_keys.clone(), + instructions, + atls.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + } + + // SIMD-0160, too many instructions are invalid + { + let too_many_instructions: Vec<_> = valid_instructions + .iter() + .cycle() + .take(65) + .cloned() + .collect(); + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + too_many_instructions.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + + let transaction = create_v0_transaction( + num_signatures, + header, + account_keys.clone(), + too_many_instructions.clone(), + atls.clone(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // SIMD-406: Limit instruction accounts to 255 + { + let mut accounts: Vec = vec![0; 254]; + accounts.push(1); + accounts.push(2); + let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts); + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + vec![instr], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_instructions(&view, true), + Err(TransactionViewError::SanitizeError) + ); + } + + // SIMD-406: Limit instruction accounts to 255 + { + let mut accounts: Vec = vec![0; 254]; + accounts.push(1); + let instr = CompiledInstruction::new_from_raw_parts(2, Vec::new(), accounts); + let transaction = create_legacy_transaction( + num_signatures, + header, + account_keys.clone(), + vec![instr], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + // Exactly 255 accounts must pass sanitization. + assert!(sanitize_instructions(&view, true).is_ok()); + } + } + + #[test] + fn test_sanitize_address_table_lookups() { + let payer = Pubkey::new_unique(); + let header = MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }; + let transaction = create_v0_transaction(1, header, vec![payer], vec![], vec![]); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_address_table_lookups(&view).is_ok()); + + let transaction = create_v0_transaction( + 1, + header, + vec![payer], + vec![], + vec![MessageAddressTableLookup { + account_key: Pubkey::new_unique(), + writable_indexes: vec![0], + readonly_indexes: vec![], + }], + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_address_table_lookups(&view), + Err(TransactionViewError::AddressLookupMismatch) + ); + } + + #[test] + fn test_sanitize_config() { + // Valid min heap size. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_config(&view).is_ok()); + } + + // Valid max heap size. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty().with_heap_size(MAX_HEAP_FRAME_BYTES), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_config(&view).is_ok()); + } + + // Heap size below min. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES - 1), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_config(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Heap size above max. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty().with_heap_size(MAX_HEAP_FRAME_BYTES + 1), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_config(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Heap size not multiple of 1024. + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty().with_heap_size(MIN_HEAP_FRAME_BYTES + 1), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert_eq!( + sanitize_config(&view), + Err(TransactionViewError::SanitizeError) + ); + } + + // Config is not set, default is OK + { + let transaction = create_v1_transaction( + 1, + MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + (0..2).map(|_| Pubkey::new_unique()).collect(), + vec![], + TransactionConfig::empty(), + ); + let data = wincode::serialize(&transaction).unwrap(); + let view = TransactionView::try_new_unsanitized(data.as_ref()).unwrap(); + assert!(sanitize_config(&view).is_ok()); + } + } +} diff --git a/solana/transaction-view/src/signature_frame.rs b/solana/transaction-view/src/signature_frame.rs new file mode 100644 index 0000000..ac893a6 --- /dev/null +++ b/solana/transaction-view/src/signature_frame.rs @@ -0,0 +1,112 @@ +use { + crate::{ + bytes::{advance_offset_for_array, read_byte, try_u32_offset}, + result::{Result, TransactionViewError}, + }, + solana_packet::PACKET_DATA_SIZE, + solana_pubkey::Pubkey, + solana_signature::Signature, +}; + +// The packet has a maximum length of 1232 bytes. +// Each signature must be paired with a unique static pubkey, so each +// signature really requires 96 bytes. This means the maximum number of +// signatures in a **valid** transaction packet is 12. +// In our u16 encoding scheme, 12 would be encoded as a single byte. +// Rather than using the u16 decoding, we can simply read the byte and +// verify that the MSB is not set. +pub(crate) const MAX_SIGNATURES_PER_PACKET: u8 = + (PACKET_DATA_SIZE / (core::mem::size_of::() + core::mem::size_of::())) as u8; + +/// Metadata for accessing transaction-level signatures in a transaction view. +#[derive(Debug)] +pub(crate) struct SignatureFrame { + /// The number of signatures in the transaction. + pub(crate) num_signatures: u8, + /// Offset to the first signature in the transaction packet. + pub(crate) offset: u32, +} + +impl SignatureFrame { + /// Get the number of signatures and the offset to the first signature in + /// the transaction packet, starting at the given `offset`. + #[inline(always)] + pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { + // Maximum number of signatures should be represented by a single byte, + // thus the MSB should not be set. + const _: () = assert!(MAX_SIGNATURES_PER_PACKET & 0b1000_0000 == 0); + + let num_signatures = read_byte(bytes, offset)?; + if num_signatures == 0 || num_signatures > MAX_SIGNATURES_PER_PACKET { + return Err(TransactionViewError::ParseError); + } + + let signature_offset = try_u32_offset(*offset)?; + advance_offset_for_array::(bytes, offset, u16::from(num_signatures))?; + + Ok(Self { + num_signatures, + offset: signature_offset, + }) + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_short_vec::ShortVec}; + + #[test] + fn test_zero_signatures() { + let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); + let mut offset = 0; + assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_one_signature() { + let bytes = bincode::serialize(&ShortVec(vec![Signature::default()])).unwrap(); + let mut offset = 0; + let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_signatures, 1); + assert_eq!(frame.offset, 1); + assert_eq!(offset, 1 + core::mem::size_of::()); + } + + #[test] + fn test_max_signatures() { + let signatures = vec![Signature::default(); usize::from(MAX_SIGNATURES_PER_PACKET)]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_signatures, 12); + assert_eq!(frame.offset, 1); + assert_eq!(offset, 1 + 12 * core::mem::size_of::()); + } + + #[test] + fn test_non_zero_offset() { + let mut bytes = bincode::serialize(&ShortVec(vec![Signature::default()])).unwrap(); + bytes.insert(0, 0); // Insert a byte at the beginning of the packet. + let mut offset = 1; // Start at the second byte. + let frame = SignatureFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_signatures, 1); + assert_eq!(frame.offset, 2); + assert_eq!(offset, 2 + core::mem::size_of::()); + } + + #[test] + fn test_too_many_signatures() { + let signatures = vec![Signature::default(); usize::from(MAX_SIGNATURES_PER_PACKET) + 1]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_u16_max_signatures() { + let signatures = vec![Signature::default(); u16::MAX as usize]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + assert!(SignatureFrame::try_new(&bytes, &mut offset).is_err()); + } +} diff --git a/solana/transaction-view/src/static_account_keys_frame.rs b/solana/transaction-view/src/static_account_keys_frame.rs new file mode 100644 index 0000000..2d39481 --- /dev/null +++ b/solana/transaction-view/src/static_account_keys_frame.rs @@ -0,0 +1,99 @@ +use { + crate::{ + bytes::{advance_offset_for_array, read_byte, try_u32_offset}, + result::{Result, TransactionViewError}, + }, + solana_packet::PACKET_DATA_SIZE, + solana_pubkey::Pubkey, +}; + +// A legacy/v0 packet has a maximum length of 1232 bytes. +// This means the maximum number of 32 byte keys is 38. +// 38 as an min-sized encoded u16 is 1 byte. +// We can simply read this byte, if it's >38 we can return None. +const LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET: u8 = + (PACKET_DATA_SIZE / core::mem::size_of::()) as u8; + +/// Contains metadata about the static account keys in a transaction packet. +#[derive(Debug, Default)] +pub(crate) struct StaticAccountKeysFrame { + /// The number of static accounts in the transaction. + pub(crate) num_static_accounts: u8, + /// The offset to the first static account in the transaction. + pub(crate) offset: u32, +} + +impl StaticAccountKeysFrame { + #[inline(always)] + pub(crate) fn try_new(bytes: &[u8], offset: &mut usize) -> Result { + // Max size must not have the MSB set so that it is size 1. + const _: () = assert!(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET & 0b1000_0000 == 0); + + let num_static_accounts = read_byte(bytes, offset)?; + if num_static_accounts == 0 + || num_static_accounts > LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET + { + return Err(TransactionViewError::ParseError); + } + + let static_accounts_offset = try_u32_offset(*offset)?; + // Update offset for array of static accounts. + advance_offset_for_array::(bytes, offset, u16::from(num_static_accounts))?; + + Ok(Self { + num_static_accounts, + offset: static_accounts_offset, + }) + } +} + +#[cfg(test)] +mod tests { + use {super::*, solana_short_vec::ShortVec}; + + #[test] + fn test_zero_accounts() { + let bytes = bincode::serialize(&ShortVec(Vec::::new())).unwrap(); + let mut offset = 0; + assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_one_account() { + let bytes = bincode::serialize(&ShortVec(vec![Pubkey::default()])).unwrap(); + let mut offset = 0; + let frame = StaticAccountKeysFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_static_accounts, 1); + assert_eq!(frame.offset, 1); + assert_eq!(offset, 1 + core::mem::size_of::()); + } + + #[test] + fn test_max_accounts() { + let signatures = + vec![Pubkey::default(); usize::from(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET)]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + let frame = StaticAccountKeysFrame::try_new(&bytes, &mut offset).unwrap(); + assert_eq!(frame.num_static_accounts, 38); + assert_eq!(frame.offset, 1); + assert_eq!(offset, 1 + 38 * core::mem::size_of::()); + } + + #[test] + fn test_too_many_accounts() { + let signatures = + vec![Pubkey::default(); usize::from(LEGACY_OR_V0_MAX_STATIC_ACCOUNTS_PER_PACKET) + 1]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); + } + + #[test] + fn test_u16_max_accounts() { + let signatures = vec![Pubkey::default(); u16::MAX as usize]; + let bytes = bincode::serialize(&ShortVec(signatures)).unwrap(); + let mut offset = 0; + assert!(StaticAccountKeysFrame::try_new(&bytes, &mut offset).is_err()); + } +} diff --git a/solana/transaction-view/src/transaction_config_frame.rs b/solana/transaction-view/src/transaction_config_frame.rs new file mode 100644 index 0000000..c2a3392 --- /dev/null +++ b/solana/transaction-view/src/transaction_config_frame.rs @@ -0,0 +1,451 @@ +use crate::{ + bytes::{advance_offset_for_array, try_u32_offset, unchecked_copy_value}, + result::{Result, TransactionViewError}, +}; + +/// Metadata for accessing the tx-v1 transaction config section. +/// +/// This frame is a permanent part of `TransactionFrame`, but it is only +/// applicable to tx-v1. For legacy and v0 transactions, use +/// `TransactionConfigFrame::not_applicable()`. +/// +/// Layout, per SIMD-0385: +/// TransactionConfigMask (u32 LE) +/// ... +/// ConfigValues [[u8; 4]] // len = popcount(mask) +/// +/// Notes: +/// - `mask_offset == 0` is reserved to mean "not applicable" (legacy/v0). +/// - Parsed tx-v1 config frames should always have `mask_offset != 0`. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)] +pub(crate) struct TransactionConfigFrame { + /// Offset of the 4-byte TransactionConfigMask. + /// + /// `0` means "not applicable" (legacy/v0). + pub(crate) mask_offset: u32, + + /// Decoded TransactionConfigMask. + pub(crate) mask: u32, + + /// Offset of the first ConfigValues word. + /// + /// `0` means "not applicable" (legacy/v0) + pub(crate) values_offset: u32, + + /// Number of 4-byte words in ConfigValues. + pub(crate) num_values: u8, +} + +#[allow(dead_code)] +impl TransactionConfigFrame { + pub(crate) const MASK_SIZE: usize = core::mem::size_of::(); + pub(crate) const CONFIG_VALUE_SIZE: usize = core::mem::size_of::(); + + /// Sentinel for legacy / v0 transactions. + #[inline(always)] + pub(crate) const fn not_applicable() -> Self { + Self { + mask_offset: 0, + mask: 0, + values_offset: 0, + num_values: 0, + } + } + + /// Returns true if this frame represents a tx-v1 transaction config. + #[inline(always)] + pub(crate) const fn is_present(&self) -> bool { + self.mask_offset != 0 + } + + /// Config Mask has been successfully parsed before advancing to `ConfigValues` + /// region; Now can try to create TransactionConfigFrame by parsing values. + #[inline(always)] + pub(crate) fn try_new( + bytes: &[u8], + mask_offset: usize, + mask: u32, + offset: &mut usize, + ) -> Result { + assert!(mask_offset > 0, "txv1 mask offset must be greater than 0"); + + Self::sanitize_mask(mask)?; + let num_values = mask.count_ones() as u8; + let mask_offset = try_u32_offset(mask_offset)?; + let values_offset = try_u32_offset(*offset)?; + + // advance offset + advance_offset_for_array::(bytes, offset, num_values as u16)?; + + Ok(Self { + mask_offset, + mask, + values_offset, + num_values, + }) + } + + /// Validate mask semantics. + /// + /// Check unknown / reserved bits are not used; And + /// Bits 0 and 1 together encode one logical 8-byte priority-fee field, + /// so they must either both be set or both be clear. + #[inline(always)] + fn sanitize_mask(mask: u32) -> Result<()> { + const ALLOWED_TRANSACTION_CONFIG_MASK: u32 = 0b1_1111; + + // Reject unknown / reserved bits + if mask & !ALLOWED_TRANSACTION_CONFIG_MASK != 0 { + return Err(TransactionViewError::SanitizeError); + } + + // priority fee uses first 2 bits + let bit0 = Self::has_bit(mask, 0); + let bit1 = Self::has_bit(mask, 1); + if bit0 ^ bit1 { + return Err(TransactionViewError::SanitizeError); + } + + Ok(()) + } + + #[inline(always)] + fn has_bit(mask: u32, bit: u8) -> bool { + bit < 32 && ((mask >> bit) & 1) != 0 + } + + /// Return the packed word index for a given set bit. Eg: counts + /// bits set below `bit`. + /// + /// Example: + /// mask = 0b0001_1100 + /// bit 2 -> 0 + /// bit 3 -> 1 + /// bit 4 -> 2 + #[inline(always)] + pub(crate) fn word_index_for_bit(&self, bit: u8) -> Option { + if !self.is_present() || !Self::has_bit(self.mask, bit) { + return None; + } + + let mask_before_bit = (1u32 << bit).wrapping_sub(1); + Some((self.mask & mask_before_bit).count_ones() as u8) + } + + #[inline(always)] + fn word_offset(&self, bit: u8) -> Option { + let word_index = usize::from(self.word_index_for_bit(bit)?); + (self.values_offset as usize).checked_add(word_index.checked_mul(Self::CONFIG_VALUE_SIZE)?) + } +} + +#[derive(Debug, Clone, Copy)] +pub struct TransactionConfigView<'a> { + pub(crate) transaction_config_frame: &'a TransactionConfigFrame, + pub(crate) bytes: &'a [u8], +} + +impl<'a> TransactionConfigView<'a> { + #[inline(always)] + pub fn priority_fee_lamports(&self) -> Option { + // bit 0 and 1 have been sanitized to be in same state, + self.transaction_config_frame.word_offset(0).map(|offset| { + // SAFETY: + // - The offsets are checked to be valid in the byte slice. + // - u64 is valid for any bytes + u64::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) + }) + } + + #[inline(always)] + pub fn compute_unit_limit(&self) -> Option { + self.transaction_config_frame.word_offset(2).map(|offset| { + // SAFETY: + // - The offsets are checked to be valid in the byte slice. + // - u32 is valid for any bytes + u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) + }) + } + + #[inline(always)] + pub fn loaded_accounts_data_size_limit(&self) -> Option { + self.transaction_config_frame.word_offset(3).map(|offset| { + // SAFETY: + // - The offsets are checked to be valid in the byte slice. + // - u32 is valid for any bytes + u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) + }) + } + + #[inline(always)] + pub fn requested_heap_size(&self) -> Option { + self.transaction_config_frame.word_offset(4).map(|offset| { + // SAFETY: + // - The offsets are checked to be valid in the byte slice. + // - u32 is valid for any bytes + u32::from_le(unsafe { unchecked_copy_value(self.bytes, offset) }) + }) + } + + #[inline(always)] + pub fn mask(&self) -> u32 { + self.transaction_config_frame.mask + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn u32le(x: u32) -> [u8; 4] { + x.to_le_bytes() + } + + fn u64_words_le(x: u64) -> ([u8; 4], [u8; 4]) { + let bytes = x.to_le_bytes(); + ( + [bytes[0], bytes[1], bytes[2], bytes[3]], + [bytes[4], bytes[5], bytes[6], bytes[7]], + ) + } + + #[test] + fn test_not_applicable_defaults() { + let frame = TransactionConfigFrame::not_applicable(); + + assert!(!frame.is_present()); + assert_eq!(TransactionConfigFrame::sanitize_mask(frame.mask), Ok(())); + } + + #[test] + fn test_try_new_zero_mask_is_present() { + let mask = 0u32; + let bytes = mask.to_le_bytes(); + let mut buf = vec![0u8; 5]; + let mask_offset = 5; + buf.extend_from_slice(&bytes); + + let mut offset = buf.len(); + let frame = TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset).unwrap(); + + assert!(frame.is_present()); + assert_eq!(frame.mask_offset, 5); + assert_eq!(frame.mask, 0); + assert_eq!(frame.num_values, 0); + assert_eq!(offset, 9); + } + + #[test] + fn test_try_new_invalid_priority_fee_half_set_low_bit() { + let mask = 0b00001u32; + let bytes = mask.to_le_bytes(); + let mut buf = vec![0u8; 5]; + let mask_offset = 5; + buf.extend_from_slice(&bytes); + let mut offset = 5; + + assert_eq!( + TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset), + Err(TransactionViewError::SanitizeError) + ); + } + + #[test] + fn test_try_new_invalid_priority_fee_half_set_high_bit() { + let mask = 0b00010u32; + let bytes = mask.to_le_bytes(); + let mut buf = vec![0u8; 5]; + let mask_offset = 5; + buf.extend_from_slice(&bytes); + let mut offset = 5; + + assert_eq!( + TransactionConfigFrame::try_new(&buf, mask_offset, mask, &mut offset), + Err(TransactionViewError::SanitizeError) + ); + } + + #[test] + fn test_try_new_invalid_config_values() { + // bits 0,1,2 => 3 words => 12 bytes needed + let mask = 0b00111u32; + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[7u8; 3]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + + let mut offset = bytes.len(); + assert_eq!( + TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset), + Err(TransactionViewError::ParseError) + ); + } + + #[test] + fn test_read_defaults_when_bits_unset() { + let mask = 0u32; + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[1u8; 2]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + + let mut offset = bytes.len(); + let frame = + TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset).unwrap(); + let view = TransactionConfigView { + transaction_config_frame: &frame, + bytes: &bytes, + }; + + assert!(view.priority_fee_lamports().is_none()); + assert!(view.compute_unit_limit().is_none()); + assert!(view.loaded_accounts_data_size_limit().is_none()); + assert!(view.requested_heap_size().is_none()); + } + + #[test] + fn test_unknown_bits_rejected() { + // Single unknown bit (bit 5) + assert_eq!( + TransactionConfigFrame::sanitize_mask(0b10_0000), + Err(TransactionViewError::SanitizeError) + ); + // Multiple unknown bits + assert_eq!( + TransactionConfigFrame::sanitize_mask(0b1111_1111), + Err(TransactionViewError::SanitizeError) + ); + // High bits set + assert_eq!( + TransactionConfigFrame::sanitize_mask(1 << 31), + Err(TransactionViewError::SanitizeError) + ); + // Unknown bits mixed with valid bits + assert_eq!( + TransactionConfigFrame::sanitize_mask(0b1_1111 | (1 << 16)), + Err(TransactionViewError::SanitizeError) + ); + } + + #[test] + fn test_priority_fee_only() { + let mask = 0b00011u32; + let fee = 123_456_789u64; + let (lo, hi) = u64_words_le(fee); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[9u8; 4]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + let values_offset = bytes.len(); + bytes.extend_from_slice(&lo); + bytes.extend_from_slice(&hi); + + let mut offset = values_offset; + let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) + .inspect(|_| assert_eq!(offset, bytes.len())) + .unwrap(); + assert!(frame.is_present()); + assert_eq!(frame.num_values, 2); + + let view = TransactionConfigView { + transaction_config_frame: &frame, + bytes: &bytes, + }; + assert_eq!(view.priority_fee_lamports().unwrap(), fee); + assert!(view.compute_unit_limit().is_none()); + assert!(view.loaded_accounts_data_size_limit().is_none()); + assert!(view.requested_heap_size().is_none()); + } + + #[test] + fn test_all_initial_fields_present() { + // bits 0,1,2,3,4 => priority fee + cu + loaded data size + heap size + let mask = 0b1_1111u32; + let fee = 99u64; + let cu = 1_400_000u32; + let loaded = 64_000u32; + let heap = 64 * 1024u32; + + let (fee_lo, fee_hi) = u64_words_le(fee); + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[9u8; 7]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + let values_offset = bytes.len(); + bytes.extend_from_slice(&fee_lo); + bytes.extend_from_slice(&fee_hi); + bytes.extend_from_slice(&u32le(cu)); + bytes.extend_from_slice(&u32le(loaded)); + bytes.extend_from_slice(&u32le(heap)); + + let mut offset = values_offset; + let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) + .inspect(|_| assert_eq!(offset, bytes.len())) + .unwrap(); + assert!(frame.is_present()); + assert_eq!(frame.num_values, 5); + + let view = TransactionConfigView { + transaction_config_frame: &frame, + bytes: &bytes, + }; + assert_eq!(view.priority_fee_lamports().unwrap(), fee); + assert_eq!(view.compute_unit_limit().unwrap(), cu); + assert_eq!(view.loaded_accounts_data_size_limit().unwrap(), loaded); + assert_eq!(view.requested_heap_size().unwrap(), heap); + } + + #[test] + fn test_sparse_bits_word_indexing() { + // bits 2 and 4 only + let mask = 0b10100u32; + let cu = 777u32; + let heap = 48 * 1024u32; + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[1u8; 3]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + let values_offset = bytes.len(); + bytes.extend_from_slice(&u32le(cu)); // bit 2 -> word 0 + bytes.extend_from_slice(&u32le(heap)); // bit 4 -> word 1 + + let mut offset = values_offset; + let frame = TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset) + .inspect(|_| assert_eq!(offset, bytes.len())) + .unwrap(); + assert_eq!(frame.word_index_for_bit(2), Some(0)); + assert_eq!(frame.word_index_for_bit(4), Some(1)); + assert_eq!(frame.word_index_for_bit(3), None); + + let view = TransactionConfigView { + transaction_config_frame: &frame, + bytes: &bytes, + }; + assert!(view.priority_fee_lamports().is_none()); + assert_eq!(view.compute_unit_limit().unwrap(), cu); + assert!(view.loaded_accounts_data_size_limit().is_none()); + assert_eq!(view.requested_heap_size().unwrap(), heap); + } + + #[test] + fn test_truncated_priority_fee_values() { + let mask = 0b00011u32; + + let mut bytes = Vec::new(); + bytes.extend_from_slice(&[5u8; 2]); + let mask_offset = bytes.len(); + bytes.extend_from_slice(&mask.to_le_bytes()); + let values_offset = bytes.len(); + bytes.extend_from_slice(&[1, 2, 3, 4]); // only one word present + + let mut offset = values_offset; + assert_eq!( + TransactionConfigFrame::try_new(&bytes, mask_offset, mask, &mut offset), + Err(TransactionViewError::ParseError) + ); + } +} diff --git a/solana/transaction-view/src/transaction_data.rs b/solana/transaction-view/src/transaction_data.rs new file mode 100644 index 0000000..323c085 --- /dev/null +++ b/solana/transaction-view/src/transaction_data.rs @@ -0,0 +1,19 @@ +/// Trait for accessing transaction data from an abstract byte container. +pub trait TransactionData { + /// Returns a reference to the serialized transaction data. + fn data(&self) -> &[u8]; +} + +impl TransactionData for &[u8] { + #[inline] + fn data(&self) -> &[u8] { + self + } +} + +impl TransactionData for std::sync::Arc> { + #[inline] + fn data(&self) -> &[u8] { + self.as_ref() + } +} diff --git a/solana/transaction-view/src/transaction_frame.rs b/solana/transaction-view/src/transaction_frame.rs new file mode 100644 index 0000000..7b49b9e --- /dev/null +++ b/solana/transaction-view/src/transaction_frame.rs @@ -0,0 +1,996 @@ +use { + crate::{ + address_table_lookup_frame::{AddressTableLookupFrame, AddressTableLookupIterator}, + bytes::{ + advance_offset_for_array, advance_offset_for_type, check_remaining, try_u32_offset, + unchecked_copy_value, unchecked_read_byte, + }, + instructions_frame::{InstructionsFrame, InstructionsIterator}, + message_header_frame::MessageHeaderFrame, + result::{Result, TransactionViewError}, + signature_frame::SignatureFrame, + static_account_keys_frame::StaticAccountKeysFrame, + transaction_config_frame::TransactionConfigFrame, + transaction_version::{MAGICBLOCK_PREFIX, TransactionVersion}, + }, + solana_hash::Hash, + solana_pubkey::Pubkey, + solana_signature::Signature, +}; + +#[derive(Debug)] +pub(crate) struct TransactionFrame { + /// Signature framing data. + signature: SignatureFrame, + /// Message header framing data. + message_header: MessageHeaderFrame, + /// Static account keys framing data. + static_account_keys: StaticAccountKeysFrame, + /// Recent blockhash offset. + recent_blockhash_offset: u32, + /// Instructions framing data. + instructions: InstructionsFrame, + /// Address table lookup framing data. + address_table_lookup: AddressTableLookupFrame, + /// Transaction config framing data + transaction_config_frame: TransactionConfigFrame, + /// The data length in bytes + data_len: u32, +} + +impl TransactionFrame { + /// Parse a serialized transaction and verify basic structure. + /// The `bytes` parameter must have no trailing data. + pub(crate) fn try_new(bytes: &[u8]) -> Result { + try_u32_offset(bytes.len())?; + if Self::is_legacy_or_v0(bytes)? { + Self::try_new_as_legacy_or_v0(bytes) + } else { + Self::try_new_as_v1(bytes) + } + } + + fn try_new_as_legacy_or_v0(bytes: &[u8]) -> Result { + let mut offset = 0; + let signature = SignatureFrame::try_new(bytes, &mut offset)?; + let message_header = MessageHeaderFrame::try_new(bytes, &mut offset)?; + let static_account_keys = StaticAccountKeysFrame::try_new(bytes, &mut offset)?; + + // The recent blockhash is the first account key after the static + // account keys. The recent blockhash is always present in a valid + // transaction and has a fixed size of 32 bytes. + let recent_blockhash_offset = try_u32_offset(offset)?; + advance_offset_for_type::(bytes, &mut offset)?; + + let instructions = InstructionsFrame::try_new_for_legacy_and_v0(bytes, &mut offset)?; + let address_table_lookup = match message_header.version { + TransactionVersion::Legacy => AddressTableLookupFrame { + num_address_table_lookups: 0, + offset: 0, + total_writable_lookup_accounts: 0, + total_readonly_lookup_accounts: 0, + }, + TransactionVersion::V0 => AddressTableLookupFrame::try_new(bytes, &mut offset)?, + TransactionVersion::V1 | TransactionVersion::Magicblock => { + unreachable!("unexpected variant") + } + }; + + // Verify that the entire transaction was parsed. + if offset != bytes.len() { + return Err(TransactionViewError::ParseError); + } + + Ok(Self { + signature, + message_header, + static_account_keys, + recent_blockhash_offset, + instructions, + address_table_lookup, + transaction_config_frame: TransactionConfigFrame::not_applicable(), + data_len: try_u32_offset(offset)?, + }) + } + + fn try_new_as_v1(bytes: &[u8]) -> Result { + let mut offset: usize = 0; + + // Fixed-size txv1 prefix up through NumAddresses: + // VersionByte (u8) + // LegacyHeader (u8, u8, u8) + // TransactionConfigMask (u32) + // LifetimeSpecifier ([u8; 32]) + // NumInstructions (u8) + // NumAddresses (u8) + const FIXED_V1_PREFIX_LEN: usize = 1 + 3 + 4 + size_of::() + 1 + 1; + + check_remaining(bytes, offset, FIXED_V1_PREFIX_LEN)?; + + // SAFETY: have checked bytes have enough space for preifx all the way up to + // NumAddresses. + + // message offset would be the first byte of txv1 packet, which is version byte + let message_offset = try_u32_offset(offset)?; + // Version Byte + let version = unsafe { unchecked_read_byte(bytes, &mut offset) }; + let version = match version { + solana_message::v1::V1_PREFIX => TransactionVersion::V1, + MAGICBLOCK_PREFIX => TransactionVersion::Magicblock, + _ => return Err(TransactionViewError::ParseError), + }; + // Legacy Header + let num_required_signatures = unsafe { unchecked_read_byte(bytes, &mut offset) }; + let num_readonly_signed_accounts = unsafe { unchecked_read_byte(bytes, &mut offset) }; + let num_readonly_unsigned_accounts = unsafe { unchecked_read_byte(bytes, &mut offset) }; + // Transaction Config Bit Mask + let transaction_config_mask_offset = offset; + let transaction_config_mask: u32 = unsafe { unchecked_copy_value(bytes, offset) }; + offset = offset.checked_add(size_of::()).ok_or(TransactionViewError::ParseError)?; + // Lifetime specifier + let recent_blockhash_offset = try_u32_offset(offset)?; + offset = offset.checked_add(size_of::()).ok_or(TransactionViewError::ParseError)?; + // Num instructions and addresses + let num_instructions = unsafe { unchecked_read_byte(bytes, &mut offset) }; + let num_addresses = unsafe { unchecked_read_byte(bytes, &mut offset) }; + + // addresses + let addresses_offset = try_u32_offset(offset)?; + advance_offset_for_array::(bytes, &mut offset, u16::from(num_addresses))?; + // config value slots: one 4-byte slot per set bit in mask + let transaction_config_frame = TransactionConfigFrame::try_new( + bytes, + transaction_config_mask_offset, + transaction_config_mask, + &mut offset, + )?; + // instruction headers and payloads + let instructions = InstructionsFrame::try_new_for_v1(bytes, &mut offset, num_instructions)?; + // signatures + let signatures_offset = try_u32_offset(offset)?; + advance_offset_for_array::( + bytes, + &mut offset, + u16::from(num_required_signatures), + )?; + // Verify that the entire transaction was parsed. + if offset != bytes.len() { + return Err(TransactionViewError::ParseError); + } + + let frame = Self { + signature: SignatureFrame { + num_signatures: num_required_signatures, + offset: signatures_offset, + }, + message_header: MessageHeaderFrame { + offset: message_offset, + version, + num_required_signatures, + num_readonly_signed_accounts, + num_readonly_unsigned_accounts, + }, + static_account_keys: StaticAccountKeysFrame { + num_static_accounts: num_addresses, // always static accounts in txv1 + offset: addresses_offset, + }, + recent_blockhash_offset, + instructions, + // Don't have ATL in txv1 + address_table_lookup: AddressTableLookupFrame { + num_address_table_lookups: 0, + offset: 0, + total_writable_lookup_accounts: 0, + total_readonly_lookup_accounts: 0, + }, + transaction_config_frame, + data_len: try_u32_offset(offset)?, + }; + + Ok(frame) + } + + fn is_legacy_or_v0(bytes: &[u8]) -> Result { + let first_byte = *bytes.first().ok_or(TransactionViewError::ParseError)?; + + // In wire format: + // - Legacy/v0 transactions start with signatures (compact-u16 count). + // The retained signature-count limit keeps the first byte below 128. + // - v1 transactions start with a version byte with MSB = 1. + Ok((first_byte & solana_message::MESSAGE_VERSION_PREFIX) == 0) + } + + /// Return the number of signatures in the transaction. + #[inline] + pub(crate) fn num_signatures(&self) -> u8 { + self.signature.num_signatures + } + + /// Return the version of the transaction. + #[inline] + pub(crate) fn version(&self) -> TransactionVersion { + self.message_header.version + } + + /// Return the number of required signatures in the transaction. + #[inline] + pub(crate) fn num_required_signatures(&self) -> u8 { + self.message_header.num_required_signatures + } + + /// Return the number of readonly signed static accounts in the transaction. + #[inline] + pub(crate) fn num_readonly_signed_static_accounts(&self) -> u8 { + self.message_header.num_readonly_signed_accounts + } + + /// Return the number of readonly unsigned static accounts in the transaction. + #[inline] + pub(crate) fn num_readonly_unsigned_static_accounts(&self) -> u8 { + self.message_header.num_readonly_unsigned_accounts + } + + /// Return the number of static account keys in the transaction. + #[inline] + pub(crate) fn num_static_account_keys(&self) -> u8 { + self.static_account_keys.num_static_accounts + } + + /// Return the number of instructions in the transaction. + #[inline] + pub(crate) fn num_instructions(&self) -> u16 { + self.instructions.num_instructions() + } + + /// Return the number of address table lookups in the transaction. + #[inline] + pub(crate) fn num_address_table_lookups(&self) -> u8 { + self.address_table_lookup.num_address_table_lookups + } + + /// Return the number of writable lookup accounts in the transaction. + #[inline] + pub(crate) fn total_writable_lookup_accounts(&self) -> u16 { + self.address_table_lookup.total_writable_lookup_accounts + } + + /// Return the number of readonly lookup accounts in the transaction. + #[inline] + pub(crate) fn total_readonly_lookup_accounts(&self) -> u16 { + self.address_table_lookup.total_readonly_lookup_accounts + } + + /// Return the range to the message as [begin, end] + #[inline] + pub(crate) fn message_range(&self) -> (u32, u32) { + let end = match self.version() { + TransactionVersion::V1 | TransactionVersion::Magicblock => self.signature.offset, + _ => self.data_len, + }; + (self.message_header.offset, end) + } + + /// Return transaction_config_frame + #[inline] + pub(crate) fn transaction_config_frame(&self) -> &TransactionConfigFrame { + &self.transaction_config_frame + } +} + +// Separate implementation for `unsafe` accessor methods. +impl TransactionFrame { + /// Return the slice of signatures in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionFrame` instance. + #[inline] + pub(crate) unsafe fn signatures<'a>(&self, bytes: &'a [u8]) -> &'a [Signature] { + // Verify at compile time there are no alignment constraints. + const _: () = assert!(align_of::() == 1, "Signature alignment"); + // The length of the slice is not greater than isize::MAX. + const _: () = assert!(u8::MAX as usize * size_of::() <= isize::MAX as usize); + + // SAFETY: + // - If this `TransactionFrame` was created from `bytes`: + // - the pointer is valid for the range and is properly aligned. + // - `num_signatures` has been verified against the bounds if + // `TransactionFrame` was created successfully. + // - `Signature` are just byte arrays; there is no possibility the + // `Signature` are not initialized properly. + // - The lifetime of the returned slice is the same as the input + // `bytes`. This means it will not be mutated or deallocated while + // holding the slice. + // - The length does not overflow `isize`. + let start = self.signature.offset as usize; + let end = start + usize::from(self.signature.num_signatures) * size_of::(); + let signature_bytes = &bytes[start..end]; + unsafe { + core::slice::from_raw_parts( + signature_bytes.as_ptr() as *const Signature, + usize::from(self.signature.num_signatures), + ) + } + } + + /// Return the slice of static account keys in the transaction. + /// + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionFrame` instance. + #[inline] + pub(crate) unsafe fn static_account_keys<'a>(&self, bytes: &'a [u8]) -> &'a [Pubkey] { + // Verify at compile time there are no alignment constraints. + const _: () = assert!(align_of::() == 1, "Pubkey alignment"); + // The length of the slice is not greater than isize::MAX. + const _: () = assert!(u8::MAX as usize * size_of::() <= isize::MAX as usize); + + // SAFETY: + // - If this `TransactionFrame` was created from `bytes`: + // - the pointer is valid for the range and is properly aligned. + // - `num_static_accounts` has been verified against the bounds if + // `TransactionFrame` was created successfully. + // - `Pubkey` are just byte arrays; there is no possibility the + // `Pubkey` are not initialized properly. + // - The lifetime of the returned slice is the same as the input + // `bytes`. This means it will not be mutated or deallocated while + // holding the slice. + // - The length does not overflow `isize`. + let start = self.static_account_keys.offset as usize; + let end = + start + usize::from(self.static_account_keys.num_static_accounts) * size_of::(); + let account_bytes = &bytes[start..end]; + unsafe { + core::slice::from_raw_parts( + bytes + .as_ptr() + .add(usize::from(self.static_account_keys.offset)) + as *const Pubkey, + usize::from(self.static_account_keys.num_static_accounts), + ) + } + } + + /// Return the recent blockhash in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionFrame` instance. + #[inline] + pub(crate) unsafe fn recent_blockhash<'a>(&self, bytes: &'a [u8]) -> &'a Hash { + // Verify at compile time there are no alignment constraints. + const _: () = assert!(align_of::() == 1, "Hash alignment"); + + // SAFETY: + // - The pointer is correctly aligned (no alignment constraints). + // - `Hash` is just a byte array; there is no possibility the `Hash` + // is not initialized properly. + // - Aliasing rules are respected because the lifetime of the returned + // reference is the same as the input/source `bytes`. + unsafe { + &*(bytes + .as_ptr() + .add(usize::from(self.recent_blockhash_offset)) as *const Hash) + } + } + + /// Return an iterator over the instructions in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionFrame` instance. + #[inline] + pub(crate) unsafe fn instructions_iter<'a>( + &'a self, + bytes: &'a [u8], + ) -> InstructionsIterator<'a> { + self.instructions.iter(bytes) + } + + /// Return an iterator over the address table lookups in the transaction. + /// # Safety + /// - This function must be called with the same `bytes` slice that was + /// used to create the `TransactionFrame` instance. + #[inline] + pub(crate) unsafe fn address_table_lookup_iter<'a>( + &self, + bytes: &'a [u8], + ) -> AddressTableLookupIterator<'a> { + AddressTableLookupIterator { + bytes, + offset: self.address_table_lookup.offset as usize, + num_address_table_lookups: self.address_table_lookup.num_address_table_lookups, + index: 0, + } + } +} + +#[cfg(test)] +impl TransactionFrame { + pub(crate) fn message_offset(&self) -> u32 { + self.message_header.offset + } + + pub(crate) fn signatures_offset(&self) -> u32 { + self.signature.offset + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_message::{ + AddressLookupTableAccount, Message, MessageHeader, VersionedMessage, + compiled_instruction::CompiledInstruction, v0, v1, + }, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_system_interface::instruction::{self as system_instruction, SystemInstruction}, + solana_transaction::versioned::VersionedTransaction, + }; + + fn verify_transaction_view_frame(tx: &VersionedTransaction) { + let bytes = wincode::serialize(tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + assert_eq!(frame.signature.num_signatures, tx.signatures.len() as u8); + assert_eq!(frame.signature.offset as usize, 1); + + assert_eq!( + frame.message_header.num_required_signatures, + tx.message.header().num_required_signatures + ); + assert_eq!( + frame.message_header.num_readonly_signed_accounts, + tx.message.header().num_readonly_signed_accounts + ); + assert_eq!( + frame.message_header.num_readonly_unsigned_accounts, + tx.message.header().num_readonly_unsigned_accounts + ); + + assert_eq!( + frame.static_account_keys.num_static_accounts, + tx.message.static_account_keys().len() as u8 + ); + assert_eq!( + frame.instructions.num_instructions(), + tx.message.instructions().len() as u16 + ); + assert_eq!( + frame.address_table_lookup.num_address_table_lookups, + tx.message + .address_table_lookups() + .map(|x| x.len() as u8) + .unwrap_or(0) + ); + } + + fn minimally_sized_transaction() -> VersionedTransaction { + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + account_keys: vec![Pubkey::default()], + recent_blockhash: Hash::default(), + instructions: vec![], + }), + } + } + + fn simple_transfer() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[system_instruction::transfer( + &payer, + &Pubkey::new_unique(), + 1, + )], + Some(&payer), + )), + } + } + + fn simple_transfer_v0() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::V0( + v0::Message::try_compile( + &payer, + &[system_instruction::transfer( + &payer, + &Pubkey::new_unique(), + 1, + )], + &[], + Hash::default(), + ) + .unwrap(), + ), + } + } + + fn multiple_transfers() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[ + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + ], + Some(&payer), + )), + } + } + + fn v0_with_single_lookup() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + let to = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::V0( + v0::Message::try_compile( + &payer, + &[system_instruction::transfer(&payer, &to, 1)], + &[AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![to], + }], + Hash::default(), + ) + .unwrap(), + ), + } + } + + fn v0_with_multiple_lookups() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + let to1 = Pubkey::new_unique(); + let to2 = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::V0( + v0::Message::try_compile( + &payer, + &[ + system_instruction::transfer(&payer, &to1, 1), + system_instruction::transfer(&payer, &to2, 1), + ], + &[ + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![to1], + }, + AddressLookupTableAccount { + key: Pubkey::new_unique(), + addresses: vec![to2], + }, + ], + Hash::default(), + ) + .unwrap(), + ), + } + } + + fn simple_v1_transaction() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + let program = Pubkey::new_unique(); + let other = Pubkey::new_unique(); + + VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V1(v1::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 1, + }, + config: v1::TransactionConfig { + priority_fee: Some(123), + compute_unit_limit: Some(456), + loaded_accounts_data_size_limit: Some(789), + heap_size: Some(1024), + }, + lifetime_specifier: Hash::default(), + account_keys: vec![payer, other, program], + instructions: vec![ + CompiledInstruction { + program_id_index: 2, + accounts: vec![0, 1], + data: vec![10, 11, 12], + }, + CompiledInstruction { + program_id_index: 2, + accounts: vec![], + data: vec![99], + }, + ], + }), + } + } + + #[test] + fn test_minimal_sized_transaction() { + verify_transaction_view_frame(&minimally_sized_transaction()); + } + + #[test] + fn test_simple_transfer() { + verify_transaction_view_frame(&simple_transfer()); + } + + #[test] + fn test_simple_transfer_v0() { + verify_transaction_view_frame(&simple_transfer_v0()); + } + + #[test] + fn test_v0_with_lookup() { + verify_transaction_view_frame(&v0_with_single_lookup()); + } + + #[test] + fn test_trailing_byte() { + let tx = simple_transfer(); + let mut bytes = wincode::serialize(&tx).unwrap(); + bytes.push(0); + assert!(TransactionFrame::try_new(&bytes).is_err()); + } + + #[test] + fn test_insufficient_bytes() { + let tx = simple_transfer(); + let bytes = wincode::serialize(&tx).unwrap(); + assert!(TransactionFrame::try_new(&bytes[..bytes.len().wrapping_sub(1)]).is_err()); + } + + #[test] + fn test_signature_overflow() { + let tx = simple_transfer(); + let mut bytes = wincode::serialize(&tx).unwrap(); + // Set the number of signatures to u16::MAX + bytes[0] = 0xff; + bytes[1] = 0xff; + bytes[2] = 0xff; + assert!(TransactionFrame::try_new(&bytes).is_err()); + } + + #[test] + fn test_account_key_overflow() { + let tx = simple_transfer(); + let mut bytes = wincode::serialize(&tx).unwrap(); + // Set the number of accounts to u16::MAX + let offset = 1 + size_of::() + 3; + bytes[offset] = 0xff; + bytes[offset + 1] = 0xff; + bytes[offset + 2] = 0xff; + assert!(TransactionFrame::try_new(&bytes).is_err()); + } + + #[test] + fn test_instructions_overflow() { + let tx = simple_transfer(); + let mut bytes = wincode::serialize(&tx).unwrap(); + // Set the number of instructions to u16::MAX + let offset = + 1 + size_of::() + 3 + 1 + 3 * size_of::() + size_of::(); + bytes[offset] = 0xff; + bytes[offset + 1] = 0xff; + bytes[offset + 2] = 0xff; + assert!(TransactionFrame::try_new(&bytes).is_err()); + } + + #[test] + fn test_alt_overflow() { + let tx = simple_transfer_v0(); + let ix_bytes = tx.message.instructions()[0].data.len(); + let mut bytes = wincode::serialize(&tx).unwrap(); + // Set the number of instructions to u16::MAX + let offset = 1 // byte for num signatures + + size_of::() // signature + + 1 // version byte + + 3 // message header + + 1 // byte for num account keys + + 3 * size_of::() // account keys + + size_of::() // recent blockhash + + 1 // byte for num instructions + + 1 // program index + + 1 // byte for num accounts + + 2 // bytes for account index + + 1 // byte for data length + + ix_bytes; + bytes[offset] = 0x01; + assert!(TransactionFrame::try_new(&bytes).is_err()); + } + + #[test] + fn test_basic_accessors() { + let tx = simple_transfer(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + assert_eq!(frame.num_signatures(), 1); + assert!(matches!(frame.version(), TransactionVersion::Legacy)); + assert_eq!(frame.num_required_signatures(), 1); + assert_eq!(frame.num_readonly_signed_static_accounts(), 0); + assert_eq!(frame.num_readonly_unsigned_static_accounts(), 1); + assert_eq!(frame.num_static_account_keys(), 3); + assert_eq!(frame.num_instructions(), 1); + assert_eq!(frame.num_address_table_lookups(), 0); + + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let signatures = frame.signatures(&bytes); + assert_eq!(signatures, &tx.signatures); + + let static_account_keys = frame.static_account_keys(&bytes); + assert_eq!(static_account_keys, tx.message.static_account_keys()); + + let recent_blockhash = frame.recent_blockhash(&bytes); + assert_eq!(recent_blockhash, tx.message.recent_blockhash()); + } + } + + #[test] + fn test_instructions_iter_empty() { + let tx = minimally_sized_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.instructions_iter(&bytes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_instructions_iter_single() { + let tx = simple_transfer(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.instructions_iter(&bytes); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 2); + assert_eq!(ix.accounts, &[0, 1]); + assert_eq!( + ix.data, + &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_instructions_iter_multiple() { + let tx = multiple_transfers(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.instructions_iter(&bytes); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 3); + assert_eq!(ix.accounts, &[0, 1]); + assert_eq!( + ix.data, + &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + let ix = iter.next().unwrap(); + assert_eq!(ix.program_id_index, 3); + assert_eq!(ix.accounts, &[0, 2]); + assert_eq!( + ix.data, + &wincode::serialize(&SystemInstruction::Transfer { lamports: 1 }).unwrap() + ); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_empty() { + let tx = simple_transfer(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.address_table_lookup_iter(&bytes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_single() { + let tx = v0_with_single_lookup(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + let atls_actual = tx.message.address_table_lookups().unwrap(); + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.address_table_lookup_iter(&bytes); + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[0].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_address_table_lookup_iter_multiple() { + let tx = v0_with_multiple_lookups(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + let atls_actual = tx.message.address_table_lookups().unwrap(); + // SAFETY: `bytes` is the same slice used to create `frame`. + unsafe { + let mut iter = frame.address_table_lookup_iter(&bytes); + + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[0].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[0].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[0].readonly_indexes); + + let lookup = iter.next().unwrap(); + assert_eq!(lookup.account_key, &atls_actual[1].account_key); + assert_eq!(lookup.writable_indexes, atls_actual[1].writable_indexes); + assert_eq!(lookup.readonly_indexes, atls_actual[1].readonly_indexes); + + assert!(iter.next().is_none()); + } + } + + #[test] + fn test_v1_transaction_frame_parses() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + assert!(matches!(frame.version(), TransactionVersion::V1)); + assert_eq!(frame.num_signatures(), 1); + assert_eq!(frame.num_required_signatures(), 1); + assert_eq!(frame.num_readonly_signed_static_accounts(), 0); + assert_eq!(frame.num_readonly_unsigned_static_accounts(), 1); + assert_eq!(frame.num_static_account_keys(), 3); + assert_eq!(frame.num_instructions(), 2); + + // txv1 should not have ALTs + assert_eq!(frame.num_address_table_lookups(), 0); + assert_eq!(frame.total_writable_lookup_accounts(), 0); + assert_eq!(frame.total_readonly_lookup_accounts(), 0); + + // new v1-only frame metadata + assert!(frame.signatures_offset() > frame.message_offset()); + } + + #[test] + fn test_magicblock_frame_preserves_large_offsets() { + let mut transaction = simple_v1_transaction(); + let VersionedMessage::V1(message) = &mut transaction.message else { + unreachable!(); + }; + message.config = v1::TransactionConfig::empty(); + message.instructions[0].data = vec![1; 40_000]; + message.instructions[1].data = vec![2; 40_000]; + + let mut bytes = wincode::serialize(&transaction).unwrap(); + bytes[0] = MAGICBLOCK_PREFIX; + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + assert!(matches!(frame.version(), TransactionVersion::Magicblock)); + assert_eq!( + solana_transaction::versioned::TransactionVersion::from(frame.version()), + solana_transaction::versioned::TransactionVersion::Number(127) + ); + assert!(frame.signatures_offset() > u32::from(u16::MAX)); + assert_eq!(frame.message_range(), (0, frame.signatures_offset())); + + let instructions: Vec<_> = unsafe { frame.instructions_iter(&bytes) }.collect(); + assert_eq!(instructions[0].data, vec![1; 40_000]); + assert_eq!(instructions[1].data, vec![2; 40_000]); + let signatures = unsafe { frame.signatures(&bytes) }; + assert_eq!(signatures, transaction.signatures); + } + + #[test] + fn test_v1_is_not_legacy_or_v0() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + + assert!(!TransactionFrame::is_legacy_or_v0(&bytes).unwrap()); + } + + #[test] + fn test_legacy_is_legacy_or_v0() { + let payer = Pubkey::new_unique(); + let tx = VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::Legacy(solana_message::Message::new(&[], Some(&payer))), + }; + let bytes = wincode::serialize(&tx).unwrap(); + + assert!(TransactionFrame::is_legacy_or_v0(&bytes).unwrap()); + } + + #[test] + fn test_is_legacy_or_v0_empty_bytes() { + assert!(matches!( + TransactionFrame::is_legacy_or_v0(&[]), + Err(TransactionViewError::ParseError), + )); + } + + #[test] + fn test_v1_rejects_unknown_version() { + let tx = simple_v1_transaction(); + let mut bytes = wincode::serialize(&tx).unwrap(); + + // First byte is version-tagged for versioned messages. + // Flip underlying version to an unsupported value. + bytes[0] = solana_message::MESSAGE_VERSION_PREFIX | 2; + + assert!(matches!( + TransactionFrame::try_new(&bytes), + Err(TransactionViewError::ParseError), + )); + } + + #[test] + fn test_v1_rejects_trailing_byte() { + let tx = simple_v1_transaction(); + let mut bytes = wincode::serialize(&tx).unwrap(); + bytes.push(0); + + assert!(matches!( + TransactionFrame::try_new(&bytes), + Err(TransactionViewError::ParseError), + )); + } + + #[test] + fn test_v1_rejects_truncated_bytes() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + + assert!(matches!( + TransactionFrame::try_new(&bytes[..bytes.len() - 1]), + Err(TransactionViewError::ParseError), + )); + } + + #[test] + fn test_v1_instruction_iteration() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + let frame = TransactionFrame::try_new(&bytes).unwrap(); + + let mut iter = unsafe { frame.instructions_iter(&bytes) }; + + let ix0 = iter.next().unwrap(); + assert_eq!(ix0.program_id_index, 2); + assert_eq!(ix0.accounts, &[0, 1]); + assert_eq!(ix0.data, &[10, 11, 12]); + + let ix1 = iter.next().unwrap(); + assert_eq!(ix1.program_id_index, 2); + assert_eq!(ix1.accounts, &[] as &[u8]); + assert_eq!(ix1.data, &[99]); + + assert!(iter.next().is_none()); + } +} diff --git a/solana/transaction-view/src/transaction_version.rs b/solana/transaction-view/src/transaction_version.rs new file mode 100644 index 0000000..36f6af8 --- /dev/null +++ b/solana/transaction-view/src/transaction_version.rs @@ -0,0 +1,26 @@ +/// Engine-private transaction version. +pub const MAGICBLOCK_VERSION: u8 = 127; +/// Engine-private versioned transaction prefix. +pub const MAGICBLOCK_PREFIX: u8 = solana_message::MESSAGE_VERSION_PREFIX | MAGICBLOCK_VERSION; + +/// A byte that represents the version of the transaction. +#[derive(Copy, Clone, Debug, Default)] +#[repr(u8)] +pub enum TransactionVersion { + #[default] + Legacy = u8::MAX, + V0 = 0, + V1 = 1, + Magicblock = MAGICBLOCK_VERSION, +} + +impl From for solana_transaction::versioned::TransactionVersion { + fn from(version: TransactionVersion) -> Self { + match version { + TransactionVersion::Legacy => Self::LEGACY, + TransactionVersion::V0 => Self::Number(0), + TransactionVersion::V1 => Self::Number(1), + TransactionVersion::Magicblock => Self::Number(MAGICBLOCK_VERSION), + } + } +} diff --git a/solana/transaction-view/src/transaction_view.rs b/solana/transaction-view/src/transaction_view.rs new file mode 100644 index 0000000..58f236e --- /dev/null +++ b/solana/transaction-view/src/transaction_view.rs @@ -0,0 +1,516 @@ +use { + crate::{ + address_table_lookup_frame::AddressTableLookupIterator, + instructions_frame::InstructionsIterator, result::Result, sanitize::sanitize, + transaction_config_frame::TransactionConfigView, transaction_data::TransactionData, + transaction_frame::TransactionFrame, transaction_version::TransactionVersion, + }, + core::fmt::{Debug, Formatter}, + solana_hash::Hash, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_svm_transaction::{ + instruction::SVMInstruction, message_address_table_lookup::SVMMessageAddressTableLookup, + svm_message::SVMStaticMessage, + }, +}; + +// alias for convenience +pub type UnsanitizedTransactionView = TransactionView; +pub type SanitizedTransactionView = TransactionView; + +/// A view into a serialized transaction. +/// +/// This struct provides access to the transaction data without +/// deserializing it. This is done by parsing and caching metadata +/// about the layout of the serialized transaction. +/// The owned `data` is abstracted through the `TransactionData` trait, +/// so that different containers for the serialized transaction can be used. +pub struct TransactionView { + data: D, + frame: TransactionFrame, +} + +impl TransactionView { + /// Creates a new `TransactionView` without running sanitization checks. + pub fn try_new_unsanitized(data: D) -> Result { + let frame = TransactionFrame::try_new(data.data())?; + Ok(Self { data, frame }) + } + + /// Sanitizes the transaction view, returning a sanitized view on success. + pub fn sanitize( + self, + enable_instruction_accounts_limit: bool, + ) -> Result> { + sanitize(&self, enable_instruction_accounts_limit)?; + Ok(SanitizedTransactionView { + data: self.data, + frame: self.frame, + }) + } +} + +impl TransactionView { + /// Creates a new `TransactionView`, running sanitization checks. + pub fn try_new_sanitized(data: D, enable_instruction_accounts_limit: bool) -> Result { + let unsanitized_view = TransactionView::try_new_unsanitized(data)?; + unsanitized_view.sanitize(enable_instruction_accounts_limit) + } +} + +impl TransactionView { + /// Return the number of signatures in the transaction. + #[inline] + pub fn num_signatures(&self) -> u8 { + self.frame.num_signatures() + } + + /// Return the version of the transaction. + #[inline] + pub fn version(&self) -> TransactionVersion { + self.frame.version() + } + + /// Return the number of required signatures in the transaction. + #[inline] + pub fn num_required_signatures(&self) -> u8 { + self.frame.num_required_signatures() + } + + /// Return the number of readonly signed static accounts in the transaction. + #[inline] + pub fn num_readonly_signed_static_accounts(&self) -> u8 { + self.frame.num_readonly_signed_static_accounts() + } + + /// Return the number of readonly unsigned static accounts in the transaction. + #[inline] + pub fn num_readonly_unsigned_static_accounts(&self) -> u8 { + self.frame.num_readonly_unsigned_static_accounts() + } + + /// Return the number of static account keys in the transaction. + #[inline] + pub fn num_static_account_keys(&self) -> u8 { + self.frame.num_static_account_keys() + } + + /// Return the number of instructions in the transaction. + #[inline] + pub fn num_instructions(&self) -> u16 { + self.frame.num_instructions() + } + + /// Return the number of address table lookups in the transaction. + #[inline] + pub fn num_address_table_lookups(&self) -> u8 { + self.frame.num_address_table_lookups() + } + + /// Return the number of writable lookup accounts in the transaction. + #[inline] + pub fn total_writable_lookup_accounts(&self) -> u16 { + self.frame.total_writable_lookup_accounts() + } + + /// Return the number of readonly lookup accounts in the transaction. + #[inline] + pub fn total_readonly_lookup_accounts(&self) -> u16 { + self.frame.total_readonly_lookup_accounts() + } + + /// Return the slice of signatures in the transaction. + #[inline] + pub fn signatures(&self) -> &[Signature] { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.signatures(data) } + } + + /// Return the slice of static account keys in the transaction. + #[inline] + pub fn static_account_keys(&self) -> &[Pubkey] { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.static_account_keys(data) } + } + + /// Return the recent blockhash in the transaction. + #[inline] + pub fn recent_blockhash(&self) -> &Hash { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.recent_blockhash(data) } + } + + /// Return an iterator over the instructions in the transaction. + #[inline] + pub fn instructions_iter(&self) -> InstructionsIterator<'_> { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.instructions_iter(data) } + } + + /// Return an iterator over the address table lookups in the transaction. + #[inline] + pub fn address_table_lookup_iter(&self) -> AddressTableLookupIterator<'_> { + let data = self.data(); + // SAFETY: `frame` was created from `data`. + unsafe { self.frame.address_table_lookup_iter(data) } + } + + /// Return Some(TransactionConfigView) for V1, None for legacy/V0 + #[inline] + pub fn transaction_config(&self) -> Option> { + let transaction_config_frame = self.frame.transaction_config_frame(); + transaction_config_frame + .is_present() + .then_some(TransactionConfigView { + transaction_config_frame, + bytes: self.data(), + }) + } + + /// Return the full serialized transaction data. + #[inline] + pub fn data(&self) -> &[u8] { + self.data.data() + } + + /// Return the serialized **message** data. + /// This does not include the signatures. + #[inline] + pub fn message_data(&self) -> &[u8] { + let (start, end) = self.frame.message_range(); + &self.data()[start as usize..end as usize] + } + + #[inline] + pub fn inner_data(&self) -> &D { + &self.data + } + + #[inline] + pub fn into_inner_data(self) -> D { + self.data + } +} + +// Implementation that relies on sanitization checks having been run. +impl TransactionView { + /// Return an iterator over the instructions paired with their program ids. + pub fn program_instructions_iter( + &self, + ) -> impl Iterator)> + Clone { + self.instructions_iter().map(|ix| { + let program_id_index = usize::from(ix.program_id_index); + let program_id = &self.static_account_keys()[program_id_index]; + (program_id, ix) + }) + } + + /// Return the number of unsigned static account keys. + #[inline] + pub(crate) fn num_static_unsigned_static_accounts(&self) -> u8 { + self.num_static_account_keys() + .wrapping_sub(self.num_required_signatures()) + } + + /// Return the number of writable unsigned static accounts. + #[inline] + pub(crate) fn num_writable_unsigned_static_accounts(&self) -> u8 { + self.num_static_unsigned_static_accounts() + .wrapping_sub(self.num_readonly_unsigned_static_accounts()) + } + + /// Return the number of writable unsigned static accounts. + #[inline] + pub(crate) fn num_writable_signed_static_accounts(&self) -> u8 { + self.num_required_signatures() + .wrapping_sub(self.num_readonly_signed_static_accounts()) + } + + /// Return the total number of accounts in the transactions. + #[inline] + pub fn total_num_accounts(&self) -> u16 { + u16::from(self.num_static_account_keys()) + .wrapping_add(self.total_writable_lookup_accounts()) + .wrapping_add(self.total_readonly_lookup_accounts()) + } + + /// Return the number of requested writable keys. + #[inline] + pub fn num_requested_write_locks(&self) -> u64 { + u64::from( + u16::from( + (self.num_static_account_keys()) + .wrapping_sub(self.num_readonly_signed_static_accounts()) + .wrapping_sub(self.num_readonly_unsigned_static_accounts()), + ) + .wrapping_add(self.total_writable_lookup_accounts()), + ) + } +} + +// Manual implementation of `Debug` - avoids bound on `D`. +// Prints nicely formatted struct-ish fields even for the iterator fields. +impl Debug for TransactionView { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.debug_struct("TransactionView") + .field("frame", &self.frame) + .field("signatures", &self.signatures()) + .field("static_account_keys", &self.static_account_keys()) + .field("recent_blockhash", &self.recent_blockhash()) + .field("instructions", &self.instructions_iter()) + .field("address_table_lookups", &self.address_table_lookup_iter()) + .finish() + } +} + +impl SVMStaticMessage for TransactionView { + fn version(&self) -> solana_transaction::versioned::TransactionVersion { + self.version().into() + } + + fn num_transaction_signatures(&self) -> u64 { + self.num_required_signatures() as u64 + } + + fn num_write_locks(&self) -> u64 { + self.num_requested_write_locks() + } + + fn recent_blockhash(&self) -> &Hash { + self.recent_blockhash() + } + + fn num_instructions(&self) -> usize { + self.num_instructions() as usize + } + + fn instructions_iter(&self) -> impl Iterator> { + self.instructions_iter() + } + + fn program_instructions_iter( + &self, + ) -> impl Iterator)> + Clone { + self.program_instructions_iter() + } + + fn static_account_keys(&self) -> &[Pubkey] { + self.static_account_keys() + } + + fn fee_payer(&self) -> &Pubkey { + &self.static_account_keys()[0] + } + + fn num_lookup_tables(&self) -> usize { + self.num_address_table_lookups() as usize + } + + fn message_address_table_lookups( + &self, + ) -> impl Iterator> { + self.address_table_lookup_iter() + } +} + +impl SVMStaticMessage for &TransactionView { + fn version(&self) -> solana_transaction::versioned::TransactionVersion { + as SVMStaticMessage>::version(self) + } + + fn num_transaction_signatures(&self) -> u64 { + as SVMStaticMessage>::num_transaction_signatures(self) + } + + fn num_write_locks(&self) -> u64 { + as SVMStaticMessage>::num_write_locks(self) + } + + fn recent_blockhash(&self) -> &Hash { + as SVMStaticMessage>::recent_blockhash(self) + } + + fn num_instructions(&self) -> usize { + as SVMStaticMessage>::num_instructions(self) + } + + fn instructions_iter(&self) -> impl Iterator> { + as SVMStaticMessage>::instructions_iter(self) + } + + fn program_instructions_iter( + &self, + ) -> impl Iterator)> + Clone { + as SVMStaticMessage>::program_instructions_iter(self) + } + + fn static_account_keys(&self) -> &[Pubkey] { + as SVMStaticMessage>::static_account_keys(self) + } + + fn fee_payer(&self) -> &Pubkey { + as SVMStaticMessage>::fee_payer(self) + } + + fn num_lookup_tables(&self) -> usize { + as SVMStaticMessage>::num_lookup_tables(self) + } + + fn message_address_table_lookups( + &self, + ) -> impl Iterator> { + as SVMStaticMessage>::message_address_table_lookups(self) + } +} + +#[cfg(test)] +mod tests { + use { + super::*, + solana_message::{ + Message, MessageHeader, VersionedMessage, compiled_instruction::CompiledInstruction, v1, + }, + solana_pubkey::Pubkey, + solana_signature::Signature, + solana_system_interface::instruction as system_instruction, + solana_transaction::versioned::VersionedTransaction, + }; + + fn verify_transaction_view_frame(tx: &VersionedTransaction) { + let bytes = wincode::serialize(tx).unwrap(); + let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); + + assert_eq!(view.num_signatures(), tx.signatures.len() as u8); + + assert_eq!( + view.num_required_signatures(), + tx.message.header().num_required_signatures + ); + assert_eq!( + view.num_readonly_signed_static_accounts(), + tx.message.header().num_readonly_signed_accounts + ); + assert_eq!( + view.num_readonly_unsigned_static_accounts(), + tx.message.header().num_readonly_unsigned_accounts + ); + + assert_eq!( + view.num_static_account_keys(), + tx.message.static_account_keys().len() as u8 + ); + assert_eq!( + view.num_instructions(), + tx.message.instructions().len() as u16 + ); + assert_eq!( + view.num_address_table_lookups(), + tx.message + .address_table_lookups() + .map(|x| x.len() as u8) + .unwrap_or(0) + ); + + assert!(view.transaction_config().is_none()); + } + + fn multiple_transfers() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + VersionedTransaction { + signatures: vec![Signature::default()], // 1 signature to be valid. + message: VersionedMessage::Legacy(Message::new( + &[ + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + system_instruction::transfer(&payer, &Pubkey::new_unique(), 1), + ], + Some(&payer), + )), + } + } + + #[test] + fn test_multiple_transfers() { + verify_transaction_view_frame(&multiple_transfers()); + } + + fn simple_v1_transaction() -> VersionedTransaction { + let payer = Pubkey::new_unique(); + let program = Pubkey::new_unique(); + + VersionedTransaction { + signatures: vec![Signature::default()], + message: VersionedMessage::V1(v1::Message { + header: MessageHeader { + num_required_signatures: 1, + num_readonly_signed_accounts: 0, + num_readonly_unsigned_accounts: 0, + }, + config: v1::TransactionConfig { + priority_fee: Some(111), + compute_unit_limit: Some(222), + loaded_accounts_data_size_limit: Some(333), + heap_size: Some(1024), + }, + lifetime_specifier: Hash::default(), + account_keys: vec![payer, program], + instructions: vec![CompiledInstruction { + program_id_index: 1, + accounts: vec![0], + data: vec![1, 2, 3, 4], + }], + }), + } + } + + #[test] + fn test_v1_transaction_config_present() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); + + assert!(matches!(view.version(), TransactionVersion::V1)); + + let config = view.transaction_config().expect("v1 should have config"); + assert_eq!(config.priority_fee_lamports().unwrap(), 111); + assert_eq!(config.compute_unit_limit().unwrap(), 222); + assert_eq!(config.loaded_accounts_data_size_limit().unwrap(), 333); + assert_eq!(config.requested_heap_size().unwrap(), 1024); + } + + #[test] + fn test_v1_message_data_excludes_signatures() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); + + let message_data = view.message_data(); + + // For v1, message_data should stop before the signatures region. + assert!(message_data.len() < bytes.len()); + + let full_message = + &bytes[view.frame.message_offset() as usize..view.frame.signatures_offset() as usize]; + assert_eq!(message_data, full_message); + } + + #[test] + fn test_v1_signatures_accessible() { + let tx = simple_v1_transaction(); + let bytes = wincode::serialize(&tx).unwrap(); + let view = TransactionView::try_new_unsanitized(bytes.as_ref()).unwrap(); + + assert_eq!(view.signatures().len(), 1); + assert_eq!(view.static_account_keys().len(), 2); + + let instructions: Vec<_> = view.instructions_iter().collect(); + assert_eq!(instructions.len(), 1); + assert_eq!(instructions[0].program_id_index, 1); + assert_eq!(instructions[0].accounts, &[0]); + assert_eq!(instructions[0].data, &[1, 2, 3, 4]); + } +}