From 8efeab0f6aeabb7b9fe700ad63560c9d1997d50f Mon Sep 17 00:00:00 2001 From: Kevin Heavey Date: Tue, 2 Apr 2024 19:07:24 +0400 Subject: [PATCH] Handle base fees and include failed transactions in transaction history (#50) --- Cargo.lock | 48 +++++- Cargo.toml | 2 + src/accounts_db.rs | 35 ++++- src/history.rs | 10 +- src/lib.rs | 246 ++++++++++++++++++++++++++----- src/types.rs | 15 +- test_programs/Cargo.lock | 7 + test_programs/Cargo.toml | 2 +- test_programs/failure/Cargo.toml | 10 ++ test_programs/failure/src/lib.rs | 19 +++ tests/compute_budget.rs | 8 +- tests/fees.rs | 77 ++++++++++ tests/spl.rs | 9 +- tests/system.rs | 25 ++-- 14 files changed, 440 insertions(+), 73 deletions(-) create mode 100644 test_programs/failure/Cargo.toml create mode 100644 test_programs/failure/src/lib.rs create mode 100644 tests/fees.rs diff --git a/Cargo.lock b/Cargo.lock index 258a4d9..ab19775 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1417,6 +1417,15 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "env_filter" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a009aa4810eb158359dda09d0c87378e4bbb89b5a801f016885a4707ba24f7ea" +dependencies = [ + "log", +] + [[package]] name = "env_logger" version = "0.9.3" @@ -1430,6 +1439,16 @@ dependencies = [ "termcolor", ] +[[package]] +name = "env_logger" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38b35839ba51819680ba087cd351788c9a3c476841207e0b8cee0b04722343b9" +dependencies = [ + "env_filter", + "log", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -2166,6 +2185,7 @@ dependencies = [ "criterion", "indexmap 2.2.6", "itertools 0.12.1", + "log", "solana-address-lookup-table-program", "solana-bpf-loader-program", "solana-compute-budget-program", @@ -2176,6 +2196,7 @@ dependencies = [ "solana-sdk", "solana-system-program", "spl-token 3.5.0", + "test-log", "thiserror", "tokio", ] @@ -2192,9 +2213,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.20" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "lru" @@ -4018,7 +4039,7 @@ version = "1.18.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c5559aeadd3adc219fa7169e96a8c5dda618c7f06985f91f2a5f55b9814c7a2" dependencies = [ - "env_logger", + "env_logger 0.9.3", "lazy_static", "log", ] @@ -5261,6 +5282,27 @@ dependencies = [ "test-case-core", ] +[[package]] +name = "test-log" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b319995299c65d522680decf80f2c108d85b861d81dfe340a10d16cee29d9e6" +dependencies = [ + "env_logger 0.11.3", + "test-log-macros", +] + +[[package]] +name = "test-log-macros" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8f546451eaa38373f549093fe9fd05e7d2bade739e2ddf834b9968621d60107" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.48", +] + [[package]] name = "textwrap" version = "0.11.0" diff --git a/Cargo.toml b/Cargo.toml index d1ff04d..4e0abcf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,12 +17,14 @@ solana-loader-v4-program = "~1.18" bincode = "1.3" indexmap = "2.2.6" solana-address-lookup-table-program = "1.18.8" +log = "0.4.21" [dev-dependencies] spl-token = "3.5.0" solana-program-test = "~1.18" criterion = "0.5" tokio = "1.35" +test-log = "0.2.15" [features] internal-test = [] diff --git a/src/accounts_db.rs b/src/accounts_db.rs index c875adf..0b9433e 100644 --- a/src/accounts_db.rs +++ b/src/accounts_db.rs @@ -21,10 +21,13 @@ use solana_program_runtime::{ sysvar_cache::SysvarCache, }; use solana_sdk::{ - account::{AccountSharedData, ReadableAccount}, + account::{AccountSharedData, ReadableAccount, WritableAccount}, account_utils::StateMut, + nonce, pubkey::Pubkey, + transaction::TransactionError, }; +use solana_system_program::{get_system_account_kind, SystemAccountKind}; use std::{collections::HashMap, sync::Arc}; use crate::types::InvalidSysvarDataError; @@ -289,6 +292,36 @@ impl AccountsDb { Err(AddressLookupError::InvalidAccountOwner) } } + + pub(crate) fn withdraw( + &mut self, + pubkey: &Pubkey, + lamports: u64, + ) -> solana_sdk::transaction::Result<()> { + match self.inner.get_mut(pubkey) { + Some(account) => { + let min_balance = match get_system_account_kind(account) { + Some(SystemAccountKind::Nonce) => self + .sysvar_cache + .get_rent() + .unwrap() + .minimum_balance(nonce::State::size()), + _ => 0, + }; + + lamports + .checked_add(min_balance) + .filter(|required_balance| *required_balance <= account.lamports()) + .ok_or(TransactionError::InsufficientFundsForFee)?; + account + .checked_sub_lamports(lamports) + .map_err(|_| TransactionError::InsufficientFundsForFee)?; + + Ok(()) + } + None => Err(TransactionError::AccountNotFound), + } + } } fn into_address_loader_error(err: AddressLookupError) -> AddressLoaderError { diff --git a/src/history.rs b/src/history.rs index b9423ac..a9d3e2e 100644 --- a/src/history.rs +++ b/src/history.rs @@ -1,8 +1,8 @@ -use crate::types::TransactionMetadata; +use crate::types::TransactionResult; use indexmap::IndexMap; use solana_sdk::signature::Signature; -pub struct TransactionHistory(IndexMap); +pub struct TransactionHistory(IndexMap); impl TransactionHistory { pub fn new() -> Self { @@ -17,17 +17,17 @@ impl TransactionHistory { } } - pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionMetadata> { + pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> { self.0.get(signature) } - pub fn add_new_transaction(&mut self, signature: Signature, meta: TransactionMetadata) { + pub fn add_new_transaction(&mut self, signature: Signature, result: TransactionResult) { let capacity = self.0.capacity(); if capacity != 0 { if self.0.len() == capacity { self.0.shift_remove_index(0); } - self.0.insert(signature, meta); + self.0.insert(signature, result); } } diff --git a/src/lib.rs b/src/lib.rs index c252008..9522ced 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ #![allow(clippy::result_large_err)] +use log::error; use itertools::Itertools; use solana_bpf_loader_program::syscalls::create_program_runtime_environment_v1; @@ -7,6 +8,7 @@ use solana_loader_v4_program::create_program_runtime_environment_v2; use solana_program::sysvar::{fees::Fees, recent_blockhashes::RecentBlockhashes}; use solana_program_runtime::{ compute_budget::ComputeBudget, + compute_budget_processor::{process_compute_budget_instructions, ComputeBudgetLimits}, invoke_context::BuiltinFunctionWithContext, loaded_programs::{LoadProgramMetrics, LoadedProgram, LoadedProgramsForTxBatch}, log_collector::LogCollector, @@ -19,7 +21,8 @@ use solana_sdk::{ clock::Clock, epoch_rewards::EpochRewards, epoch_schedule::EpochSchedule, - feature_set::FeatureSet, + feature_set::{include_loaded_accounts_data_size_in_fee_calculation, FeatureSet}, + fee::FeeStructure, hash::Hash, message::{Message, VersionedMessage}, native_loader, @@ -37,6 +40,7 @@ use solana_sdk::{ transaction::{MessageHash, SanitizedTransaction, TransactionError, VersionedTransaction}, transaction_context::{ExecutionRecord, IndexOfAccount, TransactionContext}, }; +use solana_system_program::{get_system_account_kind, SystemAccountKind}; use std::{cell::RefCell, rc::Rc, sync::Arc}; use utils::construct_instructions_account; @@ -73,6 +77,7 @@ pub struct LiteSVM { history: TransactionHistory, compute_budget: Option, sigverify: bool, + fee_structure: FeeStructure, } impl Default for LiteSVM { @@ -86,6 +91,7 @@ impl Default for LiteSVM { history: TransactionHistory::new(), compute_budget: None, sigverify: true, + fee_structure: FeeStructure::default(), } } } @@ -227,7 +233,7 @@ impl LiteSVM { bincode::deserialize(self.accounts.get_account(&T::id()).unwrap().data()).unwrap() } - pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionMetadata> { + pub fn get_transaction(&self, signature: &Signature) -> Option<&TransactionResult> { self.history.get_transaction(signature) } @@ -362,8 +368,19 @@ impl LiteSVM { fn process_transaction( &mut self, tx: &SanitizedTransaction, - compute_budget: ComputeBudget, - ) -> (Result<(), TransactionError>, u64, TransactionContext) { + compute_budget_limits: ComputeBudgetLimits, + ) -> ( + Result<(), TransactionError>, + u64, + Option, + u64, + Option, + ) { + let compute_budget = self.compute_budget.unwrap_or_else(|| ComputeBudget { + compute_unit_limit: u64::from(compute_budget_limits.compute_unit_limit), + heap_size: compute_budget_limits.updated_heap_bytes, + ..ComputeBudget::default() + }); let blockhash = tx.message().recent_blockhash(); //reload program cache let mut programs_modified_by_tx = LoadedProgramsForTxBatch::new( @@ -381,7 +398,16 @@ impl LiteSVM { .flat_map(|instruction| &instruction.accounts) .unique() .collect::>(); - let mut accounts = account_keys + let fee = self.fee_structure.calculate_fee( + message, + self.fee_structure.lamports_per_signature, + &compute_budget_limits.into(), + self.feature_set + .is_active(&include_loaded_accounts_data_size_in_fee_calculation::id()), + ); + let mut validated_fee_payer = false; + let mut payer_key = None; + let maybe_accounts = account_keys .iter() .enumerate() .map(|(i, key)| { @@ -393,8 +419,7 @@ impl LiteSVM { let instruction_account = u8::try_from(i) .map(|i| instruction_accounts.contains(&&i)) .unwrap_or(false); - - if !instruction_account + let mut account = if !instruction_account && !message.is_writable(i) && self.accounts.programs_cache.find(key).is_some() { @@ -408,12 +433,41 @@ impl LiteSVM { default_account.set_rent_epoch(0); default_account }) + }; + if !validated_fee_payer && message.is_non_loader_key(i) { + validate_fee_payer( + key, + &mut account, + i as IndexOfAccount, + &self.accounts.sysvar_cache.get_rent().unwrap(), + fee, + )?; + validated_fee_payer = true; + payer_key = Some(*key); } + account }; - (*key, account) + Ok((*key, account)) }) - .collect::>(); + .collect::>>(); + + let mut accounts = match maybe_accounts { + Ok(accs) => accs, + Err(e) => { + return (Err(e), accumulated_consume_units, None, fee, payer_key); + } + }; + if !validated_fee_payer { + error!("Failed to validate fee payer"); + return ( + Err(TransactionError::AccountNotFound), + accumulated_consume_units, + None, + fee, + payer_key, + ); + } let builtins_start_index = accounts.len(); let program_indices = tx .message() @@ -422,7 +476,7 @@ impl LiteSVM { .map(|c| { let mut account_indices: Vec = Vec::with_capacity(2); let program_index = c.program_id_index as usize; - // This command may never return error, because the transaction is sanitized + // This may never error, because the transaction is sanitized let (program_id, program_account) = accounts.get(program_index).unwrap(); if native_loader::check_id(program_id) { return account_indices; @@ -477,7 +531,13 @@ impl LiteSVM { tx_result = Err(err); }; - (tx_result, accumulated_consume_units, context) + ( + tx_result, + accumulated_consume_units, + Some(context), + fee, + payer_key, + ) } fn check_accounts_rent( @@ -521,7 +581,7 @@ impl LiteSVM { return ExecutionResult { tx_result: Err(err), ..Default::default() - } + }; } }; self.execute_sanitized_transaction(sanitized_tx) @@ -544,11 +604,16 @@ impl LiteSVM { &mut self, sanitized_tx: SanitizedTransaction, ) -> ExecutionResult { - let compute_budget = self.compute_budget.unwrap_or_else(|| { - let instructions = sanitized_tx.message().program_instructions_iter(); - ComputeBudget::try_from_instructions(instructions).unwrap_or_default() - }); - + let instructions = sanitized_tx.message().program_instructions_iter(); + let compute_budget_limits = match process_compute_budget_instructions(instructions) { + Ok(x) => x, + Err(e) => { + return ExecutionResult { + tx_result: Err(e), + ..Default::default() + }; + } + }; if self.history.check_transaction(sanitized_tx.signature()) { return ExecutionResult { tx_result: Err(TransactionError::AlreadyProcessed), @@ -556,28 +621,49 @@ impl LiteSVM { }; } - let (result, compute_units_consumed, context) = - self.process_transaction(&sanitized_tx, compute_budget); - let signature = sanitized_tx.signature().to_owned(); - let ExecutionRecord { - accounts, - return_data, - touched_account_count: _, - accounts_resize_delta: _, - } = context.into(); - let msg = sanitized_tx.message(); - let post_accounts = accounts - .into_iter() - .enumerate() - .filter_map(|(idx, pair)| msg.is_writable(idx).then_some(pair)) - .collect(); + let (result, compute_units_consumed, context, fee, payer_key) = + self.process_transaction(&sanitized_tx, compute_budget_limits); + if let Some(ctx) = context { + let signature = sanitized_tx.signature().to_owned(); + let ExecutionRecord { + accounts, + return_data, + touched_account_count: _, + accounts_resize_delta: _, + } = ctx.into(); + let msg = sanitized_tx.message(); + let post_accounts = accounts + .into_iter() + .enumerate() + .filter_map(|(idx, pair)| msg.is_writable(idx).then_some(pair)) + .collect(); + let tx_result = if result.is_ok() { + result + } else if let Some(payer) = payer_key { + let withdraw_res = self.accounts.withdraw(&payer, fee); + if withdraw_res.is_err() { + withdraw_res + } else { + result + } + } else { + result + }; - ExecutionResult { - tx_result: result, - signature, - post_accounts, - compute_units_consumed, - return_data, + ExecutionResult { + tx_result, + signature, + post_accounts, + compute_units_consumed, + return_data, + included: true, + } + } else { + ExecutionResult { + tx_result: result, + compute_units_consumed, + ..Default::default() + } } } @@ -598,6 +684,7 @@ impl LiteSVM { signature, compute_units_consumed, return_data, + included, } = if self.sigverify { self.execute_transaction(vtx) } else { @@ -612,10 +699,14 @@ impl LiteSVM { }; if let Err(tx_err) = tx_result { - TransactionResult::Err(FailedTransactionMetadata { err: tx_err, meta }) + let err = TransactionResult::Err(FailedTransactionMetadata { err: tx_err, meta }); + if included { + self.history.add_new_transaction(signature, err.clone()); + } + err } else { self.history - .add_new_transaction(meta.signature, meta.clone()); + .add_new_transaction(signature, Ok(meta.clone())); self.accounts .sync_accounts(post_accounts) .expect("It shouldn't be possible to write invalid sysvars in send_transaction."); @@ -631,6 +722,7 @@ impl LiteSVM { signature, compute_units_consumed, return_data, + .. } = if self.sigverify { self.execute_transaction(tx) } else { @@ -675,3 +767,77 @@ impl LiteSVM { self.feature_set.clone() } } + +/// Lighter version of the one in the solana-svm crate. +/// +/// 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_acount is not able to pay the +/// fee a specific error is returned. +fn validate_fee_payer( + payer_address: &Pubkey, + payer_account: &mut AccountSharedData, + payer_index: IndexOfAccount, + rent: &Rent, + fee: u64, +) -> solana_sdk::transaction::Result<()> { + if payer_account.lamports() == 0 { + error!("Payer account not found."); + return Err(TransactionError::AccountNotFound); + } + let system_account_kind = get_system_account_kind(payer_account).ok_or_else(|| { + error!("Payer account is not a system account"); + 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(solana_sdk::nonce::State::size()) + } + }; + + let payer_lamports = payer_account.lamports(); + + payer_lamports + .checked_sub(min_balance) + .and_then(|v| v.checked_sub(fee)) + .ok_or_else(|| { + error!( + "Payer account has insufficient lamports for fee. Payer lamports: \ + {payer_lamports} min_balance: {min_balance} fee: {fee}" + ); + TransactionError::InsufficientFundsForFee + })?; + + let payer_pre_rent_state = RentState::from_account(payer_account, rent); + // we already checked above if we have sufficient balance so this should never error. + payer_account.checked_sub_lamports(fee).unwrap(); + + let payer_post_rent_state = RentState::from_account(payer_account, rent); + check_rent_state_with_account( + &payer_pre_rent_state, + &payer_post_rent_state, + payer_address, + payer_index, + ) +} + +// modified version of the private fn in solana-svm +fn check_rent_state_with_account( + pre_rent_state: &RentState, + post_rent_state: &RentState, + address: &Pubkey, + account_index: IndexOfAccount, +) -> solana_sdk::transaction::Result<()> { + if !solana_sdk::incinerator::check_id(address) + && !post_rent_state.transition_allowed_from(pre_rent_state) + { + let account_index = account_index as u8; + error!("Transaction would leave account {address} with insufficient funds for rent"); + Err(TransactionError::InsufficientFundsForRent { account_index }) + } else { + Ok(()) + } +} diff --git a/src/types.rs b/src/types.rs index 4c7d9c3..0bed3fc 100644 --- a/src/types.rs +++ b/src/types.rs @@ -15,7 +15,7 @@ pub struct TransactionMetadata { pub return_data: TransactionReturnData, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct FailedTransactionMetadata { pub err: TransactionError, pub meta: TransactionMetadata, @@ -24,11 +24,13 @@ pub struct FailedTransactionMetadata { pub type TransactionResult = std::result::Result; pub(crate) struct ExecutionResult { - pub post_accounts: Vec<(Pubkey, AccountSharedData)>, - pub tx_result: Result<()>, - pub signature: Signature, - pub compute_units_consumed: u64, - pub return_data: TransactionReturnData, + pub(crate) post_accounts: Vec<(Pubkey, AccountSharedData)>, + pub(crate) tx_result: Result<()>, + pub(crate) signature: Signature, + pub(crate) compute_units_consumed: u64, + pub(crate) return_data: TransactionReturnData, + /// Whether the transaction can be included in a block + pub(crate) included: bool, } impl Default for ExecutionResult { @@ -39,6 +41,7 @@ impl Default for ExecutionResult { signature: Default::default(), compute_units_consumed: Default::default(), return_data: Default::default(), + included: false, } } } diff --git a/test_programs/Cargo.lock b/test_programs/Cargo.lock index e4af9ed..34bfe46 100644 --- a/test_programs/Cargo.lock +++ b/test_programs/Cargo.lock @@ -578,6 +578,13 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "failure" +version = "0.1.0" +dependencies = [ + "solana-program", +] + [[package]] name = "feature-probe" version = "0.1.1" diff --git a/test_programs/Cargo.toml b/test_programs/Cargo.toml index ea410ce..a2396b2 100644 --- a/test_programs/Cargo.toml +++ b/test_programs/Cargo.toml @@ -1,5 +1,5 @@ [workspace] -members = ["counter"] +members = ["counter", "failure"] resolver = "2" [workspace.dependencies] diff --git a/test_programs/failure/Cargo.toml b/test_programs/failure/Cargo.toml new file mode 100644 index 0000000..d07ea73 --- /dev/null +++ b/test_programs/failure/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "failure" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib", "lib"] + +[dependencies] +solana-program.workspace = true diff --git a/test_programs/failure/src/lib.rs b/test_programs/failure/src/lib.rs new file mode 100644 index 0000000..fddd4e8 --- /dev/null +++ b/test_programs/failure/src/lib.rs @@ -0,0 +1,19 @@ +// This program just returns an error. + +use solana_program::entrypoint; +use solana_program::{ + account_info::AccountInfo, declare_id, entrypoint::ProgramResult, program_error::ProgramError, + pubkey::Pubkey, +}; + +declare_id!("HvrRMSshMx3itvsyWDnWg2E3cy5h57iMaR7oVxSZJDSA"); + +entrypoint!(process_instruction); + +pub fn process_instruction( + _program_id: &Pubkey, + _accounts: &[AccountInfo], + _instruction_data: &[u8], +) -> ProgramResult { + Err(ProgramError::Custom(0)) +} diff --git a/tests/compute_budget.rs b/tests/compute_budget.rs index a5f9b82..d52a1e1 100644 --- a/tests/compute_budget.rs +++ b/tests/compute_budget.rs @@ -10,7 +10,7 @@ use solana_sdk::{ transaction::{Transaction, TransactionError}, }; -#[test] +#[test_log::test] fn test_set_compute_budget() { // see that the tx fails if we set a tiny limit let from_keypair = Keypair::new(); @@ -18,8 +18,9 @@ fn test_set_compute_budget() { let to = Pubkey::new_unique(); let mut svm = LiteSVM::new(); + let tx_fee = 5000; - svm.airdrop(&from, 100).unwrap(); + svm.airdrop(&from, tx_fee + 100).unwrap(); svm.set_compute_budget(ComputeBudget { compute_unit_limit: 10, ..Default::default() @@ -46,8 +47,9 @@ fn test_set_compute_unit_limit() { let to = Pubkey::new_unique(); let mut svm = LiteSVM::new(); + let tx_fee = 5000; - svm.airdrop(&from, 100).unwrap(); + svm.airdrop(&from, tx_fee + 100).unwrap(); let instruction = transfer(&from, &to, 64); let tx = Transaction::new( diff --git a/tests/fees.rs b/tests/fees.rs new file mode 100644 index 0000000..7616056 --- /dev/null +++ b/tests/fees.rs @@ -0,0 +1,77 @@ +use std::path::PathBuf; + +use litesvm::LiteSVM; +use solana_program::{message::Message, pubkey::Pubkey, system_instruction::transfer}; +use solana_sdk::{ + instruction::{Instruction, InstructionError}, + pubkey, + rent::Rent, + signature::Keypair, + signer::Signer, + transaction::{Transaction, TransactionError}, +}; + +#[test_log::test] +fn test_insufficient_funds_for_rent() { + let from_keypair = Keypair::new(); + let from = from_keypair.pubkey(); + let to = Pubkey::new_unique(); + + let mut svm = LiteSVM::new(); + + svm.airdrop(&from, svm.get_sysvar::().minimum_balance(0)) + .unwrap(); + let instruction = transfer(&from, &to, 1); + let tx = Transaction::new( + &[&from_keypair], + Message::new(&[instruction], Some(&from)), + svm.latest_blockhash(), + ); + let signature = tx.signatures[0]; + let tx_res = svm.send_transaction(tx); + + assert_eq!( + tx_res.unwrap_err().err, + TransactionError::InsufficientFundsForRent { account_index: 0 } + ); + assert!(svm.get_transaction(&signature).is_none()); +} + +fn read_failure_program() -> Vec { + let mut so_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + so_path.push("test_programs/target/deploy/failure.so"); + std::fs::read(so_path).unwrap() +} + +#[test_log::test] +fn test_fees_failed_transaction() { + let from_keypair = Keypair::new(); + let from = from_keypair.pubkey(); + + let mut svm = LiteSVM::new(); + let program_id = pubkey!("HvrRMSshMx3itvsyWDnWg2E3cy5h57iMaR7oVxSZJDSA"); + svm.add_program(program_id, &read_failure_program()); + let initial_balance = 1_000_000_000; + svm.airdrop(&from, initial_balance).unwrap(); + let instruction = Instruction { + program_id, + accounts: vec![], + data: vec![], + }; + let tx = Transaction::new( + &[&from_keypair], + Message::new(&[instruction], Some(&from)), + svm.latest_blockhash(), + ); + let signature = tx.signatures[0]; + let tx_res = svm.send_transaction(tx); + + assert_eq!( + tx_res.unwrap_err().err, + TransactionError::InstructionError(0, InstructionError::Custom(0)) + ); + let balance_after = svm.get_balance(&from).unwrap(); + let expected_fee = 5000; + assert_eq!(initial_balance - balance_after, expected_fee); + assert!(svm.get_transaction(&signature).unwrap().is_err()); +} diff --git a/tests/spl.rs b/tests/spl.rs index ca56a81..8480229 100644 --- a/tests/spl.rs +++ b/tests/spl.rs @@ -1,6 +1,6 @@ use litesvm::LiteSVM; use solana_sdk::{ - program_pack::Pack, signature::Keypair, signer::Signer, system_instruction, + program_pack::Pack, rent::Rent, signature::Keypair, signer::Signer, system_instruction, transaction::Transaction, }; @@ -27,6 +27,7 @@ fn spl_token() { spl_token::instruction::initialize_mint2(&spl_token::id(), &mint_pk, &payer_pk, None, 8) .unwrap(); let balance_before = svm.get_balance(&payer_pk).unwrap(); + let expected_fee = 2 * 5000; // two signers let tx_result = svm.send_transaction(Transaction::new_signed_with_payer( &[create_acc_ins, init_mint_ins], Some(&payer_pk), @@ -34,8 +35,12 @@ fn spl_token() { svm.latest_blockhash(), )); assert!(tx_result.is_ok()); + let expected_rent = svm + .get_sysvar::() + .minimum_balance(spl_token::state::Mint::LEN); let balance_after = svm.get_balance(&payer_pk).unwrap(); - assert!(balance_after < balance_before); + + assert_eq!(balance_before - balance_after, expected_rent + expected_fee); let mint_acc = svm.get_account(&mint_kp.pubkey()); let mint = spl_token::state::Mint::unpack(&mint_acc.unwrap().data).unwrap(); diff --git a/tests/system.rs b/tests/system.rs index dbf1eb1..527cd62 100644 --- a/tests/system.rs +++ b/tests/system.rs @@ -6,15 +6,15 @@ use solana_program::{ }; use solana_sdk::{signature::Keypair, signer::Signer, transaction::Transaction}; -#[test] +#[test_log::test] fn system_transfer() { let from_keypair = Keypair::new(); let from = from_keypair.pubkey(); let to = Pubkey::new_unique(); let mut svm = LiteSVM::new(); - - svm.airdrop(&from, 100).unwrap(); + let expected_fee = 5000; + svm.airdrop(&from, 100 + expected_fee).unwrap(); let instruction = transfer(&from, &to, 64); let tx = Transaction::new( @@ -32,22 +32,24 @@ fn system_transfer() { assert_eq!(to_account.unwrap().lamports, 64); } -#[test] +#[test_log::test] fn system_create_account() { let from_keypair = Keypair::new(); let new_account = Keypair::new(); let from = from_keypair.pubkey(); let mut svm = LiteSVM::new(); - - let lamports = svm.minimum_balance_for_rent_exemption(10); + let expected_fee = 5000 * 2; // two signers + let space = 10; + let rent_amount = svm.minimum_balance_for_rent_exemption(space); + let lamports = rent_amount + expected_fee; svm.airdrop(&from, lamports).unwrap(); let instruction = create_account( &from, &new_account.pubkey(), - lamports, - 10, + rent_amount, + space as u64, &solana_program::system_program::id(), ); let tx = Transaction::new( @@ -55,12 +57,11 @@ fn system_create_account() { Message::new(&[instruction], Some(&from)), svm.latest_blockhash(), ); - let tx_res = svm.send_transaction(tx); + svm.send_transaction(tx).unwrap(); let account = svm.get_account(&new_account.pubkey()).unwrap(); - assert!(tx_res.is_ok()); - assert_eq!(account.lamports, lamports); - assert_eq!(account.data.len(), 10); + assert_eq!(account.lamports, rent_amount); + assert_eq!(account.data.len(), space); assert_eq!(account.owner, solana_program::system_program::id()); }