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>, HashSet<Pubkey>), 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        let mut all_mutated_account_keys = HashSet::new();
1701
1702        while let Some((tx, status_tx, error)) =
1703            self.transactions_queued_for_confirmation.pop_front()
1704        {
1705            let _ = status_tx.try_send(TransactionStatusEvent::Success(
1706                TransactionConfirmationStatus::Confirmed,
1707            ));
1708            let signature = tx.signatures[0];
1709            let finalized_at = self.latest_epoch_info.absolute_slot + FINALIZATION_SLOT_THRESHOLD;
1710            self.transactions_queued_for_finalization.push_back((
1711                finalized_at,
1712                tx,
1713                status_tx,
1714                error.clone(),
1715            ));
1716
1717            self.notify_signature_subscribers(
1718                SignatureSubscriptionType::confirmed(),
1719                &signature,
1720                slot,
1721                error,
1722            );
1723
1724            let Some(SurfnetTransactionStatus::Processed(tx_data)) =
1725                self.transactions.get(&signature.to_string()).ok().flatten()
1726            else {
1727                continue;
1728            };
1729            let (tx_with_status_meta, mutated_account_keys) = tx_data.as_ref();
1730            all_mutated_account_keys.extend(mutated_account_keys);
1731
1732            for pubkey in mutated_account_keys {
1733                self.account_update_slots.insert(*pubkey, current_slot);
1734            }
1735
1736            self.notify_logs_subscribers(
1737                &signature,
1738                None,
1739                tx_with_status_meta
1740                    .meta
1741                    .log_messages
1742                    .clone()
1743                    .unwrap_or(vec![]),
1744                CommitmentLevel::Confirmed,
1745            );
1746            confirmed_transactions.push(signature);
1747        }
1748
1749        Ok((confirmed_transactions, all_mutated_account_keys))
1750    }
1751
1752    /// Finalizes transactions queued for finalization, sending finalized events as needed.
1753    ///
1754    /// # Returns
1755    /// `Ok(())` on success, or `Err(SurfpoolError)` on error.
1756    fn finalize_transactions(&mut self) -> Result<(), SurfpoolError> {
1757        let current_slot = self.latest_epoch_info.absolute_slot;
1758        let mut requeue = VecDeque::new();
1759        while let Some((finalized_at, tx, status_tx, error)) =
1760            self.transactions_queued_for_finalization.pop_front()
1761        {
1762            if current_slot >= finalized_at {
1763                let _ = status_tx.try_send(TransactionStatusEvent::Success(
1764                    TransactionConfirmationStatus::Finalized,
1765                ));
1766                let signature = &tx.signatures[0];
1767                self.notify_signature_subscribers(
1768                    SignatureSubscriptionType::finalized(),
1769                    signature,
1770                    self.latest_epoch_info.absolute_slot,
1771                    error,
1772                );
1773                let Some(SurfnetTransactionStatus::Processed(tx_data)) =
1774                    self.transactions.get(&signature.to_string()).ok().flatten()
1775                else {
1776                    continue;
1777                };
1778                let (tx_with_status_meta, _) = tx_data.as_ref();
1779                let logs = tx_with_status_meta
1780                    .meta
1781                    .log_messages
1782                    .clone()
1783                    .unwrap_or(vec![]);
1784                self.notify_logs_subscribers(signature, None, logs, CommitmentLevel::Finalized);
1785            } else {
1786                requeue.push_back((finalized_at, tx, status_tx, error));
1787            }
1788        }
1789        // Requeue any transactions that are not yet finalized
1790        self.transactions_queued_for_finalization
1791            .append(&mut requeue);
1792
1793        Ok(())
1794    }
1795
1796    /// Writes account updates to the SVM state based on the provided account update result.
1797    ///
1798    /// # Arguments
1799    /// * `account_update` - The account update result to process.
1800    pub fn write_account_update(&mut self, account_update: GetAccountResult) {
1801        let init_programdata_account = |program_account: &Account| {
1802            if !program_account.executable {
1803                return None;
1804            }
1805            if !program_account
1806                .owner
1807                .eq(&solana_sdk_ids::bpf_loader_upgradeable::id())
1808            {
1809                return None;
1810            }
1811            let Ok(UpgradeableLoaderState::Program {
1812                programdata_address,
1813            }) = bincode::deserialize::<UpgradeableLoaderState>(&program_account.data)
1814            else {
1815                return None;
1816            };
1817
1818            let programdata_state = UpgradeableLoaderState::ProgramData {
1819                upgrade_authority_address: Some(system_program::id()),
1820                slot: self.get_latest_absolute_slot(),
1821            };
1822            let mut data = bincode::serialize(&programdata_state).unwrap();
1823
1824            data.extend_from_slice(crate::surfnet::noop_program::NOOP_PROGRAM_ELF);
1825            let lamports = self.inner.minimum_balance_for_rent_exemption(data.len());
1826            Some((
1827                programdata_address,
1828                Account {
1829                    lamports,
1830                    data,
1831                    owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
1832                    executable: false,
1833                    rent_epoch: 0,
1834                },
1835            ))
1836        };
1837        match account_update {
1838            GetAccountResult::FoundAccount(pubkey, account, do_update_account) => {
1839                if do_update_account {
1840                    if let Some((programdata_address, programdata_account)) =
1841                        init_programdata_account(&account)
1842                    {
1843                        match self.get_account(&programdata_address) {
1844                            Ok(None) => {
1845                                if let Err(e) =
1846                                    self.set_account(&programdata_address, programdata_account)
1847                                {
1848                                    let _ = self
1849                                        .simnet_events_tx
1850                                        .send(SimnetEvent::error(e.to_string()));
1851                                }
1852                            }
1853                            Ok(Some(_)) => {}
1854                            Err(e) => {
1855                                let _ = self
1856                                    .simnet_events_tx
1857                                    .send(SimnetEvent::error(e.to_string()));
1858                            }
1859                        }
1860                    }
1861                    if let Err(e) = self.set_account(&pubkey, account.clone()) {
1862                        let _ = self
1863                            .simnet_events_tx
1864                            .send(SimnetEvent::error(e.to_string()));
1865                    }
1866                }
1867            }
1868            GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) => {
1869                if let Some((programdata_address, programdata_account)) =
1870                    init_programdata_account(&account)
1871                {
1872                    match self.get_account(&programdata_address) {
1873                        Ok(None) => {
1874                            if let Err(e) =
1875                                self.set_account(&programdata_address, programdata_account)
1876                            {
1877                                let _ = self
1878                                    .simnet_events_tx
1879                                    .send(SimnetEvent::error(e.to_string()));
1880                            }
1881                        }
1882                        Ok(Some(_)) => {}
1883                        Err(e) => {
1884                            let _ = self
1885                                .simnet_events_tx
1886                                .send(SimnetEvent::error(e.to_string()));
1887                        }
1888                    }
1889                }
1890                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1891                    let _ = self
1892                        .simnet_events_tx
1893                        .send(SimnetEvent::error(e.to_string()));
1894                }
1895            }
1896            GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => {
1897                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1898                    let _ = self
1899                        .simnet_events_tx
1900                        .send(SimnetEvent::error(e.to_string()));
1901                }
1902            }
1903            GetAccountResult::FoundProgramAccount(
1904                (pubkey, account),
1905                (coupled_pubkey, Some(coupled_account)),
1906            )
1907            | GetAccountResult::FoundTokenAccount(
1908                (pubkey, account),
1909                (coupled_pubkey, Some(coupled_account)),
1910            ) => {
1911                // The data account _must_ be set first, as the program account depends on it.
1912                if let Err(e) = self.set_account(&coupled_pubkey, coupled_account.clone()) {
1913                    let _ = self
1914                        .simnet_events_tx
1915                        .send(SimnetEvent::error(e.to_string()));
1916                }
1917                if let Err(e) = self.set_account(&pubkey, account.clone()) {
1918                    let _ = self
1919                        .simnet_events_tx
1920                        .send(SimnetEvent::error(e.to_string()));
1921                }
1922            }
1923            GetAccountResult::None(_) => {}
1924        }
1925    }
1926
1927    pub fn confirm_current_block(&mut self) -> SurfpoolResult<()> {
1928        let slot = self.get_latest_absolute_slot();
1929        let previous_chain_tip = self.chain_tip.clone();
1930        if slot % *GARBAGE_COLLECTION_INTERVAL_SLOTS == 0 {
1931            debug!("Clearing liteSVM cache at slot {}", slot);
1932            self.inner.garbage_collect(self.feature_set.clone());
1933        }
1934        self.chain_tip = self.new_blockhash();
1935        // Confirm processed transactions
1936        let (confirmed_signatures, all_mutated_account_keys) = self.confirm_transactions()?;
1937        let write_version = self.increment_write_version();
1938
1939        // Notify Geyser plugin of account updates
1940        for pubkey in all_mutated_account_keys {
1941            let Some(account) = self.inner.get_account(&pubkey)? else {
1942                continue;
1943            };
1944            self.geyser_events_tx
1945                .send(GeyserEvent::UpdateAccount(
1946                    GeyserAccountUpdate::block_update(pubkey, account, slot, write_version),
1947                ))
1948                .ok();
1949        }
1950
1951        let num_transactions = confirmed_signatures.len() as u64;
1952        self.updated_at += self.slot_time;
1953
1954        // Only store blocks that have transactions (sparse block storage)
1955        // Empty blocks can be reconstructed on-the-fly from their slot number
1956        if !confirmed_signatures.is_empty() {
1957            self.blocks.store(
1958                slot,
1959                BlockHeader {
1960                    hash: self.chain_tip.hash.clone(),
1961                    previous_blockhash: previous_chain_tip.hash.clone(),
1962                    block_time: self.updated_at as i64 / 1_000,
1963                    block_height: self.chain_tip.index,
1964                    parent_slot: slot,
1965                    signatures: confirmed_signatures,
1966                },
1967            )?;
1968        }
1969
1970        // Checkpoint the latest slot periodically (~every 150 slots / 1 minute at standard slot time)
1971        // This allows recovery after restart without storing every empty block
1972        if slot.saturating_sub(self.last_checkpoint_slot) >= *CHECKPOINT_INTERVAL_SLOTS {
1973            self.slot_checkpoint
1974                .store("latest_slot".to_string(), slot)?;
1975            self.last_checkpoint_slot = slot;
1976        }
1977
1978        if self.perf_samples.len() > 30 {
1979            self.perf_samples.pop_back();
1980        }
1981        self.perf_samples.push_front(RpcPerfSample {
1982            slot,
1983            num_slots: 1,
1984            sample_period_secs: 1,
1985            num_transactions,
1986            num_non_vote_transactions: Some(num_transactions),
1987        });
1988
1989        self.latest_epoch_info.slot_index += 1;
1990        self.latest_epoch_info.block_height = self.chain_tip.index;
1991        self.latest_epoch_info.absolute_slot += 1;
1992        if self.latest_epoch_info.slot_index > self.latest_epoch_info.slots_in_epoch {
1993            self.latest_epoch_info.slot_index = 0;
1994            self.latest_epoch_info.epoch += 1;
1995        }
1996        let total_transactions = self.latest_epoch_info.transaction_count.unwrap_or(0);
1997        self.latest_epoch_info.transaction_count = Some(total_transactions + num_transactions);
1998
1999        let parent_slot = self.latest_epoch_info.absolute_slot.saturating_sub(1);
2000        let new_slot = self.latest_epoch_info.absolute_slot;
2001        let root = new_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
2002        self.notify_slot_subscribers(new_slot, parent_slot, root);
2003
2004        // Notify geyser plugins of slot status (Confirmed)
2005        self.geyser_events_tx
2006            .send(GeyserEvent::UpdateSlotStatus {
2007                slot: new_slot,
2008                parent: Some(parent_slot),
2009                status: GeyserSlotStatus::Confirmed,
2010            })
2011            .ok();
2012
2013        // Notify geyser plugins of block metadata
2014        let block_metadata = GeyserBlockMetadata {
2015            slot: new_slot,
2016            blockhash: self.chain_tip.hash.clone(),
2017            parent_slot,
2018            parent_blockhash: previous_chain_tip.hash.clone(),
2019            block_time: Some(self.updated_at as i64 / 1_000),
2020            block_height: Some(self.chain_tip.index),
2021            executed_transaction_count: num_transactions,
2022            entry_count: 1, // Surfpool produces 1 entry per block
2023        };
2024        self.geyser_events_tx
2025            .send(GeyserEvent::NotifyBlockMetadata(block_metadata))
2026            .ok();
2027
2028        // Notify geyser plugins of entry (Surfpool emits 1 entry per block)
2029        let entry_hash = solana_hash::Hash::from_str(&self.chain_tip.hash)
2030            .map(|h| h.to_bytes().to_vec())
2031            .unwrap_or_else(|_| vec![0u8; 32]);
2032        let entry_info = GeyserEntryInfo {
2033            slot: new_slot,
2034            index: 0, // Single entry per block
2035            num_hashes: 1,
2036            hash: entry_hash,
2037            executed_transaction_count: num_transactions,
2038            starting_transaction_index: 0,
2039        };
2040        self.geyser_events_tx
2041            .send(GeyserEvent::NotifyEntry(entry_info))
2042            .ok();
2043
2044        let clock: Clock = Clock {
2045            slot: self.latest_epoch_info.absolute_slot,
2046            epoch: self.latest_epoch_info.epoch,
2047            unix_timestamp: self.updated_at as i64 / 1_000,
2048            epoch_start_timestamp: 0, // todo
2049            leader_schedule_epoch: 0, // todo
2050        };
2051
2052        let _ = self
2053            .simnet_events_tx
2054            .send(SimnetEvent::SystemClockUpdated(clock.clone()));
2055        self.inner.set_sysvar(&clock);
2056
2057        self.finalize_transactions()?;
2058
2059        // Notify geyser plugins of newly rooted (finalized) slot
2060        // Only emit if root is a valid slot (greater than genesis)
2061        if root >= self.genesis_slot {
2062            self.geyser_events_tx
2063                .send(GeyserEvent::UpdateSlotStatus {
2064                    slot: root,
2065                    parent: root.checked_sub(1),
2066                    status: GeyserSlotStatus::Rooted,
2067                })
2068                .ok();
2069        }
2070
2071        // Evict the accounts marked as streamed from cache to enforce them to be fetched again
2072        let accounts_to_reset: Vec<_> = self.streamed_accounts.into_iter()?.collect();
2073        for (pubkey_str, include_owned_accounts) in accounts_to_reset {
2074            let pubkey = Pubkey::from_str(&pubkey_str)
2075                .map_err(|e| SurfpoolError::invalid_pubkey(&pubkey_str, e.to_string()))?;
2076            self.reset_account(&pubkey, include_owned_accounts)?;
2077        }
2078
2079        Ok(())
2080    }
2081
2082    /// Materializes scheduled overrides for the current slot
2083    ///
2084    /// This function:
2085    /// 1. Dequeues overrides scheduled for the current slot
2086    /// 2. Resolves account addresses (Pubkey or PDA)
2087    /// 3. Optionally fetches fresh account data from remote if `fetch_before_use` is enabled
2088    /// 4. Applies the overrides to the account data
2089    /// 5. Updates the SVM state
2090    pub async fn materialize_overrides(
2091        &mut self,
2092        remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2093    ) -> SurfpoolResult<()> {
2094        let current_slot = self.latest_epoch_info.absolute_slot;
2095
2096        // Remove and get overrides for this slot
2097        let Some(overrides) = self.scheduled_overrides.take(&current_slot)? else {
2098            // No overrides for this slot
2099            return Ok(());
2100        };
2101
2102        debug!(
2103            "Materializing {} override(s) for slot {}",
2104            overrides.len(),
2105            current_slot
2106        );
2107
2108        for override_instance in overrides {
2109            if !override_instance.enabled {
2110                debug!("Skipping disabled override: {}", override_instance.id);
2111                continue;
2112            }
2113
2114            // Resolve account address
2115            let account_pubkey = match &override_instance.account {
2116                surfpool_types::AccountAddress::Pubkey(pubkey_str) => {
2117                    match Pubkey::from_str(pubkey_str) {
2118                        Ok(pubkey) => pubkey,
2119                        Err(e) => {
2120                            warn!(
2121                                "Failed to parse pubkey '{}' for override {}: {}",
2122                                pubkey_str, override_instance.id, e
2123                            );
2124                            continue;
2125                        }
2126                    }
2127                }
2128                surfpool_types::AccountAddress::Pda {
2129                    program_id: _,
2130                    seeds: _,
2131                } => unimplemented!(),
2132            };
2133
2134            debug!(
2135                "Processing override {} for account {} (label: {:?})",
2136                override_instance.id, account_pubkey, override_instance.label
2137            );
2138
2139            // Fetch fresh account data from remote if requested
2140            if override_instance.fetch_before_use {
2141                if let Some((client, _)) = remote_ctx {
2142                    debug!(
2143                        "Fetching fresh account data for {} from remote",
2144                        account_pubkey
2145                    );
2146
2147                    match client
2148                        .get_account(&account_pubkey, CommitmentConfig::confirmed())
2149                        .await
2150                    {
2151                        Ok(GetAccountResult::FoundAccount(_pubkey, remote_account, _)) => {
2152                            debug!(
2153                                "Fetched account {} from remote: {} lamports, {} bytes",
2154                                account_pubkey,
2155                                remote_account.lamports(),
2156                                remote_account.data().len()
2157                            );
2158
2159                            // Set the fresh account data in the SVM
2160                            if let Err(e) = self.inner.set_account(account_pubkey, remote_account) {
2161                                warn!(
2162                                    "Failed to set account {} from remote: {}",
2163                                    account_pubkey, e
2164                                );
2165                            }
2166                        }
2167                        Ok(GetAccountResult::None(_)) => {
2168                            debug!("Account {} not found on remote", account_pubkey);
2169                        }
2170                        Ok(_) => {
2171                            debug!("Account {} fetched (other variant)", account_pubkey);
2172                        }
2173                        Err(e) => {
2174                            warn!(
2175                                "Failed to fetch account {} from remote: {}",
2176                                account_pubkey, e
2177                            );
2178                        }
2179                    }
2180                } else {
2181                    debug!(
2182                        "fetch_before_use enabled but no remote client available for override {}",
2183                        override_instance.id
2184                    );
2185                }
2186            }
2187
2188            // Apply the override values to the account data
2189            if !override_instance.values.is_empty() {
2190                debug!(
2191                    "Override {} applying {} field modification(s) to account {}",
2192                    override_instance.id,
2193                    override_instance.values.len(),
2194                    account_pubkey
2195                );
2196
2197                // Get the account from the SVM
2198                let Some(account) = self.inner.get_account(&account_pubkey)? else {
2199                    warn!(
2200                        "Account {} not found in SVM for override {}, skipping modifications",
2201                        account_pubkey, override_instance.id
2202                    );
2203                    continue;
2204                };
2205
2206                // Get the account owner (program ID)
2207                let owner_program_id = account.owner();
2208
2209                // Look up the IDL for the owner program
2210                let idl_versions = match self.registered_idls.get(&owner_program_id.to_string()) {
2211                    Ok(Some(versions)) => versions,
2212                    Ok(None) => {
2213                        warn!(
2214                            "No IDL registered for program {} (owner of account {}), skipping override {}",
2215                            owner_program_id, account_pubkey, override_instance.id
2216                        );
2217                        continue;
2218                    }
2219                    Err(e) => {
2220                        warn!(
2221                            "Failed to get IDL for program {}: {}, skipping override {}",
2222                            owner_program_id, e, override_instance.id
2223                        );
2224                        continue;
2225                    }
2226                };
2227
2228                // Get the latest IDL version (first in the sorted Vec)
2229                let Some(versioned_idl) = idl_versions.first() else {
2230                    warn!(
2231                        "IDL versions empty for program {}, skipping override {}",
2232                        owner_program_id, override_instance.id
2233                    );
2234                    continue;
2235                };
2236
2237                let idl = &versioned_idl.1;
2238
2239                // Get account data
2240                let account_data = account.data();
2241
2242                // Use get_forged_account_data to apply the overrides
2243                let new_account_data = match self.get_forged_account_data(
2244                    &account_pubkey,
2245                    account_data,
2246                    idl,
2247                    &override_instance.values,
2248                ) {
2249                    Ok(data) => data,
2250                    Err(e) => {
2251                        warn!(
2252                            "Failed to forge account data for {} (override {}): {}",
2253                            account_pubkey, override_instance.id, e
2254                        );
2255                        continue;
2256                    }
2257                };
2258
2259                // Create a new account with modified data
2260                let modified_account = Account {
2261                    lamports: account.lamports(),
2262                    data: new_account_data,
2263                    owner: *account.owner(),
2264                    executable: account.executable(),
2265                    rent_epoch: account.rent_epoch(),
2266                };
2267
2268                // Update the account in the SVM
2269                if let Err(e) = self.inner.set_account(account_pubkey, modified_account) {
2270                    warn!(
2271                        "Failed to set modified account {} in SVM: {}",
2272                        account_pubkey, e
2273                    );
2274                } else {
2275                    debug!(
2276                        "Successfully applied {} override(s) to account {} (override {})",
2277                        override_instance.values.len(),
2278                        account_pubkey,
2279                        override_instance.id
2280                    );
2281                }
2282            }
2283        }
2284
2285        Ok(())
2286    }
2287
2288    /// Forges account data by applying overrides to existing account data
2289    ///
2290    /// This function:
2291    /// 1. Validates account data size (must be at least 8 bytes for discriminator)
2292    /// 2. Splits discriminator and serialized data
2293    /// 3. Finds the account type in the IDL using the discriminator
2294    /// 4. Deserializes the account data
2295    /// 5. Applies field overrides using dot notation
2296    /// 6. Re-serializes the modified data
2297    /// 7. Reconstructs the account data with the original discriminator
2298    ///
2299    /// # Arguments
2300    /// * `account_pubkey` - The account address (for error messages)
2301    /// * `account_data` - The original account data bytes
2302    /// * `idl` - The IDL for the account's program
2303    /// * `overrides` - Map of field paths to new values
2304    ///
2305    /// # Returns
2306    /// The forged account data as bytes, or an error
2307    pub fn get_forged_account_data(
2308        &self,
2309        account_pubkey: &Pubkey,
2310        account_data: &[u8],
2311        idl: &Idl,
2312        overrides: &HashMap<String, serde_json::Value>,
2313    ) -> SurfpoolResult<Vec<u8>> {
2314        // Validate account data size
2315        if account_data.len() < 8 {
2316            return Err(SurfpoolError::invalid_account_data(
2317                account_pubkey,
2318                "Account data too small to be an Anchor account (need at least 8 bytes for discriminator)",
2319                Some("Data length too small"),
2320            ));
2321        }
2322
2323        // Split discriminator and data
2324        let discriminator = &account_data[..8];
2325        let serialized_data = &account_data[8..];
2326
2327        // Find the account type using the discriminator
2328        let account_def = idl
2329            .accounts
2330            .iter()
2331            .find(|acc| acc.discriminator.eq(discriminator))
2332            .ok_or_else(|| {
2333                SurfpoolError::internal(format!(
2334                    "Account with discriminator '{:?}' not found in IDL",
2335                    discriminator
2336                ))
2337            })?;
2338
2339        // Find the corresponding type definition
2340        let account_type = idl
2341            .types
2342            .iter()
2343            .find(|t| t.name == account_def.name)
2344            .ok_or_else(|| {
2345                SurfpoolError::internal(format!(
2346                    "Type definition for account '{}' not found in IDL",
2347                    account_def.name
2348                ))
2349            })?;
2350
2351        // Set up generics for parsing
2352        let empty_vec = vec![];
2353        let idl_type_def_generics = idl
2354            .types
2355            .iter()
2356            .find(|t| t.name == account_type.name)
2357            .map(|t| &t.generics);
2358
2359        // Deserialize the account data using proper Borsh deserialization
2360        // Use the version that returns leftover bytes to preserve any trailing padding
2361        let (mut parsed_value, leftover_bytes) =
2362            parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes(
2363                serialized_data,
2364                &account_type.ty,
2365                &idl.types,
2366                &vec![],
2367                idl_type_def_generics.unwrap_or(&empty_vec),
2368            )
2369            .map_err(|e| {
2370                SurfpoolError::deserialize_error(
2371                    "account data",
2372                    format!("Failed to deserialize account data using Borsh: {}", e),
2373                )
2374            })?;
2375
2376        // Apply overrides to the decoded value
2377        for (path, value) in overrides {
2378            apply_override_to_decoded_account(&mut parsed_value, path, value)?;
2379        }
2380
2381        // Construct an IdlType::Defined that references the account type
2382        // This is needed because borsh_encode_value_to_idl_type expects IdlType, not IdlTypeDefTy
2383        use anchor_lang_idl::types::{IdlGenericArg, IdlType};
2384        let defined_type = IdlType::Defined {
2385            name: account_type.name.clone(),
2386            generics: account_type
2387                .generics
2388                .iter()
2389                .map(|_| IdlGenericArg::Type {
2390                    ty: IdlType::String,
2391                })
2392                .collect(),
2393        };
2394
2395        // Re-encode the value using Borsh
2396        let re_encoded_data =
2397            borsh_encode_value_to_idl_type(&parsed_value, &defined_type, &idl.types, None)
2398                .map_err(|e| {
2399                    SurfpoolError::internal(format!(
2400                        "Failed to re-encode account data using Borsh: {}",
2401                        e
2402                    ))
2403                })?;
2404
2405        // Reconstruct the account data with discriminator and preserve any trailing bytes
2406        let mut new_account_data =
2407            Vec::with_capacity(8 + re_encoded_data.len() + leftover_bytes.len());
2408        new_account_data.extend_from_slice(discriminator);
2409        new_account_data.extend_from_slice(&re_encoded_data);
2410        new_account_data.extend_from_slice(leftover_bytes);
2411
2412        Ok(new_account_data)
2413    }
2414
2415    /// Subscribes for updates on a transaction signature for a given subscription type.
2416    ///
2417    /// # Arguments
2418    /// * `signature` - The transaction signature to subscribe to.
2419    /// * `subscription_type` - The type of subscription (confirmed/finalized).
2420    ///
2421    /// # Returns
2422    /// A receiver for slot and transaction error updates.
2423    pub fn subscribe_for_signature_updates(
2424        &mut self,
2425        signature: &Signature,
2426        subscription_type: SignatureSubscriptionType,
2427    ) -> Receiver<(Slot, Option<TransactionError>)> {
2428        let (tx, rx) = unbounded();
2429        self.signature_subscriptions
2430            .entry(*signature)
2431            .or_default()
2432            .push((subscription_type, tx));
2433        rx
2434    }
2435
2436    pub fn subscribe_for_account_updates(
2437        &mut self,
2438        account_pubkey: &Pubkey,
2439        encoding: Option<UiAccountEncoding>,
2440    ) -> Receiver<UiAccount> {
2441        let (tx, rx) = unbounded();
2442        self.account_subscriptions
2443            .entry(*account_pubkey)
2444            .or_default()
2445            .push((encoding, tx));
2446        rx
2447    }
2448
2449    pub fn subscribe_for_program_updates(
2450        &mut self,
2451        program_id: &Pubkey,
2452        encoding: Option<UiAccountEncoding>,
2453        filters: Option<Vec<RpcFilterType>>,
2454    ) -> Receiver<RpcKeyedAccount> {
2455        let (tx, rx) = unbounded();
2456        self.program_subscriptions
2457            .entry(*program_id)
2458            .or_default()
2459            .push((encoding, filters, tx));
2460        rx
2461    }
2462
2463    /// Notifies signature subscribers of a status update, sending slot and error info.
2464    ///
2465    /// # Arguments
2466    /// * `status` - The subscription type (confirmed/finalized).
2467    /// * `signature` - The transaction signature.
2468    /// * `slot` - The slot number.
2469    /// * `err` - Optional transaction error.
2470    pub fn notify_signature_subscribers(
2471        &mut self,
2472        status: SignatureSubscriptionType,
2473        signature: &Signature,
2474        slot: Slot,
2475        err: Option<TransactionError>,
2476    ) {
2477        let mut remaining = vec![];
2478        if let Some(subscriptions) = self.signature_subscriptions.remove(signature) {
2479            for (subscription_type, tx) in subscriptions {
2480                if status.eq(&subscription_type) {
2481                    if tx.send((slot, err.clone())).is_err() {
2482                        // The receiver has been dropped, so we can skip notifying
2483                        continue;
2484                    }
2485                } else {
2486                    remaining.push((subscription_type, tx));
2487                }
2488            }
2489            if !remaining.is_empty() {
2490                self.signature_subscriptions.insert(*signature, remaining);
2491            }
2492        }
2493    }
2494
2495    pub fn notify_account_subscribers(
2496        &mut self,
2497        account_updated_pubkey: &Pubkey,
2498        account: &Account,
2499    ) {
2500        let mut remaining = vec![];
2501        if let Some(subscriptions) = self.account_subscriptions.remove(account_updated_pubkey) {
2502            for (encoding, tx) in subscriptions {
2503                let config = RpcAccountInfoConfig {
2504                    encoding,
2505                    ..Default::default()
2506                };
2507                let account = self
2508                    .account_to_rpc_keyed_account(account_updated_pubkey, account, &config, None)
2509                    .account;
2510                if tx.send(account).is_err() {
2511                    // The receiver has been dropped, so we can skip notifying
2512                    continue;
2513                } else {
2514                    remaining.push((encoding, tx));
2515                }
2516            }
2517            if !remaining.is_empty() {
2518                self.account_subscriptions
2519                    .insert(*account_updated_pubkey, remaining);
2520            }
2521        }
2522    }
2523
2524    pub fn notify_program_subscribers(&mut self, account_pubkey: &Pubkey, account: &Account) {
2525        let program_id = account.owner;
2526        let mut remaining = vec![];
2527        if let Some(subscriptions) = self.program_subscriptions.remove(&program_id) {
2528            for (encoding, filters, tx) in subscriptions {
2529                // Apply filters if present
2530                if let Some(ref active_filters) = filters {
2531                    match super::locker::apply_rpc_filters(&account.data, active_filters) {
2532                        Ok(true) => {} // Account matches all filters
2533                        Ok(false) => {
2534                            // Filtered out - keep subscription active but don't notify
2535                            remaining.push((encoding, filters, tx));
2536                            continue;
2537                        }
2538                        Err(_) => {
2539                            // Error applying filter - keep subscription, skip notification
2540                            remaining.push((encoding, filters, tx));
2541                            continue;
2542                        }
2543                    }
2544                }
2545
2546                let config = RpcAccountInfoConfig {
2547                    encoding,
2548                    ..Default::default()
2549                };
2550                let keyed_account =
2551                    self.account_to_rpc_keyed_account(account_pubkey, account, &config, None);
2552                if tx.send(keyed_account).is_err() {
2553                    // The receiver has been dropped, so we can skip notifying
2554                    continue;
2555                } else {
2556                    remaining.push((encoding, filters, tx));
2557                }
2558            }
2559            if !remaining.is_empty() {
2560                self.program_subscriptions.insert(program_id, remaining);
2561            }
2562        }
2563    }
2564
2565    /// Retrieves a confirmed block at the given slot, including transactions and metadata.
2566    ///
2567    /// # Arguments
2568    /// * `slot` - The slot number to retrieve the block for.
2569    /// * `config` - The configuration for the block retrieval.
2570    ///
2571    /// # Returns
2572    /// `Some(UiConfirmedBlock)` if found, or `None` if not present.
2573    pub fn get_block_at_slot(
2574        &self,
2575        slot: Slot,
2576        config: &RpcBlockConfig,
2577    ) -> SurfpoolResult<Option<UiConfirmedBlock>> {
2578        // Try to get stored block, or reconstruct empty block if within valid range
2579        let Some(block) = self.get_block_or_reconstruct(slot)? else {
2580            return Ok(None);
2581        };
2582
2583        let show_rewards = config.rewards.unwrap_or(true);
2584        let transaction_details = config
2585            .transaction_details
2586            .unwrap_or(TransactionDetails::Full);
2587
2588        let transactions = match transaction_details {
2589            TransactionDetails::Full => Some(
2590                block
2591                    .signatures
2592                    .iter()
2593                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
2594                    .map(|tx_with_meta| {
2595                        let (meta, _) = tx_with_meta.expect_processed();
2596                        meta.encode(
2597                            config.encoding.unwrap_or(
2598                                solana_transaction_status::UiTransactionEncoding::JsonParsed,
2599                            ),
2600                            config.max_supported_transaction_version,
2601                            show_rewards,
2602                        )
2603                    })
2604                    .collect::<Result<Vec<_>, _>>()
2605                    .map_err(SurfpoolError::from)?,
2606            ),
2607            TransactionDetails::Signatures => None,
2608            TransactionDetails::None => None,
2609            TransactionDetails::Accounts => Some(
2610                block
2611                    .signatures
2612                    .iter()
2613                    .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
2614                    .map(|tx_with_meta| {
2615                        let (meta, _) = tx_with_meta.expect_processed();
2616                        meta.to_json_accounts(
2617                            config.max_supported_transaction_version,
2618                            show_rewards,
2619                        )
2620                    })
2621                    .collect::<Result<Vec<_>, _>>()
2622                    .map_err(SurfpoolError::from)?,
2623            ),
2624        };
2625
2626        let signatures = match transaction_details {
2627            TransactionDetails::Signatures => {
2628                Some(block.signatures.iter().map(|t| t.to_string()).collect())
2629            }
2630            TransactionDetails::Full | TransactionDetails::Accounts | TransactionDetails::None => {
2631                None
2632            }
2633        };
2634
2635        let block = UiConfirmedBlock {
2636            previous_blockhash: block.previous_blockhash.clone(),
2637            blockhash: block.hash.clone(),
2638            parent_slot: block.parent_slot,
2639            transactions,
2640            signatures,
2641            rewards: if show_rewards { Some(vec![]) } else { None },
2642            num_reward_partitions: None,
2643            block_time: Some(block.block_time / 1000),
2644            block_height: Some(block.block_height),
2645        };
2646        Ok(Some(block))
2647    }
2648
2649    /// Returns the blockhash for a given slot, if available.
2650    pub fn blockhash_for_slot(&self, slot: Slot) -> Option<Hash> {
2651        self.blocks
2652            .get(&slot)
2653            .unwrap()
2654            .and_then(|header| header.hash.parse().ok())
2655    }
2656
2657    /// Gets all accounts owned by a specific program ID from the account registry.
2658    ///
2659    /// # Arguments
2660    ///
2661    /// * `program_id` - The program ID to search for owned accounts.
2662    ///
2663    /// # Returns
2664    ///
2665    /// * A vector of (account_pubkey, account) tuples for all accounts owned by the program.
2666    pub fn get_account_owned_by(
2667        &self,
2668        program_id: &Pubkey,
2669    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
2670        let account_pubkeys = self
2671            .accounts_by_owner
2672            .get(&program_id.to_string())
2673            .ok()
2674            .flatten()
2675            .unwrap_or_default();
2676
2677        account_pubkeys
2678            .iter()
2679            .filter_map(|pk_str| {
2680                let pk = Pubkey::from_str(pk_str).ok()?;
2681                self.get_account(&pk)
2682                    .map(|res| res.map(|account| (pk, account.clone())))
2683                    .transpose()
2684            })
2685            .collect::<Result<Vec<_>, SurfpoolError>>()
2686    }
2687
2688    fn get_additional_data(
2689        &self,
2690        pubkey: &Pubkey,
2691        token_mint: Option<Pubkey>,
2692    ) -> Option<AccountAdditionalDataV3> {
2693        let token_mint = if let Some(mint) = token_mint {
2694            Some(mint)
2695        } else {
2696            self.token_accounts
2697                .get(&pubkey.to_string())
2698                .ok()
2699                .flatten()
2700                .map(|ta| ta.mint())
2701        };
2702
2703        token_mint.and_then(|mint| {
2704            self.account_associated_data
2705                .get(&mint.to_string())
2706                .ok()
2707                .flatten()
2708                .and_then(|data| data.try_into().ok())
2709        })
2710    }
2711
2712    pub fn account_to_rpc_keyed_account<T: ReadableAccount>(
2713        &self,
2714        pubkey: &Pubkey,
2715        account: &T,
2716        config: &RpcAccountInfoConfig,
2717        token_mint: Option<Pubkey>,
2718    ) -> RpcKeyedAccount {
2719        let additional_data = self.get_additional_data(pubkey, token_mint);
2720
2721        RpcKeyedAccount {
2722            pubkey: pubkey.to_string(),
2723            account: self.encode_ui_account(
2724                pubkey,
2725                account,
2726                config.encoding.unwrap_or(UiAccountEncoding::Base64),
2727                additional_data,
2728                config.data_slice,
2729            ),
2730        }
2731    }
2732
2733    /// Gets all token accounts that have delegated authority to a specific delegate.
2734    ///
2735    /// # Arguments
2736    ///
2737    /// * `delegate` - The delegate pubkey to search for token accounts that have granted authority.
2738    ///
2739    /// # Returns
2740    ///
2741    /// * A vector of (account_pubkey, token_account) tuples for all token accounts delegated to the specified delegate.
2742    pub fn get_token_accounts_by_delegate(&self, delegate: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
2743        if let Some(account_pubkeys) = self
2744            .token_accounts_by_delegate
2745            .get(&delegate.to_string())
2746            .ok()
2747            .flatten()
2748        {
2749            account_pubkeys
2750                .iter()
2751                .filter_map(|pk_str| {
2752                    let pk = Pubkey::from_str(pk_str).ok()?;
2753                    self.token_accounts
2754                        .get(pk_str)
2755                        .ok()
2756                        .flatten()
2757                        .map(|ta| (pk, ta))
2758                })
2759                .collect()
2760        } else {
2761            Vec::new()
2762        }
2763    }
2764
2765    /// Gets all token accounts owned by a specific owner.
2766    ///
2767    /// # Arguments
2768    ///
2769    /// * `owner` - The owner pubkey to search for token accounts.
2770    ///
2771    /// # Returns
2772    ///
2773    /// * A vector of (account_pubkey, token_account) tuples for all token accounts owned by the specified owner.
2774    pub fn get_parsed_token_accounts_by_owner(
2775        &self,
2776        owner: &Pubkey,
2777    ) -> Vec<(Pubkey, TokenAccount)> {
2778        if let Some(account_pubkeys) = self
2779            .token_accounts_by_owner
2780            .get(&owner.to_string())
2781            .ok()
2782            .flatten()
2783        {
2784            account_pubkeys
2785                .iter()
2786                .filter_map(|pk_str| {
2787                    let pk = Pubkey::from_str(pk_str).ok()?;
2788                    self.token_accounts
2789                        .get(pk_str)
2790                        .ok()
2791                        .flatten()
2792                        .map(|ta| (pk, ta))
2793                })
2794                .collect()
2795        } else {
2796            Vec::new()
2797        }
2798    }
2799
2800    pub fn get_token_accounts_by_owner(
2801        &self,
2802        owner: &Pubkey,
2803    ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
2804        let account_pubkeys = self
2805            .token_accounts_by_owner
2806            .get(&owner.to_string())
2807            .ok()
2808            .flatten()
2809            .unwrap_or_default();
2810
2811        account_pubkeys
2812            .iter()
2813            .filter_map(|pk_str| {
2814                let pk = Pubkey::from_str(pk_str).ok()?;
2815                self.get_account(&pk)
2816                    .map(|res| res.map(|account| (pk, account.clone())))
2817                    .transpose()
2818            })
2819            .collect::<Result<Vec<_>, SurfpoolError>>()
2820    }
2821
2822    /// Gets all token accounts for a specific mint (token type).
2823    ///
2824    /// # Arguments
2825    ///
2826    /// * `mint` - The mint pubkey to search for token accounts.
2827    ///
2828    /// # Returns
2829    ///
2830    /// * A vector of (account_pubkey, token_account) tuples for all token accounts of the specified mint.
2831    pub fn get_token_accounts_by_mint(&self, mint: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
2832        if let Some(account_pubkeys) = self
2833            .token_accounts_by_mint
2834            .get(&mint.to_string())
2835            .ok()
2836            .flatten()
2837        {
2838            account_pubkeys
2839                .iter()
2840                .filter_map(|pk_str| {
2841                    let pk = Pubkey::from_str(pk_str).ok()?;
2842                    self.token_accounts
2843                        .get(pk_str)
2844                        .ok()
2845                        .flatten()
2846                        .map(|ta| (pk, ta))
2847                })
2848                .collect()
2849        } else {
2850            Vec::new()
2851        }
2852    }
2853
2854    pub fn subscribe_for_slot_updates(&mut self) -> Receiver<SlotInfo> {
2855        let (tx, rx) = unbounded();
2856        self.slot_subscriptions.push(tx);
2857        rx
2858    }
2859
2860    pub fn notify_slot_subscribers(&mut self, slot: Slot, parent: Slot, root: Slot) {
2861        self.slot_subscriptions
2862            .retain(|tx| tx.send(SlotInfo { slot, parent, root }).is_ok());
2863    }
2864
2865    pub fn write_simulated_profile_result(
2866        &mut self,
2867        uuid: Uuid,
2868        tag: Option<String>,
2869        profile_result: KeyedProfileResult,
2870    ) -> SurfpoolResult<()> {
2871        self.simulated_transaction_profiles
2872            .store(uuid.to_string(), profile_result)?;
2873
2874        let tag = tag.unwrap_or_else(|| uuid.to_string());
2875        let mut tags = self
2876            .profile_tag_map
2877            .get(&tag)
2878            .ok()
2879            .flatten()
2880            .unwrap_or_default();
2881        tags.push(UuidOrSignature::Uuid(uuid));
2882        self.profile_tag_map.store(tag, tags)?;
2883        Ok(())
2884    }
2885
2886    pub fn write_executed_profile_result(
2887        &mut self,
2888        signature: Signature,
2889        profile_result: KeyedProfileResult,
2890    ) -> SurfpoolResult<()> {
2891        self.executed_transaction_profiles
2892            .store(signature.to_string(), profile_result)?;
2893        let tag = signature.to_string();
2894        let mut tags = self
2895            .profile_tag_map
2896            .get(&tag)
2897            .ok()
2898            .flatten()
2899            .unwrap_or_default();
2900        tags.push(UuidOrSignature::Signature(signature));
2901        self.profile_tag_map.store(tag, tags)?;
2902        Ok(())
2903    }
2904
2905    pub fn subscribe_for_logs_updates(
2906        &mut self,
2907        commitment_level: &CommitmentLevel,
2908        filter: &RpcTransactionLogsFilter,
2909    ) -> Receiver<(Slot, RpcLogsResponse)> {
2910        let (tx, rx) = unbounded();
2911        self.logs_subscriptions
2912            .push((*commitment_level, filter.clone(), tx));
2913        rx
2914    }
2915
2916    pub fn notify_logs_subscribers(
2917        &mut self,
2918        signature: &Signature,
2919        err: Option<TransactionError>,
2920        logs: Vec<String>,
2921        commitment_level: CommitmentLevel,
2922    ) {
2923        for (expected_level, filter, tx) in self.logs_subscriptions.iter() {
2924            if !expected_level.eq(&commitment_level) {
2925                continue; // Skip if commitment level is not expected
2926            }
2927
2928            let should_notify = match filter {
2929                RpcTransactionLogsFilter::All | RpcTransactionLogsFilter::AllWithVotes => true,
2930
2931                RpcTransactionLogsFilter::Mentions(mentioned_accounts) => {
2932                    // Get the tx accounts including loaded addresses
2933                    let transaction_accounts =
2934                        if let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2935                            self.transactions.get(&signature.to_string()).ok().flatten()
2936                        {
2937                            let (tx_meta, _) = tx_data.as_ref();
2938                            let mut accounts = match &tx_meta.transaction.message {
2939                                VersionedMessage::Legacy(msg) => msg.account_keys.clone(),
2940                                VersionedMessage::V0(msg) => msg.account_keys.clone(),
2941                            };
2942
2943                            accounts.extend(&tx_meta.meta.loaded_addresses.writable);
2944                            accounts.extend(&tx_meta.meta.loaded_addresses.readonly);
2945                            Some(accounts)
2946                        } else {
2947                            None
2948                        };
2949
2950                    let Some(accounts) = transaction_accounts else {
2951                        continue;
2952                    };
2953
2954                    mentioned_accounts.iter().any(|filtered_acc| {
2955                        if let Ok(filtered_pubkey) = Pubkey::from_str(&filtered_acc) {
2956                            accounts.contains(&filtered_pubkey)
2957                        } else {
2958                            false
2959                        }
2960                    })
2961                }
2962            };
2963
2964            if should_notify {
2965                let message = RpcLogsResponse {
2966                    signature: signature.to_string(),
2967                    err: err.clone().map(|e| e.into()),
2968                    logs: logs.clone(),
2969                };
2970                let _ = tx.send((self.get_latest_absolute_slot(), message));
2971            }
2972        }
2973    }
2974
2975    /// Registers a snapshot subscription and returns a sender and receiver for notifications.
2976    /// The actual import logic should be handled by the caller (SurfnetSvmLocker).
2977    pub fn register_snapshot_subscription(
2978        &mut self,
2979    ) -> (
2980        Sender<super::SnapshotImportNotification>,
2981        Receiver<super::SnapshotImportNotification>,
2982    ) {
2983        let (tx, rx) = unbounded();
2984        self.snapshot_subscriptions.push(tx.clone());
2985        (tx, rx)
2986    }
2987
2988    pub async fn fetch_snapshot_from_url(
2989        snapshot_url: &str,
2990    ) -> Result<
2991        std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>>,
2992        Box<dyn std::error::Error + Send + Sync>,
2993    > {
2994        let response = reqwest::get(snapshot_url).await?;
2995        let text = response.text().await?;
2996
2997        // Parse the JSON snapshot data
2998        let snapshot: std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>> =
2999            serde_json::from_str(&text)?;
3000
3001        Ok(snapshot)
3002    }
3003
3004    pub fn register_idl(&mut self, idl: Idl, slot: Option<Slot>) -> SurfpoolResult<()> {
3005        let slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3006        let program_id = Pubkey::from_str_const(&idl.address);
3007        let program_id_str = program_id.to_string();
3008        let mut idl_versions = self
3009            .registered_idls
3010            .get(&program_id_str)
3011            .ok()
3012            .flatten()
3013            .unwrap_or_default();
3014        idl_versions.push(VersionedIdl(slot, idl));
3015        // Sort by slot descending so the latest IDL is first
3016        idl_versions.sort_by(|a, b| b.0.cmp(&a.0));
3017        self.registered_idls.store(program_id_str, idl_versions)?;
3018        Ok(())
3019    }
3020
3021    fn encode_ui_account_profile_state(
3022        &self,
3023        pubkey: &Pubkey,
3024        account_profile_state: AccountProfileState,
3025        encoding: &UiAccountEncoding,
3026    ) -> UiAccountProfileState {
3027        let additional_data = self.get_additional_data(pubkey, None);
3028
3029        match account_profile_state {
3030            AccountProfileState::Readonly => UiAccountProfileState::Readonly,
3031            AccountProfileState::Writable(account_change) => {
3032                let change = match account_change {
3033                    AccountChange::Create(account) => UiAccountChange::Create(
3034                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3035                    ),
3036                    AccountChange::Update(account_before, account_after) => {
3037                        UiAccountChange::Update(
3038                            self.encode_ui_account(
3039                                pubkey,
3040                                &account_before,
3041                                *encoding,
3042                                additional_data,
3043                                None,
3044                            ),
3045                            self.encode_ui_account(
3046                                pubkey,
3047                                &account_after,
3048                                *encoding,
3049                                additional_data,
3050                                None,
3051                            ),
3052                        )
3053                    }
3054                    AccountChange::Delete(account) => UiAccountChange::Delete(
3055                        self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3056                    ),
3057                    AccountChange::Unchanged(account) => {
3058                        UiAccountChange::Unchanged(account.map(|account| {
3059                            self.encode_ui_account(
3060                                pubkey,
3061                                &account,
3062                                *encoding,
3063                                additional_data,
3064                                None,
3065                            )
3066                        }))
3067                    }
3068                };
3069                UiAccountProfileState::Writable(change)
3070            }
3071        }
3072    }
3073
3074    fn encode_ui_profile_result(
3075        &self,
3076        profile_result: ProfileResult,
3077        readonly_accounts: &[Pubkey],
3078        encoding: &UiAccountEncoding,
3079    ) -> UiProfileResult {
3080        let ProfileResult {
3081            pre_execution_capture,
3082            post_execution_capture,
3083            compute_units_consumed,
3084            log_messages,
3085            error_message,
3086        } = profile_result;
3087
3088        let account_states = pre_execution_capture
3089            .into_iter()
3090            .zip(post_execution_capture)
3091            .map(|((pubkey, pre_account), (_, post_account))| {
3092                // if pubkey != post {
3093                //     panic!(
3094                //         "Pre-execution pubkey {} does not match post-execution pubkey {}",
3095                //         pubkey, post
3096                //     );
3097                // }
3098                let state =
3099                    AccountProfileState::new(pubkey, pre_account, post_account, readonly_accounts);
3100                (
3101                    pubkey,
3102                    self.encode_ui_account_profile_state(&pubkey, state, encoding),
3103                )
3104            })
3105            .collect::<IndexMap<Pubkey, UiAccountProfileState>>();
3106
3107        UiProfileResult {
3108            account_states,
3109            compute_units_consumed,
3110            log_messages,
3111            error_message,
3112        }
3113    }
3114
3115    pub fn encode_ui_keyed_profile_result(
3116        &self,
3117        keyed_profile_result: KeyedProfileResult,
3118        config: &RpcProfileResultConfig,
3119    ) -> UiKeyedProfileResult {
3120        let KeyedProfileResult {
3121            slot,
3122            key,
3123            instruction_profiles,
3124            transaction_profile,
3125            readonly_account_states,
3126        } = keyed_profile_result;
3127
3128        let encoding = config.encoding.unwrap_or(UiAccountEncoding::JsonParsed);
3129
3130        let readonly_accounts = readonly_account_states.keys().cloned().collect::<Vec<_>>();
3131
3132        let default = RpcProfileDepth::default();
3133        let instruction_profiles = match *config.depth.as_ref().unwrap_or(&default) {
3134            RpcProfileDepth::Transaction => None,
3135            RpcProfileDepth::Instruction => instruction_profiles.map(|instruction_profiles| {
3136                instruction_profiles
3137                    .into_iter()
3138                    .map(|p| self.encode_ui_profile_result(p, &readonly_accounts, &encoding))
3139                    .collect()
3140            }),
3141        };
3142
3143        let transaction_profile =
3144            self.encode_ui_profile_result(transaction_profile, &readonly_accounts, &encoding);
3145
3146        let readonly_account_states = readonly_account_states
3147            .into_iter()
3148            .map(|(pubkey, account)| {
3149                let account = self.encode_ui_account(&pubkey, &account, encoding, None, None);
3150                (pubkey, account)
3151            })
3152            .collect();
3153
3154        UiKeyedProfileResult {
3155            slot,
3156            key,
3157            instruction_profiles,
3158            transaction_profile,
3159            readonly_account_states,
3160        }
3161    }
3162
3163    pub fn encode_ui_account<T: ReadableAccount>(
3164        &self,
3165        pubkey: &Pubkey,
3166        account: &T,
3167        encoding: UiAccountEncoding,
3168        additional_data: Option<AccountAdditionalDataV3>,
3169        data_slice_config: Option<UiDataSliceConfig>,
3170    ) -> UiAccount {
3171        let owner_program_id = account.owner();
3172        let data = account.data();
3173
3174        if encoding == UiAccountEncoding::JsonParsed {
3175            if let Ok(Some(registered_idls)) =
3176                self.registered_idls.get(&owner_program_id.to_string())
3177            {
3178                let filter_slot = self.latest_epoch_info.absolute_slot;
3179                // IDLs are stored sorted by slot descending (most recent first)
3180                let ordered_available_idls = registered_idls
3181                    .iter()
3182                    // only get IDLs that are active (their slot is before the latest slot)
3183                    .filter_map(|VersionedIdl(slot, idl)| {
3184                        if *slot <= filter_slot {
3185                            Some(idl)
3186                        } else {
3187                            None
3188                        }
3189                    })
3190                    .collect::<Vec<_>>();
3191
3192                // if we have none in this loop, it means the only IDLs registered for this pubkey are for a
3193                // future slot, for some reason. if we have some, we'll try each one in this loop, starting
3194                // with the most recent one, to see if the account data can be parsed to the IDL type
3195                for idl in &ordered_available_idls {
3196                    // If we have a valid IDL, use it to parse the account data
3197                    let discriminator = &data[..8];
3198                    if let Some(matching_account) = idl
3199                        .accounts
3200                        .iter()
3201                        .find(|a| a.discriminator.eq(&discriminator))
3202                    {
3203                        // If we found a matching account, we can look up the type to parse the account
3204                        if let Some(account_type) =
3205                            idl.types.iter().find(|t| t.name == matching_account.name)
3206                        {
3207                            let empty_vec = vec![];
3208                            let idl_type_def_generics = idl
3209                                .types
3210                                .iter()
3211                                .find(|t| t.name == account_type.name)
3212                                .map(|t| &t.generics);
3213
3214                            // If we found a matching account type, we can use it to parse the account data
3215                            let rest = data[8..].as_ref();
3216                            if let Ok(parsed_value) =
3217                                parse_bytes_to_value_with_expected_idl_type_def_ty(
3218                                    rest,
3219                                    &account_type.ty,
3220                                    &idl.types,
3221                                    &vec![],
3222                                    idl_type_def_generics.unwrap_or(&empty_vec),
3223                                )
3224                            {
3225                                return UiAccount {
3226                                    lamports: account.lamports(),
3227                                    data: UiAccountData::Json(ParsedAccount {
3228                                        program: idl
3229                                            .metadata
3230                                            .name
3231                                            .to_string()
3232                                            .to_case(convert_case::Case::Kebab),
3233                                        parsed: parsed_value
3234                                            .to_json(Some(&get_txtx_value_json_converters())),
3235                                        space: data.len() as u64,
3236                                    }),
3237                                    owner: owner_program_id.to_string(),
3238                                    executable: account.executable(),
3239                                    rent_epoch: account.rent_epoch(),
3240                                    space: Some(data.len() as u64),
3241                                };
3242                            }
3243                        }
3244                    }
3245                }
3246            }
3247        }
3248
3249        // Fall back to the default encoding
3250        encode_ui_account(
3251            pubkey,
3252            account,
3253            encoding,
3254            additional_data,
3255            data_slice_config,
3256        )
3257    }
3258
3259    pub fn get_account(&self, pubkey: &Pubkey) -> SurfpoolResult<Option<Account>> {
3260        self.inner.get_account(pubkey)
3261    }
3262
3263    pub fn get_all_accounts(&self) -> SurfpoolResult<Vec<(Pubkey, AccountSharedData)>> {
3264        self.inner.get_all_accounts()
3265    }
3266
3267    pub fn get_transaction(
3268        &self,
3269        signature: &Signature,
3270    ) -> SurfpoolResult<Option<SurfnetTransactionStatus>> {
3271        Ok(self.transactions.get(&signature.to_string())?)
3272    }
3273
3274    pub fn start_runbook_execution(&mut self, runbook_id: String) {
3275        self.runbook_executions
3276            .push(RunbookExecutionStatusReport::new(runbook_id));
3277    }
3278
3279    pub fn complete_runbook_execution(&mut self, runbook_id: &str, error: Option<Vec<String>>) {
3280        if let Some(execution) = self
3281            .runbook_executions
3282            .iter_mut()
3283            .find(|e| e.runbook_id.eq(runbook_id) && e.completed_at.is_none())
3284        {
3285            execution.mark_completed(error);
3286        }
3287    }
3288
3289    /// Export all accounts to a JSON file suitable for test fixtures
3290    ///
3291    /// # Arguments
3292    /// * `encoding` - The encoding to use for account data (Base64, JsonParsed, etc.)
3293    ///
3294    /// # Returns
3295    /// A BTreeMap of pubkey -> AccountFixture that can be serialized to JSON.
3296    pub fn export_snapshot(
3297        &self,
3298        config: ExportSnapshotConfig,
3299    ) -> SurfpoolResult<BTreeMap<String, AccountSnapshot>> {
3300        let mut fixtures = BTreeMap::new();
3301        let encoding = if config.include_parsed_accounts.unwrap_or_default() {
3302            UiAccountEncoding::JsonParsed
3303        } else {
3304            UiAccountEncoding::Base64
3305        };
3306        let filter = config.filter.unwrap_or_default();
3307        let include_program_accounts = filter.include_program_accounts.unwrap_or(false);
3308        let include_accounts = filter.include_accounts.unwrap_or_default();
3309        let exclude_accounts = filter.exclude_accounts.unwrap_or_default();
3310
3311        fn is_program_account(pubkey: &Pubkey) -> bool {
3312            pubkey == &bpf_loader::id()
3313                || pubkey == &solana_sdk_ids::bpf_loader_deprecated::id()
3314                || pubkey == &solana_sdk_ids::bpf_loader_upgradeable::id()
3315        }
3316
3317        // Helper function to process an account and add it to fixtures
3318        let mut process_account = |pubkey: &Pubkey, account: &Account| {
3319            let is_include_account = include_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3320            let is_exclude_account = exclude_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3321            let is_program_account = is_program_account(&account.owner);
3322            if is_exclude_account
3323                || ((is_program_account && !include_program_accounts) && !is_include_account)
3324            {
3325                return;
3326            }
3327
3328            // For token accounts, we need to provide the mint additional data
3329            let additional_data: Option<AccountAdditionalDataV3> = if account.owner
3330                == spl_token_interface::id()
3331                || account.owner == spl_token_2022_interface::id()
3332            {
3333                if let Ok(token_account) = TokenAccount::unpack(&account.data) {
3334                    self.account_associated_data
3335                        .get(&token_account.mint().to_string())
3336                        .ok()
3337                        .flatten()
3338                        .and_then(|data| data.try_into().ok())
3339                } else {
3340                    self.account_associated_data
3341                        .get(&pubkey.to_string())
3342                        .ok()
3343                        .flatten()
3344                        .and_then(|data| data.try_into().ok())
3345                }
3346            } else {
3347                self.account_associated_data
3348                    .get(&pubkey.to_string())
3349                    .ok()
3350                    .flatten()
3351                    .and_then(|data| data.try_into().ok())
3352            };
3353
3354            let ui_account =
3355                self.encode_ui_account(pubkey, account, encoding, additional_data, None);
3356
3357            let (base64, parsed_data) = match ui_account.data {
3358                UiAccountData::Json(parsed_account) => {
3359                    (BASE64_STANDARD.encode(account.data()), Some(parsed_account))
3360                }
3361                UiAccountData::Binary(base64, _) => (base64, None),
3362                UiAccountData::LegacyBinary(_) => unreachable!(),
3363            };
3364
3365            let account_snapshot = AccountSnapshot::new(
3366                account.lamports,
3367                account.owner.to_string(),
3368                account.executable,
3369                account.rent_epoch,
3370                base64,
3371                parsed_data,
3372            );
3373
3374            fixtures.insert(pubkey.to_string(), account_snapshot);
3375        };
3376
3377        match &config.scope {
3378            ExportSnapshotScope::Network => {
3379                // Export all network accounts (current behavior)
3380                for (pubkey, account_shared_data) in self.get_all_accounts()? {
3381                    let account = Account::from(account_shared_data.clone());
3382                    process_account(&pubkey, &account);
3383                }
3384            }
3385            ExportSnapshotScope::PreTransaction(signature_str) => {
3386                // Export accounts from a specific transaction's pre-execution state
3387                if let Ok(signature) = Signature::from_str(signature_str) {
3388                    if let Ok(Some(profile)) = self
3389                        .executed_transaction_profiles
3390                        .get(&signature.to_string())
3391                    {
3392                        // Collect accounts from pre-execution capture only
3393                        // This gives us the account state BEFORE the transaction executed
3394                        for (pubkey, account_opt) in
3395                            &profile.transaction_profile.pre_execution_capture
3396                        {
3397                            if let Some(account) = account_opt {
3398                                process_account(pubkey, account);
3399                            }
3400                        }
3401
3402                        // Also collect readonly account states (these don't change)
3403                        for (pubkey, account) in &profile.readonly_account_states {
3404                            process_account(pubkey, account);
3405                        }
3406                    }
3407                }
3408            }
3409        }
3410
3411        Ok(fixtures)
3412    }
3413
3414    /// Registers a scenario for execution by scheduling its overrides
3415    ///
3416    /// The `slot` parameter is the base slot from which relative override slot heights are calculated.
3417    /// If not provided, uses the current slot.
3418    pub fn register_scenario(
3419        &mut self,
3420        scenario: surfpool_types::Scenario,
3421        slot: Option<Slot>,
3422    ) -> SurfpoolResult<()> {
3423        // Use provided slot or current slot as the base for relative slot heights
3424        let base_slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3425
3426        info!(
3427            "Registering scenario: {} ({}) with {} overrides at base slot {}",
3428            scenario.name,
3429            scenario.id,
3430            scenario.overrides.len(),
3431            base_slot
3432        );
3433
3434        // Schedule overrides by adding base slot to their scenario-relative slots
3435        for override_instance in scenario.overrides {
3436            let scenario_relative_slot = override_instance.scenario_relative_slot;
3437            let absolute_slot = base_slot + scenario_relative_slot;
3438
3439            debug!(
3440                "Scheduling override at absolute slot {} (base {} + relative {})",
3441                absolute_slot, base_slot, scenario_relative_slot
3442            );
3443
3444            let mut slot_overrides = self
3445                .scheduled_overrides
3446                .get(&absolute_slot)
3447                .ok()
3448                .flatten()
3449                .unwrap_or_default();
3450            slot_overrides.push(override_instance);
3451            self.scheduled_overrides
3452                .store(absolute_slot, slot_overrides)?;
3453        }
3454
3455        Ok(())
3456    }
3457}
3458
3459#[cfg(test)]
3460mod tests {
3461    use agave_feature_set::{
3462        blake3_syscall_enabled, curve25519_syscall_enabled, disable_fees_sysvar,
3463        enable_extend_program_checked, enable_loader_v4, enable_sbpf_v1_deployment_and_execution,
3464        enable_sbpf_v2_deployment_and_execution, enable_sbpf_v3_deployment_and_execution,
3465        formalize_loaded_transaction_data_size, move_precompile_verification_to_svm,
3466        raise_cpi_nesting_limit_to_8,
3467    };
3468    use base64::{Engine, engine::general_purpose};
3469    use borsh::BorshSerialize;
3470    // use test_log::test; // uncomment to get logs from litesvm
3471    use solana_account::Account;
3472    use solana_hash::Hash;
3473    use solana_keypair::Keypair;
3474    use solana_loader_v3_interface::get_program_data_address;
3475    use solana_message::VersionedMessage;
3476    use solana_program_pack::Pack;
3477    use solana_signer::Signer;
3478    use solana_system_interface::instruction as system_instruction;
3479    use solana_transaction::Transaction;
3480    use solana_transaction_error::TransactionError;
3481    use spl_token_interface::state::{Account as TokenAccount, AccountState};
3482    use test_case::test_case;
3483
3484    use super::*;
3485    use crate::storage::tests::TestType;
3486
3487    fn build_transfer_transaction(
3488        payer: &Keypair,
3489        recipient: &Pubkey,
3490        lamports: u64,
3491        recent_blockhash: Hash,
3492    ) -> VersionedTransaction {
3493        let tx = Transaction::new_signed_with_payer(
3494            &[system_instruction::transfer(
3495                &payer.pubkey(),
3496                recipient,
3497                lamports,
3498            )],
3499            Some(&payer.pubkey()),
3500            &[payer],
3501            recent_blockhash,
3502        );
3503
3504        VersionedTransaction {
3505            signatures: tx.signatures,
3506            message: VersionedMessage::Legacy(tx.message),
3507        }
3508    }
3509
3510    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3511    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3512    #[test_case(TestType::no_db(); "with no db")]
3513    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3514    fn test_synthetic_blockhash_generation(test_type: TestType) {
3515        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3516
3517        // Test with different chain tip indices
3518        let test_cases = vec![0, 1, 42, 255, 1000, 0x12345678];
3519
3520        for index in test_cases {
3521            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
3522
3523            // Generate the synthetic blockhash
3524            let new_blockhash = svm.new_blockhash();
3525
3526            // Verify the blockhash string contains our expected pattern
3527            let blockhash_str = new_blockhash.hash.clone();
3528            println!("Index {} -> Blockhash: {}", index, blockhash_str);
3529
3530            // The blockhash should be a valid base58 string
3531            assert!(!blockhash_str.is_empty());
3532            assert!(blockhash_str.len() > 20); // Base58 encoded 32 bytes should be around 44 chars
3533
3534            // Verify it's deterministic - same index should produce same blockhash
3535            svm.chain_tip = BlockIdentifier::new(index, "test_hash");
3536            let new_blockhash2 = svm.new_blockhash();
3537            assert_eq!(new_blockhash.hash, new_blockhash2.hash);
3538        }
3539    }
3540
3541    #[test]
3542    fn test_synthetic_blockhash_base58_encoding() {
3543        // Test the base58 encoding logic directly
3544        let test_index = 42u64;
3545        let index_hex = format!("{:08x}", test_index)
3546            .replace('0', "x")
3547            .replace('O', "x");
3548
3549        let target_length = 43;
3550        let padding_needed = target_length - SyntheticBlockhash::PREFIX.len() - index_hex.len();
3551        let padding = "x".repeat(padding_needed.max(0));
3552        let target_string = format!("{}{}{}", SyntheticBlockhash::PREFIX, padding, index_hex);
3553
3554        println!("Target string: {}", target_string);
3555
3556        // Verify the string is valid base58
3557        let decoded_bytes = bs58::decode(&target_string).into_vec();
3558        assert!(decoded_bytes.is_ok(), "String should be valid base58");
3559
3560        let bytes = decoded_bytes.unwrap();
3561        assert!(bytes.len() <= 32, "Decoded bytes should fit in 32 bytes");
3562
3563        // Test that we can create a hash from these bytes
3564        let mut blockhash_bytes = [0u8; 32];
3565        blockhash_bytes[..bytes.len().min(32)].copy_from_slice(&bytes[..bytes.len().min(32)]);
3566        let hash = Hash::new_from_array(blockhash_bytes);
3567
3568        // Verify the hash can be converted back to string
3569        let hash_str = hash.to_string();
3570        assert!(!hash_str.is_empty());
3571        println!("Generated hash: {}", hash_str);
3572    }
3573
3574    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3575    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3576    #[test_case(TestType::no_db(); "with no db")]
3577    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3578    fn test_blockhash_consistency_across_calls(test_type: TestType) {
3579        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3580
3581        // Set a specific chain tip
3582        svm.chain_tip = BlockIdentifier::new(123, "initial_hash");
3583
3584        // Generate multiple blockhashes and verify they're consistent
3585        let mut previous_hash: Option<BlockIdentifier> = None;
3586        for i in 0..5 {
3587            let new_blockhash = svm.new_blockhash();
3588            println!(
3589                "Call {}: index={}, hash={}",
3590                i, new_blockhash.index, new_blockhash.hash
3591            );
3592
3593            if let Some(prev) = previous_hash {
3594                // Each call should increment the index
3595                assert_eq!(new_blockhash.index, prev.index + 1);
3596                // But the hash should be different (since index changed)
3597                assert_ne!(new_blockhash.hash, prev.hash);
3598            } else {
3599                // First call should increment from the initial chain tip
3600                assert_eq!(new_blockhash.index, svm.chain_tip.index + 1);
3601            }
3602
3603            previous_hash = Some(new_blockhash.clone());
3604            // Update the chain tip for the next iteration
3605            svm.chain_tip = new_blockhash;
3606        }
3607    }
3608
3609    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3610    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3611    #[test_case(TestType::no_db(); "with no db")]
3612    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3613    fn test_token_account_indexing(test_type: TestType) {
3614        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3615
3616        let owner = Pubkey::new_unique();
3617        let delegate = Pubkey::new_unique();
3618        let mint = Pubkey::new_unique();
3619        let token_account_pubkey = Pubkey::new_unique();
3620
3621        // create a token account with delegate
3622        let mut token_account_data = [0u8; TokenAccount::LEN];
3623        let token_account = TokenAccount {
3624            mint,
3625            owner,
3626            amount: 1000,
3627            delegate: COption::Some(delegate),
3628            state: AccountState::Initialized,
3629            is_native: COption::None,
3630            delegated_amount: 500,
3631            close_authority: COption::None,
3632        };
3633        token_account.pack_into_slice(&mut token_account_data);
3634
3635        let account = Account {
3636            lamports: 1000000,
3637            data: token_account_data.to_vec(),
3638            owner: spl_token_interface::id(),
3639            executable: false,
3640            rent_epoch: 0,
3641        };
3642
3643        svm.set_account(&token_account_pubkey, account).unwrap();
3644
3645        // test all indexes were created correctly
3646        assert_eq!(svm.token_accounts.keys().unwrap().len(), 1);
3647
3648        // test owner index
3649        let owner_accounts = svm.get_parsed_token_accounts_by_owner(&owner);
3650        assert_eq!(owner_accounts.len(), 1);
3651        assert_eq!(owner_accounts[0].0, token_account_pubkey);
3652
3653        // test delegate index
3654        let delegate_accounts = svm.get_token_accounts_by_delegate(&delegate);
3655        assert_eq!(delegate_accounts.len(), 1);
3656        assert_eq!(delegate_accounts[0].0, token_account_pubkey);
3657
3658        // test mint index
3659        let mint_accounts = svm.get_token_accounts_by_mint(&mint);
3660        assert_eq!(mint_accounts.len(), 1);
3661        assert_eq!(mint_accounts[0].0, token_account_pubkey);
3662    }
3663
3664    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3665    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3666    #[test_case(TestType::no_db(); "with no db")]
3667    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3668    fn test_account_update_removes_old_indexes(test_type: TestType) {
3669        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3670
3671        let owner = Pubkey::new_unique();
3672        let old_delegate = Pubkey::new_unique();
3673        let new_delegate = Pubkey::new_unique();
3674        let mint = Pubkey::new_unique();
3675        let token_account_pubkey = Pubkey::new_unique();
3676
3677        //  reate initial token account with old delegate
3678        let mut token_account_data = [0u8; TokenAccount::LEN];
3679        let token_account = TokenAccount {
3680            mint,
3681            owner,
3682            amount: 1000,
3683            delegate: COption::Some(old_delegate),
3684            state: AccountState::Initialized,
3685            is_native: COption::None,
3686            delegated_amount: 500,
3687            close_authority: COption::None,
3688        };
3689        token_account.pack_into_slice(&mut token_account_data);
3690
3691        let account = Account {
3692            lamports: 1000000,
3693            data: token_account_data.to_vec(),
3694            owner: spl_token_interface::id(),
3695            executable: false,
3696            rent_epoch: 0,
3697        };
3698
3699        // insert initial account
3700        svm.set_account(&token_account_pubkey, account).unwrap();
3701
3702        // verify old delegate has the account
3703        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 1);
3704        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 0);
3705
3706        // update with new delegate
3707        let updated_token_account = TokenAccount {
3708            mint,
3709            owner,
3710            amount: 1000,
3711            delegate: COption::Some(new_delegate),
3712            state: AccountState::Initialized,
3713            is_native: COption::None,
3714            delegated_amount: 500,
3715            close_authority: COption::None,
3716        };
3717        updated_token_account.pack_into_slice(&mut token_account_data);
3718
3719        let updated_account = Account {
3720            lamports: 1000000,
3721            data: token_account_data.to_vec(),
3722            owner: spl_token_interface::id(),
3723            executable: false,
3724            rent_epoch: 0,
3725        };
3726
3727        // update the account
3728        svm.set_account(&token_account_pubkey, updated_account)
3729            .unwrap();
3730
3731        // verify indexes were updated correctly
3732        assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 0);
3733        assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 1);
3734        assert_eq!(svm.get_parsed_token_accounts_by_owner(&owner).len(), 1);
3735    }
3736
3737    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3738    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3739    #[test_case(TestType::no_db(); "with no db")]
3740    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3741    fn test_non_token_accounts_not_indexed(test_type: TestType) {
3742        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
3743
3744        let system_account_pubkey = Pubkey::new_unique();
3745        let account = Account {
3746            lamports: 1000000,
3747            data: vec![],
3748            owner: solana_system_interface::program::id(), // system program, not token program
3749            executable: false,
3750            rent_epoch: 0,
3751        };
3752
3753        svm.set_account(&system_account_pubkey, account).unwrap();
3754
3755        // should be in general registry but not token indexes
3756        assert_eq!(svm.token_accounts.keys().unwrap().len(), 0);
3757        assert_eq!(svm.token_accounts_by_owner.keys().unwrap().len(), 0);
3758        assert_eq!(svm.token_accounts_by_delegate.keys().unwrap().len(), 0);
3759        assert_eq!(svm.token_accounts_by_mint.keys().unwrap().len(), 0);
3760    }
3761
3762    fn expect_account_update_event(
3763        events_rx: &Receiver<SimnetEvent>,
3764        svm: &SurfnetSvm,
3765        pubkey: &Pubkey,
3766        expected_account: &Account,
3767    ) -> bool {
3768        match events_rx.recv() {
3769            Ok(event) => match event {
3770                SimnetEvent::AccountUpdate(_, account_pubkey) => {
3771                    assert_eq!(pubkey, &account_pubkey);
3772                    assert_eq!(
3773                        svm.get_account(&pubkey).unwrap().as_ref(),
3774                        Some(expected_account)
3775                    );
3776                    true
3777                }
3778                event => {
3779                    println!("unexpected simnet event: {:?}", event);
3780                    false
3781                }
3782            },
3783            Err(_) => false,
3784        }
3785    }
3786
3787    fn _expect_error_event(events_rx: &Receiver<SimnetEvent>, expected_error: &str) -> bool {
3788        match events_rx.recv() {
3789            Ok(event) => match event {
3790                SimnetEvent::ErrorLog(_, err) => {
3791                    assert_eq!(err, expected_error);
3792
3793                    true
3794                }
3795                event => {
3796                    println!("unexpected simnet event: {:?}", event);
3797                    false
3798                }
3799            },
3800            Err(_) => false,
3801        }
3802    }
3803
3804    fn create_program_accounts() -> (Pubkey, Account, Pubkey, Account) {
3805        let program_pubkey = Pubkey::new_unique();
3806        let program_data_address = get_program_data_address(&program_pubkey);
3807        let program_account = Account {
3808            lamports: 1000000000000,
3809            data: bincode::serialize(
3810                &solana_loader_v3_interface::state::UpgradeableLoaderState::Program {
3811                    programdata_address: program_data_address,
3812                },
3813            )
3814            .unwrap(),
3815            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3816            executable: true,
3817            rent_epoch: 10000000000000,
3818        };
3819
3820        let mut bin = include_bytes!("../tests/assets/metaplex_program.bin").to_vec();
3821        let mut data = bincode::serialize(
3822            &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3823                slot: 0,
3824                upgrade_authority_address: Some(Pubkey::new_unique()),
3825            },
3826        )
3827        .unwrap();
3828        data.append(&mut bin); // push our binary after the state data
3829        let program_data_account = Account {
3830            lamports: 10000000000000,
3831            data,
3832            owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3833            executable: false,
3834            rent_epoch: 10000000000000,
3835        };
3836        (
3837            program_pubkey,
3838            program_account,
3839            program_data_address,
3840            program_data_account,
3841        )
3842    }
3843
3844    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
3845    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
3846    #[test_case(TestType::no_db(); "with no db")]
3847    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
3848    fn test_inserting_account_updates(test_type: TestType) {
3849        let (mut svm, events_rx, _geyser_rx) = test_type.initialize_svm();
3850
3851        let pubkey = Pubkey::new_unique();
3852        let account = Account {
3853            lamports: 1000,
3854            data: vec![1, 2, 3],
3855            owner: Pubkey::new_unique(),
3856            executable: false,
3857            rent_epoch: 0,
3858        };
3859
3860        // GetAccountResult::None should be a noop when writing account updates
3861        {
3862            let index_before = svm.get_all_accounts().unwrap();
3863            let empty_update = GetAccountResult::None(pubkey);
3864            svm.write_account_update(empty_update);
3865            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
3866        }
3867
3868        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to false should be a noop
3869        {
3870            let index_before = svm.get_all_accounts().unwrap();
3871            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), false);
3872            svm.write_account_update(found_update);
3873            assert_eq!(svm.get_all_accounts().unwrap(), index_before);
3874        }
3875
3876        // GetAccountResult::FoundAccount with `DoUpdateSvm` flag to true should update the account
3877        {
3878            let index_before = svm.get_all_accounts().unwrap();
3879            let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), true);
3880            svm.write_account_update(found_update);
3881            assert_eq!(
3882                svm.get_all_accounts().unwrap().len(),
3883                index_before.len() + 1
3884            );
3885            if !expect_account_update_event(&events_rx, &svm, &pubkey, &account) {
3886                panic!(
3887                    "Expected account update event not received after GetAccountResult::FoundAccount update"
3888                );
3889            }
3890        }
3891
3892        // GetAccountResult::FoundProgramAccount with no program account inserts a default programdata account
3893        {
3894            let (program_address, program_account, program_data_address, _) =
3895                create_program_accounts();
3896
3897            let mut data = bincode::serialize(
3898                &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
3899                    slot: svm.get_latest_absolute_slot(),
3900                    upgrade_authority_address: Some(system_program::id()),
3901                },
3902            )
3903            .unwrap();
3904
3905            let mut bin = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
3906            data.append(&mut bin); // push our binary after the state data
3907            let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len());
3908            let default_program_data_account = Account {
3909                lamports,
3910                data,
3911                owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
3912                executable: false,
3913                rent_epoch: 0,
3914            };
3915
3916            let index_before = svm.get_all_accounts().unwrap();
3917            let found_program_account_update = GetAccountResult::FoundProgramAccount(
3918                (program_address, program_account.clone()),
3919                (program_data_address, None),
3920            );
3921            svm.write_account_update(found_program_account_update);
3922
3923            if !expect_account_update_event(
3924                &events_rx,
3925                &svm,
3926                &program_data_address,
3927                &default_program_data_account,
3928            ) {
3929                panic!(
3930                    "Expected account update event not received after inserting default program data account"
3931                );
3932            }
3933
3934            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
3935                panic!(
3936                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
3937                );
3938            }
3939            assert_eq!(
3940                svm.get_all_accounts().unwrap().len(),
3941                index_before.len() + 2
3942            );
3943        }
3944
3945        // GetAccountResult::FoundProgramAccount with program account + program data account inserts two accounts
3946        {
3947            let (program_address, program_account, program_data_address, program_data_account) =
3948                create_program_accounts();
3949
3950            let index_before = svm.get_all_accounts().unwrap();
3951            let found_program_account_update = GetAccountResult::FoundProgramAccount(
3952                (program_address, program_account.clone()),
3953                (program_data_address, Some(program_data_account.clone())),
3954            );
3955            svm.write_account_update(found_program_account_update);
3956            assert_eq!(
3957                svm.get_all_accounts().unwrap().len(),
3958                index_before.len() + 2
3959            );
3960            if !expect_account_update_event(
3961                &events_rx,
3962                &svm,
3963                &program_data_address,
3964                &program_data_account,
3965            ) {
3966                panic!(
3967                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program data pubkey"
3968                );
3969            }
3970
3971            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
3972                panic!(
3973                    "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
3974                );
3975            }
3976        }
3977
3978        // If we insert the program data account ahead of time, then have a GetAccountResult::FoundProgramAccount with just the program data account,
3979        // we should get one insert
3980        {
3981            let (program_address, program_account, program_data_address, program_data_account) =
3982                create_program_accounts();
3983
3984            let index_before = svm.get_all_accounts().unwrap();
3985            let found_update = GetAccountResult::FoundAccount(
3986                program_data_address,
3987                program_data_account.clone(),
3988                true,
3989            );
3990            svm.write_account_update(found_update);
3991            assert_eq!(
3992                svm.get_all_accounts().unwrap().len(),
3993                index_before.len() + 1
3994            );
3995            if !expect_account_update_event(
3996                &events_rx,
3997                &svm,
3998                &program_data_address,
3999                &program_data_account,
4000            ) {
4001                panic!(
4002                    "Expected account update event not received after GetAccountResult::FoundAccount update"
4003                );
4004            }
4005
4006            let index_before = svm.get_all_accounts().unwrap();
4007            let program_account_found_update = GetAccountResult::FoundProgramAccount(
4008                (program_address, program_account.clone()),
4009                (program_data_address, None),
4010            );
4011            svm.write_account_update(program_account_found_update);
4012            assert_eq!(
4013                svm.get_all_accounts().unwrap().len(),
4014                index_before.len() + 1
4015            );
4016            if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4017                panic!(
4018                    "Expected account update event not received after GetAccountResult::FoundAccount update"
4019                );
4020            }
4021        }
4022    }
4023
4024    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4025    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4026    #[test_case(TestType::no_db(); "with no db")]
4027    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4028    fn test_encode_ui_account(test_type: TestType) {
4029        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4030
4031        let idl_v1: Idl =
4032            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v1.json").to_vec())
4033                .unwrap();
4034
4035        svm.register_idl(idl_v1.clone(), Some(0)).unwrap();
4036
4037        let account_pubkey = Pubkey::new_unique();
4038
4039        #[derive(borsh::BorshSerialize)]
4040        pub struct CustomAccount {
4041            pub my_custom_data: u64,
4042            pub another_field: String,
4043            pub bool: bool,
4044            pub pubkey: Pubkey,
4045        }
4046
4047        // Account data not matching IDL schema should use default encoding
4048        {
4049            let account_data = vec![0; 100];
4050            let base64_data = general_purpose::STANDARD.encode(&account_data);
4051            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4052            let account = Account {
4053                lamports: 1000,
4054                data: account_data,
4055                owner: idl_v1.address.parse().unwrap(),
4056                executable: false,
4057                rent_epoch: 0,
4058            };
4059
4060            let ui_account = svm.encode_ui_account(
4061                &account_pubkey,
4062                &account,
4063                UiAccountEncoding::JsonParsed,
4064                None,
4065                None,
4066            );
4067            let expected_account = UiAccount {
4068                lamports: 1000,
4069                data: expected_data,
4070                owner: idl_v1.address.clone(),
4071                executable: false,
4072                rent_epoch: 0,
4073                space: Some(account.data.len() as u64),
4074            };
4075            assert_eq!(ui_account, expected_account);
4076        }
4077
4078        // valid account data matching IDL schema should be parsed
4079        {
4080            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4081            let pubkey = Pubkey::new_unique();
4082            CustomAccount {
4083                my_custom_data: 42,
4084                another_field: "test".to_string(),
4085                bool: true,
4086                pubkey,
4087            }
4088            .serialize(&mut account_data)
4089            .unwrap();
4090
4091            let account = Account {
4092                lamports: 1000,
4093                data: account_data,
4094                owner: idl_v1.address.parse().unwrap(),
4095                executable: false,
4096                rent_epoch: 0,
4097            };
4098
4099            let ui_account = svm.encode_ui_account(
4100                &account_pubkey,
4101                &account,
4102                UiAccountEncoding::JsonParsed,
4103                None,
4104                None,
4105            );
4106            let expected_account = UiAccount {
4107                lamports: 1000,
4108                data: UiAccountData::Json(ParsedAccount {
4109                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4110                    parsed: serde_json::json!({
4111                        "my_custom_data": 42,
4112                        "another_field": "test",
4113                        "bool": true,
4114                        "pubkey": pubkey.to_string(),
4115                    }),
4116                    space: account.data.len() as u64,
4117                }),
4118                owner: idl_v1.address.clone(),
4119                executable: false,
4120                rent_epoch: 0,
4121                space: Some(account.data.len() as u64),
4122            };
4123            assert_eq!(ui_account, expected_account);
4124        }
4125
4126        let idl_v2: Idl =
4127            serde_json::from_slice(&include_bytes!("../tests/assets/idl_v2.json").to_vec())
4128                .unwrap();
4129
4130        svm.register_idl(idl_v2.clone(), Some(100)).unwrap();
4131
4132        // even though we have a new IDL that is more recent, we should be able to match with the old IDL
4133        {
4134            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4135            let pubkey = Pubkey::new_unique();
4136            CustomAccount {
4137                my_custom_data: 42,
4138                another_field: "test".to_string(),
4139                bool: true,
4140                pubkey,
4141            }
4142            .serialize(&mut account_data)
4143            .unwrap();
4144
4145            let account = Account {
4146                lamports: 1000,
4147                data: account_data,
4148                owner: idl_v1.address.parse().unwrap(),
4149                executable: false,
4150                rent_epoch: 0,
4151            };
4152
4153            let ui_account = svm.encode_ui_account(
4154                &account_pubkey,
4155                &account,
4156                UiAccountEncoding::JsonParsed,
4157                None,
4158                None,
4159            );
4160            let expected_account = UiAccount {
4161                lamports: 1000,
4162                data: UiAccountData::Json(ParsedAccount {
4163                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4164                    parsed: serde_json::json!({
4165                        "my_custom_data": 42,
4166                        "another_field": "test",
4167                        "bool": true,
4168                        "pubkey": pubkey.to_string(),
4169                    }),
4170                    space: account.data.len() as u64,
4171                }),
4172                owner: idl_v1.address.clone(),
4173                executable: false,
4174                rent_epoch: 0,
4175                space: Some(account.data.len() as u64),
4176            };
4177            assert_eq!(ui_account, expected_account);
4178        }
4179
4180        // valid account data matching IDL v2 schema should be parsed, if svm slot reaches IDL registration slot
4181        {
4182            // use the v2 shape of the custom account
4183            #[derive(borsh::BorshSerialize)]
4184            pub struct CustomAccount {
4185                pub my_custom_data: u64,
4186                pub another_field: String,
4187                pub pubkey: Pubkey,
4188            }
4189            let mut account_data = idl_v1.accounts[0].discriminator.clone();
4190            let pubkey = Pubkey::new_unique();
4191            CustomAccount {
4192                my_custom_data: 42,
4193                another_field: "test".to_string(),
4194                pubkey,
4195            }
4196            .serialize(&mut account_data)
4197            .unwrap();
4198
4199            let account = Account {
4200                lamports: 1000,
4201                data: account_data.clone(),
4202                owner: idl_v1.address.parse().unwrap(),
4203                executable: false,
4204                rent_epoch: 0,
4205            };
4206
4207            let ui_account = svm.encode_ui_account(
4208                &account_pubkey,
4209                &account,
4210                UiAccountEncoding::JsonParsed,
4211                None,
4212                None,
4213            );
4214            let base64_data = general_purpose::STANDARD.encode(&account_data);
4215            let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4216            let expected_account = UiAccount {
4217                lamports: 1000,
4218                data: expected_data,
4219                owner: idl_v1.address.clone(),
4220                executable: false,
4221                rent_epoch: 0,
4222                space: Some(account.data.len() as u64),
4223            };
4224            assert_eq!(ui_account, expected_account);
4225
4226            svm.latest_epoch_info.absolute_slot = 100; // simulate reaching the slot where IDL v2 was registered
4227
4228            let ui_account = svm.encode_ui_account(
4229                &account_pubkey,
4230                &account,
4231                UiAccountEncoding::JsonParsed,
4232                None,
4233                None,
4234            );
4235            let expected_account = UiAccount {
4236                lamports: 1000,
4237                data: UiAccountData::Json(ParsedAccount {
4238                    program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4239                    parsed: serde_json::json!({
4240                        "my_custom_data": 42,
4241                        "another_field": "test",
4242                        "pubkey": pubkey.to_string(),
4243                    }),
4244                    space: account.data.len() as u64,
4245                }),
4246                owner: idl_v1.address.clone(),
4247                executable: false,
4248                rent_epoch: 0,
4249                space: Some(account.data.len() as u64),
4250            };
4251            assert_eq!(ui_account, expected_account);
4252        }
4253    }
4254
4255    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4256    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4257    #[test_case(TestType::no_db(); "with no db")]
4258    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4259    fn test_profiling_map_capacity_default(test_type: TestType) {
4260        let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4261        assert_eq!(svm.max_profiles, DEFAULT_PROFILING_MAP_CAPACITY);
4262    }
4263
4264    #[test]
4265    fn test_default_uses_no_db_storage() {
4266        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4267        assert!(svm.inner.db.is_none());
4268    }
4269
4270    #[cfg(feature = "sqlite")]
4271    #[test]
4272    fn test_new_with_db_uses_sqlite_storage() {
4273        let (svm, _events_rx, _geyser_rx) =
4274            SurfnetSvm::new_with_db(Some(":memory:"), SurfnetSvmConfig::default()).unwrap();
4275        assert!(svm.inner.db.is_some());
4276    }
4277
4278    #[test]
4279    fn test_constructor_applies_startup_config() {
4280        let config = SurfnetSvmConfig {
4281            surfnet_id: "constructor-test".to_string(),
4282            feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4283            slot_time: 123,
4284            instruction_profiling_enabled: false,
4285            max_profiles: 17,
4286            log_bytes_limit: None,
4287            skip_blockhash_check: true,
4288        };
4289        let (svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
4290
4291        assert_eq!(svm.slot_time, 123);
4292        assert!(!svm.instruction_profiling_enabled);
4293        assert_eq!(svm.max_profiles, 17);
4294        assert_eq!(
4295            svm.latest_epoch_info.absolute_slot,
4296            FINALIZATION_SLOT_THRESHOLD
4297        );
4298        assert_eq!(svm.genesis_slot, FINALIZATION_SLOT_THRESHOLD);
4299        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4300
4301        let epoch_schedule = svm.inner.get_sysvar::<EpochSchedule>();
4302        assert!(!epoch_schedule.warmup);
4303
4304        let registry = TemplateRegistry::new();
4305        for (_, template) in registry.templates {
4306            let program_id = template.idl.address.clone();
4307            assert!(svm.registered_idls.get(&program_id).unwrap().is_some());
4308        }
4309        assert!(svm.skip_blockhash_check);
4310    }
4311
4312    #[test]
4313    fn test_initialize_only_updates_remote_state() {
4314        let config = SurfnetSvmConfig {
4315            surfnet_id: "remote-init-test".to_string(),
4316            feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4317            slot_time: 321,
4318            instruction_profiling_enabled: false,
4319            max_profiles: 23,
4320            log_bytes_limit: None,
4321            skip_blockhash_check: false,
4322        };
4323        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
4324        let epoch_info = EpochInfo {
4325            epoch: 7,
4326            slot_index: 4,
4327            slots_in_epoch: crate::surfnet::SLOTS_PER_EPOCH,
4328            absolute_slot: 777,
4329            block_height: 777,
4330            transaction_count: None,
4331        };
4332
4333        svm.initialize(epoch_info.clone(), EpochSchedule::without_warmup());
4334
4335        assert_eq!(svm.slot_time, 321);
4336        assert!(!svm.instruction_profiling_enabled);
4337        assert_eq!(svm.max_profiles, 23);
4338        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4339        assert_eq!(svm.latest_epoch_info, epoch_info);
4340        assert_eq!(svm.genesis_slot, 777);
4341    }
4342
4343    #[test]
4344    fn test_clone_for_profiling_preserves_skip_blockhash_check() {
4345        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4346        svm.skip_blockhash_check = true;
4347
4348        let profiling_clone = svm.clone_for_profiling();
4349        assert!(profiling_clone.skip_blockhash_check);
4350    }
4351
4352    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4353    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4354    #[test_case(TestType::no_db(); "with no db")]
4355    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4356    fn test_send_transaction_rejects_invalid_blockhash_by_default(test_type: TestType) {
4357        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4358        let payer = Keypair::new();
4359        let recipient = Pubkey::new_unique();
4360        let lamports = 1_000_000_000;
4361        let invalid_blockhash = Hash::new_unique();
4362
4363        svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
4364        assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
4365
4366        let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
4367        let err = svm.send_transaction(tx, false, false).unwrap_err().err;
4368
4369        assert_eq!(err, TransactionError::BlockhashNotFound);
4370    }
4371
4372    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4373    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4374    #[test_case(TestType::no_db(); "with no db")]
4375    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4376    fn test_skip_blockhash_check_bypasses_send_simulate_and_estimate(test_type: TestType) {
4377        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4378        let payer = Keypair::new();
4379        let recipient = Pubkey::new_unique();
4380        let lamports = 1_000_000_000;
4381        let invalid_blockhash = Hash::new_unique();
4382
4383        svm.skip_blockhash_check = true;
4384        svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
4385        assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
4386
4387        let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
4388
4389        let estimate = svm.estimate_compute_units(&tx);
4390        assert!(
4391            estimate.success,
4392            "estimate should succeed when skip_blockhash_check is enabled: {:?}",
4393            estimate.error_message
4394        );
4395
4396        let simulation = svm.simulate_transaction(tx.clone(), false);
4397        assert!(
4398            simulation.is_ok(),
4399            "simulation should succeed when skip_blockhash_check is enabled: {:?}",
4400            simulation.err()
4401        );
4402
4403        let send_result = svm.send_transaction(tx, false, false);
4404        assert!(
4405            send_result.is_ok(),
4406            "send should succeed when skip_blockhash_check is enabled: {:?}",
4407            send_result.err().map(|err| err.err)
4408        );
4409    }
4410
4411    // Feature configuration tests
4412
4413    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4414    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4415    #[test_case(TestType::no_db(); "with no db")]
4416    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4417    fn test_apply_feature_config_empty(test_type: TestType) {
4418        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4419        let config = SvmFeatureConfig::new();
4420
4421        // Should not panic with empty config
4422        svm.apply_feature_config(&config);
4423    }
4424
4425    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4426    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4427    #[test_case(TestType::no_db(); "with no db")]
4428    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4429    fn test_apply_feature_config_enable_feature(test_type: TestType) {
4430        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4431
4432        // Disable a feature first
4433        let feature_id = enable_loader_v4::id();
4434        svm.feature_set.deactivate(&feature_id);
4435        assert!(!svm.feature_set.is_active(&feature_id));
4436
4437        // Now enable it via config
4438        let config = SvmFeatureConfig::new().enable(enable_loader_v4::id());
4439        svm.apply_feature_config(&config);
4440
4441        assert!(svm.feature_set.is_active(&feature_id));
4442    }
4443
4444    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4445    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4446    #[test_case(TestType::no_db(); "with no db")]
4447    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4448    fn test_apply_feature_config_disable_feature(test_type: TestType) {
4449        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4450
4451        // Feature should be inactive by default (all_disabled)
4452        let feature_id = disable_fees_sysvar::id();
4453        assert!(!svm.feature_set.is_active(&feature_id));
4454
4455        // Now applying feature config defaults to all enabled
4456        svm.apply_feature_config(&SvmFeatureConfig::new());
4457        assert!(svm.feature_set.is_active(&feature_id));
4458
4459        // But we can explicitly disable via config
4460        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
4461        svm.apply_feature_config(&config);
4462        assert!(!svm.feature_set.is_active(&feature_id));
4463    }
4464
4465    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4466    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4467    #[test_case(TestType::no_db(); "with no db")]
4468    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4469    fn test_apply_feature_config_mainnet_defaults(test_type: TestType) {
4470        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4471        let config = SvmFeatureConfig::default_mainnet_features();
4472
4473        svm.apply_feature_config(&config);
4474
4475        // Features disabled on mainnet should now be inactive
4476        assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
4477        assert!(
4478            !svm.feature_set
4479                .is_active(&enable_extend_program_checked::id())
4480        );
4481        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4482        assert!(
4483            !svm.feature_set
4484                .is_active(&enable_sbpf_v1_deployment_and_execution::id())
4485        );
4486        assert!(
4487            !svm.feature_set
4488                .is_active(&formalize_loaded_transaction_data_size::id())
4489        );
4490        assert!(
4491            !svm.feature_set
4492                .is_active(&move_precompile_verification_to_svm::id())
4493        );
4494
4495        // Features active on mainnet should still be active
4496        assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
4497        assert!(svm.feature_set.is_active(&curve25519_syscall_enabled::id()));
4498        assert!(
4499            svm.feature_set
4500                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
4501        );
4502        assert!(
4503            svm.feature_set
4504                .is_active(&enable_sbpf_v3_deployment_and_execution::id())
4505        );
4506        assert!(
4507            svm.feature_set
4508                .is_active(&raise_cpi_nesting_limit_to_8::id())
4509        );
4510    }
4511
4512    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4513    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4514    #[test_case(TestType::no_db(); "with no db")]
4515    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4516    fn test_apply_feature_config_mainnet_with_override(test_type: TestType) {
4517        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4518
4519        // Start with mainnet defaults, but enable loader v4
4520        let config = SvmFeatureConfig::default_mainnet_features().enable(enable_loader_v4::id());
4521
4522        svm.apply_feature_config(&config);
4523
4524        // Loader v4 should be enabled despite mainnet defaults
4525        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4526
4527        // Other mainnet-disabled features should still be disabled
4528        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4529        assert!(
4530            !svm.feature_set
4531                .is_active(&enable_extend_program_checked::id())
4532        );
4533    }
4534
4535    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4536    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4537    #[test_case(TestType::no_db(); "with no db")]
4538    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4539    fn test_apply_feature_config_multiple_changes(test_type: TestType) {
4540        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4541
4542        let config = SvmFeatureConfig::new()
4543            .enable(enable_loader_v4::id())
4544            .enable(enable_sbpf_v2_deployment_and_execution::id())
4545            .disable(disable_fees_sysvar::id())
4546            .disable(blake3_syscall_enabled::id());
4547
4548        svm.apply_feature_config(&config);
4549
4550        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4551        assert!(
4552            svm.feature_set
4553                .is_active(&enable_sbpf_v2_deployment_and_execution::id())
4554        );
4555        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4556        assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
4557    }
4558
4559    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4560    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4561    #[test_case(TestType::no_db(); "with no db")]
4562    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4563    fn test_apply_feature_config_preserves_native_mint(test_type: TestType) {
4564        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4565
4566        // Native mint should exist before
4567        assert!(
4568            svm.inner
4569                .get_account(&spl_token_interface::native_mint::ID)
4570                .unwrap()
4571                .is_some()
4572        );
4573
4574        let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
4575        svm.apply_feature_config(&config);
4576
4577        // Native mint should still exist after (re-added in apply_feature_config)
4578        assert!(
4579            svm.inner
4580                .get_account(&spl_token_interface::native_mint::ID)
4581                .unwrap()
4582                .is_some()
4583        );
4584    }
4585
4586    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4587    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4588    #[test_case(TestType::no_db(); "with no db")]
4589    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4590    fn test_apply_feature_config_idempotent(test_type: TestType) {
4591        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4592
4593        let config = SvmFeatureConfig::new()
4594            .enable(enable_loader_v4::id())
4595            .disable(disable_fees_sysvar::id());
4596
4597        // Apply twice
4598        svm.apply_feature_config(&config);
4599        svm.apply_feature_config(&config);
4600
4601        // State should be the same
4602        assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
4603        assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4604    }
4605
4606    // Garbage collection tests
4607
4608    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4609    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4610    #[test_case(TestType::no_db(); "with no db")]
4611    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4612    fn test_garbage_collected_account_tracking(test_type: TestType) {
4613        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4614
4615        let owner = Pubkey::new_unique();
4616        let account_pubkey = Pubkey::new_unique();
4617
4618        let account = Account {
4619            lamports: 1000000,
4620            data: vec![1, 2, 3, 4, 5],
4621            owner,
4622            executable: false,
4623            rent_epoch: 0,
4624        };
4625
4626        svm.set_account(&account_pubkey, account.clone()).unwrap();
4627
4628        assert!(svm.get_account(&account_pubkey).unwrap().is_some());
4629        assert!(
4630            !svm.offline_accounts
4631                .contains_key(&account_pubkey.to_string())
4632                .unwrap()
4633        );
4634        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 1);
4635
4636        let empty_account = Account::default();
4637        svm.update_account_registries(&account_pubkey, &empty_account)
4638            .unwrap();
4639
4640        assert!(
4641            svm.offline_accounts
4642                .contains_key(&account_pubkey.to_string())
4643                .unwrap()
4644        );
4645
4646        assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 0);
4647
4648        let owned_accounts = svm.get_account_owned_by(&owner).unwrap();
4649        assert!(!owned_accounts.iter().any(|(pk, _)| *pk == account_pubkey));
4650    }
4651
4652    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4653    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4654    #[test_case(TestType::no_db(); "with no db")]
4655    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4656    fn test_garbage_collected_token_account_cleanup(test_type: TestType) {
4657        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4658
4659        let token_owner = Pubkey::new_unique();
4660        let delegate = Pubkey::new_unique();
4661        let mint = Pubkey::new_unique();
4662        let token_account_pubkey = Pubkey::new_unique();
4663
4664        let mut token_account_data = [0u8; TokenAccount::LEN];
4665        let token_account = TokenAccount {
4666            mint,
4667            owner: token_owner,
4668            amount: 1000,
4669            delegate: COption::Some(delegate),
4670            state: AccountState::Initialized,
4671            is_native: COption::None,
4672            delegated_amount: 500,
4673            close_authority: COption::None,
4674        };
4675        token_account.pack_into_slice(&mut token_account_data);
4676
4677        let account = Account {
4678            lamports: 2000000,
4679            data: token_account_data.to_vec(),
4680            owner: spl_token_interface::id(),
4681            executable: false,
4682            rent_epoch: 0,
4683        };
4684
4685        svm.set_account(&token_account_pubkey, account).unwrap();
4686
4687        assert_eq!(
4688            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
4689            1
4690        );
4691        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 1);
4692        assert!(
4693            !svm.offline_accounts
4694                .contains_key(&token_account_pubkey.to_string())
4695                .unwrap()
4696        );
4697
4698        let empty_account = Account::default();
4699        svm.update_account_registries(&token_account_pubkey, &empty_account)
4700            .unwrap();
4701
4702        assert!(
4703            svm.offline_accounts
4704                .contains_key(&token_account_pubkey.to_string())
4705                .unwrap()
4706        );
4707
4708        assert_eq!(
4709            svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
4710            0
4711        );
4712        assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 0);
4713        assert!(
4714            svm.token_accounts
4715                .get(&token_account_pubkey.to_string())
4716                .unwrap()
4717                .is_none()
4718        );
4719    }
4720
4721    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4722    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4723    #[test_case(TestType::no_db(); "with no db")]
4724    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4725    fn test_is_slot_in_valid_range(test_type: TestType) {
4726        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4727
4728        // Set up: genesis_slot = 100, latest absolute slot = 110
4729        svm.genesis_slot = 100;
4730        svm.latest_epoch_info.absolute_slot = 110;
4731
4732        // Test slots within valid range
4733        assert!(
4734            svm.is_slot_in_valid_range(100),
4735            "genesis_slot should be valid"
4736        );
4737        assert!(
4738            svm.is_slot_in_valid_range(105),
4739            "middle slot should be valid"
4740        );
4741        assert!(
4742            svm.is_slot_in_valid_range(110),
4743            "latest slot should be valid"
4744        );
4745
4746        // Test slots outside valid range
4747        assert!(
4748            !svm.is_slot_in_valid_range(99),
4749            "slot before genesis should be invalid"
4750        );
4751        assert!(
4752            !svm.is_slot_in_valid_range(111),
4753            "slot after latest should be invalid"
4754        );
4755        assert!(
4756            !svm.is_slot_in_valid_range(0),
4757            "slot 0 should be invalid when genesis > 0"
4758        );
4759        assert!(
4760            !svm.is_slot_in_valid_range(1000),
4761            "far future slot should be invalid"
4762        );
4763    }
4764
4765    #[test]
4766    fn test_is_slot_in_valid_range_genesis_zero() {
4767        let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4768
4769        // Set up: genesis_slot = 0, latest absolute slot = 50
4770        svm.genesis_slot = 0;
4771        svm.latest_epoch_info.absolute_slot = 50;
4772
4773        // Test boundary conditions with genesis at 0
4774        assert!(
4775            svm.is_slot_in_valid_range(0),
4776            "slot 0 should be valid when genesis = 0"
4777        );
4778        assert!(
4779            svm.is_slot_in_valid_range(25),
4780            "middle slot should be valid"
4781        );
4782        assert!(
4783            svm.is_slot_in_valid_range(50),
4784            "latest slot should be valid"
4785        );
4786        assert!(
4787            !svm.is_slot_in_valid_range(51),
4788            "slot after latest should be invalid"
4789        );
4790    }
4791
4792    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4793    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4794    #[test_case(TestType::no_db(); "with no db")]
4795    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4796    fn test_get_block_or_reconstruct_stored_block(test_type: TestType) {
4797        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4798
4799        // Set up: genesis_slot = 0, latest absolute slot = 100
4800        svm.genesis_slot = 0;
4801        svm.latest_epoch_info.absolute_slot = 100;
4802
4803        // Store a block with transactions
4804        let stored_block = BlockHeader {
4805            hash: "stored_block_hash".to_string(),
4806            previous_blockhash: "prev_hash".to_string(),
4807            parent_slot: 49,
4808            block_time: 1234567890,
4809            block_height: 50,
4810            signatures: vec![Signature::new_unique()],
4811        };
4812        svm.blocks.store(50, stored_block.clone()).unwrap();
4813
4814        // Retrieve the stored block
4815        let result = svm.get_block_or_reconstruct(50).unwrap();
4816        assert!(result.is_some(), "should return stored block");
4817        let block = result.unwrap();
4818        assert_eq!(block.hash, "stored_block_hash");
4819        assert_eq!(block.signatures.len(), 1);
4820    }
4821
4822    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4823    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4824    #[test_case(TestType::no_db(); "with no db")]
4825    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4826    fn test_get_block_or_reconstruct_empty_block(test_type: TestType) {
4827        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4828
4829        // Set up: genesis_slot = 0, latest absolute slot = 100
4830        svm.genesis_slot = 0;
4831        svm.latest_epoch_info.absolute_slot = 100;
4832        svm.genesis_updated_at = 1000000; // 1 second in ms
4833        svm.slot_time = 400; // 400ms per slot
4834
4835        // Request a slot that wasn't stored (no block stored at slot 50)
4836        let result = svm.get_block_or_reconstruct(50).unwrap();
4837        assert!(
4838            result.is_some(),
4839            "should reconstruct empty block for valid slot"
4840        );
4841
4842        let block = result.unwrap();
4843        // Verify it's a reconstructed empty block
4844        assert!(
4845            block.signatures.is_empty(),
4846            "reconstructed block should have no signatures"
4847        );
4848        assert_eq!(block.block_height, 50);
4849        assert_eq!(block.parent_slot, 49);
4850
4851        // Verify the block time is calculated correctly
4852        // genesis_updated_at (1000000ms) + (50 slots * 400ms) = 1020000ms = 1020 seconds
4853        assert_eq!(block.block_time, 1020);
4854    }
4855
4856    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4857    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4858    #[test_case(TestType::no_db(); "with no db")]
4859    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4860    fn test_get_block_or_reconstruct_out_of_range(test_type: TestType) {
4861        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4862
4863        // Set up: genesis_slot = 100, latest absolute slot = 110
4864        svm.genesis_slot = 100;
4865        svm.latest_epoch_info.absolute_slot = 110;
4866
4867        // Request slot before genesis
4868        let result = svm.get_block_or_reconstruct(50).unwrap();
4869        assert!(
4870            result.is_none(),
4871            "should return None for slot before genesis"
4872        );
4873
4874        // Request slot after latest
4875        let result = svm.get_block_or_reconstruct(200).unwrap();
4876        assert!(result.is_none(), "should return None for slot after latest");
4877    }
4878
4879    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4880    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4881    #[test_case(TestType::no_db(); "with no db")]
4882    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4883    #[allow(deprecated)]
4884    fn test_reconstruct_sysvars_recent_blockhashes(test_type: TestType) {
4885        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
4886
4887        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4888
4889        // Set up: chain_tip.index = 10, genesis_slot = 0
4890        svm.chain_tip = BlockIdentifier::new(10, "test_hash");
4891        svm.genesis_slot = 0;
4892        svm.latest_epoch_info.absolute_slot = 10;
4893
4894        svm.reconstruct_sysvars();
4895
4896        // Verify RecentBlockhashes sysvar
4897        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
4898
4899        // Should have 11 entries (indices 0 through 10)
4900        assert_eq!(recent_blockhashes.len(), 11);
4901
4902        // First entry should be the hash for chain_tip.index (10)
4903        let expected_hash = SyntheticBlockhash::new(10);
4904        assert_eq!(
4905            recent_blockhashes.first().unwrap().blockhash,
4906            *expected_hash.hash(),
4907            "First blockhash should match SyntheticBlockhash for chain_tip.index"
4908        );
4909
4910        // Last entry should be the hash for index 0
4911        let expected_last_hash = SyntheticBlockhash::new(0);
4912        assert_eq!(
4913            recent_blockhashes.last().unwrap().blockhash,
4914            *expected_last_hash.hash(),
4915            "Last blockhash should match SyntheticBlockhash for index 0"
4916        );
4917    }
4918
4919    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4920    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4921    #[test_case(TestType::no_db(); "with no db")]
4922    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4923    #[allow(deprecated)]
4924    fn test_reconstruct_sysvars_slot_hashes(test_type: TestType) {
4925        use solana_slot_hashes::SlotHashes;
4926
4927        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4928
4929        // Set up: chain_tip.index = 5, genesis_slot = 100 (absolute slot = 105)
4930        svm.chain_tip = BlockIdentifier::new(5, "test_hash");
4931        svm.genesis_slot = 100;
4932        svm.latest_epoch_info.absolute_slot = 105;
4933
4934        svm.reconstruct_sysvars();
4935
4936        // Verify SlotHashes sysvar
4937        let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
4938
4939        // Should have 6 entries (indices 0 through 5, mapped to slots 100 through 105)
4940        assert_eq!(slot_hashes.len(), 6);
4941
4942        // Check that slot 105 maps to hash for index 5
4943        let expected_hash_105 = SyntheticBlockhash::new(5);
4944        let hash_for_105 = slot_hashes.get(&105);
4945        assert!(hash_for_105.is_some(), "SlotHashes should contain slot 105");
4946        assert_eq!(
4947            hash_for_105.unwrap(),
4948            expected_hash_105.hash(),
4949            "Hash for slot 105 should match SyntheticBlockhash for index 5"
4950        );
4951
4952        // Check that slot 100 maps to hash for index 0
4953        let expected_hash_100 = SyntheticBlockhash::new(0);
4954        let hash_for_100 = slot_hashes.get(&100);
4955        assert!(hash_for_100.is_some(), "SlotHashes should contain slot 100");
4956        assert_eq!(
4957            hash_for_100.unwrap(),
4958            expected_hash_100.hash(),
4959            "Hash for slot 100 should match SyntheticBlockhash for index 0"
4960        );
4961    }
4962
4963    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4964    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4965    #[test_case(TestType::no_db(); "with no db")]
4966    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4967    fn test_reconstruct_sysvars_clock(test_type: TestType) {
4968        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4969
4970        // Set up: chain_tip.index = 50, genesis_slot = 1000 (absolute slot = 1050)
4971        svm.chain_tip = BlockIdentifier::new(50, "test_hash");
4972        svm.genesis_slot = 1000;
4973        svm.latest_epoch_info.absolute_slot = 1050;
4974        svm.latest_epoch_info.epoch = 5;
4975        svm.genesis_updated_at = 2_000_000; // 2 seconds in ms
4976        svm.slot_time = 400; // 400ms per slot
4977
4978        svm.reconstruct_sysvars();
4979
4980        // Verify Clock sysvar
4981        let clock = svm.inner.get_sysvar::<Clock>();
4982
4983        assert_eq!(clock.slot, 1050, "Clock slot should be absolute slot");
4984        assert_eq!(clock.epoch, 5, "Clock epoch should match latest_epoch_info");
4985
4986        // Expected timestamp: genesis_updated_at + (50 slots * 400ms) = 2_000_000 + 20_000 = 2_020_000ms = 2020 seconds
4987        assert_eq!(
4988            clock.unix_timestamp, 2020,
4989            "Clock unix_timestamp should be calculated correctly"
4990        );
4991    }
4992
4993    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4994    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4995    #[test_case(TestType::no_db(); "with no db")]
4996    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4997    #[allow(deprecated)]
4998    fn test_reconstruct_sysvars_max_blockhashes(test_type: TestType) {
4999        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5000
5001        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5002
5003        // Set up: chain_tip.index = 200 (more than MAX_RECENT_BLOCKHASHES_STANDARD = 150)
5004        svm.chain_tip = BlockIdentifier::new(200, "test_hash");
5005        svm.genesis_slot = 0;
5006        svm.latest_epoch_info.absolute_slot = 200;
5007
5008        svm.reconstruct_sysvars();
5009
5010        // Verify RecentBlockhashes sysvar is capped at MAX_RECENT_BLOCKHASHES_STANDARD
5011        let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
5012
5013        assert_eq!(
5014            recent_blockhashes.len(),
5015            MAX_RECENT_BLOCKHASHES_STANDARD,
5016            "RecentBlockhashes should be capped at MAX_RECENT_BLOCKHASHES_STANDARD"
5017        );
5018
5019        // First entry should still be for chain_tip.index (200)
5020        let expected_hash = SyntheticBlockhash::new(200);
5021        assert_eq!(
5022            recent_blockhashes.first().unwrap().blockhash,
5023            *expected_hash.hash(),
5024            "First blockhash should match SyntheticBlockhash for chain_tip.index"
5025        );
5026
5027        // Last entry should be for index 51 (200 - 149)
5028        let expected_last_hash = SyntheticBlockhash::new(51);
5029        assert_eq!(
5030            recent_blockhashes.last().unwrap().blockhash,
5031            *expected_last_hash.hash(),
5032            "Last blockhash should match SyntheticBlockhash for start_index"
5033        );
5034    }
5035
5036    #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5037    #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5038    #[test_case(TestType::no_db(); "with no db")]
5039    #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5040    #[allow(deprecated)]
5041    fn test_reconstruct_sysvars_deterministic(test_type: TestType) {
5042        use solana_slot_hashes::SlotHashes;
5043        use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5044
5045        let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5046
5047        // Set up initial state
5048        svm.chain_tip = BlockIdentifier::new(25, "test_hash");
5049        svm.genesis_slot = 50;
5050        svm.latest_epoch_info.absolute_slot = 75;
5051        svm.latest_epoch_info.epoch = 2;
5052        svm.genesis_updated_at = 1_000_000;
5053        svm.slot_time = 400;
5054
5055        // First reconstruction
5056        svm.reconstruct_sysvars();
5057        let blockhashes_1 = svm.inner.get_sysvar::<RecentBlockhashes>();
5058        let slot_hashes_1 = svm.inner.get_sysvar::<SlotHashes>();
5059        let clock_1 = svm.inner.get_sysvar::<Clock>();
5060
5061        // Second reconstruction with same state
5062        svm.reconstruct_sysvars();
5063        let blockhashes_2 = svm.inner.get_sysvar::<RecentBlockhashes>();
5064        let slot_hashes_2 = svm.inner.get_sysvar::<SlotHashes>();
5065        let clock_2 = svm.inner.get_sysvar::<Clock>();
5066
5067        // Verify determinism - results should be identical
5068        assert_eq!(blockhashes_1.len(), blockhashes_2.len());
5069        for (b1, b2) in blockhashes_1.iter().zip(blockhashes_2.iter()) {
5070            assert_eq!(
5071                b1.blockhash, b2.blockhash,
5072                "RecentBlockhashes should be deterministic"
5073            );
5074        }
5075
5076        assert_eq!(slot_hashes_1.len(), slot_hashes_2.len());
5077        assert_eq!(clock_1.slot, clock_2.slot);
5078        assert_eq!(clock_1.epoch, clock_2.epoch);
5079        assert_eq!(clock_1.unix_timestamp, clock_2.unix_timestamp);
5080    }
5081}