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