Skip to main content

surfpool_core/surfnet/
svm.rs

1use std::{
2    cmp::max,
3    collections::{BTreeMap, HashMap, HashSet, VecDeque},
4    str::FromStr,
5    time::SystemTime,
6};
7
8use agave_feature_set::{FeatureSet, enable_extend_program_checked};
9use base64::{Engine, prelude::BASE64_STANDARD};
10use chrono::Utc;
11use convert_case::Casing;
12use crossbeam_channel::{Receiver, Sender, unbounded};
13use litesvm::types::{
14    FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult,
15};
16use solana_account::{Account, AccountSharedData, ReadableAccount};
17use solana_account_decoder::{
18    UiAccount, UiAccountData, UiAccountEncoding, UiDataSliceConfig, encode_ui_account,
19    parse_account_data::{AccountAdditionalDataV3, ParsedAccount, SplTokenAdditionalDataV2},
20};
21use solana_client::{
22    rpc_client::SerializableTransaction,
23    rpc_config::{RpcAccountInfoConfig, RpcBlockConfig, RpcTransactionLogsFilter},
24    rpc_filter::RpcFilterType,
25    rpc_response::{RpcKeyedAccount, RpcLogsResponse, RpcPerfSample},
26};
27use solana_clock::{Clock, Slot};
28use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
29use solana_epoch_info::EpochInfo;
30use solana_epoch_schedule::EpochSchedule;
31use solana_feature_gate_interface::Feature;
32use solana_genesis_config::GenesisConfig;
33use solana_hash::Hash;
34use solana_inflation::Inflation;
35use solana_loader_v3_interface::state::UpgradeableLoaderState;
36use solana_message::{
37    Message, VersionedMessage, inline_nonce::is_advance_nonce_instruction_data, v0::LoadedAddresses,
38};
39use solana_program_option::COption;
40use solana_pubkey::Pubkey;
41use solana_rpc_client_api::response::SlotInfo;
42use solana_sdk_ids::{bpf_loader, system_program};
43use solana_signature::Signature;
44use solana_system_interface::instruction as system_instruction;
45use solana_transaction::versioned::VersionedTransaction;
46use solana_transaction_error::TransactionError;
47use solana_transaction_status::{TransactionDetails, TransactionStatusMeta, UiConfirmedBlock};
48use spl_token_2022_interface::extension::{
49    BaseStateWithExtensions, StateWithExtensions, interest_bearing_mint::InterestBearingConfig,
50    scaled_ui_amount::ScaledUiAmountConfig,
51};
52use surfpool_types::{
53    AccountChange, AccountProfileState, AccountSnapshot, DEFAULT_PROFILING_MAP_CAPACITY,
54    DEFAULT_SLOT_TIME_MS, ExportSnapshotConfig, ExportSnapshotScope, FifoMap, Idl,
55    OverrideInstance, ProfileResult, RpcProfileDepth, RpcProfileResultConfig,
56    RunbookExecutionStatusReport, SimnetEvent, SvmFeatureConfig, TransactionConfirmationStatus,
57    TransactionStatusEvent, UiAccountChange, UiAccountProfileState, UiProfileResult, VersionedIdl,
58    types::{
59        ComputeUnitsEstimationResult, KeyedProfileResult, UiKeyedProfileResult, UuidOrSignature,
60    },
61};
62use txtx_addon_kit::{
63    indexmap::IndexMap,
64    types::types::{AddonJsonConverter, Value},
65};
66use txtx_addon_network_svm::codec::idl::borsh_encode_value_to_idl_type;
67use txtx_addon_network_svm_types::subgraph::idl::{
68    parse_bytes_to_value_with_expected_idl_type_def_ty,
69    parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes,
70};
71use uuid::Uuid;
72
73use super::{
74    AccountSubscriptionData, BlockHeader, BlockIdentifier, FINALIZATION_SLOT_THRESHOLD,
75    GetAccountResult, GeyserBlockMetadata, GeyserEntryInfo, GeyserEvent, GeyserSlotStatus,
76    ProgramSubscriptionData, SLOTS_PER_EPOCH, SignatureSubscriptionData, SignatureSubscriptionType,
77    remote::SurfnetRemoteClient,
78};
79use crate::{
80    error::{SurfpoolError, SurfpoolResult},
81    rpc::utils::convert_transaction_metadata_from_canonical,
82    scenarios::TemplateRegistry,
83    storage::{OverlayStorage, Storage, new_kv_store, new_kv_store_with_default},
84    surfnet::{
85        LogsSubscriptionData, locker::is_supported_token_program, surfnet_lite_svm::SurfnetLiteSvm,
86    },
87    types::{
88        GeyserAccountUpdate, MintAccount, SerializableAccountAdditionalData,
89        SurfnetTransactionStatus, SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta,
90    },
91};
92
93lazy_static::lazy_static! {
94    /// Interval (in slots) at which to perform garbage collection on the lite SVM cache.
95    /// About 1 hour at standard 400ms slot time.
96    /// Configurable via SURFPOOL_GARBAGE_COLLECTION_INTERVAL_SLOTS env var.
97    pub static ref GARBAGE_COLLECTION_INTERVAL_SLOTS: u64 = {
98        std::env::var("SURFPOOL_GARBAGE_COLLECTION_INTERVAL_SLOTS")
99            .ok()
100            .and_then(|s| s.parse().ok())
101            .unwrap_or(9_000)
102    };
103
104    /// Interval (in slots) at which to checkpoint the latest slot to storage.
105    /// About 1 minute at standard 400ms slot time (60000ms / 400ms = 150 slots).
106    /// Configurable via SURFPOOL_CHECKPOINT_INTERVAL_SLOTS env var.
107    pub static ref CHECKPOINT_INTERVAL_SLOTS: u64 = {
108        std::env::var("SURFPOOL_CHECKPOINT_INTERVAL_SLOTS")
109            .ok()
110            .and_then(|s| s.parse().ok())
111            .unwrap_or(150)
112    };
113}
114
115/// Helper function to apply an override to a decoded account value using dot notation
116pub fn apply_override_to_decoded_account(
117    decoded_value: &mut Value,
118    path: &str,
119    value: &serde_json::Value,
120) -> SurfpoolResult<()> {
121    let parts: Vec<&str> = path.split('.').collect();
122
123    if parts.is_empty() {
124        return Err(SurfpoolError::internal("Empty path provided for override"));
125    }
126
127    // Navigate to the parent of the target field
128    let mut current = decoded_value;
129    for part in &parts[..parts.len() - 1] {
130        match current {
131            Value::Object(map) => {
132                current = map.get_mut(&part.to_string()).ok_or_else(|| {
133                    SurfpoolError::internal(format!(
134                        "Path segment '{}' not found in decoded account",
135                        part
136                    ))
137                })?;
138            }
139            _ => {
140                return Err(SurfpoolError::internal(format!(
141                    "Cannot navigate through field '{}' - not an object",
142                    part
143                )));
144            }
145        }
146    }
147
148    // Set the final field
149    let final_key = parts[parts.len() - 1];
150    match current {
151        Value::Object(map) => {
152            // Convert serde_json::Value to txtx Value
153            let txtx_value = json_to_txtx_value(value)?;
154            map.insert(final_key.to_string(), txtx_value);
155            Ok(())
156        }
157        _ => Err(SurfpoolError::internal(format!(
158            "Cannot set field '{}' - parent is not an object",
159            final_key
160        ))),
161    }
162}
163
164/// Helper function to convert serde_json::Value to txtx Value
165fn json_to_txtx_value(json: &serde_json::Value) -> SurfpoolResult<Value> {
166    match json {
167        serde_json::Value::Null => Ok(Value::Null),
168        serde_json::Value::Bool(b) => Ok(Value::Bool(*b)),
169        serde_json::Value::Number(n) => {
170            if let Some(i) = n.as_i64() {
171                Ok(Value::Integer(i as i128))
172            } else if let Some(u) = n.as_u64() {
173                Ok(Value::Integer(u as i128))
174            } else if let Some(f) = n.as_f64() {
175                Ok(Value::Float(f))
176            } else {
177                Err(SurfpoolError::internal(format!(
178                    "Unable to convert number: {}",
179                    n
180                )))
181            }
182        }
183        serde_json::Value::String(s) => Ok(Value::String(s.clone())),
184        serde_json::Value::Array(arr) => {
185            let txtx_arr: Result<Vec<Value>, _> = arr.iter().map(json_to_txtx_value).collect();
186            Ok(Value::Array(Box::new(txtx_arr?)))
187        }
188        serde_json::Value::Object(obj) => {
189            let mut txtx_obj = IndexMap::new();
190            for (k, v) in obj.iter() {
191                txtx_obj.insert(k.clone(), json_to_txtx_value(v)?);
192            }
193            Ok(Value::Object(txtx_obj))
194        }
195    }
196}
197
198pub type AccountOwner = Pubkey;
199
200#[allow(deprecated)]
201use solana_sysvar::recent_blockhashes::MAX_ENTRIES;
202
203#[allow(deprecated)]
204pub const MAX_RECENT_BLOCKHASHES_STANDARD: usize = MAX_ENTRIES;
205
206pub fn get_txtx_value_json_converters() -> Vec<AddonJsonConverter<'static>> {
207    vec![
208        Box::new(move |value: &txtx_addon_kit::types::types::Value| {
209            txtx_addon_network_svm_types::SvmValue::to_json(value)
210        }) as AddonJsonConverter<'static>,
211    ]
212}
213
214/// `SurfnetSvm` provides a lightweight Solana Virtual Machine (SVM) for testing and simulation.
215///
216/// It supports a local in-memory blockchain state,
217/// remote RPC connections, transaction processing, and account management.
218///
219/// It also exposes channels to listen for simulation events (`SimnetEvent`) and Geyser plugin events (`GeyserEvent`).
220#[derive(Clone)]
221pub struct SurfnetSvm {
222    pub inner: SurfnetLiteSvm,
223    pub remote_rpc_url: Option<String>,
224    pub chain_tip: BlockIdentifier,
225    pub blocks: Box<dyn Storage<u64, BlockHeader>>,
226    pub transactions: Box<dyn Storage<String, SurfnetTransactionStatus>>,
227    pub transactions_queued_for_confirmation: VecDeque<(
228        VersionedTransaction,
229        Sender<TransactionStatusEvent>,
230        Option<TransactionError>,
231    )>,
232    pub transactions_queued_for_finalization: VecDeque<(
233        Slot,
234        VersionedTransaction,
235        Sender<TransactionStatusEvent>,
236        Option<TransactionError>,
237    )>,
238    pub perf_samples: VecDeque<RpcPerfSample>,
239    pub transactions_processed: u64,
240    pub latest_epoch_info: EpochInfo,
241    pub simnet_events_tx: Sender<SimnetEvent>,
242    pub geyser_events_tx: Sender<GeyserEvent>,
243    pub signature_subscriptions: HashMap<Signature, Vec<SignatureSubscriptionData>>,
244    pub account_subscriptions: AccountSubscriptionData,
245    pub program_subscriptions: ProgramSubscriptionData,
246    pub slot_subscriptions: Vec<Sender<SlotInfo>>,
247    pub profile_tag_map: Box<dyn Storage<String, Vec<UuidOrSignature>>>,
248    pub simulated_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
249    pub executed_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
250    pub logs_subscriptions: Vec<LogsSubscriptionData>,
251    pub snapshot_subscriptions: Vec<super::SnapshotSubscriptionData>,
252    pub updated_at: u64,
253    pub slot_time: u64,
254    pub start_time: SystemTime,
255    pub accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
256    pub account_associated_data: Box<dyn Storage<String, SerializableAccountAdditionalData>>,
257    pub token_accounts: Box<dyn Storage<String, TokenAccount>>,
258    pub token_mints: Box<dyn Storage<String, MintAccount>>,
259    pub token_accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
260    pub token_accounts_by_delegate: Box<dyn Storage<String, Vec<String>>>,
261    pub token_accounts_by_mint: Box<dyn Storage<String, Vec<String>>>,
262    pub total_supply: u64,
263    pub circulating_supply: u64,
264    pub non_circulating_supply: u64,
265    pub non_circulating_accounts: Vec<String>,
266    pub genesis_config: GenesisConfig,
267    pub inflation: Inflation,
268    /// A global monotonically increasing atomic number, which can be used to tell the order of the account update.
269    /// For example, when an account is updated in the same slot multiple times,
270    /// the update with higher write_version should supersede the one with lower write_version.
271    pub write_version: u64,
272    pub registered_idls: Box<dyn Storage<String, Vec<VersionedIdl>>>,
273    pub feature_set: FeatureSet,
274    pub instruction_profiling_enabled: bool,
275    pub max_profiles: usize,
276    pub runbook_executions: Vec<RunbookExecutionStatusReport>,
277    pub account_update_slots: HashMap<Pubkey, Slot>,
278    pub streamed_accounts: Box<dyn Storage<String, bool>>,
279    pub recent_blockhashes: VecDeque<(SyntheticBlockhash, i64)>,
280    pub scheduled_overrides: Box<dyn Storage<u64, Vec<OverrideInstance>>>,
281    /// Tracks accounts that have been explicitly closed by the user.
282    /// These accounts will not be fetched from mainnet even if they don't exist in the local cache.
283    pub closed_accounts: HashSet<Pubkey>,
284    /// The slot at which this surfnet instance started (may be non-zero when connected to remote).
285    /// Used as the lower bound for block reconstruction.
286    pub genesis_slot: Slot,
287    /// The `updated_at` timestamp when this surfnet started at `genesis_slot`.
288    /// Used to reconstruct block_time: genesis_updated_at + ((slot - genesis_slot) * slot_time)
289    pub genesis_updated_at: u64,
290    /// Storage for persisting the latest slot checkpoint.
291    /// Used for recovery on restart with sparse block storage.
292    pub slot_checkpoint: Box<dyn Storage<String, u64>>,
293    /// Tracks the slot at which we last persisted the checkpoint.
294    pub last_checkpoint_slot: u64,
295}
296
297pub const FEATURE: Feature = Feature {
298    activated_at: Some(0),
299};
300
301impl SurfnetSvm {
302    pub fn default() -> (Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>) {
303        Self::new(None, "0").unwrap()
304    }
305
306    pub fn new_with_db(
307        database_url: Option<&str>,
308        surfnet_id: &str,
309    ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
310        Self::new(database_url, surfnet_id)
311    }
312
313    /// Explicitly shutdown the SVM, performing cleanup like WAL checkpoint for SQLite.
314    /// This should be called before the application exits to ensure data is persisted.
315    pub fn shutdown(&self) {
316        self.inner.shutdown();
317        self.blocks.shutdown();
318        self.transactions.shutdown();
319        self.token_accounts.shutdown();
320        self.token_mints.shutdown();
321        self.accounts_by_owner.shutdown();
322        self.token_accounts_by_owner.shutdown();
323        self.token_accounts_by_delegate.shutdown();
324        self.token_accounts_by_mint.shutdown();
325        self.streamed_accounts.shutdown();
326        self.scheduled_overrides.shutdown();
327        self.registered_idls.shutdown();
328        self.profile_tag_map.shutdown();
329        self.simulated_transaction_profiles.shutdown();
330        self.executed_transaction_profiles.shutdown();
331        self.account_associated_data.shutdown();
332    }
333
334    /// Creates a clone of the SVM with overlay storage wrappers for all database-backed fields.
335    /// This allows profiling transactions without affecting the underlying database.
336    /// All storage writes are buffered in memory and discarded when the clone is dropped.
337    pub fn clone_for_profiling(&self) -> Self {
338        let (dummy_simnet_tx, _) = crossbeam_channel::bounded(1);
339        let (dummy_geyser_tx, _) = crossbeam_channel::bounded(1);
340
341        Self {
342            inner: self.inner.clone_for_profiling(),
343            remote_rpc_url: self.remote_rpc_url.clone(),
344            chain_tip: self.chain_tip.clone(),
345
346            // Wrap all storage fields with OverlayStorage
347            blocks: OverlayStorage::wrap(self.blocks.clone_box()),
348            transactions: OverlayStorage::wrap(self.transactions.clone_box()),
349            profile_tag_map: OverlayStorage::wrap(self.profile_tag_map.clone_box()),
350            simulated_transaction_profiles: OverlayStorage::wrap(
351                self.simulated_transaction_profiles.clone_box(),
352            ),
353            executed_transaction_profiles: OverlayStorage::wrap(
354                self.executed_transaction_profiles.clone_box(),
355            ),
356            accounts_by_owner: OverlayStorage::wrap(self.accounts_by_owner.clone_box()),
357            account_associated_data: OverlayStorage::wrap(self.account_associated_data.clone_box()),
358            token_accounts: OverlayStorage::wrap(self.token_accounts.clone_box()),
359            token_mints: OverlayStorage::wrap(self.token_mints.clone_box()),
360            token_accounts_by_owner: OverlayStorage::wrap(self.token_accounts_by_owner.clone_box()),
361            token_accounts_by_delegate: OverlayStorage::wrap(
362                self.token_accounts_by_delegate.clone_box(),
363            ),
364            token_accounts_by_mint: OverlayStorage::wrap(self.token_accounts_by_mint.clone_box()),
365            registered_idls: OverlayStorage::wrap(self.registered_idls.clone_box()),
366            streamed_accounts: OverlayStorage::wrap(self.streamed_accounts.clone_box()),
367            scheduled_overrides: OverlayStorage::wrap(self.scheduled_overrides.clone_box()),
368
369            // Clone non-storage fields normally
370            transactions_queued_for_confirmation: self.transactions_queued_for_confirmation.clone(),
371            transactions_queued_for_finalization: self.transactions_queued_for_finalization.clone(),
372            perf_samples: self.perf_samples.clone(),
373            transactions_processed: self.transactions_processed,
374            latest_epoch_info: self.latest_epoch_info.clone(),
375
376            // Use dummy channels to prevent event propagation during profiling
377            simnet_events_tx: dummy_simnet_tx,
378            geyser_events_tx: dummy_geyser_tx,
379
380            signature_subscriptions: self.signature_subscriptions.clone(),
381            account_subscriptions: self.account_subscriptions.clone(),
382            program_subscriptions: self.program_subscriptions.clone(),
383            // Don't clone subscriptions - profiling clone shouldn't send notifications
384            slot_subscriptions: Vec::new(),
385            logs_subscriptions: Vec::new(),
386            snapshot_subscriptions: Vec::new(),
387
388            updated_at: self.updated_at,
389            slot_time: self.slot_time,
390            start_time: self.start_time,
391
392            total_supply: self.total_supply,
393            circulating_supply: self.circulating_supply,
394            non_circulating_supply: self.non_circulating_supply,
395            non_circulating_accounts: self.non_circulating_accounts.clone(),
396            genesis_config: self.genesis_config.clone(),
397            inflation: self.inflation,
398            write_version: self.write_version,
399            feature_set: self.feature_set.clone(),
400            instruction_profiling_enabled: self.instruction_profiling_enabled,
401            max_profiles: self.max_profiles,
402            runbook_executions: self.runbook_executions.clone(),
403            account_update_slots: self.account_update_slots.clone(),
404            recent_blockhashes: self.recent_blockhashes.clone(),
405            closed_accounts: self.closed_accounts.clone(),
406            genesis_slot: self.genesis_slot,
407            genesis_updated_at: self.genesis_updated_at,
408            slot_checkpoint: OverlayStorage::wrap(self.slot_checkpoint.clone_box()),
409            last_checkpoint_slot: self.last_checkpoint_slot,
410        }
411    }
412
413    /// Creates a new instance of `SurfnetSvm`.
414    ///
415    /// Returns a tuple containing the SVM instance, a receiver for simulation events, and a receiver for Geyser plugin events.
416    pub fn new(
417        database_url: Option<&str>,
418        surfnet_id: &str,
419    ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
420        let (simnet_events_tx, simnet_events_rx) = crossbeam_channel::bounded(1024);
421        let (geyser_events_tx, geyser_events_rx) = crossbeam_channel::bounded(1024);
422
423        let mut feature_set = FeatureSet::all_enabled();
424
425        // todo: remove once txtx deployments upgrade solana dependencies.
426        // todo: consider making this configurable via config
427        feature_set.deactivate(&enable_extend_program_checked::id());
428
429        let inner =
430            SurfnetLiteSvm::new().initialize(feature_set.clone(), database_url, surfnet_id)?;
431
432        let native_mint_account = inner
433            .get_account(&spl_token_interface::native_mint::ID)?
434            .unwrap();
435
436        let native_mint_associated_data = {
437            let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
438                &native_mint_account.data,
439            )
440            .unwrap();
441            let unix_timestamp = inner.get_sysvar::<Clock>().unix_timestamp;
442            let interest_bearing_config = mint
443                .get_extension::<InterestBearingConfig>()
444                .map(|x| (*x, unix_timestamp))
445                .ok();
446            let scaled_ui_amount_config = mint
447                .get_extension::<ScaledUiAmountConfig>()
448                .map(|x| (*x, unix_timestamp))
449                .ok();
450            AccountAdditionalDataV3 {
451                spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
452                    decimals: mint.base.decimals,
453                    interest_bearing_config,
454                    scaled_ui_amount_config,
455                }),
456            }
457        };
458        let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
459
460        // Load native mint into owned account and token mint indexes
461        let mut accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
462            new_kv_store(&database_url, "accounts_by_owner", surfnet_id)?;
463        accounts_by_owner_db.store(
464            native_mint_account.owner.to_string(),
465            vec![spl_token_interface::native_mint::ID.to_string()],
466        )?;
467        let blocks_db = new_kv_store(&database_url, "blocks", surfnet_id)?;
468        let transactions_db = new_kv_store(&database_url, "transactions", surfnet_id)?;
469        let token_accounts_db = new_kv_store(&database_url, "token_accounts", surfnet_id)?;
470        let mut token_mints_db: Box<dyn Storage<String, MintAccount>> =
471            new_kv_store(&database_url, "token_mints", surfnet_id)?;
472        let mut account_associated_data_db: Box<
473            dyn Storage<String, SerializableAccountAdditionalData>,
474        > = new_kv_store(&database_url, "account_associated_data", surfnet_id)?;
475        // Store initial account associated data (native mint)
476        account_associated_data_db.store(
477            spl_token_interface::native_mint::ID.to_string(),
478            native_mint_associated_data.into(),
479        )?;
480        token_mints_db.store(
481            spl_token_interface::native_mint::ID.to_string(),
482            parsed_mint_account,
483        )?;
484        let token_accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
485            new_kv_store(&database_url, "token_accounts_by_owner", surfnet_id)?;
486        let token_accounts_by_delegate_db: Box<dyn Storage<String, Vec<String>>> =
487            new_kv_store(&database_url, "token_accounts_by_delegate", surfnet_id)?;
488        let token_accounts_by_mint_db: Box<dyn Storage<String, Vec<String>>> =
489            new_kv_store(&database_url, "token_accounts_by_mint", surfnet_id)?;
490        let streamed_accounts_db: Box<dyn Storage<String, bool>> =
491            new_kv_store(&database_url, "streamed_accounts", surfnet_id)?;
492        let scheduled_overrides_db: Box<dyn Storage<u64, Vec<OverrideInstance>>> =
493            new_kv_store(&database_url, "scheduled_overrides", surfnet_id)?;
494        let registered_idls_db: Box<dyn Storage<String, Vec<VersionedIdl>>> =
495            new_kv_store(&database_url, "registered_idls", surfnet_id)?;
496        let profile_tag_map_db: Box<dyn Storage<String, Vec<UuidOrSignature>>> =
497            new_kv_store(&database_url, "profile_tag_map", surfnet_id)?;
498        let simulated_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> =
499            new_kv_store(&database_url, "simulated_transaction_profiles", surfnet_id)?;
500        let executed_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> =
501            new_kv_store_with_default(
502                &database_url,
503                "executed_transaction_profiles",
504                surfnet_id,
505                // Use FifoMap for executed_transaction_profiles to maintain FIFO eviction behavior
506                // (when no on-disk DB is provided)
507                || Box::new(FifoMap::<String, KeyedProfileResult>::default()),
508            )?;
509        let slot_checkpoint_db: Box<dyn Storage<String, u64>> =
510            new_kv_store(&database_url, "slot_checkpoint", surfnet_id)?;
511
512        // Recover chain state: prefer slot checkpoint, fall back to max block in DB
513        let checkpoint_slot = slot_checkpoint_db.get(&"latest_slot".to_string())?;
514        let max_block_slot = blocks_db
515            .into_iter()
516            .unwrap()
517            .max_by_key(|(slot, _): &(u64, BlockHeader)| *slot);
518
519        let chain_tip = match (checkpoint_slot, max_block_slot) {
520            // Prefer checkpoint if it's higher than the max stored block
521            (Some(checkpoint), Some((block_slot, block))) => {
522                if checkpoint > block_slot {
523                    // Use checkpoint slot with synthetic blockhash
524                    BlockIdentifier {
525                        index: checkpoint,
526                        hash: SyntheticBlockhash::new(checkpoint).to_string(),
527                    }
528                } else {
529                    // Use the stored block
530                    BlockIdentifier {
531                        index: block.block_height,
532                        hash: block.hash,
533                    }
534                }
535            }
536            (Some(checkpoint), None) => BlockIdentifier {
537                index: checkpoint,
538                hash: SyntheticBlockhash::new(checkpoint).to_string(),
539            },
540            (None, Some((_, block))) => BlockIdentifier {
541                index: block.block_height,
542                hash: block.hash,
543            },
544            (None, None) => BlockIdentifier::zero(),
545        };
546
547        // Initialize transactions_processed from database count for persistent storage
548        let transactions_processed = transactions_db.count()?;
549
550        let mut svm = Self {
551            inner,
552            remote_rpc_url: None,
553            chain_tip,
554            blocks: blocks_db,
555            transactions: transactions_db,
556            perf_samples: VecDeque::new(),
557            transactions_processed,
558            simnet_events_tx,
559            geyser_events_tx,
560            latest_epoch_info: EpochInfo {
561                epoch: 0,
562                slot_index: 0,
563                slots_in_epoch: SLOTS_PER_EPOCH,
564                absolute_slot: 0,
565                block_height: 0,
566                transaction_count: None,
567            },
568            transactions_queued_for_confirmation: VecDeque::new(),
569            transactions_queued_for_finalization: VecDeque::new(),
570            signature_subscriptions: HashMap::new(),
571            account_subscriptions: HashMap::new(),
572            program_subscriptions: HashMap::new(),
573            slot_subscriptions: Vec::new(),
574            profile_tag_map: profile_tag_map_db,
575            simulated_transaction_profiles: simulated_transaction_profiles_db,
576            executed_transaction_profiles: executed_transaction_profiles_db,
577            logs_subscriptions: Vec::new(),
578            snapshot_subscriptions: Vec::new(),
579            updated_at: Utc::now().timestamp_millis() as u64,
580            slot_time: DEFAULT_SLOT_TIME_MS,
581            start_time: SystemTime::now(),
582            accounts_by_owner: accounts_by_owner_db,
583            account_associated_data: account_associated_data_db,
584            token_accounts: token_accounts_db,
585            token_mints: token_mints_db,
586            token_accounts_by_owner: token_accounts_by_owner_db,
587            token_accounts_by_delegate: token_accounts_by_delegate_db,
588            token_accounts_by_mint: token_accounts_by_mint_db,
589            total_supply: 0,
590            circulating_supply: 0,
591            non_circulating_supply: 0,
592            non_circulating_accounts: Vec::new(),
593            genesis_config: GenesisConfig::default(),
594            inflation: Inflation::default(),
595            write_version: 0,
596            registered_idls: registered_idls_db,
597            feature_set,
598            instruction_profiling_enabled: true,
599            max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
600            runbook_executions: Vec::new(),
601            account_update_slots: HashMap::new(),
602            streamed_accounts: streamed_accounts_db,
603            recent_blockhashes: VecDeque::new(),
604            scheduled_overrides: scheduled_overrides_db,
605            closed_accounts: HashSet::new(),
606            genesis_slot: 0, // Will be updated when connecting to remote network
607            genesis_updated_at: Utc::now().timestamp_millis() as u64,
608            slot_checkpoint: slot_checkpoint_db,
609            last_checkpoint_slot: 0,
610        };
611
612        // Generate the initial synthetic blockhash
613        svm.chain_tip = svm.new_blockhash();
614
615        Ok((svm, simnet_events_rx, geyser_events_rx))
616    }
617
618    /// Applies the SVM feature configuration to the internal feature set.
619    ///
620    /// This method enables or disables specific SVM features based on the provided configuration.
621    /// Features explicitly listed in `enable` will be activated, and features in `disable` will be deactivated.
622    ///
623    /// # Arguments
624    /// * `config` - The feature configuration specifying which features to enable/disable.
625    pub fn apply_feature_config(&mut self, config: &SvmFeatureConfig) {
626        // Apply explicit enables
627        for pubkey in &config.enable {
628            self.feature_set.activate(pubkey, 0);
629        }
630
631        // Apply explicit disables
632        for pubkey in &config.disable {
633            self.feature_set.deactivate(pubkey);
634        }
635
636        // Rebuild inner VM with updated feature set
637        self.inner.apply_feature_config(self.feature_set.clone());
638    }
639
640    pub fn increment_write_version(&mut self) -> u64 {
641        self.write_version += 1;
642        self.write_version
643    }
644
645    /// Initializes the SVM with the provided epoch info and optionally notifies about remote connection.
646    ///
647    /// Updates the internal epoch info, sends connection and epoch update events, and sets the clock sysvar.
648    ///
649    /// # Arguments
650    /// * `epoch_info` - The epoch information to initialize with.
651    /// * `remote_ctx` - Optional remote client context for event notification.
652    ///
653    pub fn initialize(
654        &mut self,
655        epoch_info: EpochInfo,
656        epoch_schedule: EpochSchedule,
657        slot_time: u64,
658        remote_ctx: &Option<SurfnetRemoteClient>,
659        do_profile_instructions: bool,
660        log_bytes_limit: Option<usize>,
661    ) {
662        self.chain_tip = self.new_blockhash();
663        self.latest_epoch_info = epoch_info.clone();
664        // Set genesis_slot to the current slot when initializing (syncing with remote)
665        // This marks the starting point for this surfnet instance
666        self.genesis_slot = epoch_info.absolute_slot;
667        self.updated_at = Utc::now().timestamp_millis() as u64;
668        // Update genesis_updated_at to match the new genesis_slot
669        self.genesis_updated_at = self.updated_at;
670        self.slot_time = slot_time;
671        self.instruction_profiling_enabled = do_profile_instructions;
672        self.set_profiling_map_capacity(self.max_profiles);
673        self.inner.set_log_bytes_limit(log_bytes_limit);
674
675        let registry = TemplateRegistry::new();
676        for (_, template) in registry.templates.into_iter() {
677            let _ = self.register_idl(template.idl, None);
678        }
679
680        self.inner.set_sysvar(&epoch_schedule);
681
682        if let Some(remote_client) = remote_ctx {
683            let _ = self
684                .simnet_events_tx
685                .send(SimnetEvent::Connected(remote_client.client.url()));
686        }
687        let _ = self
688            .simnet_events_tx
689            .send(SimnetEvent::EpochInfoUpdate(epoch_info));
690
691        // Reconstruct all sysvars (RecentBlockhashes, SlotHashes, Clock)
692        self.reconstruct_sysvars();
693    }
694
695    pub fn set_profile_instructions(&mut self, do_profile_instructions: bool) {
696        self.instruction_profiling_enabled = do_profile_instructions;
697    }
698
699    pub fn set_profiling_map_capacity(&mut self, capacity: usize) {
700        let clamped_capacity = max(1, capacity);
701        self.max_profiles = clamped_capacity;
702        let is_on_disk_db = self.inner.db.is_some();
703        if !is_on_disk_db {
704            // when using on-disk DB, we're not using the Fifo Map to manage entries
705            self.executed_transaction_profiles = Box::new(FifoMap::new(clamped_capacity));
706        }
707    }
708
709    /// Airdrops a specified amount of lamports to a single public key.
710    ///
711    /// # Arguments
712    /// * `pubkey` - The recipient public key.
713    /// * `lamports` - The amount of lamports to airdrop.
714    ///
715    /// # Returns
716    /// A `TransactionResult` indicating success or failure.
717    #[allow(clippy::result_large_err)]
718    pub fn airdrop(&mut self, pubkey: &Pubkey, lamports: u64) -> SurfpoolResult<TransactionResult> {
719        // Capture pre-airdrop balances for the airdrop account, recipient, and system program.
720        let airdrop_pubkey = self.inner.airdrop_pubkey();
721
722        let airdrop_account_before = self
723            .get_account(&airdrop_pubkey)?
724            .unwrap_or_else(|| Account::default());
725        let recipient_account_before = self
726            .get_account(pubkey)?
727            .unwrap_or_else(|| Account::default());
728        let system_account_before = self
729            .get_account(&system_program::id())?
730            .unwrap_or_else(|| Account::default());
731
732        let res = self.inner.airdrop(pubkey, lamports);
733        let (status_tx, _rx) = unbounded();
734        if let Ok(ref tx_result) = res {
735            let slot = self.latest_epoch_info.absolute_slot;
736            // Capture post-airdrop balances
737            let airdrop_account_after = self
738                .get_account(&airdrop_pubkey)?
739                .unwrap_or_else(|| Account::default());
740            let recipient_account_after = self
741                .get_account(pubkey)?
742                .unwrap_or_else(|| Account::default());
743            let system_account_after = self
744                .get_account(&system_program::id())?
745                .unwrap_or_else(|| Account::default());
746
747            // Construct a synthetic transaction that mirrors the underlying airdrop.
748            let tx = VersionedTransaction {
749                signatures: vec![tx_result.signature],
750                message: VersionedMessage::Legacy(Message::new(
751                    &[system_instruction::transfer(
752                        &airdrop_pubkey,
753                        pubkey,
754                        lamports,
755                    )],
756                    Some(&airdrop_pubkey),
757                )),
758            };
759
760            self.transactions.store(
761                tx.get_signature().to_string(),
762                SurfnetTransactionStatus::processed(
763                    TransactionWithStatusMeta {
764                        slot,
765                        transaction: tx.clone(),
766                        meta: TransactionStatusMeta {
767                            status: Ok(()),
768                            fee: 5000,
769                            pre_balances: vec![
770                                airdrop_account_before.lamports,
771                                recipient_account_before.lamports,
772                                system_account_before.lamports,
773                            ],
774                            post_balances: vec![
775                                airdrop_account_after.lamports,
776                                recipient_account_after.lamports,
777                                system_account_after.lamports,
778                            ],
779                            inner_instructions: Some(vec![]),
780                            log_messages: Some(tx_result.logs.clone()),
781                            pre_token_balances: Some(vec![]),
782                            post_token_balances: Some(vec![]),
783                            rewards: Some(vec![]),
784                            loaded_addresses: LoadedAddresses::default(),
785                            return_data: Some(tx_result.return_data.clone()),
786                            compute_units_consumed: Some(tx_result.compute_units_consumed),
787                            cost_units: None,
788                        },
789                    },
790                    HashSet::from([*pubkey]),
791                ),
792            )?;
793            self.notify_signature_subscribers(
794                SignatureSubscriptionType::processed(),
795                tx.get_signature(),
796                slot,
797                None,
798            );
799            self.notify_logs_subscribers(
800                tx.get_signature(),
801                None,
802                tx_result.logs.clone(),
803                CommitmentLevel::Processed,
804            );
805            self.transactions_queued_for_confirmation
806                .push_back((tx, status_tx.clone(), None));
807            let account = self.get_account(pubkey)?.unwrap();
808            self.set_account(pubkey, account)?;
809        }
810        Ok(res)
811    }
812
813    /// Airdrops a specified amount of lamports to a list of public keys.
814    ///
815    /// # Arguments
816    /// * `lamports` - The amount of lamports to airdrop.
817    /// * `addresses` - Slice of recipient public keys.
818    pub fn airdrop_pubkeys(&mut self, lamports: u64, addresses: &[Pubkey]) {
819        for recipient in addresses {
820            match self.airdrop(recipient, lamports) {
821                Ok(_) => {
822                    let _ = self.simnet_events_tx.send(SimnetEvent::info(format!(
823                        "Genesis airdrop successful {}: {}",
824                        recipient, lamports
825                    )));
826                }
827                Err(e) => {
828                    let _ = self.simnet_events_tx.send(SimnetEvent::error(format!(
829                        "Genesis airdrop failed {}: {}",
830                        recipient, e
831                    )));
832                }
833            };
834        }
835    }
836
837    /// Returns the latest known absolute slot from the local epoch info.
838    pub const fn get_latest_absolute_slot(&self) -> Slot {
839        self.latest_epoch_info.absolute_slot
840    }
841
842    /// Returns the latest blockhash known by the SVM.
843    pub fn latest_blockhash(&self) -> solana_hash::Hash {
844        Hash::from_str(&self.chain_tip.hash).expect("Invalid blockhash")
845    }
846
847    /// Returns the latest epoch info known by the `SurfnetSvm`.
848    pub fn latest_epoch_info(&self) -> EpochInfo {
849        self.latest_epoch_info.clone()
850    }
851
852    /// Calculates the block time for a given slot based on genesis timestamp.
853    /// Returns the time in milliseconds since genesis.
854    pub fn calculate_block_time_for_slot(&self, slot: Slot) -> u64 {
855        // Calculate time relative to genesis_slot (when this surfnet started)
856        let slots_since_genesis = slot.saturating_sub(self.genesis_slot);
857        self.genesis_updated_at + (slots_since_genesis * self.slot_time)
858    }
859
860    /// Checks if a slot is within the valid range for sparse block storage.
861    /// A slot is valid if it's between genesis_slot (inclusive) and latest_slot (inclusive).
862    ///
863    /// # Arguments
864    /// * `slot` - The slot number to check.
865    ///
866    /// # Returns
867    /// `true` if the slot is within the valid range, `false` otherwise.
868    pub fn is_slot_in_valid_range(&self, slot: Slot) -> bool {
869        let latest_slot = self.get_latest_absolute_slot();
870        slot >= self.genesis_slot && slot <= latest_slot
871    }
872
873    /// Gets a block from storage, or reconstructs an empty block if the slot is within
874    /// the valid range (sparse block storage).
875    ///
876    /// # Arguments
877    /// * `slot` - The slot number to retrieve.
878    ///
879    /// # Returns
880    /// * `Ok(Some(BlockHeader))` - If the block exists or can be reconstructed
881    /// * `Ok(None)` - If the slot is outside the valid range
882    /// * `Err(_)` - If there was an error accessing storage
883    pub fn get_block_or_reconstruct(&self, slot: Slot) -> SurfpoolResult<Option<BlockHeader>> {
884        match self.blocks.get(&slot)? {
885            Some(block) => Ok(Some(block)),
886            None => {
887                if self.is_slot_in_valid_range(slot) {
888                    Ok(Some(self.reconstruct_empty_block(slot)))
889                } else {
890                    Ok(None)
891                }
892            }
893        }
894    }
895
896    /// Reconstructs an empty block header for a slot that wasn't stored.
897    /// This is used for sparse block storage where empty blocks are not persisted.
898    pub fn reconstruct_empty_block(&self, slot: Slot) -> BlockHeader {
899        let block_height = slot;
900        BlockHeader {
901            hash: SyntheticBlockhash::new(block_height).to_string(),
902            previous_blockhash: SyntheticBlockhash::new(block_height.saturating_sub(1)).to_string(),
903            parent_slot: slot.saturating_sub(1),
904            block_time: (self.calculate_block_time_for_slot(slot) / 1_000) as i64,
905            block_height,
906            signatures: vec![],
907        }
908    }
909
910    pub fn get_account_from_feature_set(&self, pubkey: &Pubkey) -> Option<Account> {
911        // Currently, liteSVM doesn't create feature gate accounts and store them in the vm,
912        // so when a user is fetching one, we make one on the fly.
913        // TODO: remove once https://github.com/LiteSVM/litesvm/pull/308 is released
914        self.feature_set.active().get(pubkey).map(|_| {
915            let feature_bytes = bincode::serialize(&FEATURE).unwrap();
916            let lamports = self
917                .inner
918                .minimum_balance_for_rent_exemption(feature_bytes.len());
919            Account {
920                lamports,
921                data: feature_bytes,
922                owner: solana_sdk_ids::feature::id(),
923                executable: false,
924                rent_epoch: 0,
925            }
926        })
927    }
928
929    /// Reconstructs RecentBlockhashes, SlotHashes, and Clock sysvars deterministically
930    /// from the current slot. Called on startup and after garbage collection to ensure
931    /// consistent sysvar state without requiring database persistence.
932    ///
933    /// Note: SyntheticBlockhash uses chain_tip.index (relative index), while SlotHashes
934    /// and Clock use absolute slots (chain_tip.index + genesis_slot).
935    #[allow(deprecated)]
936    pub fn reconstruct_sysvars(&mut self) {
937        use solana_slot_hashes::SlotHashes;
938        use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
939
940        let current_index = self.chain_tip.index;
941        let current_absolute_slot = self.get_latest_absolute_slot();
942
943        // Calculate range for blockhashes - use relative indices for SyntheticBlockhash
944        let start_index = current_index.saturating_sub(MAX_RECENT_BLOCKHASHES_STANDARD as u64 - 1);
945
946        // Generate all synthetic blockhashes using relative indices (chain_tip.index style)
947        // This matches how new_blockhash() generates hashes
948        let synthetic_hashes: Vec<_> = (start_index..=current_index)
949            .rev()
950            .map(SyntheticBlockhash::new)
951            .collect();
952
953        // 1. Reconstruct RecentBlockhashes (last 150 blockhashes)
954        let recent_blockhashes_vec: Vec<_> = synthetic_hashes
955            .iter()
956            .enumerate()
957            .map(|(index, hash)| IterItem(index as u64, hash.hash(), 0))
958            .collect();
959        let recent_blockhashes = RecentBlockhashes::from_iter(recent_blockhashes_vec);
960        self.inner.set_sysvar(&recent_blockhashes);
961
962        // 2. Reconstruct SlotHashes - maps absolute slots to blockhashes
963        let start_absolute_slot = start_index + self.genesis_slot;
964        let slot_hashes_vec: Vec<_> = (start_absolute_slot..=current_absolute_slot)
965            .rev()
966            .zip(synthetic_hashes.iter())
967            .map(|(slot, hash)| (slot, *hash.hash()))
968            .collect();
969        let slot_hashes = SlotHashes::new(&slot_hashes_vec);
970        self.inner.set_sysvar(&slot_hashes);
971
972        // 3. Reconstruct Clock using absolute slot
973        let unix_timestamp = self.calculate_block_time_for_slot(current_absolute_slot) / 1_000;
974        let clock = Clock {
975            slot: current_absolute_slot,
976            epoch: self.latest_epoch_info.epoch,
977            unix_timestamp: unix_timestamp as i64,
978            epoch_start_timestamp: 0,
979            leader_schedule_epoch: 0,
980        };
981        self.inner.set_sysvar(&clock);
982    }
983
984    /// Generates and sets a new blockhash, updating the RecentBlockhashes sysvar.
985    ///
986    /// # Returns
987    /// A new `BlockIdentifier` for the updated blockhash.
988    #[allow(deprecated)]
989    fn new_blockhash(&mut self) -> BlockIdentifier {
990        use solana_slot_hashes::SlotHashes;
991        use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
992        // Backup the current block hashes
993        let recent_blockhashes_backup = self.inner.get_sysvar::<RecentBlockhashes>();
994        let num_blockhashes_expected = recent_blockhashes_backup
995            .len()
996            .min(MAX_RECENT_BLOCKHASHES_STANDARD);
997        // Invalidate the current block hash.
998        // LiteSVM bug / feature: calling this method empties `sysvar::<RecentBlockhashes>()`
999        self.inner.expire_blockhash();
1000        // Rebuild recent blockhashes
1001        let mut recent_blockhashes = Vec::with_capacity(num_blockhashes_expected);
1002        let recent_blockhashes_overriden = self.inner.get_sysvar::<RecentBlockhashes>();
1003        let latest_entry = recent_blockhashes_overriden
1004            .first()
1005            .expect("Latest blockhash not found");
1006
1007        let new_synthetic_blockhash = SyntheticBlockhash::new(self.chain_tip.index);
1008        let new_synthetic_blockhash_str = new_synthetic_blockhash.to_string();
1009
1010        recent_blockhashes.push(IterItem(
1011            0,
1012            new_synthetic_blockhash.hash(),
1013            latest_entry.fee_calculator.lamports_per_signature,
1014        ));
1015
1016        // Append the previous blockhashes, ignoring the first one
1017        for (index, entry) in recent_blockhashes_backup.iter().enumerate() {
1018            if recent_blockhashes.len() >= MAX_RECENT_BLOCKHASHES_STANDARD {
1019                break;
1020            }
1021            recent_blockhashes.push(IterItem(
1022                (index + 1) as u64,
1023                &entry.blockhash,
1024                entry.fee_calculator.lamports_per_signature,
1025            ));
1026        }
1027
1028        self.inner
1029            .set_sysvar(&RecentBlockhashes::from_iter(recent_blockhashes));
1030
1031        let mut slot_hashes = self.inner.get_sysvar::<SlotHashes>();
1032        slot_hashes.add(
1033            self.get_latest_absolute_slot() + 1,
1034            *new_synthetic_blockhash.hash(),
1035        );
1036        self.inner.set_sysvar(&SlotHashes::new(&slot_hashes));
1037
1038        BlockIdentifier::new(
1039            self.chain_tip.index + 1,
1040            new_synthetic_blockhash_str.as_str(),
1041        )
1042    }
1043
1044    /// Checks if the provided blockhash is recent (present in the RecentBlockhashes sysvar).
1045    ///
1046    /// # Arguments
1047    /// * `recent_blockhash` - The blockhash to check.
1048    ///
1049    /// # Returns
1050    /// `true` if the blockhash is recent, `false` otherwise.
1051    pub fn check_blockhash_is_recent(&self, recent_blockhash: &Hash) -> bool {
1052        #[allow(deprecated)]
1053        self.inner
1054            .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>()
1055            .iter()
1056            .any(|entry| entry.blockhash == *recent_blockhash)
1057    }
1058
1059    /// Validates the blockhash of a transaction, considering nonce accounts if present.
1060    /// If the transaction uses a nonce account, the blockhash is validated against the nonce account's stored blockhash.
1061    /// Otherwise, it is validated against the RecentBlockhashes sysvar.
1062    ///
1063    /// # Arguments
1064    /// * `tx` - The transaction to validate.
1065    ///
1066    /// # Returns
1067    /// `true` if the transaction blockhash is valid, `false` otherwise.
1068    pub fn validate_transaction_blockhash(&self, tx: &VersionedTransaction) -> bool {
1069        let recent_blockhash = tx.message.recent_blockhash();
1070
1071        let some_nonce_account_index = tx
1072            .message
1073            .instructions()
1074            .get(solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize)
1075            .filter(|instruction| {
1076                matches!(
1077                    tx.message.static_account_keys().get(instruction.program_id_index as usize),
1078                    Some(program_id) if system_program::check_id(program_id)
1079                ) && is_advance_nonce_instruction_data(&instruction.data)
1080            })
1081            .map(|instruction| {
1082                // nonce account is the first account in the instruction
1083                instruction.accounts.get(0)
1084            });
1085
1086        debug!(
1087            "Validating tx blockhash: {}; is nonce tx?: {}",
1088            recent_blockhash,
1089            some_nonce_account_index.is_some()
1090        );
1091
1092        if let Some(nonce_account_index) = some_nonce_account_index {
1093            trace!(
1094                "Nonce tx detected. Nonce account index: {:?}",
1095                nonce_account_index
1096            );
1097            let Some(nonce_account_index) = nonce_account_index else {
1098                return false;
1099            };
1100
1101            let Some(nonce_account_pubkey) = tx
1102                .message
1103                .static_account_keys()
1104                .get(*nonce_account_index as usize)
1105            else {
1106                return false;
1107            };
1108
1109            trace!("Nonce account pubkey: {:?}", nonce_account_pubkey,);
1110
1111            // Here we're swallowing errors in the storage - if we fail to fetch the account because of a storage error,
1112            // we're just considering the blockhash to be invalid.
1113            let Ok(Some(nonce_account)) = self.get_account(nonce_account_pubkey) else {
1114                return false;
1115            };
1116            trace!("Nonce account: {:?}", nonce_account);
1117
1118            let Some(nonce_data) =
1119                bincode::deserialize::<solana_nonce::versions::Versions>(&nonce_account.data).ok()
1120            else {
1121                return false;
1122            };
1123            trace!("Nonce account data: {:?}", nonce_data);
1124
1125            let nonce_state = nonce_data.state();
1126            let initialized_state = match nonce_state {
1127                solana_nonce::state::State::Uninitialized => return false,
1128                solana_nonce::state::State::Initialized(data) => data,
1129            };
1130            return initialized_state.blockhash() == *recent_blockhash;
1131        } else {
1132            self.check_blockhash_is_recent(recent_blockhash)
1133        }
1134    }
1135
1136    /// Verifies the signature of a transaction and validates that it hasn't already been processed.
1137    /// ### Note
1138    /// LiteSVM also can do this for our transactions, but we disable it.
1139    /// If sigverify is enabled at the LiteSVM level, the transaction simulations are always verified as well.
1140    /// So, if the user is trying to skip signature verification for a simulation, we'd need to unset and set this value,
1141    /// requiring a mutable reference to the SVM, which we don't have/want in the simulation path.
1142    /// Additionally, having this function internally lets us do this check before we start fetching accounts from mainnet.
1143    pub fn sigverify(&self, tx: &VersionedTransaction) -> Result<(), FailedTransactionMetadata> {
1144        let signature = tx.signatures[0];
1145
1146        if tx.verify_with_results().iter().any(|valid| !*valid) {
1147            return Err(FailedTransactionMetadata {
1148                err: TransactionError::SignatureFailure,
1149                meta: TransactionMetadata::default(),
1150            });
1151        }
1152
1153        if matches!(
1154            self.transactions.get(&signature.to_string()),
1155            Ok(Some(SurfnetTransactionStatus::Processed(_)))
1156        ) {
1157            return Err(FailedTransactionMetadata {
1158                err: TransactionError::AlreadyProcessed,
1159                meta: TransactionMetadata::default(),
1160            });
1161        }
1162        Ok(())
1163    }
1164
1165    /// Sets an account in the local SVM state and notifies listeners.
1166    ///
1167    /// # Arguments
1168    /// * `pubkey` - The public key of the account.
1169    /// * `account` - The [Account] to insert.
1170    ///
1171    /// # Returns
1172    /// `Ok(())` on success, or an error if the operation fails.
1173    pub fn set_account(&mut self, pubkey: &Pubkey, account: Account) -> SurfpoolResult<()> {
1174        self.inner
1175            .set_account(*pubkey, account.clone())
1176            .map_err(|e| SurfpoolError::set_account(*pubkey, e))?;
1177
1178        self.account_update_slots
1179            .insert(*pubkey, self.get_latest_absolute_slot());
1180
1181        // Update the account registries and indexes
1182        self.update_account_registries(pubkey, &account)?;
1183
1184        // Notify account subscribers
1185        self.notify_account_subscribers(pubkey, &account);
1186
1187        // Notify program subscribers
1188        self.notify_program_subscribers(pubkey, &account);
1189
1190        let _ = self
1191            .simnet_events_tx
1192            .send(SimnetEvent::account_update(*pubkey));
1193        Ok(())
1194    }
1195
1196    pub fn update_account_registries(
1197        &mut self,
1198        pubkey: &Pubkey,
1199        account: &Account,
1200    ) -> SurfpoolResult<()> {
1201        let is_deleted_account = account == &Account::default();
1202
1203        // When this function is called after processing a transaction, the account is already updated
1204        // in the inner SVM. However, the database hasn't been updated yet, so we need to manually update the db.
1205        if is_deleted_account {
1206            // This amounts to deleting the account from the db if the account is deleted in the SVM
1207            self.inner.delete_account_in_db(pubkey)?;
1208        } else {
1209            // Or updating the db account to match the SVM account if not deleted
1210            self.inner
1211                .set_account_in_db(*pubkey, account.clone().into())?;
1212        }
1213
1214        if is_deleted_account {
1215            self.closed_accounts.insert(*pubkey);
1216            if let Some(old_account) = self.get_account(pubkey)? {
1217                self.remove_from_indexes(pubkey, &old_account)?;
1218            }
1219            return Ok(());
1220        }
1221
1222        // only update our indexes if the account exists in the svm accounts db
1223        if let Some(old_account) = self.get_account(pubkey)? {
1224            self.remove_from_indexes(pubkey, &old_account)?;
1225        }
1226        // add to owner index (check for duplicates)
1227        let owner_key = account.owner.to_string();
1228        let pubkey_str = pubkey.to_string();
1229        let mut owner_accounts = self
1230            .accounts_by_owner
1231            .get(&owner_key)
1232            .ok()
1233            .flatten()
1234            .unwrap_or_default();
1235        if !owner_accounts.contains(&pubkey_str) {
1236            owner_accounts.push(pubkey_str.clone());
1237            self.accounts_by_owner.store(owner_key, owner_accounts)?;
1238        }
1239
1240        // if it's a token account, update token-specific indexes
1241        if is_supported_token_program(&account.owner) {
1242            if let Ok(token_account) = TokenAccount::unpack(&account.data) {
1243                // index by owner -> check for duplicates
1244                let owner_key = token_account.owner().to_string();
1245                let mut token_owner_accounts = self
1246                    .token_accounts_by_owner
1247                    .get(&owner_key)
1248                    .ok()
1249                    .flatten()
1250                    .unwrap_or_default();
1251                if !token_owner_accounts.contains(&pubkey_str) {
1252                    token_owner_accounts.push(pubkey_str.clone());
1253                    self.token_accounts_by_owner
1254                        .store(owner_key, token_owner_accounts)?;
1255                }
1256
1257                // index by mint -> check for duplicates
1258                let mint_key = token_account.mint().to_string();
1259                let mut mint_accounts = self
1260                    .token_accounts_by_mint
1261                    .get(&mint_key)
1262                    .ok()
1263                    .flatten()
1264                    .unwrap_or_default();
1265                if !mint_accounts.contains(&pubkey_str) {
1266                    mint_accounts.push(pubkey_str.clone());
1267                    self.token_accounts_by_mint.store(mint_key, mint_accounts)?;
1268                }
1269
1270                if let COption::Some(delegate) = token_account.delegate() {
1271                    let delegate_key = delegate.to_string();
1272                    let mut delegate_accounts = self
1273                        .token_accounts_by_delegate
1274                        .get(&delegate_key)
1275                        .ok()
1276                        .flatten()
1277                        .unwrap_or_default();
1278                    if !delegate_accounts.contains(&pubkey_str) {
1279                        delegate_accounts.push(pubkey_str);
1280                        self.token_accounts_by_delegate
1281                            .store(delegate_key, delegate_accounts)?;
1282                    }
1283                }
1284                self.token_accounts
1285                    .store(pubkey.to_string(), token_account)?;
1286            }
1287
1288            if let Ok(mint_account) = MintAccount::unpack(&account.data) {
1289                self.token_mints.store(pubkey.to_string(), mint_account)?;
1290            }
1291
1292            if let Ok(mint) =
1293                StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(&account.data)
1294            {
1295                let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1296                let interest_bearing_config = mint
1297                    .get_extension::<InterestBearingConfig>()
1298                    .map(|x| (*x, unix_timestamp))
1299                    .ok();
1300                let scaled_ui_amount_config = mint
1301                    .get_extension::<ScaledUiAmountConfig>()
1302                    .map(|x| (*x, unix_timestamp))
1303                    .ok();
1304                let additional_data: SerializableAccountAdditionalData = AccountAdditionalDataV3 {
1305                    spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1306                        decimals: mint.base.decimals,
1307                        interest_bearing_config,
1308                        scaled_ui_amount_config,
1309                    }),
1310                }
1311                .into();
1312                self.account_associated_data
1313                    .store(pubkey.to_string(), additional_data)?;
1314            };
1315        }
1316        Ok(())
1317    }
1318
1319    fn remove_from_indexes(
1320        &mut self,
1321        pubkey: &Pubkey,
1322        old_account: &Account,
1323    ) -> SurfpoolResult<()> {
1324        let owner_key = old_account.owner.to_string();
1325        let pubkey_str = pubkey.to_string();
1326        if let Some(mut accounts) = self.accounts_by_owner.get(&owner_key).ok().flatten() {
1327            accounts.retain(|pk| pk != &pubkey_str);
1328            if accounts.is_empty() {
1329                self.accounts_by_owner.take(&owner_key)?;
1330            } else {
1331                self.accounts_by_owner.store(owner_key, accounts)?;
1332            }
1333        }
1334
1335        // if it was a token account, remove from token indexes
1336        if is_supported_token_program(&old_account.owner) {
1337            if let Some(old_token_account) = self.token_accounts.take(&pubkey.to_string())? {
1338                let owner_key = old_token_account.owner().to_string();
1339                if let Some(mut accounts) =
1340                    self.token_accounts_by_owner.get(&owner_key).ok().flatten()
1341                {
1342                    accounts.retain(|pk| pk != &pubkey_str);
1343                    if accounts.is_empty() {
1344                        self.token_accounts_by_owner.take(&owner_key)?;
1345                    } else {
1346                        self.token_accounts_by_owner.store(owner_key, accounts)?;
1347                    }
1348                }
1349
1350                let mint_key = old_token_account.mint().to_string();
1351                if let Some(mut accounts) =
1352                    self.token_accounts_by_mint.get(&mint_key).ok().flatten()
1353                {
1354                    accounts.retain(|pk| pk != &pubkey_str);
1355                    if accounts.is_empty() {
1356                        self.token_accounts_by_mint.take(&mint_key)?;
1357                    } else {
1358                        self.token_accounts_by_mint.store(mint_key, accounts)?;
1359                    }
1360                }
1361
1362                if let COption::Some(delegate) = old_token_account.delegate() {
1363                    let delegate_key = delegate.to_string();
1364                    if let Some(mut accounts) = self
1365                        .token_accounts_by_delegate
1366                        .get(&delegate_key)
1367                        .ok()
1368                        .flatten()
1369                    {
1370                        accounts.retain(|pk| pk != &pubkey_str);
1371                        if accounts.is_empty() {
1372                            self.token_accounts_by_delegate.take(&delegate_key)?;
1373                        } else {
1374                            self.token_accounts_by_delegate
1375                                .store(delegate_key, accounts)?;
1376                        }
1377                    }
1378                }
1379            }
1380        }
1381        Ok(())
1382    }
1383
1384    pub fn reset_network(&mut self, epoch_info: EpochInfo) -> SurfpoolResult<()> {
1385        self.inner.reset(self.feature_set.clone())?;
1386
1387        let native_mint_account = self
1388            .inner
1389            .get_account(&spl_token_interface::native_mint::ID)?
1390            .unwrap();
1391
1392        let native_mint_associated_data = {
1393            let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
1394                &native_mint_account.data,
1395            )
1396            .unwrap();
1397            let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1398            let interest_bearing_config = mint
1399                .get_extension::<InterestBearingConfig>()
1400                .map(|x| (*x, unix_timestamp))
1401                .ok();
1402            let scaled_ui_amount_config = mint
1403                .get_extension::<ScaledUiAmountConfig>()
1404                .map(|x| (*x, unix_timestamp))
1405                .ok();
1406            AccountAdditionalDataV3 {
1407                spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1408                    decimals: mint.base.decimals,
1409                    interest_bearing_config,
1410                    scaled_ui_amount_config,
1411                }),
1412            }
1413        };
1414
1415        let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
1416
1417        self.blocks.clear()?;
1418        self.transactions.clear()?;
1419        self.transactions_queued_for_confirmation.clear();
1420        self.transactions_queued_for_finalization.clear();
1421        self.perf_samples.clear();
1422        self.transactions_processed = 0;
1423        self.profile_tag_map.clear()?;
1424        self.simulated_transaction_profiles.clear()?;
1425        self.executed_transaction_profiles.clear()?;
1426        self.accounts_by_owner.clear()?;
1427        self.accounts_by_owner.store(
1428            native_mint_account.owner.to_string(),
1429            vec![spl_token_interface::native_mint::ID.to_string()],
1430        )?;
1431        self.account_associated_data.clear()?;
1432        self.account_associated_data.store(
1433            spl_token_interface::native_mint::ID.to_string(),
1434            native_mint_associated_data.into(),
1435        )?;
1436        self.token_accounts.clear()?;
1437        self.token_mints.clear()?;
1438        self.token_mints.store(
1439            spl_token_interface::native_mint::ID.to_string(),
1440            parsed_mint_account,
1441        )?;
1442        self.token_accounts_by_owner.clear()?;
1443        self.token_accounts_by_delegate.clear()?;
1444        self.token_accounts_by_mint.clear()?;
1445        self.non_circulating_accounts.clear();
1446        self.registered_idls.clear()?;
1447        self.runbook_executions.clear();
1448        self.streamed_accounts.clear()?;
1449        self.scheduled_overrides.clear()?;
1450
1451        let current_time = chrono::Utc::now().timestamp_millis() as u64;
1452        self.updated_at = current_time;
1453        self.genesis_updated_at = current_time;
1454        self.latest_epoch_info = epoch_info.clone();
1455        // Set genesis_slot to the current slot when resetting (similar to initialize)
1456        self.genesis_slot = epoch_info.absolute_slot;
1457        let chain_tip_hash = SyntheticBlockhash::new(epoch_info.block_height).to_string();
1458        self.chain_tip = BlockIdentifier::new(epoch_info.block_height, chain_tip_hash.as_str());
1459        // Rebuild sysvars so getLatestBlockhash / sendTransaction stay aligned after reset.
1460        self.reconstruct_sysvars();
1461        // Reset checkpoint state to avoid recovering stale chain tips after a reset.
1462        self.slot_checkpoint.clear()?;
1463        self.last_checkpoint_slot = self.genesis_slot;
1464        self.recent_blockhashes.clear();
1465
1466        Ok(())
1467    }
1468
1469    pub fn reset_account(
1470        &mut self,
1471        pubkey: &Pubkey,
1472        include_owned_accounts: bool,
1473    ) -> SurfpoolResult<()> {
1474        let Some(account) = self.get_account(pubkey)? else {
1475            return Ok(());
1476        };
1477
1478        if account.executable {
1479            // Handle upgradeable program - also reset the program data account
1480            if account.owner == solana_sdk_ids::bpf_loader_upgradeable::id() {
1481                let program_data_pubkey =
1482                    solana_loader_v3_interface::get_program_data_address(pubkey);
1483
1484                // Reset the program data account first
1485                self.purge_account_from_cache(&account, &program_data_pubkey)?;
1486            }
1487        }
1488        if include_owned_accounts {
1489            let owned_accounts = self.get_account_owned_by(pubkey)?;
1490            for (owned_pubkey, _) in owned_accounts {
1491                // Avoid infinite recursion by not cascading further
1492                self.purge_account_from_cache(&account, &owned_pubkey)?;
1493            }
1494        }
1495        // Reset the account itself
1496        self.purge_account_from_cache(&account, pubkey)?;
1497        Ok(())
1498    }
1499
1500    fn purge_account_from_cache(
1501        &mut self,
1502        account: &Account,
1503        pubkey: &Pubkey,
1504    ) -> SurfpoolResult<()> {
1505        self.remove_from_indexes(pubkey, account)?;
1506
1507        self.inner.delete_account(pubkey)?;
1508
1509        Ok(())
1510    }
1511
1512    /// Sends a transaction to the system for execution.
1513    ///
1514    /// This function attempts to send a transaction to the blockchain. It first increments the `transactions_processed` counter.
1515    /// Then it sends the transaction to the system and updates its status. If the transaction is successfully processed, it is
1516    /// cached locally, and a "transaction processed" event is sent. If the transaction fails, the error is recorded and an event
1517    /// is sent indicating the failure.
1518    ///
1519    /// # Arguments
1520    /// * `tx` - The transaction to send.
1521    /// * `cu_analysis_enabled` - Whether compute unit analysis is enabled.
1522    ///
1523    /// # Returns
1524    /// `Ok(res)` if processed successfully, or `Err(tx_failure)` if failed.
1525    #[allow(clippy::result_large_err)]
1526    pub fn send_transaction(
1527        &mut self,
1528        tx: VersionedTransaction,
1529        cu_analysis_enabled: bool,
1530        sigverify: bool,
1531    ) -> TransactionResult {
1532        if sigverify {
1533            self.sigverify(&tx)?;
1534        }
1535
1536        if cu_analysis_enabled {
1537            let estimation_result = self.estimate_compute_units(&tx);
1538            let _ = self.simnet_events_tx.try_send(SimnetEvent::info(format!(
1539                "CU Estimation for tx: {} | Consumed: {} | Success: {} | Logs: {:?} | Error: {:?}",
1540                tx.signatures
1541                    .first()
1542                    .map_or_else(|| "N/A".to_string(), |s| s.to_string()),
1543                estimation_result.compute_units_consumed,
1544                estimation_result.success,
1545                estimation_result.log_messages,
1546                estimation_result.error_message
1547            )));
1548        }
1549        self.transactions_processed += 1;
1550
1551        if !self.validate_transaction_blockhash(&tx) {
1552            let meta = TransactionMetadata::default();
1553            let err = solana_transaction_error::TransactionError::BlockhashNotFound;
1554
1555            let transaction_meta = convert_transaction_metadata_from_canonical(&meta);
1556
1557            let _ = self
1558                .simnet_events_tx
1559                .try_send(SimnetEvent::transaction_processed(
1560                    transaction_meta,
1561                    Some(err.clone()),
1562                ));
1563            return Err(FailedTransactionMetadata { err, meta });
1564        }
1565
1566        match self.inner.send_transaction(tx.clone()) {
1567            Ok(res) => Ok(res),
1568            Err(tx_failure) => {
1569                let transaction_meta =
1570                    convert_transaction_metadata_from_canonical(&tx_failure.meta);
1571
1572                let _ = self
1573                    .simnet_events_tx
1574                    .try_send(SimnetEvent::transaction_processed(
1575                        transaction_meta,
1576                        Some(tx_failure.err.clone()),
1577                    ));
1578                Err(tx_failure)
1579            }
1580        }
1581    }
1582
1583    /// Estimates the compute units that a transaction will consume by simulating it.
1584    ///
1585    /// Does not commit any state changes to the SVM.
1586    ///
1587    /// # Arguments
1588    /// * `transaction` - The transaction to simulate.
1589    ///
1590    /// # Returns
1591    /// A `ComputeUnitsEstimationResult` with simulation details.
1592    pub fn estimate_compute_units(
1593        &self,
1594        transaction: &VersionedTransaction,
1595    ) -> ComputeUnitsEstimationResult {
1596        if !self.validate_transaction_blockhash(transaction) {
1597            return ComputeUnitsEstimationResult {
1598                success: false,
1599                compute_units_consumed: 0,
1600                log_messages: None,
1601                error_message: Some(
1602                    solana_transaction_error::TransactionError::BlockhashNotFound.to_string(),
1603                ),
1604            };
1605        }
1606
1607        match self.inner.simulate_transaction(transaction.clone()) {
1608            Ok(sim_info) => ComputeUnitsEstimationResult {
1609                success: true,
1610                compute_units_consumed: sim_info.meta.compute_units_consumed,
1611                log_messages: Some(sim_info.meta.logs),
1612                error_message: None,
1613            },
1614            Err(failed_meta) => ComputeUnitsEstimationResult {
1615                success: false,
1616                compute_units_consumed: failed_meta.meta.compute_units_consumed,
1617                log_messages: Some(failed_meta.meta.logs),
1618                error_message: Some(failed_meta.err.to_string()),
1619            },
1620        }
1621    }
1622
1623    /// Simulates a transaction and returns detailed simulation info or failure metadata.
1624    ///
1625    /// # Arguments
1626    /// * `tx` - The transaction to simulate.
1627    ///
1628    /// # Returns
1629    /// `Ok(SimulatedTransactionInfo)` if successful, or `Err(FailedTransactionMetadata)` if failed.
1630    #[allow(clippy::result_large_err)]
1631    pub fn simulate_transaction(
1632        &self,
1633        tx: VersionedTransaction,
1634        sigverify: bool,
1635    ) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
1636        if sigverify {
1637            self.sigverify(&tx)?;
1638        }
1639
1640        if !self.validate_transaction_blockhash(&tx) {
1641            let meta = TransactionMetadata::default();
1642            let err = TransactionError::BlockhashNotFound;
1643
1644            return Err(FailedTransactionMetadata { err, meta });
1645        }
1646        self.inner.simulate_transaction(tx)
1647    }
1648
1649    /// Confirms transactions queued for confirmation, updates epoch/slot, and sends events.
1650    ///
1651    /// # Returns
1652    /// `Ok(Vec<Signature>)` with confirmed signatures, or `Err(SurfpoolError)` on error.
1653    fn confirm_transactions(&mut self) -> Result<(Vec<Signature>, HashSet<Pubkey>), SurfpoolError> {
1654        let mut confirmed_transactions = vec![];
1655        let slot = self.latest_epoch_info.slot_index;
1656        let current_slot = self.latest_epoch_info.absolute_slot;
1657
1658        let mut all_mutated_account_keys = HashSet::new();
1659
1660        while let Some((tx, status_tx, error)) =
1661            self.transactions_queued_for_confirmation.pop_front()
1662        {
1663            let _ = status_tx.try_send(TransactionStatusEvent::Success(
1664                TransactionConfirmationStatus::Confirmed,
1665            ));
1666            let signature = tx.signatures[0];
1667            let finalized_at = self.latest_epoch_info.absolute_slot + FINALIZATION_SLOT_THRESHOLD;
1668            self.transactions_queued_for_finalization.push_back((
1669                finalized_at,
1670                tx,
1671                status_tx,
1672                error.clone(),
1673            ));
1674
1675            self.notify_signature_subscribers(
1676                SignatureSubscriptionType::confirmed(),
1677                &signature,
1678                slot,
1679                error,
1680            );
1681
1682            let Some(SurfnetTransactionStatus::Processed(tx_data)) =
1683                self.transactions.get(&signature.to_string()).ok().flatten()
1684            else {
1685                continue;
1686            };
1687            let (tx_with_status_meta, mutated_account_keys) = tx_data.as_ref();
1688            all_mutated_account_keys.extend(mutated_account_keys);
1689
1690            for pubkey in mutated_account_keys {
1691                self.account_update_slots.insert(*pubkey, current_slot);
1692            }
1693
1694            self.notify_logs_subscribers(
1695                &signature,
1696                None,
1697                tx_with_status_meta
1698                    .meta
1699                    .log_messages
1700                    .clone()
1701                    .unwrap_or(vec![]),
1702                CommitmentLevel::Confirmed,
1703            );
1704            confirmed_transactions.push(signature);
1705        }
1706
1707        Ok((confirmed_transactions, all_mutated_account_keys))
1708    }
1709
1710    /// Finalizes transactions queued for finalization, sending finalized events as needed.
1711    ///
1712    /// # Returns
1713    /// `Ok(())` on success, or `Err(SurfpoolError)` on error.
1714    fn finalize_transactions(&mut self) -> Result<(), SurfpoolError> {
1715        let current_slot = self.latest_epoch_info.absolute_slot;
1716        let mut requeue = VecDeque::new();
1717        while let Some((finalized_at, tx, status_tx, error)) =
1718            self.transactions_queued_for_finalization.pop_front()
1719        {
1720            if current_slot >= finalized_at {
1721                let _ = status_tx.try_send(TransactionStatusEvent::Success(
1722                    TransactionConfirmationStatus::Finalized,
1723                ));
1724                let signature = &tx.signatures[0];
1725                self.notify_signature_subscribers(
1726                    SignatureSubscriptionType::finalized(),
1727                    signature,
1728                    self.latest_epoch_info.absolute_slot,
1729                    error,
1730                );
1731                let Some(SurfnetTransactionStatus::Processed(tx_data)) =
1732                    self.transactions.get(&signature.to_string()).ok().flatten()
1733                else {
1734                    continue;
1735                };
1736                let (tx_with_status_meta, _) = tx_data.as_ref();
1737                let logs = tx_with_status_meta
1738                    .meta
1739                    .log_messages
1740                    .clone()
1741                    .unwrap_or(vec![]);
1742                self.notify_logs_subscribers(signature, None, logs, CommitmentLevel::Finalized);
1743            } else {
1744                requeue.push_back((finalized_at, tx, status_tx, error));
1745            }
1746        }
1747        // Requeue any transactions that are not yet finalized
1748        self.transactions_queued_for_finalization
1749            .append(&mut requeue);
1750
1751        Ok(())
1752    }
1753
1754    /// Writes account updates to the SVM state based on the provided account update result.
1755    ///
1756    /// # Arguments
1757    /// * `account_update` - The account update result to process.
1758    pub fn write_account_update(&mut self, account_update: GetAccountResult) {
1759        let init_programdata_account = |program_account: &Account| {
1760            if !program_account.executable {
1761                return None;
1762            }
1763            if !program_account
1764                .owner
1765                .eq(&solana_sdk_ids::bpf_loader_upgradeable::id())
1766            {
1767                return None;
1768            }
1769            let Ok(UpgradeableLoaderState::Program {
1770                programdata_address,
1771            }) = bincode::deserialize::<UpgradeableLoaderState>(&program_account.data)
1772            else {
1773                return None;
1774            };
1775
1776            let programdata_state = UpgradeableLoaderState::ProgramData {
1777                upgrade_authority_address: Some(system_program::id()),
1778                slot: self.get_latest_absolute_slot(),
1779            };
1780            let mut data = bincode::serialize(&programdata_state).unwrap();
1781
1782            data.extend_from_slice(&include_bytes!("../tests/assets/minimum_program.so").to_vec());
1783            let lamports = self.inner.minimum_balance_for_rent_exemption(data.len());
1784            Some((
1785                programdata_address,
1786                Account {
1787                    lamports,
1788                    data,
1789                    owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
1790                    executable: false,
1791                    rent_epoch: 0,
1792                },
1793            ))
1794        };
1795        match account_update {
1796            GetAccountResult::FoundAccount(pubkey, account, do_update_account) => {
1797                if do_update_account {
1798                    if let Some((programdata_address, programdata_account)) =
1799                        init_programdata_account(&account)
1800                    {
1801                        match self.get_account(&programdata_address) {
1802                            Ok(None) => {
1803                                if let Err(e) =
1804                                    self.set_account(&programdata_address, programdata_account)
1805                                {
1806                                    let _ = self
1807                                        .simnet_events_tx
1808                                        .send(SimnetEvent::error(e.to_string()));
1809                                }
1810                            }
1811                            Ok(Some(_)) => {}
1812                            Err(e) => {
1813                                let _ = self
1814                                    .simnet_events_tx
1815                                    .send(SimnetEvent::error(e.to_string()));
1816                            }
1817                        }
1818                    }
1819                    if let Err(e) = self.set_account(&pubkey, account.clone()) {
1820                        let _ = self
1821                            .simnet_events_tx
1822                            .send(SimnetEvent::error(e.to_string()));
1823                    }
1824                }
1825            }
1826            GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) => {
1827                if let Some((programdata_address, programdata_account)) =
1828                    init_programdata_account(&account)
1829                {
1830                    match self.get_account(&programdata_address) {
1831                        Ok(None) => {
1832                            if let Err(e) =
1833                                self.set_account(&programdata_address, programdata_account)
1834                            {
1835                                let _ = self
1836                                    .simnet_events_tx
1837                                    .send(SimnetEvent::error(e.to_string()));
1838                            }
1839                        }
1840                        Ok(Some(_)) => {}
1841                        Err(e) => {
1842                            let _ = self
1843                                .simnet_events_tx
1844                                .send(SimnetEvent::error(e.to_string()));
1845                        }
1846                    }
1847                }
1848                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1849                    let _ = self
1850                        .simnet_events_tx
1851                        .send(SimnetEvent::error(e.to_string()));
1852                }
1853            }
1854            GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => {
1855                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1856                    let _ = self
1857                        .simnet_events_tx
1858                        .send(SimnetEvent::error(e.to_string()));
1859                }
1860            }
1861            GetAccountResult::FoundProgramAccount(
1862                (pubkey, account),
1863                (coupled_pubkey, Some(coupled_account)),
1864            )
1865            | GetAccountResult::FoundTokenAccount(
1866                (pubkey, account),
1867                (coupled_pubkey, Some(coupled_account)),
1868            ) => {
1869                // The data account _must_ be set first, as the program account depends on it.
1870                if let Err(e) = self.set_account(&coupled_pubkey, coupled_account.clone()) {
1871                    let _ = self
1872                        .simnet_events_tx
1873                        .send(SimnetEvent::error(e.to_string()));
1874                }
1875                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1876                    let _ = self
1877                        .simnet_events_tx
1878                        .send(SimnetEvent::error(e.to_string()));
1879                }
1880            }
1881            GetAccountResult::None(_) => {}
1882        }
1883    }
1884
1885    pub fn confirm_current_block(&mut self) -> SurfpoolResult<()> {
1886        let slot = self.get_latest_absolute_slot();
1887        let previous_chain_tip = self.chain_tip.clone();
1888        if slot % *GARBAGE_COLLECTION_INTERVAL_SLOTS == 0 {
1889            debug!("Clearing liteSVM cache at slot {}", slot);
1890            self.inner.garbage_collect(self.feature_set.clone());
1891        }
1892        self.chain_tip = self.new_blockhash();
1893        // Confirm processed transactions
1894        let (confirmed_signatures, all_mutated_account_keys) = self.confirm_transactions()?;
1895        let write_version = self.increment_write_version();
1896
1897        // Notify Geyser plugin of account updates
1898        for pubkey in all_mutated_account_keys {
1899            let Some(account) = self.inner.get_account(&pubkey)? else {
1900                continue;
1901            };
1902            self.geyser_events_tx
1903                .send(GeyserEvent::UpdateAccount(
1904                    GeyserAccountUpdate::block_update(pubkey, account, slot, write_version),
1905                ))
1906                .ok();
1907        }
1908
1909        let num_transactions = confirmed_signatures.len() as u64;
1910        self.updated_at += self.slot_time;
1911
1912        // Only store blocks that have transactions (sparse block storage)
1913        // Empty blocks can be reconstructed on-the-fly from their slot number
1914        if !confirmed_signatures.is_empty() {
1915            self.blocks.store(
1916                slot,
1917                BlockHeader {
1918                    hash: self.chain_tip.hash.clone(),
1919                    previous_blockhash: previous_chain_tip.hash.clone(),
1920                    block_time: self.updated_at as i64 / 1_000,
1921                    block_height: self.chain_tip.index,
1922                    parent_slot: slot,
1923                    signatures: confirmed_signatures,
1924                },
1925            )?;
1926        }
1927
1928        // Checkpoint the latest slot periodically (~every 150 slots / 1 minute at standard slot time)
1929        // This allows recovery after restart without storing every empty block
1930        if slot.saturating_sub(self.last_checkpoint_slot) >= *CHECKPOINT_INTERVAL_SLOTS {
1931            self.slot_checkpoint
1932                .store("latest_slot".to_string(), slot)?;
1933            self.last_checkpoint_slot = slot;
1934        }
1935
1936        if self.perf_samples.len() > 30 {
1937            self.perf_samples.pop_back();
1938        }
1939        self.perf_samples.push_front(RpcPerfSample {
1940            slot,
1941            num_slots: 1,
1942            sample_period_secs: 1,
1943            num_transactions,
1944            num_non_vote_transactions: Some(num_transactions),
1945        });
1946
1947        self.latest_epoch_info.slot_index += 1;
1948        self.latest_epoch_info.block_height = self.chain_tip.index;
1949        self.latest_epoch_info.absolute_slot += 1;
1950        if self.latest_epoch_info.slot_index > self.latest_epoch_info.slots_in_epoch {
1951            self.latest_epoch_info.slot_index = 0;
1952            self.latest_epoch_info.epoch += 1;
1953        }
1954        let total_transactions = self.latest_epoch_info.transaction_count.unwrap_or(0);
1955        self.latest_epoch_info.transaction_count = Some(total_transactions + num_transactions);
1956
1957        let parent_slot = self.latest_epoch_info.absolute_slot.saturating_sub(1);
1958        let new_slot = self.latest_epoch_info.absolute_slot;
1959        let root = new_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
1960        self.notify_slot_subscribers(new_slot, parent_slot, root);
1961
1962        // Notify geyser plugins of slot status (Confirmed)
1963        self.geyser_events_tx
1964            .send(GeyserEvent::UpdateSlotStatus {
1965                slot: new_slot,
1966                parent: Some(parent_slot),
1967                status: GeyserSlotStatus::Confirmed,
1968            })
1969            .ok();
1970
1971        // Notify geyser plugins of block metadata
1972        let block_metadata = GeyserBlockMetadata {
1973            slot: new_slot,
1974            blockhash: self.chain_tip.hash.clone(),
1975            parent_slot,
1976            parent_blockhash: previous_chain_tip.hash.clone(),
1977            block_time: Some(self.updated_at as i64 / 1_000),
1978            block_height: Some(self.chain_tip.index),
1979            executed_transaction_count: num_transactions,
1980            entry_count: 1, // Surfpool produces 1 entry per block
1981        };
1982        self.geyser_events_tx
1983            .send(GeyserEvent::NotifyBlockMetadata(block_metadata))
1984            .ok();
1985
1986        // Notify geyser plugins of entry (Surfpool emits 1 entry per block)
1987        let entry_hash = solana_hash::Hash::from_str(&self.chain_tip.hash)
1988            .map(|h| h.to_bytes().to_vec())
1989            .unwrap_or_else(|_| vec![0u8; 32]);
1990        let entry_info = GeyserEntryInfo {
1991            slot: new_slot,
1992            index: 0, // Single entry per block
1993            num_hashes: 1,
1994            hash: entry_hash,
1995            executed_transaction_count: num_transactions,
1996            starting_transaction_index: 0,
1997        };
1998        self.geyser_events_tx
1999            .send(GeyserEvent::NotifyEntry(entry_info))
2000            .ok();
2001
2002        let clock: Clock = Clock {
2003            slot: self.latest_epoch_info.absolute_slot,
2004            epoch: self.latest_epoch_info.epoch,
2005            unix_timestamp: self.updated_at as i64 / 1_000,
2006            epoch_start_timestamp: 0, // todo
2007            leader_schedule_epoch: 0, // todo
2008        };
2009
2010        let _ = self
2011            .simnet_events_tx
2012            .send(SimnetEvent::SystemClockUpdated(clock.clone()));
2013        self.inner.set_sysvar(&clock);
2014
2015        self.finalize_transactions()?;
2016
2017        // Notify geyser plugins of newly rooted (finalized) slot
2018        // Only emit if root is a valid slot (greater than genesis)
2019        if root >= self.genesis_slot {
2020            self.geyser_events_tx
2021                .send(GeyserEvent::UpdateSlotStatus {
2022                    slot: root,
2023                    parent: root.checked_sub(1),
2024                    status: GeyserSlotStatus::Rooted,
2025                })
2026                .ok();
2027        }
2028
2029        // Evict the accounts marked as streamed from cache to enforce them to be fetched again
2030        let accounts_to_reset: Vec<_> = self.streamed_accounts.into_iter()?.collect();
2031        for (pubkey_str, include_owned_accounts) in accounts_to_reset {
2032            let pubkey = Pubkey::from_str(&pubkey_str)
2033                .map_err(|e| SurfpoolError::invalid_pubkey(&pubkey_str, e.to_string()))?;
2034            self.reset_account(&pubkey, include_owned_accounts)?;
2035        }
2036
2037        Ok(())
2038    }
2039
2040    /// Materializes scheduled overrides for the current slot
2041    ///
2042    /// This function:
2043    /// 1. Dequeues overrides scheduled for the current slot
2044    /// 2. Resolves account addresses (Pubkey or PDA)
2045    /// 3. Optionally fetches fresh account data from remote if `fetch_before_use` is enabled
2046    /// 4. Applies the overrides to the account data
2047    /// 5. Updates the SVM state
2048    pub async fn materialize_overrides(
2049        &mut self,
2050        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2051    ) -> SurfpoolResult<()> {
2052        let current_slot = self.latest_epoch_info.absolute_slot;
2053
2054        // Remove and get overrides for this slot
2055        let Some(overrides) = self.scheduled_overrides.take(&current_slot)? else {
2056            // No overrides for this slot
2057            return Ok(());
2058        };
2059
2060        debug!(
2061            "Materializing {} override(s) for slot {}",
2062            overrides.len(),
2063            current_slot
2064        );
2065
2066        for override_instance in overrides {
2067            if !override_instance.enabled {
2068                debug!("Skipping disabled override: {}", override_instance.id);
2069                continue;
2070            }
2071
2072            // Resolve account address
2073            let account_pubkey = match &override_instance.account {
2074                surfpool_types::AccountAddress::Pubkey(pubkey_str) => {
2075                    match Pubkey::from_str(pubkey_str) {
2076                        Ok(pubkey) => pubkey,
2077                        Err(e) => {
2078                            warn!(
2079                                "Failed to parse pubkey '{}' for override {}: {}",
2080                                pubkey_str, override_instance.id, e
2081                            );
2082                            continue;
2083                        }
2084                    }
2085                }
2086                surfpool_types::AccountAddress::Pda {
2087                    program_id: _,
2088                    seeds: _,
2089                } => unimplemented!(),
2090            };
2091
2092            debug!(
2093                "Processing override {} for account {} (label: {:?})",
2094                override_instance.id, account_pubkey, override_instance.label
2095            );
2096
2097            // Fetch fresh account data from remote if requested
2098            if override_instance.fetch_before_use {
2099                if let Some((client, _)) = remote_ctx {
2100                    debug!(
2101                        "Fetching fresh account data for {} from remote",
2102                        account_pubkey
2103                    );
2104
2105                    match client
2106                        .get_account(&account_pubkey, CommitmentConfig::confirmed())
2107                        .await
2108                    {
2109                        Ok(GetAccountResult::FoundAccount(_pubkey, remote_account, _)) => {
2110                            debug!(
2111                                "Fetched account {} from remote: {} lamports, {} bytes",
2112                                account_pubkey,
2113                                remote_account.lamports(),
2114                                remote_account.data().len()
2115                            );
2116
2117                            // Set the fresh account data in the SVM
2118                            if let Err(e) = self.inner.set_account(account_pubkey, remote_account) {
2119                                warn!(
2120                                    "Failed to set account {} from remote: {}",
2121                                    account_pubkey, e
2122                                );
2123                            }
2124                        }
2125                        Ok(GetAccountResult::None(_)) => {
2126                            debug!("Account {} not found on remote", account_pubkey);
2127                        }
2128                        Ok(_) => {
2129                            debug!("Account {} fetched (other variant)", account_pubkey);
2130                        }
2131                        Err(e) => {
2132                            warn!(
2133                                "Failed to fetch account {} from remote: {}",
2134                                account_pubkey, e
2135                            );
2136                        }
2137                    }
2138                } else {
2139                    debug!(
2140                        "fetch_before_use enabled but no remote client available for override {}",
2141                        override_instance.id
2142                    );
2143                }
2144            }
2145
2146            // Apply the override values to the account data
2147            if !override_instance.values.is_empty() {
2148                debug!(
2149                    "Override {} applying {} field modification(s) to account {}",
2150                    override_instance.id,
2151                    override_instance.values.len(),
2152                    account_pubkey
2153                );
2154
2155                // Get the account from the SVM
2156                let Some(account) = self.inner.get_account(&account_pubkey)? else {
2157                    warn!(
2158                        "Account {} not found in SVM for override {}, skipping modifications",
2159                        account_pubkey, override_instance.id
2160                    );
2161                    continue;
2162                };
2163
2164                // Get the account owner (program ID)
2165                let owner_program_id = account.owner();
2166
2167                // Look up the IDL for the owner program
2168                let idl_versions = match self.registered_idls.get(&owner_program_id.to_string()) {
2169                    Ok(Some(versions)) => versions,
2170                    Ok(None) => {
2171                        warn!(
2172                            "No IDL registered for program {} (owner of account {}), skipping override {}",
2173                            owner_program_id, account_pubkey, override_instance.id
2174                        );
2175                        continue;
2176                    }
2177                    Err(e) => {
2178                        warn!(
2179                            "Failed to get IDL for program {}: {}, skipping override {}",
2180                            owner_program_id, e, override_instance.id
2181                        );
2182                        continue;
2183                    }
2184                };
2185
2186                // Get the latest IDL version (first in the sorted Vec)
2187                let Some(versioned_idl) = idl_versions.first() else {
2188                    warn!(
2189                        "IDL versions empty for program {}, skipping override {}",
2190                        owner_program_id, override_instance.id
2191                    );
2192                    continue;
2193                };
2194
2195                let idl = &versioned_idl.1;
2196
2197                // Get account data
2198                let account_data = account.data();
2199
2200                // Use get_forged_account_data to apply the overrides
2201                let new_account_data = match self.get_forged_account_data(
2202                    &account_pubkey,
2203                    account_data,
2204                    idl,
2205                    &override_instance.values,
2206                ) {
2207                    Ok(data) => data,
2208                    Err(e) => {
2209                        warn!(
2210                            "Failed to forge account data for {} (override {}): {}",
2211                            account_pubkey, override_instance.id, e
2212                        );
2213                        continue;
2214                    }
2215                };
2216
2217                // Create a new account with modified data
2218                let modified_account = Account {
2219                    lamports: account.lamports(),
2220                    data: new_account_data,
2221                    owner: *account.owner(),
2222                    executable: account.executable(),
2223                    rent_epoch: account.rent_epoch(),
2224                };
2225
2226                // Update the account in the SVM
2227                if let Err(e) = self.inner.set_account(account_pubkey, modified_account) {
2228                    warn!(
2229                        "Failed to set modified account {} in SVM: {}",
2230                        account_pubkey, e
2231                    );
2232                } else {
2233                    debug!(
2234                        "Successfully applied {} override(s) to account {} (override {})",
2235                        override_instance.values.len(),
2236                        account_pubkey,
2237                        override_instance.id
2238                    );
2239                }
2240            }
2241        }
2242
2243        Ok(())
2244    }
2245
2246    /// Forges account data by applying overrides to existing account data
2247    ///
2248    /// This function:
2249    /// 1. Validates account data size (must be at least 8 bytes for discriminator)
2250    /// 2. Splits discriminator and serialized data
2251    /// 3. Finds the account type in the IDL using the discriminator
2252    /// 4. Deserializes the account data
2253    /// 5. Applies field overrides using dot notation
2254    /// 6. Re-serializes the modified data
2255    /// 7. Reconstructs the account data with the original discriminator
2256    ///
2257    /// # Arguments
2258    /// * `account_pubkey` - The account address (for error messages)
2259    /// * `account_data` - The original account data bytes
2260    /// * `idl` - The IDL for the account's program
2261    /// * `overrides` - Map of field paths to new values
2262    ///
2263    /// # Returns
2264    /// The forged account data as bytes, or an error
2265    pub fn get_forged_account_data(
2266        &self,
2267        account_pubkey: &Pubkey,
2268        account_data: &[u8],
2269        idl: &Idl,
2270        overrides: &HashMap<String, serde_json::Value>,
2271    ) -> SurfpoolResult<Vec<u8>> {
2272        // Validate account data size
2273        if account_data.len() < 8 {
2274            return Err(SurfpoolError::invalid_account_data(
2275                account_pubkey,
2276                "Account data too small to be an Anchor account (need at least 8 bytes for discriminator)",
2277                Some("Data length too small"),
2278            ));
2279        }
2280
2281        // Split discriminator and data
2282        let discriminator = &account_data[..8];
2283        let serialized_data = &account_data[8..];
2284
2285        // Find the account type using the discriminator
2286        let account_def = idl
2287            .accounts
2288            .iter()
2289            .find(|acc| acc.discriminator.eq(discriminator))
2290            .ok_or_else(|| {
2291                SurfpoolError::internal(format!(
2292                    "Account with discriminator '{:?}' not found in IDL",
2293                    discriminator
2294                ))
2295            })?;
2296
2297        // Find the corresponding type definition
2298        let account_type = idl
2299            .types
2300            .iter()
2301            .find(|t| t.name == account_def.name)
2302            .ok_or_else(|| {
2303                SurfpoolError::internal(format!(
2304                    "Type definition for account '{}' not found in IDL",
2305                    account_def.name
2306                ))
2307            })?;
2308
2309        // Set up generics for parsing
2310        let empty_vec = vec![];
2311        let idl_type_def_generics = idl
2312            .types
2313            .iter()
2314            .find(|t| t.name == account_type.name)
2315            .map(|t| &t.generics);
2316
2317        // Deserialize the account data using proper Borsh deserialization
2318        // Use the version that returns leftover bytes to preserve any trailing padding
2319        let (mut parsed_value, leftover_bytes) =
2320            parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes(
2321                serialized_data,
2322                &account_type.ty,
2323                &idl.types,
2324                &vec![],
2325                idl_type_def_generics.unwrap_or(&empty_vec),
2326            )
2327            .map_err(|e| {
2328                SurfpoolError::deserialize_error(
2329                    "account data",
2330                    format!("Failed to deserialize account data using Borsh: {}", e),
2331                )
2332            })?;
2333
2334        // Apply overrides to the decoded value
2335        for (path, value) in overrides {
2336            apply_override_to_decoded_account(&mut parsed_value, path, value)?;
2337        }
2338
2339        // Construct an IdlType::Defined that references the account type
2340        // This is needed because borsh_encode_value_to_idl_type expects IdlType, not IdlTypeDefTy
2341        use anchor_lang_idl::types::{IdlGenericArg, IdlType};
2342        let defined_type = IdlType::Defined {
2343            name: account_type.name.clone(),
2344            generics: account_type
2345                .generics
2346                .iter()
2347                .map(|_| IdlGenericArg::Type {
2348                    ty: IdlType::String,
2349                })
2350                .collect(),
2351        };
2352
2353        // Re-encode the value using Borsh
2354        let re_encoded_data =
2355            borsh_encode_value_to_idl_type(&parsed_value, &defined_type, &idl.types, None)
2356                .map_err(|e| {
2357                    SurfpoolError::internal(format!(
2358                        "Failed to re-encode account data using Borsh: {}",
2359                        e
2360                    ))
2361                })?;
2362
2363        // Reconstruct the account data with discriminator and preserve any trailing bytes
2364        let mut new_account_data =
2365            Vec::with_capacity(8 + re_encoded_data.len() + leftover_bytes.len());
2366        new_account_data.extend_from_slice(discriminator);
2367        new_account_data.extend_from_slice(&re_encoded_data);
2368        new_account_data.extend_from_slice(leftover_bytes);
2369
2370        Ok(new_account_data)
2371    }
2372
2373    /// Subscribes for updates on a transaction signature for a given subscription type.
2374    ///
2375    /// # Arguments
2376    /// * `signature` - The transaction signature to subscribe to.
2377    /// * `subscription_type` - The type of subscription (confirmed/finalized).
2378    ///
2379    /// # Returns
2380    /// A receiver for slot and transaction error updates.
2381    pub fn subscribe_for_signature_updates(
2382        &mut self,
2383        signature: &Signature,
2384        subscription_type: SignatureSubscriptionType,
2385    ) -> Receiver<(Slot, Option<TransactionError>)> {
2386        let (tx, rx) = unbounded();
2387        self.signature_subscriptions
2388            .entry(*signature)
2389            .or_default()
2390            .push((subscription_type, tx));
2391        rx
2392    }
2393
2394    pub fn subscribe_for_account_updates(
2395        &mut self,
2396        account_pubkey: &Pubkey,
2397        encoding: Option<UiAccountEncoding>,
2398    ) -> Receiver<UiAccount> {
2399        let (tx, rx) = unbounded();
2400        self.account_subscriptions
2401            .entry(*account_pubkey)
2402            .or_default()
2403            .push((encoding, tx));
2404        rx
2405    }
2406
2407    pub fn subscribe_for_program_updates(
2408        &mut self,
2409        program_id: &Pubkey,
2410        encoding: Option<UiAccountEncoding>,
2411        filters: Option<Vec<RpcFilterType>>,
2412    ) -> Receiver<RpcKeyedAccount> {
2413        let (tx, rx) = unbounded();
2414        self.program_subscriptions
2415            .entry(*program_id)
2416            .or_default()
2417            .push((encoding, filters, tx));
2418        rx
2419    }
2420
2421    /// Notifies signature subscribers of a status update, sending slot and error info.
2422    ///
2423    /// # Arguments
2424    /// * `status` - The subscription type (confirmed/finalized).
2425    /// * `signature` - The transaction signature.
2426    /// * `slot` - The slot number.
2427    /// * `err` - Optional transaction error.
2428    pub fn notify_signature_subscribers(
2429        &mut self,
2430        status: SignatureSubscriptionType,
2431        signature: &Signature,
2432        slot: Slot,
2433        err: Option<TransactionError>,
2434    ) {
2435        let mut remaining = vec![];
2436        if let Some(subscriptions) = self.signature_subscriptions.remove(signature) {
2437            for (subscription_type, tx) in subscriptions {
2438                if status.eq(&subscription_type) {
2439                    if tx.send((slot, err.clone())).is_err() {
2440                        // The receiver has been dropped, so we can skip notifying
2441                        continue;
2442                    }
2443                } else {
2444                    remaining.push((subscription_type, tx));
2445                }
2446            }
2447            if !remaining.is_empty() {
2448                self.signature_subscriptions.insert(*signature, remaining);
2449            }
2450        }
2451    }
2452
2453    pub fn notify_account_subscribers(
2454        &mut self,
2455        account_updated_pubkey: &Pubkey,
2456        account: &Account,
2457    ) {
2458        let mut remaining = vec![];
2459        if let Some(subscriptions) = self.account_subscriptions.remove(account_updated_pubkey) {
2460            for (encoding, tx) in subscriptions {
2461                let config = RpcAccountInfoConfig {
2462                    encoding,
2463                    ..Default::default()
2464                };
2465                let account = self
2466                    .account_to_rpc_keyed_account(account_updated_pubkey, account, &config, None)
2467                    .account;
2468                if tx.send(account).is_err() {
2469                    // The receiver has been dropped, so we can skip notifying
2470                    continue;
2471                } else {
2472                    remaining.push((encoding, tx));
2473                }
2474            }
2475            if !remaining.is_empty() {
2476                self.account_subscriptions
2477                    .insert(*account_updated_pubkey, remaining);
2478            }
2479        }
2480    }
2481
2482    pub fn notify_program_subscribers(&mut self, account_pubkey: &Pubkey, account: &Account) {
2483        let program_id = account.owner;
2484        let mut remaining = vec![];
2485        if let Some(subscriptions) = self.program_subscriptions.remove(&program_id) {
2486            for (encoding, filters, tx) in subscriptions {
2487                // Apply filters if present
2488                if let Some(ref active_filters) = filters {
2489                    match super::locker::apply_rpc_filters(&account.data, active_filters) {
2490                        Ok(true) => {} // Account matches all filters
2491                        Ok(false) => {
2492                            // Filtered out - keep subscription active but don't notify
2493                            remaining.push((encoding, filters, tx));
2494                            continue;
2495                        }
2496                        Err(_) => {
2497                            // Error applying filter - keep subscription, skip notification
2498                            remaining.push((encoding, filters, tx));
2499                            continue;
2500                        }
2501                    }
2502                }
2503
2504                let config = RpcAccountInfoConfig {
2505                    encoding,
2506                    ..Default::default()
2507                };
2508                let keyed_account =
2509                    self.account_to_rpc_keyed_account(account_pubkey, account, &config, None);
2510                if tx.send(keyed_account).is_err() {
2511                    // The receiver has been dropped, so we can skip notifying
2512                    continue;
2513                } else {
2514                    remaining.push((encoding, filters, tx));
2515                }
2516            }
2517            if !remaining.is_empty() {
2518                self.program_subscriptions.insert(program_id, remaining);
2519            }
2520        }
2521    }
2522
2523    /// Retrieves a confirmed block at the given slot, including transactions and metadata.
2524    ///
2525    /// # Arguments
2526    /// * `slot` - The slot number to retrieve the block for.
2527    /// * `config` - The configuration for the block retrieval.
2528    ///
2529    /// # Returns
2530    /// `Some(UiConfirmedBlock)` if found, or `None` if not present.
2531    pub fn get_block_at_slot(
2532        &self,
2533        slot: Slot,
2534        config: &RpcBlockConfig,
2535    ) -> SurfpoolResult<Option<UiConfirmedBlock>> {
2536        // Try to get stored block, or reconstruct empty block if within valid range
2537        let Some(block) = self.get_block_or_reconstruct(slot)? else {
2538            return Ok(None);
2539        };
2540
2541        let show_rewards = config.rewards.unwrap_or(true);
2542        let transaction_details = config
2543            .transaction_details
2544            .unwrap_or(TransactionDetails::Full);
2545
2546        let transactions = match transaction_details {
2547            TransactionDetails::Full => Some(
2548                block
2549                    .signatures
2550                    .iter()
2551                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
2552                    .map(|tx_with_meta| {
2553                        let (meta, _) = tx_with_meta.expect_processed();
2554                        meta.encode(
2555                            config.encoding.unwrap_or(
2556                                solana_transaction_status::UiTransactionEncoding::JsonParsed,
2557                            ),
2558                            config.max_supported_transaction_version,
2559                            show_rewards,
2560                        )
2561                    })
2562                    .collect::<Result<Vec<_>, _>>()
2563                    .map_err(SurfpoolError::from)?,
2564            ),
2565            TransactionDetails::Signatures => None,
2566            TransactionDetails::None => None,
2567            TransactionDetails::Accounts => Some(
2568                block
2569                    .signatures
2570                    .iter()
2571                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
2572                    .map(|tx_with_meta| {
2573                        let (meta, _) = tx_with_meta.expect_processed();
2574                        meta.to_json_accounts(
2575                            config.max_supported_transaction_version,
2576                            show_rewards,
2577                        )
2578                    })
2579                    .collect::<Result<Vec<_>, _>>()
2580                    .map_err(SurfpoolError::from)?,
2581            ),
2582        };
2583
2584        let signatures = match transaction_details {
2585            TransactionDetails::Signatures => {
2586                Some(block.signatures.iter().map(|t| t.to_string()).collect())
2587            }
2588            TransactionDetails::Full | TransactionDetails::Accounts | TransactionDetails::None => {
2589                None
2590            }
2591        };
2592
2593        let block = UiConfirmedBlock {
2594            previous_blockhash: block.previous_blockhash.clone(),
2595            blockhash: block.hash.clone(),
2596            parent_slot: block.parent_slot,
2597            transactions,
2598            signatures,
2599            rewards: if show_rewards { Some(vec![]) } else { None },
2600            num_reward_partitions: None,
2601            block_time: Some(block.block_time / 1000),
2602            block_height: Some(block.block_height),
2603        };
2604        Ok(Some(block))
2605    }
2606
2607    /// Returns the blockhash for a given slot, if available.
2608    pub fn blockhash_for_slot(&self, slot: Slot) -> Option<Hash> {
2609        self.blocks
2610            .get(&slot)
2611            .unwrap()
2612            .and_then(|header| header.hash.parse().ok())
2613    }
2614
2615    /// Gets all accounts owned by a specific program ID from the account registry.
2616    ///
2617    /// # Arguments
2618    ///
2619    /// * `program_id` - The program ID to search for owned accounts.
2620    ///
2621    /// # Returns
2622    ///
2623    /// * A vector of (account_pubkey, account) tuples for all accounts owned by the program.
2624    pub fn get_account_owned_by(
2625        &self,
2626        program_id: &Pubkey,
2627    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
2628        let account_pubkeys = self
2629            .accounts_by_owner
2630            .get(&program_id.to_string())
2631            .ok()
2632            .flatten()
2633            .unwrap_or_default();
2634
2635        account_pubkeys
2636            .iter()
2637            .filter_map(|pk_str| {
2638                let pk = Pubkey::from_str(pk_str).ok()?;
2639                self.get_account(&pk)
2640                    .map(|res| res.map(|account| (pk, account.clone())))
2641                    .transpose()
2642            })
2643            .collect::<Result<Vec<_>, SurfpoolError>>()
2644    }
2645
2646    fn get_additional_data(
2647        &self,
2648        pubkey: &Pubkey,
2649        token_mint: Option<Pubkey>,
2650    ) -> Option<AccountAdditionalDataV3> {
2651        let token_mint = if let Some(mint) = token_mint {
2652            Some(mint)
2653        } else {
2654            self.token_accounts
2655                .get(&pubkey.to_string())
2656                .ok()
2657                .flatten()
2658                .map(|ta| ta.mint())
2659        };
2660
2661        token_mint.and_then(|mint| {
2662            self.account_associated_data
2663                .get(&mint.to_string())
2664                .ok()
2665                .flatten()
2666                .and_then(|data| data.try_into().ok())
2667        })
2668    }
2669
2670    pub fn account_to_rpc_keyed_account<T: ReadableAccount>(
2671        &self,
2672        pubkey: &Pubkey,
2673        account: &T,
2674        config: &RpcAccountInfoConfig,
2675        token_mint: Option<Pubkey>,
2676    ) -> RpcKeyedAccount {
2677        let additional_data = self.get_additional_data(pubkey, token_mint);
2678
2679        RpcKeyedAccount {
2680            pubkey: pubkey.to_string(),
2681            account: self.encode_ui_account(
2682                pubkey,
2683                account,
2684                config.encoding.unwrap_or(UiAccountEncoding::Base64),
2685                additional_data,
2686                config.data_slice,
2687            ),
2688        }
2689    }
2690
2691    /// Gets all token accounts that have delegated authority to a specific delegate.
2692    ///
2693    /// # Arguments
2694    ///
2695    /// * `delegate` - The delegate pubkey to search for token accounts that have granted authority.
2696    ///
2697    /// # Returns
2698    ///
2699    /// * A vector of (account_pubkey, token_account) tuples for all token accounts delegated to the specified delegate.
2700    pub fn get_token_accounts_by_delegate(&self, delegate: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
2701        if let Some(account_pubkeys) = self
2702            .token_accounts_by_delegate
2703            .get(&delegate.to_string())
2704            .ok()
2705            .flatten()
2706        {
2707            account_pubkeys
2708                .iter()
2709                .filter_map(|pk_str| {
2710                    let pk = Pubkey::from_str(pk_str).ok()?;
2711                    self.token_accounts
2712                        .get(pk_str)
2713                        .ok()
2714                        .flatten()
2715                        .map(|ta| (pk, ta))
2716                })
2717                .collect()
2718        } else {
2719            Vec::new()
2720        }
2721    }
2722
2723    /// Gets all token accounts owned by a specific owner.
2724    ///
2725    /// # Arguments
2726    ///
2727    /// * `owner` - The owner pubkey to search for token accounts.
2728    ///
2729    /// # Returns
2730    ///
2731    /// * A vector of (account_pubkey, token_account) tuples for all token accounts owned by the specified owner.
2732    pub fn get_parsed_token_accounts_by_owner(
2733        &self,
2734        owner: &Pubkey,
2735    ) -> Vec<(Pubkey, TokenAccount)> {
2736        if let Some(account_pubkeys) = self
2737            .token_accounts_by_owner
2738            .get(&owner.to_string())
2739            .ok()
2740            .flatten()
2741        {
2742            account_pubkeys
2743                .iter()
2744                .filter_map(|pk_str| {
2745                    let pk = Pubkey::from_str(pk_str).ok()?;
2746                    self.token_accounts
2747                        .get(pk_str)
2748                        .ok()
2749                        .flatten()
2750                        .map(|ta| (pk, ta))
2751                })
2752                .collect()
2753        } else {
2754            Vec::new()
2755        }
2756    }
2757
2758    pub fn get_token_accounts_by_owner(
2759        &self,
2760        owner: &Pubkey,
2761    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
2762        let account_pubkeys = self
2763            .token_accounts_by_owner
2764            .get(&owner.to_string())
2765            .ok()
2766            .flatten()
2767            .unwrap_or_default();
2768
2769        account_pubkeys
2770            .iter()
2771            .filter_map(|pk_str| {
2772                let pk = Pubkey::from_str(pk_str).ok()?;
2773                self.get_account(&pk)
2774                    .map(|res| res.map(|account| (pk, account.clone())))
2775                    .transpose()
2776            })
2777            .collect::<Result<Vec<_>, SurfpoolError>>()
2778    }
2779
2780    /// Gets all token accounts for a specific mint (token type).
2781    ///
2782    /// # Arguments
2783    ///
2784    /// * `mint` - The mint pubkey to search for token accounts.
2785    ///
2786    /// # Returns
2787    ///
2788    /// * A vector of (account_pubkey, token_account) tuples for all token accounts of the specified mint.
2789    pub fn get_token_accounts_by_mint(&self, mint: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
2790        if let Some(account_pubkeys) = self
2791            .token_accounts_by_mint
2792            .get(&mint.to_string())
2793            .ok()
2794            .flatten()
2795        {
2796            account_pubkeys
2797                .iter()
2798                .filter_map(|pk_str| {
2799                    let pk = Pubkey::from_str(pk_str).ok()?;
2800                    self.token_accounts
2801                        .get(pk_str)
2802                        .ok()
2803                        .flatten()
2804                        .map(|ta| (pk, ta))
2805                })
2806                .collect()
2807        } else {
2808            Vec::new()
2809        }
2810    }
2811
2812    pub fn subscribe_for_slot_updates(&mut self) -> Receiver<SlotInfo> {
2813        let (tx, rx) = unbounded();
2814        self.slot_subscriptions.push(tx);
2815        rx
2816    }
2817
2818    pub fn notify_slot_subscribers(&mut self, slot: Slot, parent: Slot, root: Slot) {
2819        self.slot_subscriptions
2820            .retain(|tx| tx.send(SlotInfo { slot, parent, root }).is_ok());
2821    }
2822
2823    pub fn write_simulated_profile_result(
2824        &mut self,
2825        uuid: Uuid,
2826        tag: Option<String>,
2827        profile_result: KeyedProfileResult,
2828    ) -> SurfpoolResult<()> {
2829        self.simulated_transaction_profiles
2830            .store(uuid.to_string(), profile_result)?;
2831
2832        let tag = tag.unwrap_or_else(|| uuid.to_string());
2833        let mut tags = self
2834            .profile_tag_map
2835            .get(&tag)
2836            .ok()
2837            .flatten()
2838            .unwrap_or_default();
2839        tags.push(UuidOrSignature::Uuid(uuid));
2840        self.profile_tag_map.store(tag, tags)?;
2841        Ok(())
2842    }
2843
2844    pub fn write_executed_profile_result(
2845        &mut self,
2846        signature: Signature,
2847        profile_result: KeyedProfileResult,
2848    ) -> SurfpoolResult<()> {
2849        self.executed_transaction_profiles
2850            .store(signature.to_string(), profile_result)?;
2851        let tag = signature.to_string();
2852        let mut tags = self
2853            .profile_tag_map
2854            .get(&tag)
2855            .ok()
2856            .flatten()
2857            .unwrap_or_default();
2858        tags.push(UuidOrSignature::Signature(signature));
2859        self.profile_tag_map.store(tag, tags)?;
2860        Ok(())
2861    }
2862
2863    pub fn subscribe_for_logs_updates(
2864        &mut self,
2865        commitment_level: &CommitmentLevel,
2866        filter: &RpcTransactionLogsFilter,
2867    ) -> Receiver<(Slot, RpcLogsResponse)> {
2868        let (tx, rx) = unbounded();
2869        self.logs_subscriptions
2870            .push((*commitment_level, filter.clone(), tx));
2871        rx
2872    }
2873
2874    pub fn notify_logs_subscribers(
2875        &mut self,
2876        signature: &Signature,
2877        err: Option<TransactionError>,
2878        logs: Vec<String>,
2879        commitment_level: CommitmentLevel,
2880    ) {
2881        for (expected_level, filter, tx) in self.logs_subscriptions.iter() {
2882            if !expected_level.eq(&commitment_level) {
2883                continue; // Skip if commitment level is not expected
2884            }
2885
2886            let should_notify = match filter {
2887                RpcTransactionLogsFilter::All | RpcTransactionLogsFilter::AllWithVotes => true,
2888
2889                RpcTransactionLogsFilter::Mentions(mentioned_accounts) => {
2890                    // Get the tx accounts including loaded addresses
2891                    let transaction_accounts =
2892                        if let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2893                            self.transactions.get(&signature.to_string()).ok().flatten()
2894                        {
2895                            let (tx_meta, _) = tx_data.as_ref();
2896                            let mut accounts = match &tx_meta.transaction.message {
2897                                VersionedMessage::Legacy(msg) => msg.account_keys.clone(),
2898                                VersionedMessage::V0(msg) => msg.account_keys.clone(),
2899                            };
2900
2901                            accounts.extend(&tx_meta.meta.loaded_addresses.writable);
2902                            accounts.extend(&tx_meta.meta.loaded_addresses.readonly);
2903                            Some(accounts)
2904                        } else {
2905                            None
2906                        };
2907
2908                    let Some(accounts) = transaction_accounts else {
2909                        continue;
2910                    };
2911
2912                    mentioned_accounts.iter().any(|filtered_acc| {
2913                        if let Ok(filtered_pubkey) = Pubkey::from_str(&filtered_acc) {
2914                            accounts.contains(&filtered_pubkey)
2915                        } else {
2916                            false
2917                        }
2918                    })
2919                }
2920            };
2921
2922            if should_notify {
2923                let message = RpcLogsResponse {
2924                    signature: signature.to_string(),
2925                    err: err.clone().map(|e| e.into()),
2926                    logs: logs.clone(),
2927                };
2928                let _ = tx.send((self.get_latest_absolute_slot(), message));
2929            }
2930        }
2931    }
2932
2933    /// Registers a snapshot subscription and returns a sender and receiver for notifications.
2934    /// The actual import logic should be handled by the caller (SurfnetSvmLocker).
2935    pub fn register_snapshot_subscription(
2936        &mut self,
2937    ) -> (
2938        Sender<super::SnapshotImportNotification>,
2939        Receiver<super::SnapshotImportNotification>,
2940    ) {
2941        let (tx, rx) = unbounded();
2942        self.snapshot_subscriptions.push(tx.clone());
2943        (tx, rx)
2944    }
2945
2946    pub async fn fetch_snapshot_from_url(
2947        snapshot_url: &str,
2948    ) -> Result<
2949        std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>>,
2950        Box<dyn std::error::Error + Send + Sync>,
2951    > {
2952        let response = reqwest::get(snapshot_url).await?;
2953        let text = response.text().await?;
2954
2955        // Parse the JSON snapshot data
2956        let snapshot: std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>> =
2957            serde_json::from_str(&text)?;
2958
2959        Ok(snapshot)
2960    }
2961
2962    pub fn register_idl(&mut self, idl: Idl, slot: Option<Slot>) -> SurfpoolResult<()> {
2963        let slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
2964        let program_id = Pubkey::from_str_const(&idl.address);
2965        let program_id_str = program_id.to_string();
2966        let mut idl_versions = self
2967            .registered_idls
2968            .get(&program_id_str)
2969            .ok()
2970            .flatten()
2971            .unwrap_or_default();
2972        idl_versions.push(VersionedIdl(slot, idl));
2973        // Sort by slot descending so the latest IDL is first
2974        idl_versions.sort_by(|a, b| b.0.cmp(&a.0));
2975        self.registered_idls.store(program_id_str, idl_versions)?;
2976        Ok(())
2977    }
2978
2979    fn encode_ui_account_profile_state(
2980        &self,
2981        pubkey: &Pubkey,
2982        account_profile_state: AccountProfileState,
2983        encoding: &UiAccountEncoding,
2984    ) -> UiAccountProfileState {
2985        let additional_data = self.get_additional_data(pubkey, None);
2986
2987        match account_profile_state {
2988            AccountProfileState::Readonly => UiAccountProfileState::Readonly,
2989            AccountProfileState::Writable(account_change) => {
2990                let change = match account_change {
2991                    AccountChange::Create(account) => UiAccountChange::Create(
2992                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
2993                    ),
2994                    AccountChange::Update(account_before, account_after) => {
2995                        UiAccountChange::Update(
2996                            self.encode_ui_account(
2997                                pubkey,
2998                                &account_before,
2999                                *encoding,
3000                                additional_data,
3001                                None,
3002                            ),
3003                            self.encode_ui_account(
3004                                pubkey,
3005                                &account_after,
3006                                *encoding,
3007                                additional_data,
3008                                None,
3009                            ),
3010                        )
3011                    }
3012                    AccountChange::Delete(account) => UiAccountChange::Delete(
3013                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3014                    ),
3015                    AccountChange::Unchanged(account) => {
3016                        UiAccountChange::Unchanged(account.map(|account| {
3017                            self.encode_ui_account(
3018                                pubkey,
3019                                &account,
3020                                *encoding,
3021                                additional_data,
3022                                None,
3023                            )
3024                        }))
3025                    }
3026                };
3027                UiAccountProfileState::Writable(change)
3028            }
3029        }
3030    }
3031
3032    fn encode_ui_profile_result(
3033        &self,
3034        profile_result: ProfileResult,
3035        readonly_accounts: &[Pubkey],
3036        encoding: &UiAccountEncoding,
3037    ) -> UiProfileResult {
3038        let ProfileResult {
3039            pre_execution_capture,
3040            post_execution_capture,
3041            compute_units_consumed,
3042            log_messages,
3043            error_message,
3044        } = profile_result;
3045
3046        let account_states = pre_execution_capture
3047            .into_iter()
3048            .zip(post_execution_capture)
3049            .map(|((pubkey, pre_account), (_, post_account))| {
3050                // if pubkey != post {
3051                //     panic!(
3052                //         "Pre-execution pubkey {} does not match post-execution pubkey {}",
3053                //         pubkey, post
3054                //     );
3055                // }
3056                let state =
3057                    AccountProfileState::new(pubkey, pre_account, post_account, readonly_accounts);
3058                (
3059                    pubkey,
3060                    self.encode_ui_account_profile_state(&pubkey, state, encoding),
3061                )
3062            })
3063            .collect::<IndexMap<Pubkey, UiAccountProfileState>>();
3064
3065        UiProfileResult {
3066            account_states,
3067            compute_units_consumed,
3068            log_messages,
3069            error_message,
3070        }
3071    }
3072
3073    pub fn encode_ui_keyed_profile_result(
3074        &self,
3075        keyed_profile_result: KeyedProfileResult,
3076        config: &RpcProfileResultConfig,
3077    ) -> UiKeyedProfileResult {
3078        let KeyedProfileResult {
3079            slot,
3080            key,
3081            instruction_profiles,
3082            transaction_profile,
3083            readonly_account_states,
3084        } = keyed_profile_result;
3085
3086        let encoding = config.encoding.unwrap_or(UiAccountEncoding::JsonParsed);
3087
3088        let readonly_accounts = readonly_account_states.keys().cloned().collect::<Vec<_>>();
3089
3090        let default = RpcProfileDepth::default();
3091        let instruction_profiles = match *config.depth.as_ref().unwrap_or(&default) {
3092            RpcProfileDepth::Transaction => None,
3093            RpcProfileDepth::Instruction => instruction_profiles.map(|instruction_profiles| {
3094                instruction_profiles
3095                    .into_iter()
3096                    .map(|p| self.encode_ui_profile_result(p, &readonly_accounts, &encoding))
3097                    .collect()
3098            }),
3099        };
3100
3101        let transaction_profile =
3102            self.encode_ui_profile_result(transaction_profile, &readonly_accounts, &encoding);
3103
3104        let readonly_account_states = readonly_account_states
3105            .into_iter()
3106            .map(|(pubkey, account)| {
3107                let account = self.encode_ui_account(&pubkey, &account, encoding, None, None);
3108                (pubkey, account)
3109            })
3110            .collect();
3111
3112        UiKeyedProfileResult {
3113            slot,
3114            key,
3115            instruction_profiles,
3116            transaction_profile,
3117            readonly_account_states,
3118        }
3119    }
3120
3121    pub fn encode_ui_account<T: ReadableAccount>(
3122        &self,
3123        pubkey: &Pubkey,
3124        account: &T,
3125        encoding: UiAccountEncoding,
3126        additional_data: Option<AccountAdditionalDataV3>,
3127        data_slice_config: Option<UiDataSliceConfig>,
3128    ) -> UiAccount {
3129        let owner_program_id = account.owner();
3130        let data = account.data();
3131
3132        if encoding == UiAccountEncoding::JsonParsed {
3133            if let Ok(Some(registered_idls)) =
3134                self.registered_idls.get(&owner_program_id.to_string())
3135            {
3136                let filter_slot = self.latest_epoch_info.absolute_slot;
3137                // IDLs are stored sorted by slot descending (most recent first)
3138                let ordered_available_idls = registered_idls
3139                    .iter()
3140                    // only get IDLs that are active (their slot is before the latest slot)
3141                    .filter_map(|VersionedIdl(slot, idl)| {
3142                        if *slot <= filter_slot {
3143                            Some(idl)
3144                        } else {
3145                            None
3146                        }
3147                    })
3148                    .collect::<Vec<_>>();
3149
3150                // if we have none in this loop, it means the only IDLs registered for this pubkey are for a
3151                // future slot, for some reason. if we have some, we'll try each one in this loop, starting
3152                // with the most recent one, to see if the account data can be parsed to the IDL type
3153                for idl in &ordered_available_idls {
3154                    // If we have a valid IDL, use it to parse the account data
3155                    let discriminator = &data[..8];
3156                    if let Some(matching_account) = idl
3157                        .accounts
3158                        .iter()
3159                        .find(|a| a.discriminator.eq(&discriminator))
3160                    {
3161                        // If we found a matching account, we can look up the type to parse the account
3162                        if let Some(account_type) =
3163                            idl.types.iter().find(|t| t.name == matching_account.name)
3164                        {
3165                            let empty_vec = vec![];
3166                            let idl_type_def_generics = idl
3167                                .types
3168                                .iter()
3169                                .find(|t| t.name == account_type.name)
3170                                .map(|t| &t.generics);
3171
3172                            // If we found a matching account type, we can use it to parse the account data
3173                            let rest = data[8..].as_ref();
3174                            if let Ok(parsed_value) =
3175                                parse_bytes_to_value_with_expected_idl_type_def_ty(
3176                                    rest,
3177                                    &account_type.ty,
3178                                    &idl.types,
3179                                    &vec![],
3180                                    idl_type_def_generics.unwrap_or(&empty_vec),
3181                                )
3182                            {
3183                                return UiAccount {
3184                                    lamports: account.lamports(),
3185                                    data: UiAccountData::Json(ParsedAccount {
3186                                        program: idl
3187                                            .metadata
3188                                            .name
3189                                            .to_string()
3190                                            .to_case(convert_case::Case::Kebab),
3191                                        parsed: parsed_value
3192                                            .to_json(Some(&get_txtx_value_json_converters())),
3193                                        space: data.len() as u64,
3194                                    }),
3195                                    owner: owner_program_id.to_string(),
3196                                    executable: account.executable(),
3197                                    rent_epoch: account.rent_epoch(),
3198                                    space: Some(data.len() as u64),
3199                                };
3200                            }
3201                        }
3202                    }
3203                }
3204            }
3205        }
3206
3207        // Fall back to the default encoding
3208        encode_ui_account(
3209            pubkey,
3210            account,
3211            encoding,
3212            additional_data,
3213            data_slice_config,
3214        )
3215    }
3216
3217    pub fn get_account(&self, pubkey: &Pubkey) -> SurfpoolResult<Option<Account>> {
3218        self.inner.get_account(pubkey)
3219    }
3220
3221    pub fn get_all_accounts(&self) -> SurfpoolResult<Vec<(Pubkey, AccountSharedData)>> {
3222        self.inner.get_all_accounts()
3223    }
3224
3225    pub fn get_transaction(
3226        &self,
3227        signature: &Signature,
3228    ) -> SurfpoolResult<Option<SurfnetTransactionStatus>> {
3229        Ok(self.transactions.get(&signature.to_string())?)
3230    }
3231
3232    pub fn start_runbook_execution(&mut self, runbook_id: String) {
3233        self.runbook_executions
3234            .push(RunbookExecutionStatusReport::new(runbook_id));
3235    }
3236
3237    pub fn complete_runbook_execution(&mut self, runbook_id: &str, error: Option<Vec<String>>) {
3238        if let Some(execution) = self
3239            .runbook_executions
3240            .iter_mut()
3241            .find(|e| e.runbook_id.eq(runbook_id) && e.completed_at.is_none())
3242        {
3243            execution.mark_completed(error);
3244        }
3245    }
3246
3247    /// Export all accounts to a JSON file suitable for test fixtures
3248    ///
3249    /// # Arguments
3250    /// * `encoding` - The encoding to use for account data (Base64, JsonParsed, etc.)
3251    ///
3252    /// # Returns
3253    /// A BTreeMap of pubkey -> AccountFixture that can be serialized to JSON.
3254    pub fn export_snapshot(
3255        &self,
3256        config: ExportSnapshotConfig,
3257    ) -> SurfpoolResult<BTreeMap<String, AccountSnapshot>> {
3258        let mut fixtures = BTreeMap::new();
3259        let encoding = if config.include_parsed_accounts.unwrap_or_default() {
3260            UiAccountEncoding::JsonParsed
3261        } else {
3262            UiAccountEncoding::Base64
3263        };
3264        let filter = config.filter.unwrap_or_default();
3265        let include_program_accounts = filter.include_program_accounts.unwrap_or(false);
3266        let include_accounts = filter.include_accounts.unwrap_or_default();
3267        let exclude_accounts = filter.exclude_accounts.unwrap_or_default();
3268
3269        fn is_program_account(pubkey: &Pubkey) -> bool {
3270            pubkey == &bpf_loader::id()
3271                || pubkey == &solana_sdk_ids::bpf_loader_deprecated::id()
3272                || pubkey == &solana_sdk_ids::bpf_loader_upgradeable::id()
3273        }
3274
3275        // Helper function to process an account and add it to fixtures
3276        let mut process_account = |pubkey: &Pubkey, account: &Account| {
3277            let is_include_account = include_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3278            let is_exclude_account = exclude_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3279            let is_program_account = is_program_account(&account.owner);
3280            if is_exclude_account
3281                || ((is_program_account && !include_program_accounts) && !is_include_account)
3282            {
3283                return;
3284            }
3285
3286            // For token accounts, we need to provide the mint additional data
3287            let additional_data: Option<AccountAdditionalDataV3> = if account.owner
3288                == spl_token_interface::id()
3289                || account.owner == spl_token_2022_interface::id()
3290            {
3291                if let Ok(token_account) = TokenAccount::unpack(&account.data) {
3292                    self.account_associated_data
3293                        .get(&token_account.mint().to_string())
3294                        .ok()
3295                        .flatten()
3296                        .and_then(|data| data.try_into().ok())
3297                } else {
3298                    self.account_associated_data
3299                        .get(&pubkey.to_string())
3300                        .ok()
3301                        .flatten()
3302                        .and_then(|data| data.try_into().ok())
3303                }
3304            } else {
3305                self.account_associated_data
3306                    .get(&pubkey.to_string())
3307                    .ok()
3308                    .flatten()
3309                    .and_then(|data| data.try_into().ok())
3310            };
3311
3312            let ui_account =
3313                self.encode_ui_account(pubkey, account, encoding, additional_data, None);
3314
3315            let (base64, parsed_data) = match ui_account.data {
3316                UiAccountData::Json(parsed_account) => {
3317                    (BASE64_STANDARD.encode(account.data()), Some(parsed_account))
3318                }
3319                UiAccountData::Binary(base64, _) => (base64, None),
3320                UiAccountData::LegacyBinary(_) => unreachable!(),
3321            };
3322
3323            let account_snapshot = AccountSnapshot::new(
3324                account.lamports,
3325                account.owner.to_string(),
3326                account.executable,
3327                account.rent_epoch,
3328                base64,
3329                parsed_data,
3330            );
3331
3332            fixtures.insert(pubkey.to_string(), account_snapshot);
3333        };
3334
3335        match &config.scope {
3336            ExportSnapshotScope::Network => {
3337                // Export all network accounts (current behavior)
3338                for (pubkey, account_shared_data) in self.get_all_accounts()? {
3339                    let account = Account::from(account_shared_data.clone());
3340                    process_account(&pubkey, &account);
3341                }
3342            }
3343            ExportSnapshotScope::PreTransaction(signature_str) => {
3344                // Export accounts from a specific transaction's pre-execution state
3345                if let Ok(signature) = Signature::from_str(signature_str) {
3346                    if let Ok(Some(profile)) = self
3347                        .executed_transaction_profiles
3348                        .get(&signature.to_string())
3349                    {
3350                        // Collect accounts from pre-execution capture only
3351                        // This gives us the account state BEFORE the transaction executed
3352                        for (pubkey, account_opt) in
3353                            &profile.transaction_profile.pre_execution_capture
3354                        {
3355                            if let Some(account) = account_opt {
3356                                process_account(pubkey, account);
3357                            }
3358                        }
3359
3360                        // Also collect readonly account states (these don't change)
3361                        for (pubkey, account) in &profile.readonly_account_states {
3362                            process_account(pubkey, account);
3363                        }
3364                    }
3365                }
3366            }
3367        }
3368
3369        Ok(fixtures)
3370    }
3371
3372    /// Registers a scenario for execution by scheduling its overrides
3373    ///
3374    /// The `slot` parameter is the base slot from which relative override slot heights are calculated.
3375    /// If not provided, uses the current slot.
3376    pub fn register_scenario(
3377        &mut self,
3378        scenario: surfpool_types::Scenario,
3379        slot: Option<Slot>,
3380    ) -> SurfpoolResult<()> {
3381        // Use provided slot or current slot as the base for relative slot heights
3382        let base_slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3383
3384        info!(
3385            "Registering scenario: {} ({}) with {} overrides at base slot {}",
3386            scenario.name,
3387            scenario.id,
3388            scenario.overrides.len(),
3389            base_slot
3390        );
3391
3392        // Schedule overrides by adding base slot to their scenario-relative slots
3393        for override_instance in scenario.overrides {
3394            let scenario_relative_slot = override_instance.scenario_relative_slot;
3395            let absolute_slot = base_slot + scenario_relative_slot;
3396
3397            debug!(
3398                "Scheduling override at absolute slot {} (base {} + relative {})",
3399                absolute_slot, base_slot, scenario_relative_slot
3400            );
3401
3402            let mut slot_overrides = self
3403                .scheduled_overrides
3404                .get(&absolute_slot)
3405                .ok()
3406                .flatten()
3407                .unwrap_or_default();
3408            slot_overrides.push(override_instance);
3409            self.scheduled_overrides
3410                .store(absolute_slot, slot_overrides)?;
3411        }
3412
3413        Ok(())
3414    }
3415}
3416
3417#[cfg(test)]
3418mod tests {
3419    use agave_feature_set::{
3420        blake3_syscall_enabled, curve25519_syscall_enabled, disable_fees_sysvar,
3421        enable_extend_program_checked, enable_loader_v4, enable_sbpf_v1_deployment_and_execution,
3422        enable_sbpf_v2_deployment_and_execution, enable_sbpf_v3_deployment_and_execution,
3423        formalize_loaded_transaction_data_size, move_precompile_verification_to_svm,
3424        raise_cpi_nesting_limit_to_8,
3425    };
3426    use base64::{Engine, engine::general_purpose};
3427    use borsh::BorshSerialize;
3428    // use test_log::test; // uncomment to get logs from litesvm
3429    use solana_account::Account;
3430    use solana_loader_v3_interface::get_program_data_address;
3431    use solana_program_pack::Pack;
3432    use spl_token_interface::state::{Account as TokenAccount, AccountState};
3433    use test_case::test_case;
3434
3435    use super::*;
3436    use crate::storage::tests::TestType;
3437
3438    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3439    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3440    #[test_case(TestType::no_db(); "with no db")]
3441    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3442    fn test_synthetic_blockhash_generation(test_type: TestType) {
3443        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3444
3445        // Test with different chain tip indices
3446        let test_cases = vec![0, 1, 42, 255, 1000, 0x12345678];
3447
3448        for index in test_cases {
3449            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
3450
3451            // Generate the synthetic blockhash
3452            let new_blockhash = svm.new_blockhash();
3453
3454            // Verify the blockhash string contains our expected pattern
3455            let blockhash_str = new_blockhash.hash.clone();
3456            println!("Index {} -> Blockhash: {}", index, blockhash_str);
3457
3458            // The blockhash should be a valid base58 string
3459            assert!(!blockhash_str.is_empty());
3460            assert!(blockhash_str.len() > 20); // Base58 encoded 32 bytes should be around 44 chars
3461
3462            // Verify it's deterministic - same index should produce same blockhash
3463            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
3464            let new_blockhash2 = svm.new_blockhash();
3465            assert_eq!(new_blockhash.hash, new_blockhash2.hash);
3466        }
3467    }
3468
3469    #[test]
3470    fn test_synthetic_blockhash_base58_encoding() {
3471        // Test the base58 encoding logic directly
3472        let test_index = 42u64;
3473        let index_hex = format!("{:08x}", test_index)
3474            .replace('0', "x")
3475            .replace('O', "x");
3476
3477        let target_length = 43;
3478        let padding_needed = target_length - SyntheticBlockhash::PREFIX.len() - index_hex.len();
3479        let padding = "x".repeat(padding_needed.max(0));
3480        let target_string = format!("{}{}{}", SyntheticBlockhash::PREFIX, padding, index_hex);
3481
3482        println!("Target string: {}", target_string);
3483
3484        // Verify the string is valid base58
3485        let decoded_bytes = bs58::decode(&target_string).into_vec();
3486        assert!(decoded_bytes.is_ok(), "String should be valid base58");
3487
3488        let bytes = decoded_bytes.unwrap();
3489        assert!(bytes.len() <= 32, "Decoded bytes should fit in 32 bytes");
3490
3491        // Test that we can create a hash from these bytes
3492        let mut blockhash_bytes = [0u8; 32];
3493        blockhash_bytes[..bytes.len().min(32)].copy_from_slice(&bytes[..bytes.len().min(32)]);
3494        let hash = Hash::new_from_array(blockhash_bytes);
3495
3496        // Verify the hash can be converted back to string
3497        let hash_str = hash.to_string();
3498        assert!(!hash_str.is_empty());
3499        println!("Generated hash: {}", hash_str);
3500    }
3501
3502    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3503    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3504    #[test_case(TestType::no_db(); "with no db")]
3505    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3506    fn test_blockhash_consistency_across_calls(test_type: TestType) {
3507        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3508
3509        // Set a specific chain tip
3510        svm.chain_tip = BlockIdentifier::new(123, "initial_hash");
3511
3512        // Generate multiple blockhashes and verify they're consistent
3513        let mut previous_hash: Option<BlockIdentifier> = None;
3514        for i in 0..5 {
3515            let new_blockhash = svm.new_blockhash();
3516            println!(
3517                "Call {}: index={}, hash={}",
3518                i, new_blockhash.index, new_blockhash.hash
3519            );
3520
3521            if let Some(prev) = previous_hash {
3522                // Each call should increment the index
3523                assert_eq!(new_blockhash.index, prev.index + 1);
3524                // But the hash should be different (since index changed)
3525                assert_ne!(new_blockhash.hash, prev.hash);
3526            } else {
3527                // First call should increment from the initial chain tip
3528                assert_eq!(new_blockhash.index, svm.chain_tip.index + 1);
3529            }
3530
3531            previous_hash = Some(new_blockhash.clone());
3532            // Update the chain tip for the next iteration
3533            svm.chain_tip = new_blockhash;
3534        }
3535    }
3536
3537    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3538    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3539    #[test_case(TestType::no_db(); "with no db")]
3540    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3541    fn test_token_account_indexing(test_type: TestType) {
3542        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3543
3544        let owner = Pubkey::new_unique();
3545        let delegate = Pubkey::new_unique();
3546        let mint = Pubkey::new_unique();
3547        let token_account_pubkey = Pubkey::new_unique();
3548
3549        // create a token account with delegate
3550        let mut token_account_data = [0u8; TokenAccount::LEN];
3551        let token_account = TokenAccount {
3552            mint,
3553            owner,
3554            amount: 1000,
3555            delegate: COption::Some(delegate),
3556            state: AccountState::Initialized,
3557            is_native: COption::None,
3558            delegated_amount: 500,
3559            close_authority: COption::None,
3560        };
3561        token_account.pack_into_slice(&mut token_account_data);
3562
3563        let account = Account {
3564            lamports: 1000000,
3565            data: token_account_data.to_vec(),
3566            owner: spl_token_interface::id(),
3567            executable: false,
3568            rent_epoch: 0,
3569        };
3570
3571        svm.set_account(&token_account_pubkey, account).unwrap();
3572
3573        // test all indexes were created correctly
3574        assert_eq!(svm.token_accounts.keys().unwrap().len(), 1);
3575
3576        // test owner index
3577        let owner_accounts = svm.get_parsed_token_accounts_by_owner(&owner);
3578        assert_eq!(owner_accounts.len(), 1);
3579        assert_eq!(owner_accounts[0].0, token_account_pubkey);
3580
3581        // test delegate index
3582        let delegate_accounts = svm.get_token_accounts_by_delegate(&delegate);
3583        assert_eq!(delegate_accounts.len(), 1);
3584        assert_eq!(delegate_accounts[0].0, token_account_pubkey);
3585
3586        // test mint index
3587        let mint_accounts = svm.get_token_accounts_by_mint(&mint);
3588        assert_eq!(mint_accounts.len(), 1);
3589        assert_eq!(mint_accounts[0].0, token_account_pubkey);
3590    }
3591
3592    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3593    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3594    #[test_case(TestType::no_db(); "with no db")]
3595    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3596    fn test_account_update_removes_old_indexes(test_type: TestType) {
3597        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3598
3599        let owner = Pubkey::new_unique();
3600        let old_delegate = Pubkey::new_unique();
3601        let new_delegate = Pubkey::new_unique();
3602        let mint = Pubkey::new_unique();
3603        let token_account_pubkey = Pubkey::new_unique();
3604
3605        //  reate initial token account with old delegate
3606        let mut token_account_data = [0u8; TokenAccount::LEN];
3607        let token_account = TokenAccount {
3608            mint,
3609            owner,
3610            amount: 1000,
3611            delegate: COption::Some(old_delegate),
3612            state: AccountState::Initialized,
3613            is_native: COption::None,
3614            delegated_amount: 500,
3615            close_authority: COption::None,
3616        };
3617        token_account.pack_into_slice(&mut token_account_data);
3618
3619        let account = Account {
3620            lamports: 1000000,
3621            data: token_account_data.to_vec(),
3622            owner: spl_token_interface::id(),
3623            executable: false,
3624            rent_epoch: 0,
3625        };
3626
3627        // insert initial account
3628        svm.set_account(&token_account_pubkey, account).unwrap();
3629
3630        // verify old delegate has the account
3631        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 1);
3632        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 0);
3633
3634        // update with new delegate
3635        let updated_token_account = TokenAccount {
3636            mint,
3637            owner,
3638            amount: 1000,
3639            delegate: COption::Some(new_delegate),
3640            state: AccountState::Initialized,
3641            is_native: COption::None,
3642            delegated_amount: 500,
3643            close_authority: COption::None,
3644        };
3645        updated_token_account.pack_into_slice(&mut token_account_data);
3646
3647        let updated_account = Account {
3648            lamports: 1000000,
3649            data: token_account_data.to_vec(),
3650            owner: spl_token_interface::id(),
3651            executable: false,
3652            rent_epoch: 0,
3653        };
3654
3655        // update the account
3656        svm.set_account(&token_account_pubkey, updated_account)
3657            .unwrap();
3658
3659        // verify indexes were updated correctly
3660        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 0);
3661        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 1);
3662        assert_eq!(svm.get_parsed_token_accounts_by_owner(&owner).len(), 1);
3663    }
3664
3665    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3666    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3667    #[test_case(TestType::no_db(); "with no db")]
3668    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3669    fn test_non_token_accounts_not_indexed(test_type: TestType) {
3670        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3671
3672        let system_account_pubkey = Pubkey::new_unique();
3673        let account = Account {
3674            lamports: 1000000,
3675            data: vec![],
3676            owner: solana_system_interface::program::id(), // system program, not token program
3677            executable: false,
3678            rent_epoch: 0,
3679        };
3680
3681        svm.set_account(&system_account_pubkey, account).unwrap();
3682
3683        // should be in general registry but not token indexes
3684        assert_eq!(svm.token_accounts.keys().unwrap().len(), 0);
3685        assert_eq!(svm.token_accounts_by_owner.keys().unwrap().len(), 0);
3686        assert_eq!(svm.token_accounts_by_delegate.keys().unwrap().len(), 0);
3687        assert_eq!(svm.token_accounts_by_mint.keys().unwrap().len(), 0);
3688    }
3689
3690    fn expect_account_update_event(
3691        events_rx: &Receiver<SimnetEvent>,
3692        svm: &SurfnetSvm,
3693        pubkey: &Pubkey,
3694        expected_account: &Account,
3695    ) -> bool {
3696        match events_rx.recv() {
3697            Ok(event) => match event {
3698                SimnetEvent::AccountUpdate(_, account_pubkey) => {
3699                    assert_eq!(pubkey, &account_pubkey);
3700                    assert_eq!(
3701                        svm.get_account(&pubkey).unwrap().as_ref(),
3702                        Some(expected_account)
3703                    );
3704                    true
3705                }
3706                event => {
3707                    println!("unexpected simnet event: {:?}", event);
3708                    false
3709                }
3710            },
3711            Err(_) => false,
3712        }
3713    }
3714
3715    fn _expect_error_event(events_rx: &Receiver<SimnetEvent>, expected_error: &str) -> bool {
3716        match events_rx.recv() {
3717            Ok(event) => match event {
3718                SimnetEvent::ErrorLog(_, err) => {
3719                    assert_eq!(err, expected_error);
3720
3721                    true
3722                }
3723                event => {
3724                    println!("unexpected simnet event: {:?}", event);
3725                    false
3726                }
3727            },
3728            Err(_) => false,
3729        }
3730    }
3731
3732    fn create_program_accounts() -> (Pubkey, Account, Pubkey, Account) {
3733        let program_pubkey = Pubkey::new_unique();
3734        let program_data_address = get_program_data_address(&program_pubkey);
3735        let program_account = Account {
3736            lamports: 1000000000000,
3737            data: bincode::serialize(
3738                &solana_loader_v3_interface::state::UpgradeableLoaderState::Program {
3739                    programdata_address: program_data_address,
3740                },
3741            )
3742            .unwrap(),
3743            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3744            executable: true,
3745            rent_epoch: 10000000000000,
3746        };
3747
3748        let mut bin = include_bytes!("../tests/assets/metaplex_program.bin").to_vec();
3749        let mut data = bincode::serialize(
3750            &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3751                slot: 0,
3752                upgrade_authority_address: Some(Pubkey::new_unique()),
3753            },
3754        )
3755        .unwrap();
3756        data.append(&mut bin); // push our binary after the state data
3757        let program_data_account = Account {
3758            lamports: 10000000000000,
3759            data,
3760            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3761            executable: false,
3762            rent_epoch: 10000000000000,
3763        };
3764        (
3765            program_pubkey,
3766            program_account,
3767            program_data_address,
3768            program_data_account,
3769        )
3770    }
3771
3772    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3773    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3774    #[test_case(TestType::no_db(); "with no db")]
3775    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3776    fn test_inserting_account_updates(test_type: TestType) {
3777        let (mut svm, events_rx, _geyser_rx) = test_type.initialize_svm();
3778
3779        let pubkey = Pubkey::new_unique();
3780        let account = Account {
3781            lamports: 1000,
3782            data: vec![1, 2, 3],
3783            owner: Pubkey::new_unique(),
3784            executable: false,
3785            rent_epoch: 0,
3786        };
3787
3788        // GetAccountResult::None should be a noop when writing account updates
3789        {
3790            let index_before = svm.get_all_accounts().unwrap();
3791            let empty_update = GetAccountResult::None(pubkey);
3792            svm.write_account_update(empty_update);
3793            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
3794        }
3795
3796        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to false should be a noop
3797        {
3798            let index_before = svm.get_all_accounts().unwrap();
3799            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), false);
3800            svm.write_account_update(found_update);
3801            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
3802        }
3803
3804        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to true should update the account
3805        {
3806            let index_before = svm.get_all_accounts().unwrap();
3807            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), true);
3808            svm.write_account_update(found_update);
3809            assert_eq!(
3810                svm.get_all_accounts().unwrap().len(),
3811                index_before.len() + 1
3812            );
3813            if !expect_account_update_event(&events_rx, &svm, &pubkey, &account) {
3814                panic!(
3815                    "Expected account update event not received after GetAccountResult::FoundAccount update"
3816                );
3817            }
3818        }
3819
3820        // GetAccountResult::FoundProgramAccount with no program account inserts a default programdata account
3821        {
3822            let (program_address, program_account, program_data_address, _) =
3823                create_program_accounts();
3824
3825            let mut data = bincode::serialize(
3826                &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3827                    slot: svm.get_latest_absolute_slot(),
3828                    upgrade_authority_address: Some(system_program::id()),
3829                },
3830            )
3831            .unwrap();
3832
3833            let mut bin = include_bytes!("../tests/assets/minimum_program.so").to_vec();
3834            data.append(&mut bin); // push our binary after the state data
3835            let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len());
3836            let default_program_data_account = Account {
3837                lamports,
3838                data,
3839                owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3840                executable: false,
3841                rent_epoch: 0,
3842            };
3843
3844            let index_before = svm.get_all_accounts().unwrap();
3845            let found_program_account_update = GetAccountResult::FoundProgramAccount(
3846                (program_address, program_account.clone()),
3847                (program_data_address, None),
3848            );
3849            svm.write_account_update(found_program_account_update);
3850
3851            if !expect_account_update_event(
3852                &events_rx,
3853                &svm,
3854                &program_data_address,
3855                &default_program_data_account,
3856            ) {
3857                panic!(
3858                    "Expected account update event not received after inserting default program data account"
3859                );
3860            }
3861
3862            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
3863                panic!(
3864                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
3865                );
3866            }
3867            assert_eq!(
3868                svm.get_all_accounts().unwrap().len(),
3869                index_before.len() + 2
3870            );
3871        }
3872
3873        // GetAccountResult::FoundProgramAccount with program account + program data account inserts two accounts
3874        {
3875            let (program_address, program_account, program_data_address, program_data_account) =
3876                create_program_accounts();
3877
3878            let index_before = svm.get_all_accounts().unwrap();
3879            let found_program_account_update = GetAccountResult::FoundProgramAccount(
3880                (program_address, program_account.clone()),
3881                (program_data_address, Some(program_data_account.clone())),
3882            );
3883            svm.write_account_update(found_program_account_update);
3884            assert_eq!(
3885                svm.get_all_accounts().unwrap().len(),
3886                index_before.len() + 2
3887            );
3888            if !expect_account_update_event(
3889                &events_rx,
3890                &svm,
3891                &program_data_address,
3892                &program_data_account,
3893            ) {
3894                panic!(
3895                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program data pubkey"
3896                );
3897            }
3898
3899            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
3900                panic!(
3901                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
3902                );
3903            }
3904        }
3905
3906        // If we insert the program data account ahead of time, then have a GetAccountResult::FoundProgramAccount with just the program data account,
3907        // we should get one insert
3908        {
3909            let (program_address, program_account, program_data_address, program_data_account) =
3910                create_program_accounts();
3911
3912            let index_before = svm.get_all_accounts().unwrap();
3913            let found_update = GetAccountResult::FoundAccount(
3914                program_data_address,
3915                program_data_account.clone(),
3916                true,
3917            );
3918            svm.write_account_update(found_update);
3919            assert_eq!(
3920                svm.get_all_accounts().unwrap().len(),
3921                index_before.len() + 1
3922            );
3923            if !expect_account_update_event(
3924                &events_rx,
3925                &svm,
3926                &program_data_address,
3927                &program_data_account,
3928            ) {
3929                panic!(
3930                    "Expected account update event not received after GetAccountResult::FoundAccount update"
3931                );
3932            }
3933
3934            let index_before = svm.get_all_accounts().unwrap();
3935            let program_account_found_update = GetAccountResult::FoundProgramAccount(
3936                (program_address, program_account.clone()),
3937                (program_data_address, None),
3938            );
3939            svm.write_account_update(program_account_found_update);
3940            assert_eq!(
3941                svm.get_all_accounts().unwrap().len(),
3942                index_before.len() + 1
3943            );
3944            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
3945                panic!(
3946                    "Expected account update event not received after GetAccountResult::FoundAccount update"
3947                );
3948            }
3949        }
3950    }
3951
3952    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3953    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3954    #[test_case(TestType::no_db(); "with no db")]
3955    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3956    fn test_encode_ui_account(test_type: TestType) {
3957        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3958
3959        let idl_v1: Idl =
3960            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v1.json").to_vec())
3961                .unwrap();
3962
3963        svm.register_idl(idl_v1.clone(), Some(0)).unwrap();
3964
3965        let account_pubkey = Pubkey::new_unique();
3966
3967        #[derive(borsh::BorshSerialize)]
3968        pub struct CustomAccount {
3969            pub my_custom_data: u64,
3970            pub another_field: String,
3971            pub bool: bool,
3972            pub pubkey: Pubkey,
3973        }
3974
3975        // Account data not matching IDL schema should use default encoding
3976        {
3977            let account_data = vec![0; 100];
3978            let base64_data = general_purpose::STANDARD.encode(&account_data);
3979            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
3980            let account = Account {
3981                lamports: 1000,
3982                data: account_data,
3983                owner: idl_v1.address.parse().unwrap(),
3984                executable: false,
3985                rent_epoch: 0,
3986            };
3987
3988            let ui_account = svm.encode_ui_account(
3989                &account_pubkey,
3990                &account,
3991                UiAccountEncoding::JsonParsed,
3992                None,
3993                None,
3994            );
3995            let expected_account = UiAccount {
3996                lamports: 1000,
3997                data: expected_data,
3998                owner: idl_v1.address.clone(),
3999                executable: false,
4000                rent_epoch: 0,
4001                space: Some(account.data.len() as u64),
4002            };
4003            assert_eq!(ui_account, expected_account);
4004        }
4005
4006        // valid account data matching IDL schema should be parsed
4007        {
4008            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4009            let pubkey = Pubkey::new_unique();
4010            CustomAccount {
4011                my_custom_data: 42,
4012                another_field: "test".to_string(),
4013                bool: true,
4014                pubkey,
4015            }
4016            .serialize(&mut account_data)
4017            .unwrap();
4018
4019            let account = Account {
4020                lamports: 1000,
4021                data: account_data,
4022                owner: idl_v1.address.parse().unwrap(),
4023                executable: false,
4024                rent_epoch: 0,
4025            };
4026
4027            let ui_account = svm.encode_ui_account(
4028                &account_pubkey,
4029                &account,
4030                UiAccountEncoding::JsonParsed,
4031                None,
4032                None,
4033            );
4034            let expected_account = UiAccount {
4035                lamports: 1000,
4036                data: UiAccountData::Json(ParsedAccount {
4037                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4038                    parsed: serde_json::json!({
4039                        "my_custom_data": 42,
4040                        "another_field": "test",
4041                        "bool": true,
4042                        "pubkey": pubkey.to_string(),
4043                    }),
4044                    space: account.data.len() as u64,
4045                }),
4046                owner: idl_v1.address.clone(),
4047                executable: false,
4048                rent_epoch: 0,
4049                space: Some(account.data.len() as u64),
4050            };
4051            assert_eq!(ui_account, expected_account);
4052        }
4053
4054        let idl_v2: Idl =
4055            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v2.json").to_vec())
4056                .unwrap();
4057
4058        svm.register_idl(idl_v2.clone(), Some(100)).unwrap();
4059
4060        // even though we have a new IDL that is more recent, we should be able to match with the old IDL
4061        {
4062            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4063            let pubkey = Pubkey::new_unique();
4064            CustomAccount {
4065                my_custom_data: 42,
4066                another_field: "test".to_string(),
4067                bool: true,
4068                pubkey,
4069            }
4070            .serialize(&mut account_data)
4071            .unwrap();
4072
4073            let account = Account {
4074                lamports: 1000,
4075                data: account_data,
4076                owner: idl_v1.address.parse().unwrap(),
4077                executable: false,
4078                rent_epoch: 0,
4079            };
4080
4081            let ui_account = svm.encode_ui_account(
4082                &account_pubkey,
4083                &account,
4084                UiAccountEncoding::JsonParsed,
4085                None,
4086                None,
4087            );
4088            let expected_account = UiAccount {
4089                lamports: 1000,
4090                data: UiAccountData::Json(ParsedAccount {
4091                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4092                    parsed: serde_json::json!({
4093                        "my_custom_data": 42,
4094                        "another_field": "test",
4095                        "bool": true,
4096                        "pubkey": pubkey.to_string(),
4097                    }),
4098                    space: account.data.len() as u64,
4099                }),
4100                owner: idl_v1.address.clone(),
4101                executable: false,
4102                rent_epoch: 0,
4103                space: Some(account.data.len() as u64),
4104            };
4105            assert_eq!(ui_account, expected_account);
4106        }
4107
4108        // valid account data matching IDL v2 schema should be parsed, if svm slot reaches IDL registration slot
4109        {
4110            // use the v2 shape of the custom account
4111            #[derive(borsh::BorshSerialize)]
4112            pub struct CustomAccount {
4113                pub my_custom_data: u64,
4114                pub another_field: String,
4115                pub pubkey: Pubkey,
4116            }
4117            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4118            let pubkey = Pubkey::new_unique();
4119            CustomAccount {
4120                my_custom_data: 42,
4121                another_field: "test".to_string(),
4122                pubkey,
4123            }
4124            .serialize(&mut account_data)
4125            .unwrap();
4126
4127            let account = Account {
4128                lamports: 1000,
4129                data: account_data.clone(),
4130                owner: idl_v1.address.parse().unwrap(),
4131                executable: false,
4132                rent_epoch: 0,
4133            };
4134
4135            let ui_account = svm.encode_ui_account(
4136                &account_pubkey,
4137                &account,
4138                UiAccountEncoding::JsonParsed,
4139                None,
4140                None,
4141            );
4142            let base64_data = general_purpose::STANDARD.encode(&account_data);
4143            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4144            let expected_account = UiAccount {
4145                lamports: 1000,
4146                data: expected_data,
4147                owner: idl_v1.address.clone(),
4148                executable: false,
4149                rent_epoch: 0,
4150                space: Some(account.data.len() as u64),
4151            };
4152            assert_eq!(ui_account, expected_account);
4153
4154            svm.latest_epoch_info.absolute_slot = 100; // simulate reaching the slot where IDL v2 was registered
4155
4156            let ui_account = svm.encode_ui_account(
4157                &account_pubkey,
4158                &account,
4159                UiAccountEncoding::JsonParsed,
4160                None,
4161                None,
4162            );
4163            let expected_account = UiAccount {
4164                lamports: 1000,
4165                data: UiAccountData::Json(ParsedAccount {
4166                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4167                    parsed: serde_json::json!({
4168                        "my_custom_data": 42,
4169                        "another_field": "test",
4170                        "pubkey": pubkey.to_string(),
4171                    }),
4172                    space: account.data.len() as u64,
4173                }),
4174                owner: idl_v1.address.clone(),
4175                executable: false,
4176                rent_epoch: 0,
4177                space: Some(account.data.len() as u64),
4178            };
4179            assert_eq!(ui_account, expected_account);
4180        }
4181    }
4182
4183    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4184    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4185    #[test_case(TestType::no_db(); "with no db")]
4186    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4187    fn test_profiling_map_capacity_default(test_type: TestType) {
4188        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4189        assert_eq!(svm.max_profiles, DEFAULT_PROFILING_MAP_CAPACITY);
4190    }
4191
4192    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4193    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4194    #[test_case(TestType::no_db(); "with no db")]
4195    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4196    fn test_profiling_map_capacity_set(test_type: TestType) {
4197        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4198        svm.set_profiling_map_capacity(10);
4199        assert_eq!(svm.max_profiles, 10);
4200    }
4201
4202    // Feature configuration tests
4203
4204    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4205    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4206    #[test_case(TestType::no_db(); "with no db")]
4207    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4208    fn test_apply_feature_config_empty(test_type: TestType) {
4209        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4210        let config = SvmFeatureConfig::new();
4211
4212        // Should not panic with empty config
4213        svm.apply_feature_config(&config);
4214    }
4215
4216    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4217    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4218    #[test_case(TestType::no_db(); "with no db")]
4219    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4220    fn test_apply_feature_config_enable_feature(test_type: TestType) {
4221        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4222
4223        // Disable a feature first
4224        let feature_id = enable_loader_v4::id();
4225        svm.feature_set.deactivate(&feature_id);
4226        assert!(!svm.feature_set.is_active(&feature_id));
4227
4228        // Now enable it via config
4229        let config = SvmFeatureConfig::new().enable(enable_loader_v4::id());
4230        svm.apply_feature_config(&config);
4231
4232        assert!(svm.feature_set.is_active(&feature_id));
4233    }
4234
4235    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4236    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4237    #[test_case(TestType::no_db(); "with no db")]
4238    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4239    fn test_apply_feature_config_disable_feature(test_type: TestType) {
4240        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4241
4242        // Feature should be active by default (all_enabled)
4243        let feature_id = disable_fees_sysvar::id();
4244        assert!(svm.feature_set.is_active(&feature_id));
4245
4246        // Now disable it via config
4247        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
4248        svm.apply_feature_config(&config);
4249
4250        assert!(!svm.feature_set.is_active(&feature_id));
4251    }
4252
4253    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4254    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4255    #[test_case(TestType::no_db(); "with no db")]
4256    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4257    fn test_apply_feature_config_mainnet_defaults(test_type: TestType) {
4258        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4259        let config = SvmFeatureConfig::default_mainnet_features();
4260
4261        svm.apply_feature_config(&config);
4262
4263        // Features disabled on mainnet should now be inactive
4264        assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
4265        assert!(
4266            !svm.feature_set
4267                .is_active(&enable_extend_program_checked::id())
4268        );
4269        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4270        assert!(
4271            !svm.feature_set
4272                .is_active(&enable_sbpf_v1_deployment_and_execution::id())
4273        );
4274        assert!(
4275            !svm.feature_set
4276                .is_active(&formalize_loaded_transaction_data_size::id())
4277        );
4278        assert!(
4279            !svm.feature_set
4280                .is_active(&move_precompile_verification_to_svm::id())
4281        );
4282
4283        // Features active on mainnet should still be active
4284        assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
4285        assert!(svm.feature_set.is_active(&curve25519_syscall_enabled::id()));
4286        assert!(
4287            svm.feature_set
4288                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
4289        );
4290        assert!(
4291            svm.feature_set
4292                .is_active(&enable_sbpf_v3_deployment_and_execution::id())
4293        );
4294        assert!(
4295            svm.feature_set
4296                .is_active(&raise_cpi_nesting_limit_to_8::id())
4297        );
4298    }
4299
4300    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4301    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4302    #[test_case(TestType::no_db(); "with no db")]
4303    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4304    fn test_apply_feature_config_mainnet_with_override(test_type: TestType) {
4305        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4306
4307        // Start with mainnet defaults, but enable loader v4
4308        let config = SvmFeatureConfig::default_mainnet_features().enable(enable_loader_v4::id());
4309
4310        svm.apply_feature_config(&config);
4311
4312        // Loader v4 should be enabled despite mainnet defaults
4313        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4314
4315        // Other mainnet-disabled features should still be disabled
4316        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4317        assert!(
4318            !svm.feature_set
4319                .is_active(&enable_extend_program_checked::id())
4320        );
4321    }
4322
4323    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4324    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4325    #[test_case(TestType::no_db(); "with no db")]
4326    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4327    fn test_apply_feature_config_multiple_changes(test_type: TestType) {
4328        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4329
4330        let config = SvmFeatureConfig::new()
4331            .enable(enable_loader_v4::id())
4332            .enable(enable_sbpf_v2_deployment_and_execution::id())
4333            .disable(disable_fees_sysvar::id())
4334            .disable(blake3_syscall_enabled::id());
4335
4336        svm.apply_feature_config(&config);
4337
4338        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4339        assert!(
4340            svm.feature_set
4341                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
4342        );
4343        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4344        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4345    }
4346
4347    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4348    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4349    #[test_case(TestType::no_db(); "with no db")]
4350    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4351    fn test_apply_feature_config_preserves_native_mint(test_type: TestType) {
4352        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4353
4354        // Native mint should exist before
4355        assert!(
4356            svm.inner
4357                .get_account(&spl_token_interface::native_mint::ID)
4358                .unwrap()
4359                .is_some()
4360        );
4361
4362        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
4363        svm.apply_feature_config(&config);
4364
4365        // Native mint should still exist after (re-added in apply_feature_config)
4366        assert!(
4367            svm.inner
4368                .get_account(&spl_token_interface::native_mint::ID)
4369                .unwrap()
4370                .is_some()
4371        );
4372    }
4373
4374    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4375    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4376    #[test_case(TestType::no_db(); "with no db")]
4377    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4378    fn test_apply_feature_config_idempotent(test_type: TestType) {
4379        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4380
4381        let config = SvmFeatureConfig::new()
4382            .enable(enable_loader_v4::id())
4383            .disable(disable_fees_sysvar::id());
4384
4385        // Apply twice
4386        svm.apply_feature_config(&config);
4387        svm.apply_feature_config(&config);
4388
4389        // State should be the same
4390        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4391        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4392    }
4393
4394    // Garbage collection tests
4395
4396    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4397    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4398    #[test_case(TestType::no_db(); "with no db")]
4399    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4400    fn test_garbage_collected_account_tracking(test_type: TestType) {
4401        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4402
4403        let owner = Pubkey::new_unique();
4404        let account_pubkey = Pubkey::new_unique();
4405
4406        let account = Account {
4407            lamports: 1000000,
4408            data: vec![1, 2, 3, 4, 5],
4409            owner,
4410            executable: false,
4411            rent_epoch: 0,
4412        };
4413
4414        svm.set_account(&account_pubkey, account.clone()).unwrap();
4415
4416        assert!(svm.get_account(&account_pubkey).unwrap().is_some());
4417        assert!(!svm.closed_accounts.contains(&account_pubkey));
4418        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 1);
4419
4420        let empty_account = Account::default();
4421        svm.update_account_registries(&account_pubkey, &empty_account)
4422            .unwrap();
4423
4424        assert!(svm.closed_accounts.contains(&account_pubkey));
4425
4426        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 0);
4427
4428        let owned_accounts = svm.get_account_owned_by(&owner).unwrap();
4429        assert!(!owned_accounts.iter().any(|(pk, _)| *pk == account_pubkey));
4430    }
4431
4432    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4433    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4434    #[test_case(TestType::no_db(); "with no db")]
4435    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4436    fn test_garbage_collected_token_account_cleanup(test_type: TestType) {
4437        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4438
4439        let token_owner = Pubkey::new_unique();
4440        let delegate = Pubkey::new_unique();
4441        let mint = Pubkey::new_unique();
4442        let token_account_pubkey = Pubkey::new_unique();
4443
4444        let mut token_account_data = [0u8; TokenAccount::LEN];
4445        let token_account = TokenAccount {
4446            mint,
4447            owner: token_owner,
4448            amount: 1000,
4449            delegate: COption::Some(delegate),
4450            state: AccountState::Initialized,
4451            is_native: COption::None,
4452            delegated_amount: 500,
4453            close_authority: COption::None,
4454        };
4455        token_account.pack_into_slice(&mut token_account_data);
4456
4457        let account = Account {
4458            lamports: 2000000,
4459            data: token_account_data.to_vec(),
4460            owner: spl_token_interface::id(),
4461            executable: false,
4462            rent_epoch: 0,
4463        };
4464
4465        svm.set_account(&token_account_pubkey, account).unwrap();
4466
4467        assert_eq!(
4468            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
4469            1
4470        );
4471        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 1);
4472        assert!(!svm.closed_accounts.contains(&token_account_pubkey));
4473
4474        let empty_account = Account::default();
4475        svm.update_account_registries(&token_account_pubkey, &empty_account)
4476            .unwrap();
4477
4478        assert!(svm.closed_accounts.contains(&token_account_pubkey));
4479
4480        assert_eq!(
4481            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
4482            0
4483        );
4484        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 0);
4485        assert!(
4486            svm.token_accounts
4487                .get(&token_account_pubkey.to_string())
4488                .unwrap()
4489                .is_none()
4490        );
4491    }
4492
4493    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4494    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4495    #[test_case(TestType::no_db(); "with no db")]
4496    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4497    fn test_is_slot_in_valid_range(test_type: TestType) {
4498        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4499
4500        // Set up: genesis_slot = 100, latest absolute slot = 110
4501        svm.genesis_slot = 100;
4502        svm.latest_epoch_info.absolute_slot = 110;
4503
4504        // Test slots within valid range
4505        assert!(
4506            svm.is_slot_in_valid_range(100),
4507            "genesis_slot should be valid"
4508        );
4509        assert!(
4510            svm.is_slot_in_valid_range(105),
4511            "middle slot should be valid"
4512        );
4513        assert!(
4514            svm.is_slot_in_valid_range(110),
4515            "latest slot should be valid"
4516        );
4517
4518        // Test slots outside valid range
4519        assert!(
4520            !svm.is_slot_in_valid_range(99),
4521            "slot before genesis should be invalid"
4522        );
4523        assert!(
4524            !svm.is_slot_in_valid_range(111),
4525            "slot after latest should be invalid"
4526        );
4527        assert!(
4528            !svm.is_slot_in_valid_range(0),
4529            "slot 0 should be invalid when genesis > 0"
4530        );
4531        assert!(
4532            !svm.is_slot_in_valid_range(1000),
4533            "far future slot should be invalid"
4534        );
4535    }
4536
4537    #[test]
4538    fn test_is_slot_in_valid_range_genesis_zero() {
4539        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4540
4541        // Set up: genesis_slot = 0, latest absolute slot = 50
4542        svm.genesis_slot = 0;
4543        svm.latest_epoch_info.absolute_slot = 50;
4544
4545        // Test boundary conditions with genesis at 0
4546        assert!(
4547            svm.is_slot_in_valid_range(0),
4548            "slot 0 should be valid when genesis = 0"
4549        );
4550        assert!(
4551            svm.is_slot_in_valid_range(25),
4552            "middle slot should be valid"
4553        );
4554        assert!(
4555            svm.is_slot_in_valid_range(50),
4556            "latest slot should be valid"
4557        );
4558        assert!(
4559            !svm.is_slot_in_valid_range(51),
4560            "slot after latest should be invalid"
4561        );
4562    }
4563
4564    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4565    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4566    #[test_case(TestType::no_db(); "with no db")]
4567    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4568    fn test_get_block_or_reconstruct_stored_block(test_type: TestType) {
4569        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4570
4571        // Set up: genesis_slot = 0, latest absolute slot = 100
4572        svm.genesis_slot = 0;
4573        svm.latest_epoch_info.absolute_slot = 100;
4574
4575        // Store a block with transactions
4576        let stored_block = BlockHeader {
4577            hash: "stored_block_hash".to_string(),
4578            previous_blockhash: "prev_hash".to_string(),
4579            parent_slot: 49,
4580            block_time: 1234567890,
4581            block_height: 50,
4582            signatures: vec![Signature::new_unique()],
4583        };
4584        svm.blocks.store(50, stored_block.clone()).unwrap();
4585
4586        // Retrieve the stored block
4587        let result = svm.get_block_or_reconstruct(50).unwrap();
4588        assert!(result.is_some(), "should return stored block");
4589        let block = result.unwrap();
4590        assert_eq!(block.hash, "stored_block_hash");
4591        assert_eq!(block.signatures.len(), 1);
4592    }
4593
4594    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4595    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4596    #[test_case(TestType::no_db(); "with no db")]
4597    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4598    fn test_get_block_or_reconstruct_empty_block(test_type: TestType) {
4599        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4600
4601        // Set up: genesis_slot = 0, latest absolute slot = 100
4602        svm.genesis_slot = 0;
4603        svm.latest_epoch_info.absolute_slot = 100;
4604        svm.genesis_updated_at = 1000000; // 1 second in ms
4605        svm.slot_time = 400; // 400ms per slot
4606
4607        // Request a slot that wasn't stored (no block stored at slot 50)
4608        let result = svm.get_block_or_reconstruct(50).unwrap();
4609        assert!(
4610            result.is_some(),
4611            "should reconstruct empty block for valid slot"
4612        );
4613
4614        let block = result.unwrap();
4615        // Verify it's a reconstructed empty block
4616        assert!(
4617            block.signatures.is_empty(),
4618            "reconstructed block should have no signatures"
4619        );
4620        assert_eq!(block.block_height, 50);
4621        assert_eq!(block.parent_slot, 49);
4622
4623        // Verify the block time is calculated correctly
4624        // genesis_updated_at (1000000ms) + (50 slots * 400ms) = 1020000ms = 1020 seconds
4625        assert_eq!(block.block_time, 1020);
4626    }
4627
4628    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4629    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4630    #[test_case(TestType::no_db(); "with no db")]
4631    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4632    fn test_get_block_or_reconstruct_out_of_range(test_type: TestType) {
4633        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4634
4635        // Set up: genesis_slot = 100, latest absolute slot = 110
4636        svm.genesis_slot = 100;
4637        svm.latest_epoch_info.absolute_slot = 110;
4638
4639        // Request slot before genesis
4640        let result = svm.get_block_or_reconstruct(50).unwrap();
4641        assert!(
4642            result.is_none(),
4643            "should return None for slot before genesis"
4644        );
4645
4646        // Request slot after latest
4647        let result = svm.get_block_or_reconstruct(200).unwrap();
4648        assert!(result.is_none(), "should return None for slot after latest");
4649    }
4650
4651    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4652    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4653    #[test_case(TestType::no_db(); "with no db")]
4654    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4655    #[allow(deprecated)]
4656    fn test_reconstruct_sysvars_recent_blockhashes(test_type: TestType) {
4657        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
4658
4659        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4660
4661        // Set up: chain_tip.index = 10, genesis_slot = 0
4662        svm.chain_tip = BlockIdentifier::new(10, "test_hash");
4663        svm.genesis_slot = 0;
4664        svm.latest_epoch_info.absolute_slot = 10;
4665
4666        svm.reconstruct_sysvars();
4667
4668        // Verify RecentBlockhashes sysvar
4669        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
4670
4671        // Should have 11 entries (indices 0 through 10)
4672        assert_eq!(recent_blockhashes.len(), 11);
4673
4674        // First entry should be the hash for chain_tip.index (10)
4675        let expected_hash = SyntheticBlockhash::new(10);
4676        assert_eq!(
4677            recent_blockhashes.first().unwrap().blockhash,
4678            *expected_hash.hash(),
4679            "First blockhash should match SyntheticBlockhash for chain_tip.index"
4680        );
4681
4682        // Last entry should be the hash for index 0
4683        let expected_last_hash = SyntheticBlockhash::new(0);
4684        assert_eq!(
4685            recent_blockhashes.last().unwrap().blockhash,
4686            *expected_last_hash.hash(),
4687            "Last blockhash should match SyntheticBlockhash for index 0"
4688        );
4689    }
4690
4691    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4692    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4693    #[test_case(TestType::no_db(); "with no db")]
4694    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4695    #[allow(deprecated)]
4696    fn test_reconstruct_sysvars_slot_hashes(test_type: TestType) {
4697        use solana_slot_hashes::SlotHashes;
4698
4699        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4700
4701        // Set up: chain_tip.index = 5, genesis_slot = 100 (absolute slot = 105)
4702        svm.chain_tip = BlockIdentifier::new(5, "test_hash");
4703        svm.genesis_slot = 100;
4704        svm.latest_epoch_info.absolute_slot = 105;
4705
4706        svm.reconstruct_sysvars();
4707
4708        // Verify SlotHashes sysvar
4709        let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
4710
4711        // Should have 6 entries (indices 0 through 5, mapped to slots 100 through 105)
4712        assert_eq!(slot_hashes.len(), 6);
4713
4714        // Check that slot 105 maps to hash for index 5
4715        let expected_hash_105 = SyntheticBlockhash::new(5);
4716        let hash_for_105 = slot_hashes.get(&105);
4717        assert!(hash_for_105.is_some(), "SlotHashes should contain slot 105");
4718        assert_eq!(
4719            hash_for_105.unwrap(),
4720            expected_hash_105.hash(),
4721            "Hash for slot 105 should match SyntheticBlockhash for index 5"
4722        );
4723
4724        // Check that slot 100 maps to hash for index 0
4725        let expected_hash_100 = SyntheticBlockhash::new(0);
4726        let hash_for_100 = slot_hashes.get(&100);
4727        assert!(hash_for_100.is_some(), "SlotHashes should contain slot 100");
4728        assert_eq!(
4729            hash_for_100.unwrap(),
4730            expected_hash_100.hash(),
4731            "Hash for slot 100 should match SyntheticBlockhash for index 0"
4732        );
4733    }
4734
4735    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4736    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4737    #[test_case(TestType::no_db(); "with no db")]
4738    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4739    fn test_reconstruct_sysvars_clock(test_type: TestType) {
4740        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4741
4742        // Set up: chain_tip.index = 50, genesis_slot = 1000 (absolute slot = 1050)
4743        svm.chain_tip = BlockIdentifier::new(50, "test_hash");
4744        svm.genesis_slot = 1000;
4745        svm.latest_epoch_info.absolute_slot = 1050;
4746        svm.latest_epoch_info.epoch = 5;
4747        svm.genesis_updated_at = 2_000_000; // 2 seconds in ms
4748        svm.slot_time = 400; // 400ms per slot
4749
4750        svm.reconstruct_sysvars();
4751
4752        // Verify Clock sysvar
4753        let clock = svm.inner.get_sysvar::<Clock>();
4754
4755        assert_eq!(clock.slot, 1050, "Clock slot should be absolute slot");
4756        assert_eq!(clock.epoch, 5, "Clock epoch should match latest_epoch_info");
4757
4758        // Expected timestamp: genesis_updated_at + (50 slots * 400ms) = 2_000_000 + 20_000 = 2_020_000ms = 2020 seconds
4759        assert_eq!(
4760            clock.unix_timestamp, 2020,
4761            "Clock unix_timestamp should be calculated correctly"
4762        );
4763    }
4764
4765    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4766    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4767    #[test_case(TestType::no_db(); "with no db")]
4768    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4769    #[allow(deprecated)]
4770    fn test_reconstruct_sysvars_max_blockhashes(test_type: TestType) {
4771        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
4772
4773        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4774
4775        // Set up: chain_tip.index = 200 (more than MAX_RECENT_BLOCKHASHES_STANDARD = 150)
4776        svm.chain_tip = BlockIdentifier::new(200, "test_hash");
4777        svm.genesis_slot = 0;
4778        svm.latest_epoch_info.absolute_slot = 200;
4779
4780        svm.reconstruct_sysvars();
4781
4782        // Verify RecentBlockhashes sysvar is capped at MAX_RECENT_BLOCKHASHES_STANDARD
4783        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
4784
4785        assert_eq!(
4786            recent_blockhashes.len(),
4787            MAX_RECENT_BLOCKHASHES_STANDARD,
4788            "RecentBlockhashes should be capped at MAX_RECENT_BLOCKHASHES_STANDARD"
4789        );
4790
4791        // First entry should still be for chain_tip.index (200)
4792        let expected_hash = SyntheticBlockhash::new(200);
4793        assert_eq!(
4794            recent_blockhashes.first().unwrap().blockhash,
4795            *expected_hash.hash(),
4796            "First blockhash should match SyntheticBlockhash for chain_tip.index"
4797        );
4798
4799        // Last entry should be for index 51 (200 - 149)
4800        let expected_last_hash = SyntheticBlockhash::new(51);
4801        assert_eq!(
4802            recent_blockhashes.last().unwrap().blockhash,
4803            *expected_last_hash.hash(),
4804            "Last blockhash should match SyntheticBlockhash for start_index"
4805        );
4806    }
4807
4808    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4809    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4810    #[test_case(TestType::no_db(); "with no db")]
4811    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4812    #[allow(deprecated)]
4813    fn test_reconstruct_sysvars_deterministic(test_type: TestType) {
4814        use solana_slot_hashes::SlotHashes;
4815        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
4816
4817        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4818
4819        // Set up initial state
4820        svm.chain_tip = BlockIdentifier::new(25, "test_hash");
4821        svm.genesis_slot = 50;
4822        svm.latest_epoch_info.absolute_slot = 75;
4823        svm.latest_epoch_info.epoch = 2;
4824        svm.genesis_updated_at = 1_000_000;
4825        svm.slot_time = 400;
4826
4827        // First reconstruction
4828        svm.reconstruct_sysvars();
4829        let blockhashes_1 = svm.inner.get_sysvar::<RecentBlockhashes>();
4830        let slot_hashes_1 = svm.inner.get_sysvar::<SlotHashes>();
4831        let clock_1 = svm.inner.get_sysvar::<Clock>();
4832
4833        // Second reconstruction with same state
4834        svm.reconstruct_sysvars();
4835        let blockhashes_2 = svm.inner.get_sysvar::<RecentBlockhashes>();
4836        let slot_hashes_2 = svm.inner.get_sysvar::<SlotHashes>();
4837        let clock_2 = svm.inner.get_sysvar::<Clock>();
4838
4839        // Verify determinism - results should be identical
4840        assert_eq!(blockhashes_1.len(), blockhashes_2.len());
4841        for (b1, b2) in blockhashes_1.iter().zip(blockhashes_2.iter()) {
4842            assert_eq!(
4843                b1.blockhash, b2.blockhash,
4844                "RecentBlockhashes should be deterministic"
4845            );
4846        }
4847
4848        assert_eq!(slot_hashes_1.len(), slot_hashes_2.len());
4849        assert_eq!(clock_1.slot, clock_2.slot);
4850        assert_eq!(clock_1.epoch, clock_2.epoch);
4851        assert_eq!(clock_1.unix_timestamp, clock_2.unix_timestamp);
4852    }
4853}