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