From cb0cc0dd0b47ed9b608718047e528f565bf93163 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 28 Sep 2026 12:33:39 +0200 Subject: [PATCH 1/2] fix: count programdata toward the loaded accounts data size --- CHANGELOG.md | 4 ++ crates/litesvm/src/lib.rs | 32 +++++++++++++- crates/litesvm/tests/compute_budget.rs | 58 +++++++++++++++++++++++++- 3 files changed, 91 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7b903d5c..ae5b7a30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Fixed + +- Count the programdata of loader-v3 programs toward the loaded accounts data size, once per transaction, as Agave does. + ## [0.17.0] - 2026-09-28 ### Added diff --git a/crates/litesvm/src/lib.rs b/crates/litesvm/src/lib.rs index d92239b6..6ca091e7 100644 --- a/crates/litesvm/src/lib.rs +++ b/crates/litesvm/src/lib.rs @@ -310,7 +310,7 @@ much easier. #[cfg(feature = "register-tracing")] use crate::register_tracing::DefaultRegisterTracingCallback; #[cfg(feature = "hashbrown")] -use hashbrown::{hash_map::Entry, HashMap}; +use hashbrown::{hash_map::Entry, HashMap, HashSet}; #[cfg(feature = "persistence-internal")] use indexmap::IndexMap; #[cfg(feature = "precompiles")] @@ -322,7 +322,7 @@ use solana_sysvar::recent_blockhashes::IterItem; #[allow(deprecated)] use solana_sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes}; #[cfg(not(feature = "hashbrown"))] -use std::collections::{hash_map::Entry, HashMap}; +use std::collections::{hash_map::Entry, HashMap, HashSet}; use { crate::{ accounts_db::{load_preverified, visible_deployment_slot, AccountsDb}, @@ -1309,6 +1309,7 @@ impl LiteSVM { } let mut pre_rent_state_infos = Vec::with_capacity(account_keys.len()); + let mut counted_programdata = HashSet::new(); let maybe_accounts = account_keys .iter() .enumerate() @@ -1377,6 +1378,20 @@ impl LiteSVM { )); loaded_tx_data_size.increase_calculated_data_size(loaded_size)?; + // A loader-v3 program also counts its programdata, once, unless the transaction + // loads that account itself: + // https://github.com/anza-xyz/agave/blob/v4.2.0/svm/src/account_loader.rs#L543-L563 + if let Some(programdata_address) = programdata_address(&account).filter(|address| { + !account_keys.iter().any(|key| key == address) + && !counted_programdata.contains(address) + }) { + if let Some(programdata) = self.accounts.get_account(&programdata_address) { + counted_programdata.insert(programdata_address); + loaded_tx_data_size.increase_calculated_data_size( + TRANSACTION_ACCOUNT_BASE_SIZE.saturating_add(programdata.data().len()), + )?; + } + } Ok((*key, account)) }) .collect::>>(); @@ -2035,6 +2050,19 @@ fn execution_result_if_context( } } +/// The programdata account a loader-v3 program account points at. +fn programdata_address(account: &AccountSharedData) -> Option
{ + if !bpf_loader_upgradeable::check_id(account.owner()) { + return None; + } + match UpgradeableLoaderState::deserialize_from(account.data()) { + Ok(UpgradeableLoaderState::Program { + programdata_address, + }) => Some(programdata_address), + _ => None, + } +} + fn execute_tx_helper( sanitized_tx: &SanitizedTransaction, ctx: TransactionContext, diff --git a/crates/litesvm/tests/compute_budget.rs b/crates/litesvm/tests/compute_budget.rs index 9bc5a43d..131d60fb 100644 --- a/crates/litesvm/tests/compute_budget.rs +++ b/crates/litesvm/tests/compute_budget.rs @@ -10,8 +10,9 @@ use { compute_budget::ComputeBudget, compute_budget_limits::MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES, }, solana_compute_budget_interface::ComputeBudgetInstruction, - solana_instruction::error::InstructionError, + solana_instruction::{error::InstructionError, AccountMeta}, solana_keypair::Keypair, + solana_loader_v3_interface::get_program_data_address, solana_message::{ v0::Message as MessageV0, AddressLookupTableAccount, Message, VersionedMessage, }, @@ -331,3 +332,58 @@ fn test_loaded_accounts_data_size_counts_address_lookup_tables() { TransactionError::MaxLoadedAccountsDataSizeExceeded ); } + +#[test_log::test] +fn test_loaded_accounts_data_size_counts_programdata() { + // a loader-v3 program also counts its programdata, once: on its own when + // the transaction does not list it, and only as a listed account when it does + let from_keypair = Keypair::new(); + let from = from_keypair.pubkey(); + let program_id = Address::new_unique(); + let programdata_address = get_program_data_address(&program_id); + + let mut svm = LiteSVM::new(); + svm.airdrop(&from, LAMPORTS_PER_SOL).unwrap(); + let to = create_account_with_data(&mut svm, 0); + svm.add_program( + program_id, + include_bytes!("../test_programs/DF1ow3DqMj3HvTj8i8J9yM2hE9hCrLLXpdbaKZu4ZPnz.so"), + ) + .unwrap(); + let programdata_size = TRANSACTION_ACCOUNT_BASE_SIZE + + svm.get_account(&programdata_address).unwrap().data.len() as u32; + + let tx = |extra_accounts: &[Address], limit| { + let mut transfer = transfer(&from, &to, 1); + transfer.accounts.extend( + extra_accounts + .iter() + .map(|address| AccountMeta::new_readonly(*address, false)), + ); + Transaction::new( + &[&from_keypair], + Message::new( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(limit), + transfer, + ], + Some(&from), + ), + svm.latest_blockhash(), + ) + }; + let unlisted = [program_id]; + let listed = [program_id, programdata_address]; + let unlisted_limit = expected_loaded_data_size(&svm, &tx(&unlisted, 1)) + programdata_size; + let listed_limit = expected_loaded_data_size(&svm, &tx(&listed, 1)); + let txs = [ + tx(&unlisted, unlisted_limit - 1), + tx(&unlisted, unlisted_limit), + tx(&listed, listed_limit - 1), + tx(&listed, listed_limit), + ]; + + let results = txs.map(|tx| svm.send_transaction(tx).map(|_| ()).map_err(|e| e.err)); + let exceeded = Err(TransactionError::MaxLoadedAccountsDataSizeExceeded); + assert_eq!(results, [exceeded.clone(), Ok(()), exceeded, Ok(())]); +} From b7ced6e1851c0a6256085037b73a434d932eba58 Mon Sep 17 00:00:00 2001 From: L0STE Date: Mon, 28 Sep 2026 12:36:02 +0200 Subject: [PATCH 2/2] feat: return the loaded accounts data size in TransactionMetadata --- CHANGELOG.md | 4 ++ crates/litesvm/src/lib.rs | 63 +++++++++++++++++++++++--- crates/litesvm/src/types.rs | 6 +++ crates/litesvm/src/utils/mod.rs | 5 ++ crates/litesvm/tests/compute_budget.rs | 36 +++++++++++++++ crates/persistence/src/types.rs | 3 +- 6 files changed, 109 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ae5b7a30..b71224e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- `TransactionMetadata::loaded_accounts_data_size`: the loaded accounts data size the transaction was metered at, as SIMD-0186 counts it. Code that builds `TransactionMetadata` with a struct literal needs to set it. Persistence snapshots do not store it, so restored history reports 0. + ### Fixed - Count the programdata of loader-v3 programs toward the loaded accounts data size, once per transaction, as Agave does. diff --git a/crates/litesvm/src/lib.rs b/crates/litesvm/src/lib.rs index 6ca091e7..3dc69cda 100644 --- a/crates/litesvm/src/lib.rs +++ b/crates/litesvm/src/lib.rs @@ -1265,6 +1265,7 @@ impl LiteSVM { Option>, u64, Option
, + u32, ) where 'a: 'b, @@ -1305,7 +1306,14 @@ impl LiteSVM { .num_lookup_tables() .saturating_mul(ADDRESS_LOOKUP_TABLE_BASE_SIZE), ) { - return (Err(e), accumulated_consume_units, None, fee, payer_key); + return ( + Err(e), + accumulated_consume_units, + None, + fee, + payer_key, + loaded_tx_data_size.size(), + ); } let mut pre_rent_state_infos = Vec::with_capacity(account_keys.len()); @@ -1398,7 +1406,14 @@ impl LiteSVM { let accounts = match maybe_accounts { Ok(accs) => accs, Err(e) => { - return (Err(e), accumulated_consume_units, None, fee, payer_key); + return ( + Err(e), + accumulated_consume_units, + None, + fee, + payer_key, + loaded_tx_data_size.size(), + ); } }; if !validated_fee_payer { @@ -1409,6 +1424,7 @@ impl LiteSVM { None, fee, payer_key, + loaded_tx_data_size.size(), ); } let maybe_program_indices = tx @@ -1507,9 +1523,17 @@ impl LiteSVM { Some(context), fee, payer_key, + loaded_tx_data_size.size(), ) } - Err(e) => (Err(e), accumulated_consume_units, None, fee, payer_key), + Err(e) => ( + Err(e), + accumulated_consume_units, + None, + fee, + payer_key, + loaded_tx_data_size.size(), + ), } } @@ -1547,13 +1571,20 @@ impl LiteSVM { }, fee, payer_key, + loaded_accounts_data_size, } = match self.check_and_process_transaction(sanitized_tx, log_collector) { Ok(value) => value, Err(value) => return value, }; if let Some(ctx) = context { - let mut exec_result = - execution_result_if_context(sanitized_tx, ctx, result, compute_units_consumed, fee); + let mut exec_result = execution_result_if_context( + sanitized_tx, + ctx, + result, + compute_units_consumed, + fee, + loaded_accounts_data_size, + ); if let Some(payer) = payer_key.filter(|_| exec_result.tx_result.is_err()) { exec_result.tx_result = self @@ -1567,6 +1598,7 @@ impl LiteSVM { tx_result: result, compute_units_consumed, fee, + loaded_accounts_data_size, ..Default::default() } } @@ -1585,18 +1617,27 @@ impl LiteSVM { context, }, fee, + loaded_accounts_data_size, .. } = match self.check_and_process_transaction(sanitized_tx, log_collector) { Ok(value) => value, Err(value) => return value, }; if let Some(ctx) = context { - execution_result_if_context(sanitized_tx, ctx, result, compute_units_consumed, fee) + execution_result_if_context( + sanitized_tx, + ctx, + result, + compute_units_consumed, + fee, + loaded_accounts_data_size, + ) } else { ExecutionResult { tx_result: result, compute_units_consumed, fee, + loaded_accounts_data_size, ..Default::default() } } @@ -1613,7 +1654,7 @@ impl LiteSVM { self.maybe_blockhash_check(sanitized_tx)?; let tx_config = get_transaction_config(sanitized_tx, &self.feature_set)?; self.maybe_history_check(sanitized_tx)?; - let (result, compute_units_consumed, context, fee, payer_key) = + let (result, compute_units_consumed, context, fee, payer_key, loaded_accounts_data_size) = self.process_transaction(sanitized_tx, tx_config, log_collector); #[cfg(target_arch = "x86_64")] unsafe { @@ -1629,6 +1670,7 @@ impl LiteSVM { }, fee, payer_key, + loaded_accounts_data_size, }) } @@ -1692,6 +1734,7 @@ impl LiteSVM { return_data, included, fee, + loaded_accounts_data_size, } = if self.sigverify { self.execute_transaction(vtx, log_collector.clone()) } else { @@ -1707,6 +1750,7 @@ impl LiteSVM { return_data, signature, fee, + loaded_accounts_data_size, }; if let Err(tx_err) = tx_result { @@ -1746,6 +1790,7 @@ impl LiteSVM { inner_instructions, return_data, fee, + loaded_accounts_data_size, .. } = if self.sigverify { self.execute_transaction_readonly(tx.into(), log_collector.clone()) @@ -1762,6 +1807,7 @@ impl LiteSVM { compute_units_consumed, return_data, fee, + loaded_accounts_data_size, }; if let Err(tx_err) = tx_result { @@ -2027,6 +2073,7 @@ struct CheckAndProcessTransactionSuccess<'ix_data> { core: CheckAndProcessTransactionSuccessCore<'ix_data>, fee: u64, payer_key: Option
, + loaded_accounts_data_size: u32, } fn execution_result_if_context( @@ -2035,6 +2082,7 @@ fn execution_result_if_context( result: Result<(), TransactionError>, compute_units_consumed: u64, fee: u64, + loaded_accounts_data_size: u32, ) -> ExecutionResult { let (signature, return_data, inner_instructions, post_accounts) = execute_tx_helper(sanitized_tx, ctx); @@ -2047,6 +2095,7 @@ fn execution_result_if_context( return_data, included: true, fee, + loaded_accounts_data_size, } } diff --git a/crates/litesvm/src/types.rs b/crates/litesvm/src/types.rs index 5e8c5350..0992e888 100644 --- a/crates/litesvm/src/types.rs +++ b/crates/litesvm/src/types.rs @@ -20,6 +20,10 @@ pub struct TransactionMetadata { pub compute_units_consumed: u64, pub return_data: TransactionReturnData, pub fee: u64, + /// The size of the accounts the transaction loaded, as SIMD-0186 counts it + /// against the transaction's loaded accounts data size limit. + #[cfg_attr(feature = "serde", serde(default))] + pub loaded_accounts_data_size: u32, } impl TransactionMetadata { @@ -66,6 +70,7 @@ pub(crate) struct ExecutionResult { /// Whether the transaction can be included in a block pub(crate) included: bool, pub(crate) fee: u64, + pub(crate) loaded_accounts_data_size: u32, } impl Default for ExecutionResult { @@ -79,6 +84,7 @@ impl Default for ExecutionResult { return_data: Default::default(), included: false, fee: 0, + loaded_accounts_data_size: 0, } } } diff --git a/crates/litesvm/src/utils/mod.rs b/crates/litesvm/src/utils/mod.rs index f8a6f215..34bed2c1 100644 --- a/crates/litesvm/src/utils/mod.rs +++ b/crates/litesvm/src/utils/mod.rs @@ -58,6 +58,11 @@ impl LoadedTransactionDataSize { } } + /// The loaded accounts data size counted so far. + pub(crate) fn size(&self) -> u32 { + self.loaded_accounts_data_size + } + /// Increases the loaded accounts data size by the given delta, and checks if it exceeds the requested limit. pub(crate) fn increase_calculated_data_size( &mut self, diff --git a/crates/litesvm/tests/compute_budget.rs b/crates/litesvm/tests/compute_budget.rs index 131d60fb..677c4ab2 100644 --- a/crates/litesvm/tests/compute_budget.rs +++ b/crates/litesvm/tests/compute_budget.rs @@ -279,6 +279,42 @@ fn test_loaded_accounts_data_size_counts_account_base_size() { svm.send_transaction(tx).unwrap(); } +#[test_log::test] +fn test_transaction_metadata_reports_loaded_accounts_data_size() { + // the size metered against the limit is reported when the transaction is + // simulated, when it succeeds, and when it fails in execution + let from_keypair = Keypair::new(); + let from = from_keypair.pubkey(); + + let mut svm = LiteSVM::new(); + svm.airdrop(&from, LAMPORTS_PER_SOL).unwrap(); + let to = create_account_with_data(&mut svm, 10_000); + + let limit = MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES.get(); + let tx = transfer_tx_with_data_size_limit(&svm, &from_keypair, &to, limit); + let expected = expected_loaded_data_size(&svm, &tx); + let overdraft = Transaction::new( + &[&from_keypair], + Message::new( + &[ + ComputeBudgetInstruction::set_loaded_accounts_data_size_limit(limit), + transfer(&from, &to, 2 * LAMPORTS_PER_SOL), + ], + Some(&from), + ), + svm.latest_blockhash(), + ); + + let simulated = svm.simulate_transaction(tx.clone()).unwrap().meta; + let succeeded = svm.send_transaction(tx).unwrap(); + let failed = svm.send_transaction(overdraft).unwrap_err().meta; + + assert_eq!( + [simulated, succeeded, failed].map(|meta| meta.loaded_accounts_data_size), + [expected; 3] + ); +} + #[test_log::test] fn test_loaded_accounts_data_size_counts_address_lookup_tables() { // a resolved lookup table is charged a base size of its own, on top of the diff --git a/crates/persistence/src/types.rs b/crates/persistence/src/types.rs index daf550cb..69f16b80 100644 --- a/crates/persistence/src/types.rs +++ b/crates/persistence/src/types.rs @@ -204,7 +204,8 @@ impl From for (Address, u64) { } } -wire!(TransactionMetadataWire => TransactionMetadata { +// `loaded_accounts_data_size` is not persisted, so restored history reports 0. +wire!(TransactionMetadataWire => TransactionMetadata [..Default::default()] { signature: Signature, logs: Vec, #[wincode(with = "Vec>>")]