| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411341234133414341534163417341834193420342134223423342434253426342734283429343034313432343334343435343634373438343934403441344234433444344534463447344834493450345134523453345434553456345734583459346034613462346334643465346634673468346934703471347234733474347534763477347834793480348134823483348434853486348734883489349034913492349334943495349634973498349935003501350235033504350535063507350835093510351135123513351435153516351735183519352035213522352335243525352635273528352935303531353235333534353535363537353835393540354135423543354435453546354735483549355035513552355335543555355635573558355935603561356235633564356535663567356835693570357135723573357435753576357735783579358035813582358335843585358635873588358935903591359235933594359535963597359835993600360136023603360436053606360736083609361036113612361336143615361636173618361936203621362236233624362536263627362836293630363136323633363436353636363736383639364036413642364336443645364636473648364936503651365236533654365536563657365836593660366136623663366436653666366736683669367036713672367336743675367636773678367936803681368236833684368536863687368836893690369136923693369436953696369736983699370037013702370337043705370637073708370937103711371237133714371537163717371837193720372137223723372437253726372737283729373037313732373337343735373637373738373937403741374237433744374537463747374837493750375137523753375437553756375737583759376037613762376337643765376637673768376937703771377237733774377537763777377837793780378137823783378437853786378737883789379037913792379337943795379637973798379938003801380238033804380538063807380838093810381138123813381438153816381738183819382038213822382338243825382638273828382938303831383238333834383538363837383838393840384138423843384438453846384738483849385038513852385338543855385638573858385938603861386238633864386538663867386838693870387138723873387438753876387738783879388038813882388338843885388638873888388938903891389238933894389538963897389838993900390139023903390439053906390739083909391039113912391339143915391639173918 |
- #![cfg(test)]
- #![allow(clippy::arithmetic_side_effects)]
- use {
- crate::mock_bank::{
- create_custom_loader, deploy_program_with_upgrade_authority, load_program, program_address,
- program_data_size, register_builtins, MockBankCallback, MockForkGraph, EXECUTION_EPOCH,
- EXECUTION_SLOT, WALLCLOCK_TIME,
- },
- solana_account::{AccountSharedData, ReadableAccount, WritableAccount, PROGRAM_OWNERS},
- 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::{
- SVMTransactionExecutionAndFeeBudgetLimits, MAX_LOADED_ACCOUNTS_DATA_SIZE_BYTES,
- },
- solana_pubkey::Pubkey,
- solana_sdk_ids::{bpf_loader_upgradeable, compute_budget, native_loader},
- solana_signer::Signer,
- solana_svm::{
- account_loader::{CheckedTransactionDetails, 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::{sanitized::SanitizedTransaction, Transaction},
- solana_transaction_context::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<Item = (&'a Pubkey, SVMInstruction<'a>)> + Clone,
- ) -> Result<ComputeBudgetLimits, TransactionError> {
- 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<Pubkey, AccountSharedData>;
- // 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<RwLock<MockForkGraph>>,
- pub batch_processor: TransactionBatchProcessor<MockForkGraph>,
- 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(Arc::new(create_custom_loader())),
- None, // We are not using program runtime v2.
- );
- // 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, test_entry.with_loader_v4);
- 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,
- feature_set: test_entry.feature_set,
- blockhash_lamports_per_signature: LAMPORTS_PER_SIGNATURE,
- program_runtime_environments_for_execution: batch_processor
- .get_environments_for_epoch(EXECUTION_EPOCH),
- program_runtime_environments_for_deployment: batch_processor
- .get_environments_for_epoch(EXECUTION_EPOCH),
- ..TransactionProcessingEnvironment::default()
- };
- 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)) => {
- 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(),
- );
- }
- }
- }
- 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::<Vec<_>>()
- .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.environments, 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,
- // until LoaderV4 is live on mainnet, we default to omitting it, but can also test it
- pub with_loader_v4: bool,
- // 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<Pubkey>)>,
- // 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<TransactionBatchItem>,
- // expected final account states, checked after transaction execution
- pub final_accounts: AccountsMap,
- }
- impl Default for SvmTestEntry {
- fn default() -> Self {
- Self {
- feature_set: SVMFeatureSet::all_enabled(),
- with_loader_v4: false,
- all_or_nothing: false,
- drop_on_failure: false,
- initial_programs: Vec::new(),
- initial_accounts: HashMap::new(),
- transaction_batch: Vec::new(),
- final_accounts: HashMap::new(),
- }
- }
- }
- impl SvmTestEntry {
- pub fn with_loader_v4() -> Self {
- Self {
- with_loader_v4: true,
- ..Self::default()
- }
- }
- // add a new a 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
- // we accept the prior nonce state and advance it for the check status, since this happens before svm
- pub fn push_nonce_transaction(&mut self, transaction: Transaction, nonce_info: NonceInfo) {
- self.push_nonce_transaction_with_status(transaction, nonce_info, ExecutionStatus::Succeeded)
- }
- // convenience function that adds a nonce transaction with an expected execution status
- // we accept the prior nonce state and advance it for the check status, since this happens before svm
- pub fn push_nonce_transaction_with_status(
- &mut self,
- transaction: Transaction,
- mut nonce_info: NonceInfo,
- status: ExecutionStatus,
- ) {
- if status != ExecutionStatus::Discarded {
- nonce_info
- .try_advance_nonce(
- DurableNonce::from_blockhash(&LAST_BLOCKHASH),
- LAMPORTS_PER_SIGNATURE,
- )
- .unwrap();
- }
- self.transaction_batch.push(TransactionBatchItem {
- transaction,
- asserts: TransactionBatchItemAsserts {
- status,
- ..TransactionBatchItemAsserts::default()
- },
- ..TransactionBatchItem::with_nonce(nonce_info)
- });
- }
- // internal helper to gather SanitizedTransaction objects for execution
- fn prepare_transactions(&self) -> (Vec<SanitizedTransaction>, Vec<TransactionCheckResult>) {
- 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, compute_budget)
- });
- (message, check_result)
- })
- .unzip()
- }
- // internal helper to gather test items for post-execution checks
- fn asserts(&self) -> Vec<TransactionBatchItemAsserts> {
- 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_info: NonceInfo) -> Self {
- Self {
- check_result: Ok(CheckedTransactionDetails::new(
- Some(nonce_info),
- 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 (exect match only), and rodata
- #[derive(Clone, Debug, Default)]
- pub struct TransactionBatchItemAsserts {
- pub status: ExecutionStatus,
- pub logs: Vec<String>,
- 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<ExecutionStatus> 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<Option<TransactionReturnData>> for ReturnDataAssert {
- fn from(option_ro_data: Option<TransactionReturnData>) -> Self {
- match option_ro_data {
- Some(ro_data) => Self::Some(ro_data),
- None => Self::None,
- }
- }
- }
- fn program_medley(drop_on_failure: bool) -> Vec<SvmTestEntry> {
- 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);
- 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);
- 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);
- 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<SvmTestEntry> {
- 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);
- 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<SvmTestEntry> {
- 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);
- 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 nonce_account = AccountSharedData::new_data(
- nonce_balance,
- &nonce::versions::Versions::new(nonce::state::State::Initialized(nonce_data.clone())),
- &system_program::id(),
- )
- .unwrap();
- 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.clone());
- 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
- .final_accounts
- .entry(*nonce_info.address())
- .and_modify(|account| account.set_rent_epoch(0));
- test_entry.push_nonce_transaction_with_status(
- transaction,
- nonce_info,
- 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.clone(),
- 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.clone(),
- 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());
- // if the nonce account pays fees, it keeps its new rent epoch, otherwise it resets
- if !fee_paying_nonce {
- test_entry
- .final_accounts
- .get_mut(nonce_info.address())
- .unwrap()
- .set_rent_epoch(0);
- }
- }
- // 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
- .final_accounts
- .get_mut(nonce_info.address())
- .unwrap()
- .set_rent_epoch(0);
- test_entry.push_nonce_transaction_with_status(
- transaction,
- nonce_info.clone(),
- 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
- .final_accounts
- .get_mut(nonce_info.address())
- .unwrap()
- .set_rent_epoch(0);
- test_entry.push_nonce_transaction_with_status(
- transaction,
- nonce_info.clone(),
- ExecutionStatus::Discarded,
- );
- }
- vec![test_entry]
- }
- fn simd83_intrabatch_account_reuse() -> Vec<SvmTestEntry> {
- 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<SvmTestEntry> {
- 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 initial_nonce_account = AccountSharedData::new_data(
- LAMPORTS_PER_SOL,
- &nonce::versions::Versions::new(nonce::state::State::Initialized(
- initial_nonce_data.clone(),
- )),
- &system_program::id(),
- )
- .unwrap();
- 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.clone();
- 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,
- initial_nonce_info.clone(),
- 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, initial_nonce_info.clone());
- test_entry.push_nonce_transaction_with_status(
- second_transaction.clone(),
- advanced_nonce_info.clone(),
- 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,
- initial_nonce_info.clone(),
- ExecutionStatus::ExecutedFailed,
- );
- test_entry.push_nonce_transaction_with_status(
- second_transaction.clone(),
- advanced_nonce_info.clone(),
- 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,
- initial_nonce_info.clone(),
- ExecutionStatus::ProcessedFailed,
- );
- test_entry.push_nonce_transaction_with_status(
- second_transaction.clone(),
- advanced_nonce_info.clone(),
- ExecutionStatus::Discarded,
- );
- // if the nonce account pays fees, it keeps its new rent epoch, otherwise it resets
- if !fee_paying_nonce {
- test_entry
- .final_accounts
- .get_mut(&nonce_pubkey)
- .unwrap()
- .set_rent_epoch(0);
- }
- 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.clone(),
- ],
- Some(&fee_payer),
- &[&fee_payer_keypair],
- Hash::default(),
- );
- test_entry.push_nonce_transaction(first_transaction, initial_nonce_info.clone());
- test_entry.push_nonce_transaction_with_status(
- second_transaction.clone(),
- advanced_nonce_info.clone(),
- ExecutionStatus::Discarded,
- );
- test_entries.push(test_entry);
- }
- for test_entry in &mut test_entries {
- test_entry.add_initial_program(program_name);
- }
- // 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(),
- advanced_nonce_info.clone(),
- 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(),
- advanced_nonce_info.clone(),
- 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(),
- advanced_nonce_info.clone(),
- ExecutionStatus::Discarded,
- );
- test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
- let new_nonce_state = AccountSharedData::create(
- LAMPORTS_PER_SOL,
- 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(),
- advanced_nonce_info.clone(),
- 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(),
- advanced_nonce_info.clone(),
- 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.clone(),
- advanced_nonce_info.clone(),
- 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.clone();
- 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.clone(),
- ],
- Some(&fee_payer),
- &[&fee_payer_keypair, &new_authority_keypair],
- *advanced_durable.as_hash(),
- );
- test_entry.push_transaction(first_transaction);
- test_entry.push_nonce_transaction(second_transaction.clone(), advanced_nonce_info.clone());
- 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<u32>,
- ) -> 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<SvmTestEntry> {
- 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(
- Rent::default().minimum_balance(1),
- 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<SvmTestEntry> {
- 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);
- // 0/1: a rent-paying fee-payer 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);
- 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);
- // 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 rent-paying non-nonce fee-payer 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 initial_nonce_account = AccountSharedData::new_data(
- LAMPORTS_PER_SOL,
- &nonce::versions::Versions::new(nonce::state::State::Initialized(
- initial_nonce_data.clone(),
- )),
- &system_program::id(),
- )
- .unwrap();
- 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.clone();
- advanced_nonce_info
- .try_advance_nonce(advanced_durable, LAMPORTS_PER_SIGNATURE)
- .unwrap();
- 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],
- Hash::default(),
- );
- test_entry.push_transaction_with_status(transaction, ExecutionStatus::ProcessedFailed);
- 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(formalize_loaded_transaction_data_size: bool) -> Vec<SvmTestEntry> {
- 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);
- if !formalize_loaded_transaction_data_size {
- common_test_entry
- .feature_set
- .formalize_loaded_transaction_data_size = false;
- }
- 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(
- LAMPORTS_PER_SOL * 10,
- 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
- // the relevant feature counts programdata size, so if enabled, we add breathing room
- // this test has nothing to do with the feature
- let size_budget = Some(if formalize_loaded_transaction_data_size {
- (program_size + MAX_PERMITTED_DATA_INCREASE) as u32
- } else {
- 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.clone();
- 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.clone(),
- 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<SvmTestEntry> {
- 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<SvmTestEntry> {
- 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(false))]
- #[test_case(simd83_account_reallocate(true))]
- #[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<SvmTestEntry>) {
- for test_entry in test_entries {
- let env = SvmTestEnvironment::create(test_entry);
- env.execute();
- }
- }
- #[test]
- fn program_cache_create_account() {
- for loader_id in PROGRAM_OWNERS {
- let mut test_entry = SvmTestEntry::with_loader_v4();
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- 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 succesful_noop_instruction = Instruction::new_with_bytes(noop_program, &[], vec![]);
- let succesful_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(
- &succesful_noop_instruction,
- )));
- noop_tx_usage += 1;
- test_entry.push_transaction(make_transaction(slice::from_ref(
- &succesful_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(&[
- succesful_noop_instruction.clone(),
- succesful_noop_instruction.clone(),
- succesful_transfer_instruction.clone(),
- succesful_transfer_instruction.clone(),
- succesful_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.clone(),
- succesful_noop_instruction.clone(),
- succesful_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(&[
- succesful_noop_instruction.clone(),
- fee_only_noop_instruction.clone(),
- ]),
- 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(&succesful_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::<Vec<_>>();
- let (_, noop_entry) = global_program_cache
- .iter()
- .find(|(pubkey, _)| *pubkey == noop_program)
- .unwrap();
- assert_eq!(
- noop_entry.tx_usage_counter.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.tx_usage_counter.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(&succesful_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.tx_usage_counter.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(&succesful_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.tx_usage_counter.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<Inspect<'_>> for (Option<AccountSharedData>, 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<Pubkey, Vec<(Option<AccountSharedData>, bool)>>);
- impl InspectedAccounts {
- fn inspect(&mut self, pubkey: Pubkey, inspect: Inspect) {
- self.0.entry(pubkey).or_default().push(inspect.into())
- }
- }
- #[test_case(false, false; "separate_nonce::old")]
- #[test_case(false, true; "separate_nonce::simd186")]
- #[test_case(true, false; "fee_paying_nonce::old")]
- #[test_case(true, true; "fee_paying_nonce::simd186")]
- fn svm_inspect_nonce_load_failure(
- fee_paying_nonce: bool,
- formalize_loaded_transaction_data_size: bool,
- ) {
- let mut test_entry = SvmTestEntry::default();
- let mut expected_inspected_accounts = InspectedAccounts::default();
- if !formalize_loaded_transaction_data_size {
- test_entry
- .feature_set
- .formalize_loaded_transaction_data_size = false;
- }
- let fee_payer_keypair = Keypair::new();
- let dummy_keypair = Keypair::new();
- let separate_nonce_keypair = Keypair::new();
- let fee_payer = fee_payer_keypair.pubkey();
- let dummy = dummy_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.clone(),
- )),
- &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.clone();
- 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![AccountMeta {
- pubkey: dummy,
- is_writable: true,
- is_signer: true,
- }],
- );
- 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;
- let dummy_account =
- AccountSharedData::create(1, vec![0; 2], system_program::id(), false, u64::MAX);
- test_entry.add_initial_account(dummy, &dummy_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));
- }
- // with simd186, transaction loading aborts when we hit the fee-payer because of TRANSACTION_ACCOUNT_BASE_SIZE
- // without simd186, transaction loading aborts on the dummy account, so it also happens to be inspected
- // the difference is immaterial to the test as long as it happens before the nonce is loaded for the transaction
- if !fee_paying_nonce && !formalize_loaded_transaction_data_size {
- expected_inspected_accounts.inspect(dummy, Inspect::LiveWrite(&dummy_account));
- }
- // by signing with the dummy account we ensure it precedes a separate nonce
- let transaction = Transaction::new_signed_with_payer(
- &[
- compute_instruction,
- advance_instruction,
- fee_only_noop_instruction,
- ],
- Some(&fee_payer),
- &[&fee_payer_keypair, &dummy_keypair],
- *initial_durable.as_hash(),
- );
- if !fee_paying_nonce {
- let sanitized = SanitizedTransaction::from_transaction_for_tests(transaction.clone());
- let dummy_index = sanitized
- .account_keys()
- .iter()
- .position(|key| *key == dummy)
- .unwrap();
- let nonce_index = sanitized
- .account_keys()
- .iter()
- .position(|key| *key == nonce_pubkey)
- .unwrap();
- assert!(dummy_index < nonce_index);
- }
- test_entry.push_nonce_transaction_with_status(
- transaction,
- initial_nonce_info.clone(),
- ExecutionStatus::ProcessedFailed,
- );
- test_entry.decrease_expected_lamports(&fee_payer, LAMPORTS_PER_SIGNATURE * 2);
- 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(85_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(
- 5000,
- "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.clone(),
- ..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,
- );
- }
- // 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::*,
- rand0_7::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 = rand0_7::thread_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.gen_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 = rand0_7::thread_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(
- STARTING_BALANCE,
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- token_buf,
- spl_token_interface::id(),
- false,
- u64::MAX,
- );
- let (_, spl_token) =
- solana_program_binaries::by_id(&spl_token_interface::id(), &Rent::default())
- .unwrap()
- .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(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.gen::<f64>() {
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- 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(
- LAMPORTS_PER_SOL,
- 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()
- );
- }
- }
- }
- }
|