1use std::{
2 cmp::max,
3 collections::{BTreeMap, HashMap, HashSet, VecDeque},
4 str::FromStr,
5 sync::Arc,
6 time::SystemTime,
7};
8
9use agave_feature_set::FeatureSet;
10use base64::{Engine, prelude::BASE64_STANDARD};
11use chrono::Utc;
12use convert_case::Casing;
13use crossbeam_channel::{Receiver, Sender, unbounded};
14use litesvm::{
15 LiteSVM,
16 types::{
17 FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult,
18 },
19};
20use solana_account::{Account, AccountSharedData, ReadableAccount};
21use solana_account_decoder::{
22 UiAccount, UiAccountData, UiAccountEncoding, UiDataSliceConfig, encode_ui_account,
23 parse_account_data::{AccountAdditionalDataV3, ParsedAccount, SplTokenAdditionalDataV2},
24};
25use solana_client::{
26 rpc_client::SerializableTransaction,
27 rpc_config::{RpcAccountInfoConfig, RpcBlockConfig, RpcTransactionLogsFilter},
28 rpc_filter::RpcFilterType,
29 rpc_response::{RpcKeyedAccount, RpcLogsResponse, RpcPerfSample},
30};
31use solana_clock::{Clock, Slot};
32use solana_commitment_config::{CommitmentConfig, CommitmentLevel};
33use solana_epoch_info::EpochInfo;
34use solana_epoch_schedule::EpochSchedule;
35use solana_genesis_config::GenesisConfig;
36use solana_hash::Hash;
37use solana_inflation::Inflation;
38use solana_loader_v3_interface::state::UpgradeableLoaderState;
39use solana_message::{
40 Message, VersionedMessage, inline_nonce::is_advance_nonce_instruction_data, v0::LoadedAddresses,
41};
42use solana_program_option::COption;
43use solana_pubkey::Pubkey;
44use solana_rpc_client_api::response::{SlotInfo, SlotTransactionStats, SlotUpdate};
45use solana_sdk_ids::{bpf_loader, system_program};
46use solana_signature::Signature;
47use solana_slot_hashes::MAX_ENTRIES as MAX_SLOT_HASHES_ENTRIES;
48use solana_system_interface::instruction as system_instruction;
49use solana_transaction::versioned::VersionedTransaction;
50use solana_transaction_error::TransactionError;
51use solana_transaction_status::{TransactionDetails, TransactionStatusMeta, UiConfirmedBlock};
52use spl_token_2022_interface::extension::{
53 BaseStateWithExtensions, StateWithExtensions, interest_bearing_mint::InterestBearingConfig,
54 scaled_ui_amount::ScaledUiAmountConfig,
55};
56use surfpool_types::{
57 AccountChange, AccountProfileState, AccountSnapshot, DEFAULT_PROFILING_MAP_CAPACITY,
58 DEFAULT_SLOT_TIME_MS, ExportSnapshotConfig, ExportSnapshotScope, FifoMap, Idl,
59 OverrideInstance, ProfileResult, RpcProfileDepth, RpcProfileResultConfig,
60 RunbookExecutionStatusReport, SimnetEvent, SvmFeatureConfig, TransactionConfirmationStatus,
61 TransactionStatusEvent, UiAccountChange, UiAccountProfileState, UiProfileResult, VersionedIdl,
62 types::{
63 ComputeUnitsEstimationResult, KeyedProfileResult, UiKeyedProfileResult, UuidOrSignature,
64 },
65};
66use txtx_addon_kit::{
67 indexmap::IndexMap,
68 types::types::{AddonJsonConverter, Value},
69};
70use txtx_addon_network_svm::codec::idl::borsh_encode_value_to_idl_type;
71use txtx_addon_network_svm_types::idl::{
72 parse_bytes_to_value_with_expected_idl_type_def_ty,
73 parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes,
74};
75use uuid::Uuid;
76
77use super::{
78 AccountSubscriptionData, BlockHeader, BlockIdentifier, FINALIZATION_SLOT_THRESHOLD,
79 GetAccountResult, GeyserBlockMetadata, GeyserEntryInfo, GeyserEvent, GeyserSlotStatus,
80 ProgramSubscriptionData, SignatureSubscriptionData, SignatureSubscriptionType,
81 SlotsUpdatesSubscriptionData, remote::SurfnetRemoteClient,
82};
83use crate::{
84 error::{AirdropError, SurfpoolError, SurfpoolResult},
85 rpc::utils::convert_transaction_metadata_from_canonical,
86 scenarios::TemplateRegistry,
87 storage::{OverlayStorage, Storage, new_kv_store, new_kv_store_with_default},
88 surfnet::{
89 LogsSubscriptionData, locker::is_supported_token_program, surfnet_lite_svm::SurfnetLiteSvm,
90 },
91 types::{
92 MintAccount, OfflineAccountConfig, SerializableAccountAdditionalData,
93 SurfnetTransactionStatus, SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta,
94 },
95};
96
97lazy_static::lazy_static! {
98 pub static ref GARBAGE_COLLECTION_INTERVAL_SLOTS: u64 = {
102 std::env::var("SURFPOOL_GARBAGE_COLLECTION_INTERVAL_SLOTS")
103 .ok()
104 .and_then(|s| s.parse().ok())
105 .unwrap_or(9_000)
106 };
107
108 pub static ref CHECKPOINT_INTERVAL_SLOTS: u64 = {
112 std::env::var("SURFPOOL_CHECKPOINT_INTERVAL_SLOTS")
113 .ok()
114 .and_then(|s| s.parse().ok())
115 .unwrap_or(150)
116 };
117}
118
119pub fn apply_override_to_decoded_account(
121 decoded_value: &mut Value,
122 path: &str,
123 value: &serde_json::Value,
124) -> SurfpoolResult<()> {
125 let parts: Vec<&str> = path.split('.').collect();
126
127 if parts.is_empty() {
128 return Err(SurfpoolError::internal("Empty path provided for override"));
129 }
130
131 let mut current = decoded_value;
133 for part in &parts[..parts.len() - 1] {
134 match current {
135 Value::Object(map) => {
136 current = map.get_mut(&part.to_string()).ok_or_else(|| {
137 SurfpoolError::internal(format!(
138 "Path segment '{}' not found in decoded account",
139 part
140 ))
141 })?;
142 }
143 _ => {
144 return Err(SurfpoolError::internal(format!(
145 "Cannot navigate through field '{}' - not an object",
146 part
147 )));
148 }
149 }
150 }
151
152 let final_key = parts[parts.len() - 1];
154 match current {
155 Value::Object(map) => {
156 let txtx_value = json_to_txtx_value(value)?;
158 map.insert(final_key.to_string(), txtx_value);
159 Ok(())
160 }
161 _ => Err(SurfpoolError::internal(format!(
162 "Cannot set field '{}' - parent is not an object",
163 final_key
164 ))),
165 }
166}
167
168fn json_to_txtx_value(json: &serde_json::Value) -> SurfpoolResult<Value> {
170 match json {
171 serde_json::Value::Null => Ok(Value::Null),
172 serde_json::Value::Bool(b) => Ok(Value::Bool(*b)),
173 serde_json::Value::Number(n) => {
174 if let Some(i) = n.as_i64() {
175 Ok(Value::Integer(i as i128))
176 } else if let Some(u) = n.as_u64() {
177 Ok(Value::Integer(u as i128))
178 } else if let Some(f) = n.as_f64() {
179 Ok(Value::Float(f))
180 } else {
181 Err(SurfpoolError::internal(format!(
182 "Unable to convert number: {}",
183 n
184 )))
185 }
186 }
187 serde_json::Value::String(s) => Ok(Value::String(s.clone())),
188 serde_json::Value::Array(arr) => {
189 let txtx_arr: Result<Vec<Value>, _> = arr.iter().map(json_to_txtx_value).collect();
190 Ok(Value::Array(Box::new(txtx_arr?)))
191 }
192 serde_json::Value::Object(obj) => {
193 let mut txtx_obj = IndexMap::new();
194 for (k, v) in obj.iter() {
195 txtx_obj.insert(k.clone(), json_to_txtx_value(v)?);
196 }
197 Ok(Value::Object(txtx_obj))
198 }
199 }
200}
201
202pub type AccountOwner = Pubkey;
203
204#[allow(deprecated)]
205use solana_sysvar::recent_blockhashes::MAX_ENTRIES;
206
207#[allow(deprecated)]
208pub const MAX_RECENT_BLOCKHASHES_STANDARD: usize = MAX_ENTRIES;
209
210pub fn get_txtx_value_json_converters() -> Vec<AddonJsonConverter<'static>> {
211 vec![
212 Box::new(move |value: &txtx_addon_kit::types::types::Value| {
213 txtx_addon_network_svm_types::SvmValue::to_json(value)
214 }) as AddonJsonConverter<'static>,
215 ]
216}
217
218const DEFAULT_LOG_BYTES_LIMIT: Option<usize> = Some(10_000);
219
220#[derive(Debug, Clone)]
221pub struct SurfnetSvmConfig {
222 pub surfnet_id: String,
223 pub feature_config: SvmFeatureConfig,
224 pub slot_time: u64,
225 pub instruction_profiling_enabled: bool,
226 pub max_profiles: usize,
227 pub log_bytes_limit: Option<usize>,
228 pub skip_blockhash_check: bool,
229}
230
231impl Default for SurfnetSvmConfig {
232 fn default() -> Self {
233 Self {
234 surfnet_id: "default".to_string(),
235 feature_config: SvmFeatureConfig::default(),
236 slot_time: DEFAULT_SLOT_TIME_MS,
237 instruction_profiling_enabled: true,
238 max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
239 log_bytes_limit: DEFAULT_LOG_BYTES_LIMIT,
240 skip_blockhash_check: false,
241 }
242 }
243}
244
245#[derive(Clone)]
252pub struct SurfnetSvm {
253 pub inner: SurfnetLiteSvm,
254 pub remote_rpc_url: Option<String>,
255 pub chain_tip: BlockIdentifier,
256 pub blocks: Box<dyn Storage<u64, BlockHeader>>,
257 pub transactions: Box<dyn Storage<String, SurfnetTransactionStatus>>,
258 pub jito_bundles: Box<dyn Storage<String, Vec<String>>>,
259 pub transactions_queued_for_confirmation: VecDeque<(
260 VersionedTransaction,
261 Sender<TransactionStatusEvent>,
262 Option<TransactionError>,
263 )>,
264 pub transactions_queued_for_finalization: VecDeque<(
265 Slot,
266 VersionedTransaction,
267 Sender<TransactionStatusEvent>,
268 Option<TransactionError>,
269 )>,
270 pub perf_samples: VecDeque<RpcPerfSample>,
271 pub transactions_processed: u64,
272 pub latest_epoch_info: EpochInfo,
273 pub simnet_events_tx: Sender<SimnetEvent>,
274 pub geyser_events_tx: Sender<GeyserEvent>,
275 pub signature_subscriptions: HashMap<Signature, Vec<SignatureSubscriptionData>>,
276 pub account_subscriptions: AccountSubscriptionData,
277 pub program_subscriptions: ProgramSubscriptionData,
278 pub slot_subscriptions: Vec<Sender<SlotInfo>>,
279 pub slots_updates_subscriptions: Vec<SlotsUpdatesSubscriptionData>,
283 pub profile_tag_map: Box<dyn Storage<String, Vec<UuidOrSignature>>>,
284 pub simulated_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
285 pub executed_transaction_profiles: Box<dyn Storage<String, KeyedProfileResult>>,
286 pub logs_subscriptions: Vec<LogsSubscriptionData>,
287 pub snapshot_subscriptions: Vec<super::SnapshotSubscriptionData>,
288 pub updated_at: u64,
289 pub slot_time: u64,
290 pub start_time: SystemTime,
291 pub accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
292 pub account_associated_data: Box<dyn Storage<String, SerializableAccountAdditionalData>>,
293 pub token_accounts: Box<dyn Storage<String, TokenAccount>>,
294 pub token_mints: Box<dyn Storage<String, MintAccount>>,
295 pub token_accounts_by_owner: Box<dyn Storage<String, Vec<String>>>,
296 pub token_accounts_by_delegate: Box<dyn Storage<String, Vec<String>>>,
297 pub token_accounts_by_mint: Box<dyn Storage<String, Vec<String>>>,
298 pub total_supply: u64,
299 pub circulating_supply: u64,
300 pub non_circulating_supply: u64,
301 pub non_circulating_accounts: Vec<String>,
302 pub genesis_config: GenesisConfig,
303 pub inflation: Inflation,
304 pub write_version: u64,
308 pub registered_idls: Box<dyn Storage<String, Vec<VersionedIdl>>>,
309 pub feature_set: FeatureSet,
310 pub instruction_profiling_enabled: bool,
311 pub max_profiles: usize,
312 pub skip_blockhash_check: bool,
313 pub runbook_executions: Vec<RunbookExecutionStatusReport>,
314 pub account_update_slots: HashMap<Pubkey, Slot>,
315 pub streamed_accounts: Box<dyn Storage<String, bool>>,
316 pub recent_blockhashes: VecDeque<(SyntheticBlockhash, i64)>,
317 pub scheduled_overrides: Box<dyn Storage<u64, Vec<OverrideInstance>>>,
318 pub offline_accounts: Box<dyn Storage<String, OfflineAccountConfig>>,
323 pub genesis_slot: Slot,
326 pub genesis_updated_at: u64,
329 pub slot_checkpoint: Box<dyn Storage<String, u64>>,
332 pub last_checkpoint_slot: u64,
334}
335
336fn add_pubkey_to_index(
344 index: &mut Box<dyn Storage<String, Vec<String>>>,
345 key: String,
346 pubkey_str: &str,
347) -> SurfpoolResult<()> {
348 let mut accounts = index.get(&key).ok().flatten().unwrap_or_default();
349 if !accounts.iter().any(|pk| pk == pubkey_str) {
350 accounts.push(pubkey_str.to_string());
351 index.store(key, accounts)?;
352 }
353 Ok(())
354}
355
356fn remove_pubkey_from_index(
360 index: &mut Box<dyn Storage<String, Vec<String>>>,
361 key: &str,
362 pubkey_str: &str,
363) -> SurfpoolResult<()> {
364 let key_owned = key.to_string();
365 if let Some(mut accounts) = index.get(&key_owned).ok().flatten() {
366 accounts.retain(|pk| pk != pubkey_str);
367 if accounts.is_empty() {
368 index.take(&key_owned)?;
369 } else {
370 index.store(key_owned, accounts)?;
371 }
372 }
373 Ok(())
374}
375
376pub struct BundleSandbox {
381 pub svm: SurfnetSvm,
382 pub geyser_rx: Receiver<GeyserEvent>,
383 pub simnet_rx: Receiver<SimnetEvent>,
384}
385
386fn commit_overlay_storage<K, V>(
395 sandbox_storage: &dyn Storage<K, V>,
396 target_storage: &mut dyn Storage<K, V>,
397) -> SurfpoolResult<()> {
398 let Some(overlay) = sandbox_storage.as_overlay() else {
399 return Ok(());
400 };
401 let delta = overlay.extract_overlay()?;
402 if delta.base_cleared {
403 target_storage.clear()?;
404 }
405 for k in delta.deletes {
406 target_storage.take(&k)?;
407 }
408 for (k, v) in delta.writes {
409 target_storage.store(k, v)?;
410 }
411 Ok(())
412}
413
414fn compose_feature_set(config: &SvmFeatureConfig) -> FeatureSet {
422 let mut feature_set = LiteSVM::mainnet_feature_set();
423 for pubkey in &config.enable {
424 debug!("Activating feature {}", pubkey);
425 feature_set.activate(pubkey, 0);
426 }
427 for pubkey in &config.disable {
428 debug!("Deactivating feature {}", pubkey);
429 feature_set.deactivate(pubkey);
430 }
431 feature_set
432}
433
434fn synthetic_chain_tip_at_index(index: u64) -> BlockIdentifier {
435 if index == 0 {
436 return BlockIdentifier::zero();
437 }
438
439 BlockIdentifier {
440 index,
441 hash: SyntheticBlockhash::new(index - 1).to_string(),
442 }
443}
444
445fn synthetic_blockhash_for_slot(slot: Slot, genesis_slot: Slot) -> SyntheticBlockhash {
446 if slot >= genesis_slot {
447 return SyntheticBlockhash::new(slot - genesis_slot);
448 }
449
450 SyntheticBlockhash::new(u64::MAX - (genesis_slot - slot - 1))
453}
454
455impl SurfnetSvm {
456 pub fn default() -> (Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>) {
457 Self::new(SurfnetSvmConfig::default()).unwrap()
458 }
459
460 pub fn new(
461 config: SurfnetSvmConfig,
462 ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
463 Self::build(None, config)
464 }
465
466 pub fn new_with_db(
467 database_url: Option<&str>,
468 config: SurfnetSvmConfig,
469 ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
470 Self::build(database_url, config)
471 }
472
473 pub fn shutdown(&self) {
476 self.inner.shutdown();
477 self.blocks.shutdown();
478 self.transactions.shutdown();
479 self.jito_bundles.shutdown();
480 self.token_accounts.shutdown();
481 self.token_mints.shutdown();
482 self.accounts_by_owner.shutdown();
483 self.token_accounts_by_owner.shutdown();
484 self.token_accounts_by_delegate.shutdown();
485 self.token_accounts_by_mint.shutdown();
486 self.streamed_accounts.shutdown();
487 self.scheduled_overrides.shutdown();
488 self.registered_idls.shutdown();
489 self.profile_tag_map.shutdown();
490 self.simulated_transaction_profiles.shutdown();
491 self.executed_transaction_profiles.shutdown();
492 self.account_associated_data.shutdown();
493 self.offline_accounts.shutdown();
494 self.slot_checkpoint.shutdown();
495 }
496
497 pub fn clone_for_profiling(&self) -> Self {
514 let (dummy_simnet_tx, _) = crossbeam_channel::bounded(1);
515 let (dummy_geyser_tx, _) = crossbeam_channel::bounded(1);
516
517 Self {
518 inner: self.inner.clone_for_profiling(),
519 remote_rpc_url: self.remote_rpc_url.clone(),
520 chain_tip: self.chain_tip.clone(),
521
522 blocks: OverlayStorage::wrap(self.blocks.clone_box()),
524 transactions: OverlayStorage::wrap(self.transactions.clone_box()),
525 jito_bundles: OverlayStorage::wrap(self.jito_bundles.clone_box()),
526 profile_tag_map: OverlayStorage::wrap(self.profile_tag_map.clone_box()),
527 simulated_transaction_profiles: OverlayStorage::wrap(
528 self.simulated_transaction_profiles.clone_box(),
529 ),
530 executed_transaction_profiles: OverlayStorage::wrap(
531 self.executed_transaction_profiles.clone_box(),
532 ),
533 accounts_by_owner: OverlayStorage::wrap(self.accounts_by_owner.clone_box()),
534 account_associated_data: OverlayStorage::wrap(self.account_associated_data.clone_box()),
535 token_accounts: OverlayStorage::wrap(self.token_accounts.clone_box()),
536 token_mints: OverlayStorage::wrap(self.token_mints.clone_box()),
537 token_accounts_by_owner: OverlayStorage::wrap(self.token_accounts_by_owner.clone_box()),
538 token_accounts_by_delegate: OverlayStorage::wrap(
539 self.token_accounts_by_delegate.clone_box(),
540 ),
541 token_accounts_by_mint: OverlayStorage::wrap(self.token_accounts_by_mint.clone_box()),
542 registered_idls: OverlayStorage::wrap(self.registered_idls.clone_box()),
543 streamed_accounts: OverlayStorage::wrap(self.streamed_accounts.clone_box()),
544 scheduled_overrides: OverlayStorage::wrap(self.scheduled_overrides.clone_box()),
545
546 transactions_queued_for_confirmation: self.transactions_queued_for_confirmation.clone(),
548 transactions_queued_for_finalization: self.transactions_queued_for_finalization.clone(),
549 perf_samples: self.perf_samples.clone(),
550 transactions_processed: self.transactions_processed,
551 latest_epoch_info: self.latest_epoch_info.clone(),
552
553 simnet_events_tx: dummy_simnet_tx,
555 geyser_events_tx: dummy_geyser_tx,
556
557 signature_subscriptions: HashMap::new(),
558 account_subscriptions: HashMap::new(),
559 program_subscriptions: HashMap::new(),
560 slot_subscriptions: Vec::new(),
566 slots_updates_subscriptions: Vec::new(),
567 logs_subscriptions: Vec::new(),
568 snapshot_subscriptions: Vec::new(),
569
570 updated_at: self.updated_at,
571 slot_time: self.slot_time,
572 start_time: self.start_time,
573
574 total_supply: self.total_supply,
575 circulating_supply: self.circulating_supply,
576 non_circulating_supply: self.non_circulating_supply,
577 non_circulating_accounts: self.non_circulating_accounts.clone(),
578 genesis_config: self.genesis_config.clone(),
579 inflation: self.inflation,
580 write_version: self.write_version,
581 feature_set: self.feature_set.clone(),
582 instruction_profiling_enabled: self.instruction_profiling_enabled,
583 max_profiles: self.max_profiles,
584 skip_blockhash_check: self.skip_blockhash_check,
585 runbook_executions: self.runbook_executions.clone(),
586 account_update_slots: self.account_update_slots.clone(),
587 recent_blockhashes: self.recent_blockhashes.clone(),
588 offline_accounts: OverlayStorage::wrap(self.offline_accounts.clone_box()),
589 genesis_slot: self.genesis_slot,
590 genesis_updated_at: self.genesis_updated_at,
591 slot_checkpoint: OverlayStorage::wrap(self.slot_checkpoint.clone_box()),
592 last_checkpoint_slot: self.last_checkpoint_slot,
593 }
594 }
595
596 pub(crate) fn default_epoch_schedule() -> EpochSchedule {
597 EpochSchedule::without_warmup()
598 }
599
600 pub(crate) fn default_epoch_info(epoch_schedule: &EpochSchedule) -> EpochInfo {
601 EpochInfo {
602 epoch: 0,
603 slot_index: 0,
604 slots_in_epoch: epoch_schedule.slots_per_epoch,
605 absolute_slot: FINALIZATION_SLOT_THRESHOLD,
606 block_height: FINALIZATION_SLOT_THRESHOLD,
607 transaction_count: None,
608 }
609 }
610
611 fn register_builtin_template_idls(&mut self) {
612 let registry = TemplateRegistry::new();
613 for (_, template) in registry.templates.into_iter() {
614 let _ = self.register_idl(template.idl, None);
615 }
616 }
617
618 pub fn clone_for_bundle_sandbox(&self) -> BundleSandbox {
630 let mut svm = self.clone_for_profiling();
631 let (geyser_tx, geyser_rx) = crossbeam_channel::unbounded();
632 let (simnet_tx, simnet_rx) = crossbeam_channel::unbounded();
633 svm.geyser_events_tx = geyser_tx;
634 svm.simnet_events_tx = simnet_tx;
635 BundleSandbox {
636 svm,
637 geyser_rx,
638 simnet_rx,
639 }
640 }
641
642 pub fn commit_sandbox(
674 &mut self,
675 sandbox: BundleSandbox,
676 bundle_status_tx: Sender<TransactionStatusEvent>,
677 ) -> SurfpoolResult<Vec<Signature>> {
678 let BundleSandbox {
679 mut svm,
680 geyser_rx,
681 simnet_rx,
682 } = sandbox;
683
684 commit_overlay_storage(svm.blocks.as_ref(), self.blocks.as_mut())?;
686 commit_overlay_storage(svm.transactions.as_ref(), self.transactions.as_mut())?;
687 commit_overlay_storage(svm.profile_tag_map.as_ref(), self.profile_tag_map.as_mut())?;
688 commit_overlay_storage(
689 svm.simulated_transaction_profiles.as_ref(),
690 self.simulated_transaction_profiles.as_mut(),
691 )?;
692 commit_overlay_storage(
693 svm.executed_transaction_profiles.as_ref(),
694 self.executed_transaction_profiles.as_mut(),
695 )?;
696 commit_overlay_storage(
697 svm.accounts_by_owner.as_ref(),
698 self.accounts_by_owner.as_mut(),
699 )?;
700 commit_overlay_storage(
701 svm.account_associated_data.as_ref(),
702 self.account_associated_data.as_mut(),
703 )?;
704 commit_overlay_storage(svm.token_accounts.as_ref(), self.token_accounts.as_mut())?;
705 commit_overlay_storage(svm.token_mints.as_ref(), self.token_mints.as_mut())?;
706 commit_overlay_storage(
707 svm.token_accounts_by_owner.as_ref(),
708 self.token_accounts_by_owner.as_mut(),
709 )?;
710 commit_overlay_storage(
711 svm.token_accounts_by_delegate.as_ref(),
712 self.token_accounts_by_delegate.as_mut(),
713 )?;
714 commit_overlay_storage(
715 svm.token_accounts_by_mint.as_ref(),
716 self.token_accounts_by_mint.as_mut(),
717 )?;
718 commit_overlay_storage(svm.registered_idls.as_ref(), self.registered_idls.as_mut())?;
719 commit_overlay_storage(
720 svm.streamed_accounts.as_ref(),
721 self.streamed_accounts.as_mut(),
722 )?;
723 commit_overlay_storage(
724 svm.scheduled_overrides.as_ref(),
725 self.scheduled_overrides.as_mut(),
726 )?;
727 commit_overlay_storage(
728 svm.offline_accounts.as_ref(),
729 self.offline_accounts.as_mut(),
730 )?;
731 commit_overlay_storage(svm.slot_checkpoint.as_ref(), self.slot_checkpoint.as_mut())?;
732
733 std::mem::swap(&mut self.inner.svm, &mut svm.inner.svm);
735
736 if let (Some(sandbox_db), Some(target_db)) = (svm.inner.db.as_ref(), self.inner.db.as_mut())
738 {
739 commit_overlay_storage(sandbox_db.as_ref(), target_db.as_mut())?;
740 }
741
742 self.transactions_processed = svm.transactions_processed;
744 self.write_version = svm.write_version;
745 for (k, v) in svm.account_update_slots.drain() {
746 self.account_update_slots.insert(k, v);
747 }
748 self.perf_samples = svm.perf_samples.clone();
749 self.recent_blockhashes = svm.recent_blockhashes.clone();
750
751 let mut signatures = Vec::new();
755 for (tx, _sandbox_status_tx, err) in svm.transactions_queued_for_confirmation.drain(..) {
756 signatures.push(tx.signatures[0]);
757 self.transactions_queued_for_confirmation.push_back((
758 tx,
759 bundle_status_tx.clone(),
760 err,
761 ));
762 }
763 for (slot, tx, _sandbox_status_tx, err) in
764 svm.transactions_queued_for_finalization.drain(..)
765 {
766 self.transactions_queued_for_finalization.push_back((
767 slot,
768 tx,
769 bundle_status_tx.clone(),
770 err,
771 ));
772 }
773
774 while let Ok(event) = geyser_rx.try_recv() {
777 if let GeyserEvent::UpdateAccount(update) = &event {
778 self.notify_account_subscribers(&update.pubkey, &update.account);
779 self.notify_program_subscribers(&update.pubkey, &update.account);
780 }
781 let _ = self.geyser_events_tx.send(event);
782 }
783
784 while let Ok(event) = simnet_rx.try_recv() {
786 let _ = self.simnet_events_tx.try_send(event);
787 }
788
789 let slot = self.get_latest_absolute_slot();
792 for sig in &signatures {
793 let (err, logs) = match self.transactions.get(&sig.to_string()).ok().flatten() {
794 Some(SurfnetTransactionStatus::Processed(boxed)) => {
795 let (meta, _mutated) = boxed.as_ref();
796 let err = meta.meta.status.clone().err();
797 let logs = meta.meta.log_messages.clone().unwrap_or_default();
798 (err, logs)
799 }
800 _ => (None, Vec::new()),
801 };
802 self.notify_signature_subscribers(
803 SignatureSubscriptionType::processed(),
804 sig,
805 slot,
806 err.clone(),
807 );
808 self.notify_logs_subscribers(sig, err, logs, CommitmentLevel::Processed);
809 let _ = bundle_status_tx.try_send(TransactionStatusEvent::Success(
810 TransactionConfirmationStatus::Processed,
811 ));
812 }
813
814 Ok(signatures)
815 }
816
817 fn build(
821 database_url: Option<&str>,
822 config: SurfnetSvmConfig,
823 ) -> SurfpoolResult<(Self, Receiver<SimnetEvent>, Receiver<GeyserEvent>)> {
824 let (simnet_events_tx, simnet_events_rx) = crossbeam_channel::bounded(1024);
825 let (geyser_events_tx, geyser_events_rx) = crossbeam_channel::bounded(1024);
826 let surfnet_id = config.surfnet_id;
827
828 let feature_set = compose_feature_set(&config.feature_config);
833 let inner = SurfnetLiteSvm::new(database_url, &surfnet_id, feature_set.clone())?;
834
835 let native_mint_account = inner
836 .get_account(&spl_token_interface::native_mint::ID)?
837 .unwrap();
838
839 let native_mint_associated_data = {
840 let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
841 &native_mint_account.data,
842 )
843 .unwrap();
844 let unix_timestamp = inner.get_sysvar::<Clock>().unix_timestamp;
845 let interest_bearing_config = mint
846 .get_extension::<InterestBearingConfig>()
847 .map(|x| (*x, unix_timestamp))
848 .ok();
849 let scaled_ui_amount_config = mint
850 .get_extension::<ScaledUiAmountConfig>()
851 .map(|x| (*x, unix_timestamp))
852 .ok();
853 AccountAdditionalDataV3 {
854 spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
855 decimals: mint.base.decimals,
856 interest_bearing_config,
857 scaled_ui_amount_config,
858 }),
859 }
860 };
861 let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
862
863 let mut accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
865 new_kv_store(&database_url, "accounts_by_owner", &surfnet_id)?;
866 accounts_by_owner_db.store(
867 native_mint_account.owner.to_string(),
868 vec![spl_token_interface::native_mint::ID.to_string()],
869 )?;
870 let blocks_db = new_kv_store(&database_url, "blocks", &surfnet_id)?;
871 let transactions_db = new_kv_store(&database_url, "transactions", &surfnet_id)?;
872 let jito_bundles_db = new_kv_store(&database_url, "jito_bundles", &surfnet_id)?;
873 let token_accounts_db = new_kv_store(&database_url, "token_accounts", &surfnet_id)?;
874 let mut token_mints_db: Box<dyn Storage<String, MintAccount>> =
875 new_kv_store(&database_url, "token_mints", &surfnet_id)?;
876 let mut account_associated_data_db: Box<
877 dyn Storage<String, SerializableAccountAdditionalData>,
878 > = new_kv_store(&database_url, "account_associated_data", &surfnet_id)?;
879 account_associated_data_db.store(
881 spl_token_interface::native_mint::ID.to_string(),
882 native_mint_associated_data.into(),
883 )?;
884 token_mints_db.store(
885 spl_token_interface::native_mint::ID.to_string(),
886 parsed_mint_account,
887 )?;
888 let token_accounts_by_owner_db: Box<dyn Storage<String, Vec<String>>> =
889 new_kv_store(&database_url, "token_accounts_by_owner", &surfnet_id)?;
890 let token_accounts_by_delegate_db: Box<dyn Storage<String, Vec<String>>> =
891 new_kv_store(&database_url, "token_accounts_by_delegate", &surfnet_id)?;
892 let token_accounts_by_mint_db: Box<dyn Storage<String, Vec<String>>> =
893 new_kv_store(&database_url, "token_accounts_by_mint", &surfnet_id)?;
894 let streamed_accounts_db: Box<dyn Storage<String, bool>> =
895 new_kv_store(&database_url, "streamed_accounts", &surfnet_id)?;
896 let scheduled_overrides_db: Box<dyn Storage<u64, Vec<OverrideInstance>>> =
897 new_kv_store(&database_url, "scheduled_overrides", &surfnet_id)?;
898 let offline_accounts_db: Box<dyn Storage<String, OfflineAccountConfig>> =
899 new_kv_store(&database_url, "offline_accounts", &surfnet_id)?;
900 let registered_idls_db: Box<dyn Storage<String, Vec<VersionedIdl>>> =
901 new_kv_store(&database_url, "registered_idls", &surfnet_id)?;
902 let profile_tag_map_db: Box<dyn Storage<String, Vec<UuidOrSignature>>> =
903 new_kv_store(&database_url, "profile_tag_map", &surfnet_id)?;
904 let simulated_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> =
905 new_kv_store(&database_url, "simulated_transaction_profiles", &surfnet_id)?;
906 let executed_transaction_profiles_db: Box<dyn Storage<String, KeyedProfileResult>> = {
907 let max_profiles = max(1, config.max_profiles);
909 new_kv_store_with_default(
910 &database_url,
911 "executed_transaction_profiles",
912 &surfnet_id,
913 move || Box::new(FifoMap::<String, KeyedProfileResult>::new(max_profiles)),
916 )?
917 };
918 let epoch_schedule = Self::default_epoch_schedule();
919 let mut epoch_info = Self::default_epoch_info(&epoch_schedule);
920 let default_genesis_slot = epoch_info.absolute_slot;
921 let slot_checkpoint_db: Box<dyn Storage<String, u64>> =
922 new_kv_store(&database_url, "slot_checkpoint", &surfnet_id)?;
923
924 let checkpoint_slot = slot_checkpoint_db.get(&"latest_slot".to_string())?;
926 let max_block_slot = blocks_db
927 .into_iter()
928 .unwrap()
929 .max_by_key(|(slot, _): &(u64, BlockHeader)| *slot);
930 let has_persisted_chain_state = checkpoint_slot.is_some() || max_block_slot.is_some();
931
932 let (chain_tip, recovered_slot, recovered_block_height) =
933 match (checkpoint_slot, max_block_slot) {
934 (Some(checkpoint), Some((block_slot, block))) => {
936 if checkpoint > block_slot {
937 (
938 synthetic_chain_tip_at_index(checkpoint),
939 checkpoint,
940 checkpoint,
941 )
942 } else {
943 (
944 BlockIdentifier {
945 index: block.block_height,
946 hash: block.hash,
947 },
948 block_slot,
949 block.block_height,
950 )
951 }
952 }
953 (Some(checkpoint), None) => (
954 synthetic_chain_tip_at_index(checkpoint),
955 checkpoint,
956 checkpoint,
957 ),
958 (None, Some((block_slot, block))) => (
959 BlockIdentifier {
960 index: block.block_height,
961 hash: block.hash,
962 },
963 block_slot,
964 block.block_height,
965 ),
966 (None, None) => (
967 BlockIdentifier::zero(),
968 epoch_info.absolute_slot,
969 epoch_info.block_height,
970 ),
971 };
972
973 if has_persisted_chain_state {
974 let (epoch, slot_index) = epoch_schedule.get_epoch_and_slot_index(recovered_slot);
975 epoch_info.epoch = epoch;
976 epoch_info.slot_index = slot_index;
977 epoch_info.absolute_slot = recovered_slot;
978 epoch_info.block_height = recovered_block_height;
979 }
980
981 let transactions_processed = transactions_db.count()?;
983 let updated_at = Utc::now().timestamp_millis() as u64;
984 let last_checkpoint_slot = checkpoint_slot.unwrap_or_else(|| {
985 if has_persisted_chain_state {
986 recovered_slot
987 } else {
988 0
989 }
990 });
991
992 let mut svm = Self {
993 inner,
994 remote_rpc_url: None,
995 chain_tip,
996 blocks: blocks_db,
997 transactions: transactions_db,
998 jito_bundles: jito_bundles_db,
999 perf_samples: VecDeque::new(),
1000 transactions_processed,
1001 simnet_events_tx,
1002 geyser_events_tx,
1003 latest_epoch_info: epoch_info.clone(),
1004 transactions_queued_for_confirmation: VecDeque::new(),
1005 transactions_queued_for_finalization: VecDeque::new(),
1006 signature_subscriptions: HashMap::new(),
1007 account_subscriptions: HashMap::new(),
1008 program_subscriptions: HashMap::new(),
1009 slot_subscriptions: Vec::new(),
1010 slots_updates_subscriptions: Vec::new(),
1011 profile_tag_map: profile_tag_map_db,
1012 simulated_transaction_profiles: simulated_transaction_profiles_db,
1013 executed_transaction_profiles: executed_transaction_profiles_db,
1014 logs_subscriptions: Vec::new(),
1015 snapshot_subscriptions: Vec::new(),
1016 updated_at,
1017 slot_time: config.slot_time,
1018 start_time: SystemTime::now(),
1019 accounts_by_owner: accounts_by_owner_db,
1020 account_associated_data: account_associated_data_db,
1021 token_accounts: token_accounts_db,
1022 token_mints: token_mints_db,
1023 token_accounts_by_owner: token_accounts_by_owner_db,
1024 token_accounts_by_delegate: token_accounts_by_delegate_db,
1025 token_accounts_by_mint: token_accounts_by_mint_db,
1026 total_supply: 0,
1027 circulating_supply: 0,
1028 non_circulating_supply: 0,
1029 non_circulating_accounts: Vec::new(),
1030 genesis_config: GenesisConfig::default(),
1031 inflation: Inflation::default(),
1032 write_version: 0,
1033 registered_idls: registered_idls_db,
1034 feature_set,
1035 instruction_profiling_enabled: config.instruction_profiling_enabled,
1036 max_profiles: config.max_profiles,
1037 skip_blockhash_check: config.skip_blockhash_check,
1038 runbook_executions: Vec::new(),
1039 account_update_slots: HashMap::new(),
1040 streamed_accounts: streamed_accounts_db,
1041 recent_blockhashes: VecDeque::new(),
1042 scheduled_overrides: scheduled_overrides_db,
1043 offline_accounts: offline_accounts_db,
1044 genesis_slot: default_genesis_slot,
1045 genesis_updated_at: updated_at,
1046 slot_checkpoint: slot_checkpoint_db,
1047 last_checkpoint_slot,
1048 };
1049
1050 svm.inner.set_log_bytes_limit(config.log_bytes_limit);
1051 if !has_persisted_chain_state {
1052 svm.chain_tip = svm.new_blockhash();
1053 }
1054 svm.register_builtin_template_idls();
1055 svm.inner.set_sysvar(&epoch_schedule);
1056 svm.reconstruct_sysvars();
1057
1058 Ok((svm, simnet_events_rx, geyser_events_rx))
1059 }
1060
1061 pub fn increment_write_version(&mut self) -> u64 {
1062 self.write_version += 1;
1063 self.write_version
1064 }
1065
1066 pub fn initialize(&mut self, epoch_info: EpochInfo, epoch_schedule: EpochSchedule) {
1074 self.chain_tip = self.new_blockhash();
1075 self.latest_epoch_info = epoch_info.clone();
1076 self.genesis_slot = epoch_info.absolute_slot;
1079 self.updated_at = Utc::now().timestamp_millis() as u64;
1080 self.genesis_updated_at = self.updated_at;
1082
1083 self.inner.set_sysvar(&epoch_schedule);
1084
1085 self.reconstruct_sysvars();
1087 }
1088
1089 pub fn set_profile_instructions(&mut self, do_profile_instructions: bool) {
1090 self.instruction_profiling_enabled = do_profile_instructions;
1091 }
1092
1093 pub fn airdrop(
1106 &mut self,
1107 pubkey: &Pubkey,
1108 lamports: u64,
1109 ) -> Result<TransactionResult, AirdropError> {
1110 if lamports == 0 {
1111 return Err(AirdropError::ZeroAmount);
1112 }
1113 let min_rent = self.inner.minimum_balance_for_rent_exemption(0);
1114 if lamports < min_rent {
1115 return Err(AirdropError::BelowRentExemption { lamports, min_rent });
1116 }
1117
1118 let airdrop_pubkey = self.inner.airdrop_pubkey();
1120
1121 let airdrop_account_before = self
1122 .get_account(&airdrop_pubkey)?
1123 .unwrap_or_else(|| Account::default());
1124 let recipient_account_before = self
1125 .get_account(pubkey)?
1126 .unwrap_or_else(|| Account::default());
1127 let system_account_before = self
1128 .get_account(&system_program::id())?
1129 .unwrap_or_else(|| Account::default());
1130
1131 let res = self.inner.airdrop(pubkey, lamports);
1132 let (status_tx, _rx) = unbounded();
1133 if let Ok(ref tx_result) = res {
1134 let slot = self.latest_epoch_info.absolute_slot;
1135 let airdrop_account_after = self
1137 .get_account(&airdrop_pubkey)?
1138 .unwrap_or_else(|| Account::default());
1139 let recipient_account_after = self
1140 .get_account(pubkey)?
1141 .unwrap_or_else(|| Account::default());
1142 let system_account_after = self
1143 .get_account(&system_program::id())?
1144 .unwrap_or_else(|| Account::default());
1145
1146 let tx = VersionedTransaction {
1148 signatures: vec![tx_result.signature],
1149 message: VersionedMessage::Legacy(Message::new(
1150 &[system_instruction::transfer(
1151 &airdrop_pubkey,
1152 pubkey,
1153 lamports,
1154 )],
1155 Some(&airdrop_pubkey),
1156 )),
1157 };
1158
1159 self.transactions.store(
1160 tx.get_signature().to_string(),
1161 SurfnetTransactionStatus::processed(
1162 TransactionWithStatusMeta {
1163 slot,
1164 transaction: tx.clone(),
1165 meta: TransactionStatusMeta {
1166 status: Ok(()),
1167 fee: 5000,
1168 pre_balances: vec![
1169 airdrop_account_before.lamports,
1170 recipient_account_before.lamports,
1171 system_account_before.lamports,
1172 ],
1173 post_balances: vec![
1174 airdrop_account_after.lamports,
1175 recipient_account_after.lamports,
1176 system_account_after.lamports,
1177 ],
1178 inner_instructions: Some(vec![]),
1179 log_messages: Some(tx_result.logs.clone()),
1180 pre_token_balances: Some(vec![]),
1181 post_token_balances: Some(vec![]),
1182 rewards: Some(vec![]),
1183 loaded_addresses: LoadedAddresses::default(),
1184 return_data: Some(tx_result.return_data.clone()),
1185 compute_units_consumed: Some(tx_result.compute_units_consumed),
1186 cost_units: None,
1187 },
1188 },
1189 HashSet::from([*pubkey]),
1190 ),
1191 )?;
1192 self.notify_signature_subscribers(
1193 SignatureSubscriptionType::processed(),
1194 tx.get_signature(),
1195 slot,
1196 None,
1197 );
1198 self.notify_logs_subscribers(
1199 tx.get_signature(),
1200 None,
1201 tx_result.logs.clone(),
1202 CommitmentLevel::Processed,
1203 );
1204 self.transactions_queued_for_confirmation
1205 .push_back((tx, status_tx.clone(), None));
1206 if let Some(account) = self.get_account(pubkey)? {
1207 self.set_account(pubkey, account)?;
1208 }
1209 }
1210 Ok(res)
1211 }
1212
1213 pub fn airdrop_pubkeys(&mut self, lamports: u64, addresses: &[Pubkey]) {
1224 for recipient in addresses {
1225 match self.airdrop(recipient, lamports) {
1226 Ok(_) => {
1227 let _ = self.simnet_events_tx.send(SimnetEvent::info(format!(
1228 "Genesis airdrop successful {}: {}",
1229 recipient, lamports
1230 )));
1231 }
1232 Err(AirdropError::ZeroAmount) => {
1233 let _ = self
1234 .simnet_events_tx
1235 .send(SimnetEvent::info("Skipping 0 lamport airdrop"));
1236 return;
1237 }
1238 Err(AirdropError::BelowRentExemption { lamports, min_rent }) => {
1239 let _ = self.simnet_events_tx.send(SimnetEvent::error(format!(
1240 "Skipping invalid airdrop: amount {lamports} is below the rent-exempt minimum of {min_rent} lamports"
1241 )));
1242 return;
1243 }
1244 Err(AirdropError::Other(e)) => {
1245 let _ = self.simnet_events_tx.send(SimnetEvent::error(format!(
1246 "Genesis airdrop failed {}: {}",
1247 recipient, e
1248 )));
1249 }
1250 };
1251 }
1252 }
1253
1254 pub const fn get_latest_absolute_slot(&self) -> Slot {
1256 self.latest_epoch_info.absolute_slot
1257 }
1258
1259 pub fn latest_blockhash(&self) -> solana_hash::Hash {
1261 Hash::from_str(&self.chain_tip.hash).expect("Invalid blockhash")
1262 }
1263
1264 pub fn latest_epoch_info(&self) -> EpochInfo {
1266 self.latest_epoch_info.clone()
1267 }
1268
1269 pub fn calculate_block_time_for_slot(&self, slot: Slot) -> u64 {
1272 let slots_since_genesis = slot.saturating_sub(self.genesis_slot);
1274 self.genesis_updated_at + (slots_since_genesis * self.slot_time)
1275 }
1276
1277 pub fn is_slot_in_valid_range(&self, slot: Slot) -> bool {
1286 let latest_slot = self.get_latest_absolute_slot();
1287 slot >= self.genesis_slot && slot <= latest_slot
1288 }
1289
1290 pub fn get_block_or_reconstruct(&self, slot: Slot) -> SurfpoolResult<Option<BlockHeader>> {
1301 match self.blocks.get(&slot)? {
1302 Some(block) => Ok(Some(block)),
1303 None => {
1304 if self.is_slot_in_valid_range(slot) {
1305 Ok(Some(self.reconstruct_empty_block(slot)))
1306 } else {
1307 Ok(None)
1308 }
1309 }
1310 }
1311 }
1312
1313 pub fn reconstruct_empty_block(&self, slot: Slot) -> BlockHeader {
1316 let block_height = slot;
1317 BlockHeader {
1318 hash: SyntheticBlockhash::new(block_height).to_string(),
1319 previous_blockhash: SyntheticBlockhash::new(block_height.saturating_sub(1)).to_string(),
1320 parent_slot: slot.saturating_sub(1),
1321 block_time: (self.calculate_block_time_for_slot(slot) / 1_000) as i64,
1322 block_height,
1323 signatures: vec![],
1324 }
1325 }
1326
1327 #[allow(deprecated)]
1334 pub fn reconstruct_sysvars(&mut self) {
1335 use solana_slot_hashes::SlotHashes;
1336 use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
1337
1338 let current_index = self.chain_tip.index;
1339 let current_absolute_slot = self.get_latest_absolute_slot();
1340
1341 let start_index = current_index.saturating_sub(MAX_RECENT_BLOCKHASHES_STANDARD as u64 - 1);
1343
1344 let synthetic_hashes: Vec<_> = (start_index..=current_index)
1347 .rev()
1348 .map(SyntheticBlockhash::new)
1349 .collect();
1350
1351 let recent_blockhashes_vec: Vec<_> = synthetic_hashes
1353 .iter()
1354 .enumerate()
1355 .map(|(index, hash)| IterItem(index as u64, hash.hash(), 0))
1356 .collect();
1357 let recent_blockhashes = RecentBlockhashes::from_iter(recent_blockhashes_vec);
1358 self.inner.set_sysvar(&recent_blockhashes);
1359
1360 let slot_hash_start_index =
1367 current_index.saturating_sub(MAX_SLOT_HASHES_ENTRIES as u64 - 1);
1368 let local_start_absolute_slot = slot_hash_start_index + self.genesis_slot;
1369 let finalized_start_absolute_slot =
1370 current_absolute_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
1371 let earliest_retained_slot =
1372 current_absolute_slot.saturating_sub(MAX_SLOT_HASHES_ENTRIES as u64 - 1);
1373 let start_absolute_slot = local_start_absolute_slot
1374 .min(finalized_start_absolute_slot)
1375 .max(earliest_retained_slot);
1376 let slot_hashes_vec: Vec<_> = (start_absolute_slot..=current_absolute_slot)
1377 .rev()
1378 .map(|slot| {
1379 (
1380 slot,
1381 *synthetic_blockhash_for_slot(slot, self.genesis_slot).hash(),
1382 )
1383 })
1384 .collect();
1385 let slot_hashes = SlotHashes::new(&slot_hashes_vec);
1386 self.inner.set_sysvar(&slot_hashes);
1387
1388 let unix_timestamp = self.calculate_block_time_for_slot(current_absolute_slot) / 1_000;
1390 let clock = Clock {
1391 slot: current_absolute_slot,
1392 epoch: self.latest_epoch_info.epoch,
1393 unix_timestamp: unix_timestamp as i64,
1394 epoch_start_timestamp: 0,
1395 leader_schedule_epoch: 0,
1396 };
1397 self.inner.set_sysvar(&clock);
1398 }
1399
1400 #[allow(deprecated)]
1405 fn new_blockhash(&mut self) -> BlockIdentifier {
1406 use solana_slot_hashes::SlotHashes;
1407 use solana_sysvar::recent_blockhashes::{IterItem, RecentBlockhashes};
1408 let recent_blockhashes_backup = self.inner.get_sysvar::<RecentBlockhashes>();
1410 let num_blockhashes_expected = recent_blockhashes_backup
1411 .len()
1412 .min(MAX_RECENT_BLOCKHASHES_STANDARD);
1413 self.inner.expire_blockhash();
1416 let mut recent_blockhashes = Vec::with_capacity(num_blockhashes_expected);
1418 let recent_blockhashes_overriden = self.inner.get_sysvar::<RecentBlockhashes>();
1419 let latest_entry = recent_blockhashes_overriden
1420 .first()
1421 .expect("Latest blockhash not found");
1422
1423 let new_synthetic_blockhash = SyntheticBlockhash::new(self.chain_tip.index);
1424 let new_synthetic_blockhash_str = new_synthetic_blockhash.to_string();
1425
1426 recent_blockhashes.push(IterItem(
1427 0,
1428 new_synthetic_blockhash.hash(),
1429 latest_entry.fee_calculator.lamports_per_signature,
1430 ));
1431
1432 for (index, entry) in recent_blockhashes_backup.iter().enumerate() {
1434 if recent_blockhashes.len() >= MAX_RECENT_BLOCKHASHES_STANDARD {
1435 break;
1436 }
1437 recent_blockhashes.push(IterItem(
1438 (index + 1) as u64,
1439 &entry.blockhash,
1440 entry.fee_calculator.lamports_per_signature,
1441 ));
1442 }
1443
1444 self.inner
1445 .set_sysvar(&RecentBlockhashes::from_iter(recent_blockhashes));
1446
1447 let mut slot_hashes = self.inner.get_sysvar::<SlotHashes>();
1448 slot_hashes.add(
1449 self.get_latest_absolute_slot() + 1,
1450 *new_synthetic_blockhash.hash(),
1451 );
1452 self.inner.set_sysvar(&SlotHashes::new(&slot_hashes));
1453
1454 BlockIdentifier::new(
1455 self.chain_tip.index + 1,
1456 new_synthetic_blockhash_str.as_str(),
1457 )
1458 }
1459
1460 pub fn check_blockhash_is_recent(&self, recent_blockhash: &Hash) -> bool {
1468 #[allow(deprecated)]
1469 self.inner
1470 .get_sysvar::<solana_sysvar::recent_blockhashes::RecentBlockhashes>()
1471 .iter()
1472 .any(|entry| entry.blockhash == *recent_blockhash)
1473 }
1474
1475 pub fn validate_transaction_blockhash(&self, tx: &VersionedTransaction) -> bool {
1485 if self.skip_blockhash_check {
1486 return true;
1487 }
1488
1489 let recent_blockhash = tx.message.recent_blockhash();
1490
1491 let some_nonce_account_index = tx
1492 .message
1493 .instructions()
1494 .get(solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize)
1495 .filter(|instruction| {
1496 matches!(
1497 tx.message.static_account_keys().get(instruction.program_id_index as usize),
1498 Some(program_id) if system_program::check_id(program_id)
1499 ) && is_advance_nonce_instruction_data(&instruction.data)
1500 })
1501 .map(|instruction| {
1502 instruction.accounts.get(0)
1504 });
1505
1506 debug!(
1507 "Validating tx blockhash: {}; is nonce tx?: {}",
1508 recent_blockhash,
1509 some_nonce_account_index.is_some()
1510 );
1511
1512 if let Some(nonce_account_index) = some_nonce_account_index {
1513 trace!(
1514 "Nonce tx detected. Nonce account index: {:?}",
1515 nonce_account_index
1516 );
1517 let Some(nonce_account_index) = nonce_account_index else {
1518 return false;
1519 };
1520
1521 let Some(nonce_account_pubkey) = tx
1522 .message
1523 .static_account_keys()
1524 .get(*nonce_account_index as usize)
1525 else {
1526 return false;
1527 };
1528
1529 trace!("Nonce account pubkey: {:?}", nonce_account_pubkey,);
1530
1531 let Ok(Some(nonce_account)) = self.get_account(nonce_account_pubkey) else {
1534 return false;
1535 };
1536 trace!("Nonce account: {:?}", nonce_account);
1537
1538 let Some(nonce_data) =
1539 bincode::deserialize::<solana_nonce::versions::Versions>(&nonce_account.data).ok()
1540 else {
1541 return false;
1542 };
1543 trace!("Nonce account data: {:?}", nonce_data);
1544
1545 let nonce_state = nonce_data.state();
1546 let initialized_state = match nonce_state {
1547 solana_nonce::state::State::Uninitialized => return false,
1548 solana_nonce::state::State::Initialized(data) => data,
1549 };
1550 return initialized_state.blockhash() == *recent_blockhash;
1551 } else {
1552 self.check_blockhash_is_recent(recent_blockhash)
1553 }
1554 }
1555
1556 pub fn sigverify(&self, tx: &VersionedTransaction) -> Result<(), FailedTransactionMetadata> {
1564 let signature = tx.signatures[0];
1565
1566 if tx.verify_with_results().iter().any(|valid| !*valid) {
1567 return Err(FailedTransactionMetadata {
1568 err: TransactionError::SignatureFailure,
1569 meta: TransactionMetadata::default(),
1570 });
1571 }
1572
1573 if matches!(
1574 self.transactions.get(&signature.to_string()),
1575 Ok(Some(SurfnetTransactionStatus::Processed(_)))
1576 ) {
1577 return Err(FailedTransactionMetadata {
1578 err: TransactionError::AlreadyProcessed,
1579 meta: TransactionMetadata::default(),
1580 });
1581 }
1582 Ok(())
1583 }
1584
1585 pub fn set_account(&mut self, pubkey: &Pubkey, account: Account) -> SurfpoolResult<()> {
1594 self.inner
1595 .set_account(*pubkey, account.clone())
1596 .map_err(|e| SurfpoolError::set_account(*pubkey, e))?;
1597
1598 self.account_update_slots
1599 .insert(*pubkey, self.get_latest_absolute_slot());
1600
1601 self.update_account_registries(pubkey, &account)?;
1603
1604 self.notify_account_subscribers(pubkey, &account);
1606
1607 self.notify_program_subscribers(pubkey, &account);
1609
1610 let _ = self
1611 .simnet_events_tx
1612 .send(SimnetEvent::account_update(*pubkey));
1613 Ok(())
1614 }
1615
1616 pub fn update_account_registries(
1617 &mut self,
1618 pubkey: &Pubkey,
1619 account: &Account,
1620 ) -> SurfpoolResult<()> {
1621 let is_deleted_account = account == &Account::default();
1622
1623 if is_deleted_account {
1627 self.inner.delete_account_in_db(pubkey)?;
1628 } else {
1629 self.inner
1630 .set_account_in_db(*pubkey, account.clone().into())?;
1631 }
1632
1633 if is_deleted_account {
1634 self.offline_accounts.store(
1638 pubkey.to_string(),
1639 OfflineAccountConfig {
1640 include_owned_accounts: false,
1641 },
1642 )?;
1643 if let Some(old_account) = self.get_account(pubkey)? {
1644 self.remove_from_indexes(pubkey, &old_account)?;
1645 }
1646 return Ok(());
1647 }
1648
1649 if let Some(old_account) = self.get_account(pubkey)? {
1653 self.remove_from_indexes(pubkey, &old_account)?;
1654 }
1655
1656 let pubkey_str = pubkey.to_string();
1657 add_pubkey_to_index(
1658 &mut self.accounts_by_owner,
1659 account.owner.to_string(),
1660 &pubkey_str,
1661 )?;
1662
1663 if is_supported_token_program(&account.owner) {
1664 self.index_token_account_variant(pubkey, &pubkey_str, account)?;
1665 self.index_mint_account_variant(pubkey, account)?;
1666 self.index_token_2022_mint_extensions(pubkey, account)?;
1667 }
1668 Ok(())
1669 }
1670
1671 fn index_token_account_variant(
1677 &mut self,
1678 pubkey: &Pubkey,
1679 pubkey_str: &str,
1680 account: &Account,
1681 ) -> SurfpoolResult<()> {
1682 let Ok(token_account) = TokenAccount::unpack(&account.data) else {
1683 return Ok(());
1684 };
1685 add_pubkey_to_index(
1686 &mut self.token_accounts_by_owner,
1687 token_account.owner().to_string(),
1688 pubkey_str,
1689 )?;
1690 add_pubkey_to_index(
1691 &mut self.token_accounts_by_mint,
1692 token_account.mint().to_string(),
1693 pubkey_str,
1694 )?;
1695 if let COption::Some(delegate) = token_account.delegate() {
1696 add_pubkey_to_index(
1697 &mut self.token_accounts_by_delegate,
1698 delegate.to_string(),
1699 pubkey_str,
1700 )?;
1701 }
1702 self.token_accounts
1703 .store(pubkey.to_string(), token_account)?;
1704 Ok(())
1705 }
1706
1707 fn index_mint_account_variant(
1709 &mut self,
1710 pubkey: &Pubkey,
1711 account: &Account,
1712 ) -> SurfpoolResult<()> {
1713 let Ok(mint_account) = MintAccount::unpack(&account.data) else {
1714 return Ok(());
1715 };
1716 self.token_mints.store(pubkey.to_string(), mint_account)?;
1717 Ok(())
1718 }
1719
1720 fn index_token_2022_mint_extensions(
1726 &mut self,
1727 pubkey: &Pubkey,
1728 account: &Account,
1729 ) -> SurfpoolResult<()> {
1730 let Ok(mint) =
1731 StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(&account.data)
1732 else {
1733 return Ok(());
1734 };
1735 let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1736 let interest_bearing_config = mint
1737 .get_extension::<InterestBearingConfig>()
1738 .map(|x| (*x, unix_timestamp))
1739 .ok();
1740 let scaled_ui_amount_config = mint
1741 .get_extension::<ScaledUiAmountConfig>()
1742 .map(|x| (*x, unix_timestamp))
1743 .ok();
1744 let additional_data: SerializableAccountAdditionalData = AccountAdditionalDataV3 {
1745 spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1746 decimals: mint.base.decimals,
1747 interest_bearing_config,
1748 scaled_ui_amount_config,
1749 }),
1750 }
1751 .into();
1752 self.account_associated_data
1753 .store(pubkey.to_string(), additional_data)?;
1754 Ok(())
1755 }
1756
1757 fn remove_from_indexes(
1758 &mut self,
1759 pubkey: &Pubkey,
1760 old_account: &Account,
1761 ) -> SurfpoolResult<()> {
1762 let pubkey_str = pubkey.to_string();
1763 remove_pubkey_from_index(
1764 &mut self.accounts_by_owner,
1765 &old_account.owner.to_string(),
1766 &pubkey_str,
1767 )?;
1768
1769 if is_supported_token_program(&old_account.owner)
1770 && let Some(old_token_account) = self.token_accounts.take(&pubkey_str)?
1771 {
1772 remove_pubkey_from_index(
1773 &mut self.token_accounts_by_owner,
1774 &old_token_account.owner().to_string(),
1775 &pubkey_str,
1776 )?;
1777 remove_pubkey_from_index(
1778 &mut self.token_accounts_by_mint,
1779 &old_token_account.mint().to_string(),
1780 &pubkey_str,
1781 )?;
1782 if let COption::Some(delegate) = old_token_account.delegate() {
1783 remove_pubkey_from_index(
1784 &mut self.token_accounts_by_delegate,
1785 &delegate.to_string(),
1786 &pubkey_str,
1787 )?;
1788 }
1789 }
1790 Ok(())
1791 }
1792
1793 pub fn reset_network(
1794 &mut self,
1795 epoch_info: EpochInfo,
1796 epoch_schedule: EpochSchedule,
1797 ) -> SurfpoolResult<()> {
1798 self.inner.reset(self.feature_set.clone())?;
1799
1800 let native_mint_account = self
1801 .inner
1802 .get_account(&spl_token_interface::native_mint::ID)?
1803 .unwrap();
1804
1805 let native_mint_associated_data = {
1806 let mint = StateWithExtensions::<spl_token_2022_interface::state::Mint>::unpack(
1807 &native_mint_account.data,
1808 )
1809 .unwrap();
1810 let unix_timestamp = self.inner.get_sysvar::<Clock>().unix_timestamp;
1811 let interest_bearing_config = mint
1812 .get_extension::<InterestBearingConfig>()
1813 .map(|x| (*x, unix_timestamp))
1814 .ok();
1815 let scaled_ui_amount_config = mint
1816 .get_extension::<ScaledUiAmountConfig>()
1817 .map(|x| (*x, unix_timestamp))
1818 .ok();
1819 AccountAdditionalDataV3 {
1820 spl_token_additional_data: Some(SplTokenAdditionalDataV2 {
1821 decimals: mint.base.decimals,
1822 interest_bearing_config,
1823 scaled_ui_amount_config,
1824 }),
1825 }
1826 };
1827
1828 let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap();
1829
1830 self.blocks.clear()?;
1831 self.transactions.clear()?;
1832 self.transactions_queued_for_confirmation.clear();
1833 self.transactions_queued_for_finalization.clear();
1834 self.perf_samples.clear();
1835 self.transactions_processed = 0;
1836 self.profile_tag_map.clear()?;
1837 self.simulated_transaction_profiles.clear()?;
1838 self.executed_transaction_profiles.clear()?;
1839 self.accounts_by_owner.clear()?;
1840 self.accounts_by_owner.store(
1841 native_mint_account.owner.to_string(),
1842 vec![spl_token_interface::native_mint::ID.to_string()],
1843 )?;
1844 self.account_associated_data.clear()?;
1845 self.account_associated_data.store(
1846 spl_token_interface::native_mint::ID.to_string(),
1847 native_mint_associated_data.into(),
1848 )?;
1849 self.token_accounts.clear()?;
1850 self.token_mints.clear()?;
1851 self.token_mints.store(
1852 spl_token_interface::native_mint::ID.to_string(),
1853 parsed_mint_account,
1854 )?;
1855 self.token_accounts_by_owner.clear()?;
1856 self.token_accounts_by_delegate.clear()?;
1857 self.token_accounts_by_mint.clear()?;
1858 self.non_circulating_accounts.clear();
1859 self.registered_idls.clear()?;
1860 self.register_builtin_template_idls();
1861 self.runbook_executions.clear();
1862 self.streamed_accounts.clear()?;
1863 self.scheduled_overrides.clear()?;
1864
1865 let current_time = chrono::Utc::now().timestamp_millis() as u64;
1866 self.updated_at = current_time;
1867 self.genesis_updated_at = current_time;
1868 self.latest_epoch_info = epoch_info.clone();
1869 self.genesis_slot = epoch_info.absolute_slot;
1871 let chain_tip_hash = SyntheticBlockhash::new(epoch_info.block_height).to_string();
1872 self.chain_tip = BlockIdentifier::new(epoch_info.block_height, chain_tip_hash.as_str());
1873 self.inner.set_sysvar(&epoch_schedule);
1874 self.reconstruct_sysvars();
1876 self.slot_checkpoint.clear()?;
1878 self.last_checkpoint_slot = self.genesis_slot;
1879 self.recent_blockhashes.clear();
1880
1881 Ok(())
1882 }
1883
1884 pub fn reset_account(
1885 &mut self,
1886 pubkey: &Pubkey,
1887 include_owned_accounts: bool,
1888 ) -> SurfpoolResult<()> {
1889 let Some(account) = self.get_account(pubkey)? else {
1890 return Ok(());
1891 };
1892
1893 if account.executable {
1894 if account.owner == solana_sdk_ids::bpf_loader_upgradeable::id() {
1896 let program_data_pubkey =
1897 solana_loader_v3_interface::get_program_data_address(pubkey);
1898
1899 self.purge_account_from_cache(&account, &program_data_pubkey)?;
1901 }
1902 }
1903 if include_owned_accounts {
1904 let owned_accounts = self.get_account_owned_by(pubkey)?;
1905 for (owned_pubkey, _) in owned_accounts {
1906 self.purge_account_from_cache(&account, &owned_pubkey)?;
1908 }
1909 }
1910 self.purge_account_from_cache(&account, pubkey)?;
1912 Ok(())
1913 }
1914
1915 fn purge_account_from_cache(
1916 &mut self,
1917 account: &Account,
1918 pubkey: &Pubkey,
1919 ) -> SurfpoolResult<()> {
1920 self.remove_from_indexes(pubkey, account)?;
1921
1922 self.inner.delete_account(pubkey)?;
1923
1924 Ok(())
1925 }
1926
1927 pub fn restore_from_snapshot(&mut self, snapshot_path: &str) -> SurfpoolResult<usize> {
1938 use std::{collections::HashMap, fs, str::FromStr};
1939
1940 use surfpool_types::AccountSnapshot;
1941
1942 let content = fs::read_to_string(snapshot_path).map_err(|e| {
1943 SurfpoolError::internal(format!(
1944 "Failed to read snapshot file '{}': {}",
1945 snapshot_path, e
1946 ))
1947 })?;
1948
1949 let snapshot: HashMap<String, AccountSnapshot> =
1950 serde_json::from_str(&content).map_err(|e| {
1951 SurfpoolError::internal(format!(
1952 "Failed to parse snapshot file '{}': {}",
1953 snapshot_path, e
1954 ))
1955 })?;
1956
1957 let mut restored_count = 0;
1958 let mut errors: Vec<String> = Vec::new();
1959
1960 for (pubkey_str, account_snapshot) in snapshot.iter() {
1961 let pubkey = match Pubkey::from_str(pubkey_str) {
1962 Ok(pk) => pk,
1963 Err(e) => {
1964 errors.push(format!("Invalid pubkey '{}': {}", pubkey_str, e));
1965 continue;
1966 }
1967 };
1968
1969 let account = match account_snapshot.to_account() {
1970 Ok(acc) => acc,
1971 Err(e) => {
1972 errors.push(format!("Failed to convert account '{}': {}", pubkey_str, e));
1973 continue;
1974 }
1975 };
1976
1977 if let Err(e) = self.set_account(&pubkey, account) {
1978 errors.push(format!("Failed to set account '{}': {}", pubkey_str, e));
1979 continue;
1980 }
1981
1982 restored_count += 1;
1983 }
1984
1985 if !errors.is_empty() {
1987 let _ = self.simnet_events_tx.send(SimnetEvent::warn(format!(
1988 "Snapshot restore completed with {} errors: {}",
1989 errors.len(),
1990 errors.join("; ")
1991 )));
1992 }
1993
1994 let _ = self.simnet_events_tx.send(SimnetEvent::info(format!(
1995 "Restored {} accounts from snapshot",
1996 restored_count
1997 )));
1998
1999 Ok(restored_count)
2000 }
2001
2002 #[allow(clippy::result_large_err)]
2016 pub fn send_transaction(
2017 &mut self,
2018 tx: VersionedTransaction,
2019 cu_analysis_enabled: bool,
2020 sigverify: bool,
2021 ) -> TransactionResult {
2022 if sigverify {
2023 self.sigverify(&tx)?;
2024 }
2025
2026 if cu_analysis_enabled {
2027 let estimation_result = self.estimate_compute_units(&tx);
2028 let _ = self.simnet_events_tx.try_send(SimnetEvent::info(format!(
2029 "CU Estimation for tx: {} | Consumed: {} | Success: {} | Logs: {:?} | Error: {:?}",
2030 tx.signatures
2031 .first()
2032 .map_or_else(|| "N/A".to_string(), |s| s.to_string()),
2033 estimation_result.compute_units_consumed,
2034 estimation_result.success,
2035 estimation_result.log_messages,
2036 estimation_result.error_message
2037 )));
2038 }
2039 self.transactions_processed += 1;
2040
2041 if !self.validate_transaction_blockhash(&tx) {
2042 let meta = TransactionMetadata::default();
2043 let err = solana_transaction_error::TransactionError::BlockhashNotFound;
2044
2045 let transaction_meta = convert_transaction_metadata_from_canonical(&meta);
2046
2047 let _ = self
2048 .simnet_events_tx
2049 .try_send(SimnetEvent::transaction_processed(
2050 transaction_meta,
2051 Some(err.clone()),
2052 ));
2053 return Err(FailedTransactionMetadata { err, meta });
2054 }
2055
2056 match self.inner.send_transaction(tx.clone()) {
2057 Ok(res) => Ok(res),
2058 Err(tx_failure) => {
2059 let transaction_meta =
2060 convert_transaction_metadata_from_canonical(&tx_failure.meta);
2061
2062 let _ = self
2063 .simnet_events_tx
2064 .try_send(SimnetEvent::transaction_processed(
2065 transaction_meta,
2066 Some(tx_failure.err.clone()),
2067 ));
2068 Err(tx_failure)
2069 }
2070 }
2071 }
2072
2073 pub fn estimate_compute_units(
2083 &self,
2084 transaction: &VersionedTransaction,
2085 ) -> ComputeUnitsEstimationResult {
2086 if !self.validate_transaction_blockhash(transaction) {
2087 return ComputeUnitsEstimationResult {
2088 success: false,
2089 compute_units_consumed: 0,
2090 log_messages: None,
2091 error_message: Some(
2092 solana_transaction_error::TransactionError::BlockhashNotFound.to_string(),
2093 ),
2094 };
2095 }
2096
2097 match self.inner.simulate_transaction(transaction.clone()) {
2098 Ok(sim_info) => ComputeUnitsEstimationResult {
2099 success: true,
2100 compute_units_consumed: sim_info.meta.compute_units_consumed,
2101 log_messages: Some(sim_info.meta.logs),
2102 error_message: None,
2103 },
2104 Err(failed_meta) => ComputeUnitsEstimationResult {
2105 success: false,
2106 compute_units_consumed: failed_meta.meta.compute_units_consumed,
2107 log_messages: Some(failed_meta.meta.logs),
2108 error_message: Some(failed_meta.err.to_string()),
2109 },
2110 }
2111 }
2112
2113 #[allow(clippy::result_large_err)]
2121 pub fn simulate_transaction(
2122 &self,
2123 tx: VersionedTransaction,
2124 sigverify: bool,
2125 ) -> Result<SimulatedTransactionInfo, FailedTransactionMetadata> {
2126 if sigverify {
2127 self.sigverify(&tx)?;
2128 }
2129
2130 if !self.validate_transaction_blockhash(&tx) {
2131 let meta = TransactionMetadata::default();
2132 let err = TransactionError::BlockhashNotFound;
2133
2134 return Err(FailedTransactionMetadata { err, meta });
2135 }
2136 self.inner.simulate_transaction(tx)
2137 }
2138
2139 fn confirm_transactions(&mut self) -> Result<(Vec<Signature>, u64), SurfpoolError> {
2148 let mut confirmed_transactions = vec![];
2149 let mut num_failed: u64 = 0;
2150 let slot = self.latest_epoch_info.slot_index;
2151 let current_slot = self.latest_epoch_info.absolute_slot;
2152
2153 while let Some((tx, status_tx, error)) =
2154 self.transactions_queued_for_confirmation.pop_front()
2155 {
2156 if error.is_some() {
2157 num_failed = num_failed.saturating_add(1);
2158 }
2159 let _ = status_tx.try_send(TransactionStatusEvent::Success(
2160 TransactionConfirmationStatus::Confirmed,
2161 ));
2162 let signature = tx.signatures[0];
2163 let finalized_at = self.latest_epoch_info.absolute_slot + FINALIZATION_SLOT_THRESHOLD;
2164 self.transactions_queued_for_finalization.push_back((
2165 finalized_at,
2166 tx,
2167 status_tx,
2168 error.clone(),
2169 ));
2170
2171 self.notify_signature_subscribers(
2172 SignatureSubscriptionType::confirmed(),
2173 &signature,
2174 slot,
2175 error,
2176 );
2177
2178 let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2179 self.transactions.get(&signature.to_string()).ok().flatten()
2180 else {
2181 continue;
2182 };
2183 let (tx_with_status_meta, mutated_account_keys) = tx_data.as_ref();
2184
2185 for pubkey in mutated_account_keys {
2186 self.account_update_slots.insert(*pubkey, current_slot);
2187 }
2188
2189 self.notify_logs_subscribers(
2190 &signature,
2191 None,
2192 tx_with_status_meta
2193 .meta
2194 .log_messages
2195 .clone()
2196 .unwrap_or(vec![]),
2197 CommitmentLevel::Confirmed,
2198 );
2199 confirmed_transactions.push(signature);
2200 }
2201
2202 Ok((confirmed_transactions, num_failed))
2203 }
2204
2205 fn finalize_transactions(&mut self) -> Result<(), SurfpoolError> {
2210 let current_slot = self.latest_epoch_info.absolute_slot;
2211 let mut requeue = VecDeque::new();
2212 while let Some((finalized_at, tx, status_tx, error)) =
2213 self.transactions_queued_for_finalization.pop_front()
2214 {
2215 if current_slot >= finalized_at {
2216 let _ = status_tx.try_send(TransactionStatusEvent::Success(
2217 TransactionConfirmationStatus::Finalized,
2218 ));
2219 let signature = &tx.signatures[0];
2220 self.notify_signature_subscribers(
2221 SignatureSubscriptionType::finalized(),
2222 signature,
2223 self.latest_epoch_info.absolute_slot,
2224 error,
2225 );
2226 let Some(SurfnetTransactionStatus::Processed(tx_data)) =
2227 self.transactions.get(&signature.to_string()).ok().flatten()
2228 else {
2229 continue;
2230 };
2231 let (tx_with_status_meta, _) = tx_data.as_ref();
2232 let logs = tx_with_status_meta
2233 .meta
2234 .log_messages
2235 .clone()
2236 .unwrap_or(vec![]);
2237 self.notify_logs_subscribers(signature, None, logs, CommitmentLevel::Finalized);
2238 } else {
2239 requeue.push_back((finalized_at, tx, status_tx, error));
2240 }
2241 }
2242 self.transactions_queued_for_finalization
2244 .append(&mut requeue);
2245
2246 Ok(())
2247 }
2248
2249 pub fn write_account_update(&mut self, account_update: GetAccountResult) {
2254 let init_programdata_account = |program_account: &Account| {
2255 if !program_account.executable {
2256 return None;
2257 }
2258 if !program_account
2259 .owner
2260 .eq(&solana_sdk_ids::bpf_loader_upgradeable::id())
2261 {
2262 return None;
2263 }
2264 let Ok(UpgradeableLoaderState::Program {
2265 programdata_address,
2266 }) = bincode::deserialize::<UpgradeableLoaderState>(&program_account.data)
2267 else {
2268 return None;
2269 };
2270
2271 let programdata_state = UpgradeableLoaderState::ProgramData {
2272 upgrade_authority_address: Some(system_program::id()),
2273 slot: self.get_latest_absolute_slot(),
2274 };
2275 let mut data = bincode::serialize(&programdata_state).unwrap();
2276
2277 data.extend_from_slice(crate::surfnet::noop_program::NOOP_PROGRAM_ELF);
2278 let lamports = self.inner.minimum_balance_for_rent_exemption(data.len());
2279 Some((
2280 programdata_address,
2281 Account {
2282 lamports,
2283 data,
2284 owner: solana_sdk_ids::bpf_loader_upgradeable::id(),
2285 executable: false,
2286 rent_epoch: 0,
2287 },
2288 ))
2289 };
2290 match account_update {
2291 GetAccountResult::FoundAccount(pubkey, account, do_update_account) => {
2292 if do_update_account {
2293 if let Some((programdata_address, programdata_account)) =
2294 init_programdata_account(&account)
2295 {
2296 match self.get_account(&programdata_address) {
2297 Ok(None) => {
2298 if let Err(e) =
2299 self.set_account(&programdata_address, programdata_account)
2300 {
2301 let _ = self
2302 .simnet_events_tx
2303 .send(SimnetEvent::error(e.to_string()));
2304 }
2305 }
2306 Ok(Some(_)) => {}
2307 Err(e) => {
2308 let _ = self
2309 .simnet_events_tx
2310 .send(SimnetEvent::error(e.to_string()));
2311 }
2312 }
2313 }
2314 if let Err(e) = self.set_account(&pubkey, account.clone()) {
2315 let _ = self
2316 .simnet_events_tx
2317 .send(SimnetEvent::error(e.to_string()));
2318 }
2319 }
2320 }
2321 GetAccountResult::FoundProgramAccount((pubkey, account), (_, None)) => {
2322 if let Some((programdata_address, programdata_account)) =
2323 init_programdata_account(&account)
2324 {
2325 match self.get_account(&programdata_address) {
2326 Ok(None) => {
2327 if let Err(e) =
2328 self.set_account(&programdata_address, programdata_account)
2329 {
2330 let _ = self
2331 .simnet_events_tx
2332 .send(SimnetEvent::error(e.to_string()));
2333 }
2334 }
2335 Ok(Some(_)) => {}
2336 Err(e) => {
2337 let _ = self
2338 .simnet_events_tx
2339 .send(SimnetEvent::error(e.to_string()));
2340 }
2341 }
2342 }
2343 if let Err(e) = self.set_account(&pubkey, account.clone()) {
2344 let _ = self
2345 .simnet_events_tx
2346 .send(SimnetEvent::error(e.to_string()));
2347 }
2348 }
2349 GetAccountResult::FoundTokenAccount((pubkey, account), (_, None)) => {
2350 if let Err(e) = self.set_account(&pubkey, account.clone()) {
2351 let _ = self
2352 .simnet_events_tx
2353 .send(SimnetEvent::error(e.to_string()));
2354 }
2355 }
2356 GetAccountResult::FoundProgramAccount(
2357 (pubkey, account),
2358 (coupled_pubkey, Some(coupled_account)),
2359 )
2360 | GetAccountResult::FoundTokenAccount(
2361 (pubkey, account),
2362 (coupled_pubkey, Some(coupled_account)),
2363 ) => {
2364 if let Err(e) = self.set_account(&coupled_pubkey, coupled_account.clone()) {
2366 let _ = self
2367 .simnet_events_tx
2368 .send(SimnetEvent::error(e.to_string()));
2369 }
2370 if let Err(e) = self.set_account(&pubkey, account.clone()) {
2371 let _ = self
2372 .simnet_events_tx
2373 .send(SimnetEvent::error(e.to_string()));
2374 }
2375 }
2376 GetAccountResult::None(_) => {}
2377 }
2378 }
2379
2380 pub fn confirm_current_block(&mut self) -> SurfpoolResult<()> {
2381 let slot = self.get_latest_absolute_slot();
2382 let slots_update_ts: u64 = Utc::now().timestamp_millis().max(0) as u64;
2387 let previous_chain_tip = self.chain_tip.clone();
2388 if slot % *GARBAGE_COLLECTION_INTERVAL_SLOTS == 0 {
2389 debug!("Clearing liteSVM cache at slot {}", slot);
2390 self.inner.garbage_collect(self.feature_set.clone());
2391 }
2392 self.chain_tip = self.new_blockhash();
2393 let (confirmed_signatures, num_failed_transactions) = self.confirm_transactions()?;
2395
2396 let num_transactions = confirmed_signatures.len() as u64;
2397 let num_successful_transactions = num_transactions.saturating_sub(num_failed_transactions);
2398 self.updated_at += self.slot_time;
2399
2400 if !confirmed_signatures.is_empty() {
2403 self.blocks.store(
2404 slot,
2405 BlockHeader {
2406 hash: self.chain_tip.hash.clone(),
2407 previous_blockhash: previous_chain_tip.hash.clone(),
2408 block_time: self.updated_at as i64 / 1_000,
2409 block_height: self.chain_tip.index,
2410 parent_slot: slot,
2411 signatures: confirmed_signatures,
2412 },
2413 )?;
2414 }
2415
2416 if slot.saturating_sub(self.last_checkpoint_slot) >= *CHECKPOINT_INTERVAL_SLOTS {
2419 self.slot_checkpoint
2420 .store("latest_slot".to_string(), slot)?;
2421 self.last_checkpoint_slot = slot;
2422 }
2423
2424 if self.perf_samples.len() > 30 {
2425 self.perf_samples.pop_back();
2426 }
2427 self.perf_samples.push_front(RpcPerfSample {
2428 slot,
2429 num_slots: 1,
2430 sample_period_secs: 1,
2431 num_transactions,
2432 num_non_vote_transactions: Some(num_transactions),
2433 });
2434
2435 self.latest_epoch_info.slot_index += 1;
2436 self.latest_epoch_info.block_height = self.chain_tip.index;
2437 self.latest_epoch_info.absolute_slot += 1;
2438 if self.latest_epoch_info.slot_index > self.latest_epoch_info.slots_in_epoch {
2439 self.latest_epoch_info.slot_index = 0;
2440 self.latest_epoch_info.epoch += 1;
2441 }
2442 let total_transactions = self.latest_epoch_info.transaction_count.unwrap_or(0);
2443 self.latest_epoch_info.transaction_count = Some(total_transactions + num_transactions);
2444
2445 let parent_slot = self.latest_epoch_info.absolute_slot.saturating_sub(1);
2446 let new_slot = self.latest_epoch_info.absolute_slot;
2447 let root = new_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
2448 self.notify_slot_subscribers(new_slot, parent_slot, root);
2449
2450 const SURFPOOL_ENTRIES_PER_BLOCK: u64 = 1;
2464 self.notify_slots_updates_subscribers(SlotUpdate::Frozen {
2465 slot,
2466 timestamp: slots_update_ts,
2467 stats: SlotTransactionStats {
2468 num_transaction_entries: SURFPOOL_ENTRIES_PER_BLOCK,
2469 num_successful_transactions,
2470 num_failed_transactions,
2471 max_transactions_per_entry: num_transactions,
2472 },
2473 });
2474 self.notify_slots_updates_subscribers(SlotUpdate::CreatedBank {
2475 slot: new_slot,
2476 parent: parent_slot,
2477 timestamp: slots_update_ts,
2478 });
2479
2480 let geyser_parent_slot = slot.saturating_sub(1);
2481
2482 self.geyser_events_tx
2484 .send(GeyserEvent::UpdateSlotStatus {
2485 slot,
2486 parent: slot.checked_sub(1),
2487 status: GeyserSlotStatus::Confirmed,
2488 })
2489 .ok();
2490 self.notify_slots_updates_subscribers(SlotUpdate::OptimisticConfirmation {
2493 slot,
2494 timestamp: slots_update_ts,
2495 });
2496
2497 let block_metadata = GeyserBlockMetadata {
2499 slot,
2500 blockhash: self.chain_tip.hash.clone(),
2501 parent_slot: geyser_parent_slot,
2502 parent_blockhash: previous_chain_tip.hash.clone(),
2503 block_time: Some(self.updated_at as i64 / 1_000),
2504 block_height: Some(self.chain_tip.index),
2505 executed_transaction_count: num_transactions,
2506 entry_count: 1, };
2508 self.geyser_events_tx
2509 .send(GeyserEvent::NotifyBlockMetadata(block_metadata))
2510 .ok();
2511
2512 let entry_hash = solana_hash::Hash::from_str(&self.chain_tip.hash)
2514 .map(|h| h.to_bytes().to_vec())
2515 .unwrap_or_else(|_| vec![0u8; 32]);
2516 let entry_info = GeyserEntryInfo {
2517 slot,
2518 index: 0, num_hashes: 1,
2520 hash: entry_hash,
2521 executed_transaction_count: num_transactions,
2522 starting_transaction_index: 0,
2523 };
2524 self.geyser_events_tx
2525 .send(GeyserEvent::NotifyEntry(entry_info))
2526 .ok();
2527
2528 let clock: Clock = Clock {
2529 slot: self.latest_epoch_info.absolute_slot,
2530 epoch: self.latest_epoch_info.epoch,
2531 unix_timestamp: self.updated_at as i64 / 1_000,
2532 epoch_start_timestamp: 0, leader_schedule_epoch: 0, };
2535
2536 let _ = self
2537 .simnet_events_tx
2538 .send(SimnetEvent::SystemClockUpdated(clock.clone()));
2539 self.inner.set_sysvar(&clock);
2540
2541 self.finalize_transactions()?;
2542
2543 if root >= self.genesis_slot {
2546 self.geyser_events_tx
2547 .send(GeyserEvent::UpdateSlotStatus {
2548 slot: root,
2549 parent: root.checked_sub(1),
2550 status: GeyserSlotStatus::Rooted,
2551 })
2552 .ok();
2553 self.notify_slots_updates_subscribers(SlotUpdate::Root {
2556 slot: root,
2557 timestamp: slots_update_ts,
2558 });
2559 }
2560
2561 let accounts_to_reset: Vec<_> = self.streamed_accounts.into_iter()?.collect();
2563 for (pubkey_str, include_owned_accounts) in accounts_to_reset {
2564 let pubkey = Pubkey::from_str(&pubkey_str)
2565 .map_err(|e| SurfpoolError::invalid_pubkey(&pubkey_str, e.to_string()))?;
2566 self.reset_account(&pubkey, include_owned_accounts)?;
2567 }
2568
2569 Ok(())
2570 }
2571
2572 pub async fn materialize_overrides(
2581 &mut self,
2582 remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2583 ) -> SurfpoolResult<()> {
2584 let current_slot = self.latest_epoch_info.absolute_slot;
2585
2586 self.materialize_overrides_for_slot(remote_ctx, current_slot)
2587 .await
2588 }
2589
2590 pub async fn materialize_overrides_for_slot(
2592 &mut self,
2593 remote_ctx: &Option<(SurfnetRemoteClient, CommitmentConfig)>,
2594 target_slot: Slot,
2595 ) -> SurfpoolResult<()> {
2596 let Some(overrides) = self.scheduled_overrides.take(&target_slot)? else {
2598 return Ok(());
2600 };
2601
2602 debug!(
2603 "Materializing {} override(s) for slot {}",
2604 overrides.len(),
2605 target_slot
2606 );
2607
2608 for override_instance in overrides {
2609 if !override_instance.enabled {
2610 debug!("Skipping disabled override: {}", override_instance.id);
2611 continue;
2612 }
2613
2614 let account_pubkey = match override_instance
2616 .account
2617 .resolve(Some(&override_instance.values))
2618 {
2619 Some(pubkey) => {
2620 if matches!(
2621 &override_instance.account,
2622 surfpool_types::AccountAddress::Pda { .. }
2623 ) {
2624 debug!(
2625 "Derived PDA {} for override {}",
2626 pubkey, override_instance.id
2627 );
2628 }
2629 pubkey
2630 }
2631 None => {
2632 warn!(
2633 "Failed to resolve account address for override {}",
2634 override_instance.id
2635 );
2636 continue;
2637 }
2638 };
2639
2640 debug!(
2641 "Processing override {} for account {} (label: {:?})",
2642 override_instance.id, account_pubkey, override_instance.label
2643 );
2644
2645 if override_instance.fetch_before_use {
2647 if let Some((client, _)) = remote_ctx {
2648 debug!(
2649 "Fetching fresh account data for {} from remote",
2650 account_pubkey
2651 );
2652
2653 match client
2654 .get_account(&account_pubkey, CommitmentConfig::confirmed())
2655 .await
2656 {
2657 Ok(GetAccountResult::FoundAccount(_pubkey, remote_account, _)) => {
2658 debug!(
2659 "Fetched account {} from remote: {} lamports, {} bytes",
2660 account_pubkey,
2661 remote_account.lamports(),
2662 remote_account.data().len()
2663 );
2664
2665 if let Err(e) = self.inner.set_account(account_pubkey, remote_account) {
2667 warn!(
2668 "Failed to set account {} from remote: {}",
2669 account_pubkey, e
2670 );
2671 }
2672 }
2673 Ok(GetAccountResult::None(_)) => {
2674 debug!("Account {} not found on remote", account_pubkey);
2675 }
2676 Ok(_) => {
2677 debug!("Account {} fetched (other variant)", account_pubkey);
2678 }
2679 Err(e) => {
2680 warn!(
2681 "Failed to fetch account {} from remote: {}",
2682 account_pubkey, e
2683 );
2684 }
2685 }
2686 } else {
2687 debug!(
2688 "fetch_before_use enabled but no remote client available for override {}",
2689 override_instance.id
2690 );
2691 }
2692 }
2693
2694 if !override_instance.values.is_empty() {
2696 let pda_refs = override_instance.account.get_pda_seed_references();
2698 let account_values: HashMap<String, serde_json::Value> = override_instance
2699 .values
2700 .iter()
2701 .filter(|(key, _)| !pda_refs.contains(key))
2702 .map(|(k, v)| (k.clone(), v.clone()))
2703 .collect();
2704
2705 if account_values.is_empty() {
2706 debug!(
2707 "Override {} has no account data modifications (all values are PDA seeds)",
2708 override_instance.id
2709 );
2710 continue;
2711 }
2712
2713 debug!(
2714 "Override {} applying {} field modification(s) to account {} (filtered {} PDA seed refs)",
2715 override_instance.id,
2716 account_values.len(),
2717 account_pubkey,
2718 pda_refs.len()
2719 );
2720
2721 let Some(account) = self.inner.get_account(&account_pubkey)? else {
2723 warn!(
2724 "Account {} not found in SVM for override {}, skipping modifications",
2725 account_pubkey, override_instance.id
2726 );
2727 continue;
2728 };
2729
2730 let owner_program_id = account.owner();
2732
2733 let idl_versions = match self.registered_idls.get(&owner_program_id.to_string()) {
2735 Ok(Some(versions)) => versions,
2736 Ok(None) => {
2737 warn!(
2738 "No IDL registered for program {} (owner of account {}), skipping override {}",
2739 owner_program_id, account_pubkey, override_instance.id
2740 );
2741 continue;
2742 }
2743 Err(e) => {
2744 warn!(
2745 "Failed to get IDL for program {}: {}, skipping override {}",
2746 owner_program_id, e, override_instance.id
2747 );
2748 continue;
2749 }
2750 };
2751
2752 let Some(versioned_idl) = idl_versions.first() else {
2754 warn!(
2755 "IDL versions empty for program {}, skipping override {}",
2756 owner_program_id, override_instance.id
2757 );
2758 continue;
2759 };
2760
2761 let idl = &versioned_idl.1;
2762
2763 let account_data = account.data();
2765
2766 if account_data.len() < 8 {
2768 warn!(
2769 "Account {} has insufficient data ({} bytes) for override {}. \
2770 Enable fetchBeforeUse: true to fetch account data from mainnet first.",
2771 account_pubkey,
2772 account_data.len(),
2773 override_instance.id
2774 );
2775 continue;
2776 }
2777
2778 let new_account_data = match self.get_forged_account_data(
2780 &account_pubkey,
2781 account_data,
2782 idl,
2783 &account_values,
2784 ) {
2785 Ok(data) => data,
2786 Err(e) => {
2787 warn!(
2788 "Failed to forge account data for {} (override {}): {}. \
2789 If the account doesn't exist locally, enable fetchBeforeUse: true.",
2790 account_pubkey, override_instance.id, e
2791 );
2792 continue;
2793 }
2794 };
2795
2796 let modified_account = Account {
2798 lamports: account.lamports(),
2799 data: new_account_data,
2800 owner: *account.owner(),
2801 executable: account.executable(),
2802 rent_epoch: account.rent_epoch(),
2803 };
2804
2805 if let Err(e) = self.inner.set_account(account_pubkey, modified_account) {
2807 warn!(
2808 "Failed to set modified account {} in SVM: {}",
2809 account_pubkey, e
2810 );
2811 } else {
2812 debug!(
2813 "Successfully applied {} override(s) to account {} (override {})",
2814 override_instance.values.len(),
2815 account_pubkey,
2816 override_instance.id
2817 );
2818 }
2819 }
2820 }
2821
2822 Ok(())
2823 }
2824
2825 pub fn get_forged_account_data(
2845 &self,
2846 account_pubkey: &Pubkey,
2847 account_data: &[u8],
2848 idl: &Idl,
2849 overrides: &HashMap<String, serde_json::Value>,
2850 ) -> SurfpoolResult<Vec<u8>> {
2851 if account_data.len() < 8 {
2853 return Err(SurfpoolError::invalid_account_data(
2854 account_pubkey,
2855 "Account data too small to be an Anchor account (need at least 8 bytes for discriminator)",
2856 Some("Data length too small"),
2857 ));
2858 }
2859
2860 let discriminator = &account_data[..8];
2862 let serialized_data = &account_data[8..];
2863
2864 let account_def = idl
2866 .accounts
2867 .iter()
2868 .find(|acc| acc.discriminator.eq(discriminator))
2869 .ok_or_else(|| {
2870 SurfpoolError::internal(format!(
2871 "Account with discriminator '{:?}' not found in IDL",
2872 discriminator
2873 ))
2874 })?;
2875
2876 let account_type = idl
2878 .types
2879 .iter()
2880 .find(|t| t.name == account_def.name)
2881 .ok_or_else(|| {
2882 SurfpoolError::internal(format!(
2883 "Type definition for account '{}' not found in IDL",
2884 account_def.name
2885 ))
2886 })?;
2887
2888 let empty_vec = vec![];
2890 let idl_type_def_generics = idl
2891 .types
2892 .iter()
2893 .find(|t| t.name == account_type.name)
2894 .map(|t| &t.generics);
2895
2896 let (mut parsed_value, leftover_bytes) =
2899 parse_bytes_to_value_with_expected_idl_type_def_ty_with_leftover_bytes(
2900 serialized_data,
2901 &account_type.ty,
2902 &idl.types,
2903 &vec![],
2904 idl_type_def_generics.unwrap_or(&empty_vec),
2905 )
2906 .map_err(|e| {
2907 SurfpoolError::deserialize_error(
2908 "account data",
2909 format!("Failed to deserialize account data using Borsh: {}", e),
2910 )
2911 })?;
2912
2913 for (path, value) in overrides {
2915 apply_override_to_decoded_account(&mut parsed_value, path, value)?;
2916 }
2917
2918 use anchor_lang_idl::types::{IdlGenericArg, IdlType};
2921 let defined_type = IdlType::Defined {
2922 name: account_type.name.clone(),
2923 generics: account_type
2924 .generics
2925 .iter()
2926 .map(|_| IdlGenericArg::Type {
2927 ty: IdlType::String,
2928 })
2929 .collect(),
2930 };
2931
2932 let re_encoded_data =
2934 borsh_encode_value_to_idl_type(&parsed_value, &defined_type, &idl.types, None)
2935 .map_err(|e| {
2936 SurfpoolError::internal(format!(
2937 "Failed to re-encode account data using Borsh: {}",
2938 e
2939 ))
2940 })?;
2941
2942 let mut new_account_data =
2944 Vec::with_capacity(8 + re_encoded_data.len() + leftover_bytes.len());
2945 new_account_data.extend_from_slice(discriminator);
2946 new_account_data.extend_from_slice(&re_encoded_data);
2947 new_account_data.extend_from_slice(leftover_bytes);
2948
2949 Ok(new_account_data)
2950 }
2951
2952 pub fn subscribe_for_signature_updates(
2961 &mut self,
2962 signature: &Signature,
2963 subscription_type: SignatureSubscriptionType,
2964 ) -> Receiver<(Slot, Option<TransactionError>)> {
2965 let (tx, rx) = unbounded();
2966 self.signature_subscriptions
2967 .entry(*signature)
2968 .or_default()
2969 .push((subscription_type, tx));
2970 rx
2971 }
2972
2973 pub fn subscribe_for_account_updates(
2974 &mut self,
2975 account_pubkey: &Pubkey,
2976 encoding: Option<UiAccountEncoding>,
2977 ) -> Receiver<UiAccount> {
2978 let (tx, rx) = unbounded();
2979 self.account_subscriptions
2980 .entry(*account_pubkey)
2981 .or_default()
2982 .push((encoding, tx));
2983 rx
2984 }
2985
2986 pub fn subscribe_for_program_updates(
2987 &mut self,
2988 program_id: &Pubkey,
2989 encoding: Option<UiAccountEncoding>,
2990 filters: Option<Vec<RpcFilterType>>,
2991 ) -> Receiver<RpcKeyedAccount> {
2992 let (tx, rx) = unbounded();
2993 self.program_subscriptions
2994 .entry(*program_id)
2995 .or_default()
2996 .push((encoding, filters, tx));
2997 rx
2998 }
2999
3000 pub fn notify_signature_subscribers(
3008 &mut self,
3009 status: SignatureSubscriptionType,
3010 signature: &Signature,
3011 slot: Slot,
3012 err: Option<TransactionError>,
3013 ) {
3014 let mut remaining = vec![];
3015 if let Some(subscriptions) = self.signature_subscriptions.remove(signature) {
3016 for (subscription_type, tx) in subscriptions {
3017 if status.eq(&subscription_type) {
3018 if tx.send((slot, err.clone())).is_err() {
3019 continue;
3021 }
3022 } else {
3023 remaining.push((subscription_type, tx));
3024 }
3025 }
3026 if !remaining.is_empty() {
3027 self.signature_subscriptions.insert(*signature, remaining);
3028 }
3029 }
3030 }
3031
3032 pub fn notify_account_subscribers(
3033 &mut self,
3034 account_updated_pubkey: &Pubkey,
3035 account: &Account,
3036 ) {
3037 let mut remaining = vec![];
3038 if let Some(subscriptions) = self.account_subscriptions.remove(account_updated_pubkey) {
3039 for (encoding, tx) in subscriptions {
3040 let config = RpcAccountInfoConfig {
3041 encoding,
3042 ..Default::default()
3043 };
3044 let account = self
3045 .account_to_rpc_keyed_account(account_updated_pubkey, account, &config, None)
3046 .account;
3047 if tx.send(account).is_err() {
3048 continue;
3050 } else {
3051 remaining.push((encoding, tx));
3052 }
3053 }
3054 if !remaining.is_empty() {
3055 self.account_subscriptions
3056 .insert(*account_updated_pubkey, remaining);
3057 }
3058 }
3059 }
3060
3061 pub fn notify_program_subscribers(&mut self, account_pubkey: &Pubkey, account: &Account) {
3062 let program_id = account.owner;
3063 let mut remaining = vec![];
3064 if let Some(subscriptions) = self.program_subscriptions.remove(&program_id) {
3065 for (encoding, filters, tx) in subscriptions {
3066 if let Some(ref active_filters) = filters {
3068 match super::locker::apply_rpc_filters(&account.data, active_filters) {
3069 Ok(true) => {} Ok(false) => {
3071 remaining.push((encoding, filters, tx));
3073 continue;
3074 }
3075 Err(_) => {
3076 remaining.push((encoding, filters, tx));
3078 continue;
3079 }
3080 }
3081 }
3082
3083 let config = RpcAccountInfoConfig {
3084 encoding,
3085 ..Default::default()
3086 };
3087 let keyed_account =
3088 self.account_to_rpc_keyed_account(account_pubkey, account, &config, None);
3089 if tx.send(keyed_account).is_err() {
3090 continue;
3092 } else {
3093 remaining.push((encoding, filters, tx));
3094 }
3095 }
3096 if !remaining.is_empty() {
3097 self.program_subscriptions.insert(program_id, remaining);
3098 }
3099 }
3100 }
3101
3102 pub fn get_block_at_slot(
3111 &self,
3112 slot: Slot,
3113 config: &RpcBlockConfig,
3114 ) -> SurfpoolResult<Option<UiConfirmedBlock>> {
3115 let Some(block) = self.get_block_or_reconstruct(slot)? else {
3117 return Ok(None);
3118 };
3119
3120 let show_rewards = config.rewards.unwrap_or(true);
3121 let transaction_details = config
3122 .transaction_details
3123 .unwrap_or(TransactionDetails::Full);
3124
3125 let transactions = match transaction_details {
3126 TransactionDetails::Full => Some(
3127 block
3128 .signatures
3129 .iter()
3130 .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
3131 .map(|tx_with_meta| {
3132 let (meta, _) = tx_with_meta.expect_processed();
3133 meta.encode(
3134 config.encoding.unwrap_or(
3135 solana_transaction_status::UiTransactionEncoding::JsonParsed,
3136 ),
3137 config.max_supported_transaction_version,
3138 show_rewards,
3139 )
3140 })
3141 .collect::<Result<Vec<_>, _>>()
3142 .map_err(SurfpoolError::from)?,
3143 ),
3144 TransactionDetails::Signatures => None,
3145 TransactionDetails::None => None,
3146 TransactionDetails::Accounts => Some(
3147 block
3148 .signatures
3149 .iter()
3150 .filter_map(|sig| self.transactions.get(&sig.to_string()).ok().flatten())
3151 .map(|tx_with_meta| {
3152 let (meta, _) = tx_with_meta.expect_processed();
3153 meta.to_json_accounts(
3154 config.max_supported_transaction_version,
3155 show_rewards,
3156 )
3157 })
3158 .collect::<Result<Vec<_>, _>>()
3159 .map_err(SurfpoolError::from)?,
3160 ),
3161 };
3162
3163 let signatures = match transaction_details {
3164 TransactionDetails::Signatures => {
3165 Some(block.signatures.iter().map(|t| t.to_string()).collect())
3166 }
3167 TransactionDetails::Full | TransactionDetails::Accounts | TransactionDetails::None => {
3168 None
3169 }
3170 };
3171
3172 let block = UiConfirmedBlock {
3173 previous_blockhash: block.previous_blockhash.clone(),
3174 blockhash: block.hash.clone(),
3175 parent_slot: block.parent_slot,
3176 transactions,
3177 signatures,
3178 rewards: if show_rewards { Some(vec![]) } else { None },
3179 num_reward_partitions: None,
3180 block_time: Some(block.block_time / 1000),
3181 block_height: Some(block.block_height),
3182 };
3183 Ok(Some(block))
3184 }
3185
3186 pub fn blockhash_for_slot(&self, slot: Slot) -> Option<Hash> {
3188 self.blocks
3189 .get(&slot)
3190 .unwrap()
3191 .and_then(|header| header.hash.parse().ok())
3192 }
3193
3194 pub fn get_account_owned_by(
3204 &self,
3205 program_id: &Pubkey,
3206 ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
3207 let account_pubkeys = self
3208 .accounts_by_owner
3209 .get(&program_id.to_string())
3210 .ok()
3211 .flatten()
3212 .unwrap_or_default();
3213
3214 account_pubkeys
3215 .iter()
3216 .filter_map(|pk_str| {
3217 let pk = Pubkey::from_str(pk_str).ok()?;
3218 self.get_account(&pk)
3219 .map(|res| res.map(|account| (pk, account.clone())))
3220 .transpose()
3221 })
3222 .collect::<Result<Vec<_>, SurfpoolError>>()
3223 }
3224
3225 fn get_additional_data(
3226 &self,
3227 pubkey: &Pubkey,
3228 token_mint: Option<Pubkey>,
3229 ) -> Option<AccountAdditionalDataV3> {
3230 let token_mint = if let Some(mint) = token_mint {
3231 Some(mint)
3232 } else {
3233 self.token_accounts
3234 .get(&pubkey.to_string())
3235 .ok()
3236 .flatten()
3237 .map(|ta| ta.mint())
3238 };
3239
3240 token_mint.and_then(|mint| {
3241 self.account_associated_data
3242 .get(&mint.to_string())
3243 .ok()
3244 .flatten()
3245 .and_then(|data| data.try_into().ok())
3246 })
3247 }
3248
3249 pub fn account_to_rpc_keyed_account<T: ReadableAccount>(
3250 &self,
3251 pubkey: &Pubkey,
3252 account: &T,
3253 config: &RpcAccountInfoConfig,
3254 token_mint: Option<Pubkey>,
3255 ) -> RpcKeyedAccount {
3256 let additional_data = self.get_additional_data(pubkey, token_mint);
3257
3258 RpcKeyedAccount {
3259 pubkey: pubkey.to_string(),
3260 account: self.encode_ui_account(
3261 pubkey,
3262 account,
3263 config.encoding.unwrap_or(UiAccountEncoding::Base64),
3264 additional_data,
3265 config.data_slice,
3266 ),
3267 }
3268 }
3269
3270 pub fn get_token_accounts_by_delegate(&self, delegate: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
3280 if let Some(account_pubkeys) = self
3281 .token_accounts_by_delegate
3282 .get(&delegate.to_string())
3283 .ok()
3284 .flatten()
3285 {
3286 account_pubkeys
3287 .iter()
3288 .filter_map(|pk_str| {
3289 let pk = Pubkey::from_str(pk_str).ok()?;
3290 self.token_accounts
3291 .get(pk_str)
3292 .ok()
3293 .flatten()
3294 .map(|ta| (pk, ta))
3295 })
3296 .collect()
3297 } else {
3298 Vec::new()
3299 }
3300 }
3301
3302 pub fn get_parsed_token_accounts_by_owner(
3312 &self,
3313 owner: &Pubkey,
3314 ) -> Vec<(Pubkey, TokenAccount)> {
3315 if let Some(account_pubkeys) = self
3316 .token_accounts_by_owner
3317 .get(&owner.to_string())
3318 .ok()
3319 .flatten()
3320 {
3321 account_pubkeys
3322 .iter()
3323 .filter_map(|pk_str| {
3324 let pk = Pubkey::from_str(pk_str).ok()?;
3325 self.token_accounts
3326 .get(pk_str)
3327 .ok()
3328 .flatten()
3329 .map(|ta| (pk, ta))
3330 })
3331 .collect()
3332 } else {
3333 Vec::new()
3334 }
3335 }
3336
3337 pub fn get_token_accounts_by_owner(
3338 &self,
3339 owner: &Pubkey,
3340 ) -> SurfpoolResult<Vec<(Pubkey, Account)>> {
3341 let account_pubkeys = self
3342 .token_accounts_by_owner
3343 .get(&owner.to_string())
3344 .ok()
3345 .flatten()
3346 .unwrap_or_default();
3347
3348 account_pubkeys
3349 .iter()
3350 .filter_map(|pk_str| {
3351 let pk = Pubkey::from_str(pk_str).ok()?;
3352 self.get_account(&pk)
3353 .map(|res| res.map(|account| (pk, account.clone())))
3354 .transpose()
3355 })
3356 .collect::<Result<Vec<_>, SurfpoolError>>()
3357 }
3358
3359 pub fn get_token_accounts_by_mint(&self, mint: &Pubkey) -> Vec<(Pubkey, TokenAccount)> {
3369 if let Some(account_pubkeys) = self
3370 .token_accounts_by_mint
3371 .get(&mint.to_string())
3372 .ok()
3373 .flatten()
3374 {
3375 account_pubkeys
3376 .iter()
3377 .filter_map(|pk_str| {
3378 let pk = Pubkey::from_str(pk_str).ok()?;
3379 self.token_accounts
3380 .get(pk_str)
3381 .ok()
3382 .flatten()
3383 .map(|ta| (pk, ta))
3384 })
3385 .collect()
3386 } else {
3387 Vec::new()
3388 }
3389 }
3390
3391 pub fn subscribe_for_slot_updates(&mut self) -> Receiver<SlotInfo> {
3392 let (tx, rx) = unbounded();
3393 self.slot_subscriptions.push(tx);
3394 rx
3395 }
3396
3397 pub fn notify_slot_subscribers(&mut self, slot: Slot, parent: Slot, root: Slot) {
3398 self.slot_subscriptions
3399 .retain(|tx| tx.send(SlotInfo { slot, parent, root }).is_ok());
3400 }
3401
3402 pub fn subscribe_for_slots_updates(&mut self) -> Receiver<Arc<SlotUpdate>> {
3410 let (tx, rx) = unbounded();
3411 self.slots_updates_subscriptions.push(tx);
3412 rx
3413 }
3414
3415 pub fn notify_slots_updates_subscribers(&mut self, update: SlotUpdate) {
3421 if self.slots_updates_subscriptions.is_empty() {
3422 return;
3423 }
3424 let arc = Arc::new(update);
3425 self.slots_updates_subscriptions
3426 .retain(|tx| tx.send(arc.clone()).is_ok());
3427 }
3428
3429 pub fn write_simulated_profile_result(
3430 &mut self,
3431 uuid: Uuid,
3432 tag: Option<String>,
3433 profile_result: KeyedProfileResult,
3434 ) -> SurfpoolResult<()> {
3435 self.simulated_transaction_profiles
3436 .store(uuid.to_string(), profile_result)?;
3437
3438 let tag = tag.unwrap_or_else(|| uuid.to_string());
3439 let mut tags = self
3440 .profile_tag_map
3441 .get(&tag)
3442 .ok()
3443 .flatten()
3444 .unwrap_or_default();
3445 tags.push(UuidOrSignature::Uuid(uuid));
3446 self.profile_tag_map.store(tag, tags)?;
3447 Ok(())
3448 }
3449
3450 pub fn write_executed_profile_result(
3451 &mut self,
3452 signature: Signature,
3453 profile_result: KeyedProfileResult,
3454 ) -> SurfpoolResult<()> {
3455 self.executed_transaction_profiles
3456 .store(signature.to_string(), profile_result)?;
3457 let tag = signature.to_string();
3458 let mut tags = self
3459 .profile_tag_map
3460 .get(&tag)
3461 .ok()
3462 .flatten()
3463 .unwrap_or_default();
3464 tags.push(UuidOrSignature::Signature(signature));
3465 self.profile_tag_map.store(tag, tags)?;
3466 Ok(())
3467 }
3468
3469 pub fn subscribe_for_logs_updates(
3470 &mut self,
3471 commitment_level: &CommitmentLevel,
3472 filter: &RpcTransactionLogsFilter,
3473 ) -> Receiver<(Slot, RpcLogsResponse)> {
3474 let (tx, rx) = unbounded();
3475 self.logs_subscriptions
3476 .push((*commitment_level, filter.clone(), tx));
3477 rx
3478 }
3479
3480 pub fn notify_logs_subscribers(
3481 &mut self,
3482 signature: &Signature,
3483 err: Option<TransactionError>,
3484 logs: Vec<String>,
3485 commitment_level: CommitmentLevel,
3486 ) {
3487 for (expected_level, filter, tx) in self.logs_subscriptions.iter() {
3488 if !expected_level.eq(&commitment_level) {
3489 continue; }
3491
3492 let should_notify = match filter {
3493 RpcTransactionLogsFilter::All | RpcTransactionLogsFilter::AllWithVotes => true,
3494
3495 RpcTransactionLogsFilter::Mentions(mentioned_accounts) => {
3496 let transaction_accounts =
3498 if let Some(SurfnetTransactionStatus::Processed(tx_data)) =
3499 self.transactions.get(&signature.to_string()).ok().flatten()
3500 {
3501 let (tx_meta, _) = tx_data.as_ref();
3502 let mut accounts =
3503 tx_meta.transaction.message.static_account_keys().to_vec();
3504
3505 accounts.extend(&tx_meta.meta.loaded_addresses.writable);
3506 accounts.extend(&tx_meta.meta.loaded_addresses.readonly);
3507 Some(accounts)
3508 } else {
3509 None
3510 };
3511
3512 let Some(accounts) = transaction_accounts else {
3513 continue;
3514 };
3515
3516 mentioned_accounts.iter().any(|filtered_acc| {
3517 if let Ok(filtered_pubkey) = Pubkey::from_str(&filtered_acc) {
3518 accounts.contains(&filtered_pubkey)
3519 } else {
3520 false
3521 }
3522 })
3523 }
3524 };
3525
3526 if should_notify {
3527 let message = RpcLogsResponse {
3528 signature: signature.to_string(),
3529 err: err.clone().map(|e| e.into()),
3530 logs: logs.clone(),
3531 };
3532 let _ = tx.send((self.get_latest_absolute_slot(), message));
3533 }
3534 }
3535 }
3536
3537 pub fn register_snapshot_subscription(
3540 &mut self,
3541 ) -> (
3542 Sender<super::SnapshotImportNotification>,
3543 Receiver<super::SnapshotImportNotification>,
3544 ) {
3545 let (tx, rx) = unbounded();
3546 self.snapshot_subscriptions.push(tx.clone());
3547 (tx, rx)
3548 }
3549
3550 pub async fn fetch_snapshot_from_url(
3551 snapshot_url: &str,
3552 ) -> Result<
3553 std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>>,
3554 Box<dyn std::error::Error + Send + Sync>,
3555 > {
3556 let response = reqwest::get(snapshot_url).await?;
3557 let text = response.text().await?;
3558
3559 let snapshot: std::collections::BTreeMap<String, Option<surfpool_types::AccountSnapshot>> =
3561 serde_json::from_str(&text)?;
3562
3563 Ok(snapshot)
3564 }
3565
3566 pub fn register_idl(&mut self, idl: Idl, slot: Option<Slot>) -> SurfpoolResult<()> {
3567 let slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3568 let program_id = Pubkey::from_str_const(&idl.address);
3569 let program_id_str = program_id.to_string();
3570 let mut idl_versions = self
3571 .registered_idls
3572 .get(&program_id_str)
3573 .ok()
3574 .flatten()
3575 .unwrap_or_default();
3576 idl_versions.push(VersionedIdl(slot, idl));
3577 idl_versions.sort_by(|a, b| b.0.cmp(&a.0));
3579 self.registered_idls.store(program_id_str, idl_versions)?;
3580 Ok(())
3581 }
3582
3583 fn encode_ui_account_profile_state(
3584 &self,
3585 pubkey: &Pubkey,
3586 account_profile_state: AccountProfileState,
3587 encoding: &UiAccountEncoding,
3588 ) -> UiAccountProfileState {
3589 let additional_data = self.get_additional_data(pubkey, None);
3590
3591 match account_profile_state {
3592 AccountProfileState::Readonly => UiAccountProfileState::Readonly,
3593 AccountProfileState::Writable(account_change) => {
3594 let change = match account_change {
3595 AccountChange::Create(account) => UiAccountChange::Create(
3596 self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3597 ),
3598 AccountChange::Update(account_before, account_after) => {
3599 UiAccountChange::Update(
3600 self.encode_ui_account(
3601 pubkey,
3602 &account_before,
3603 *encoding,
3604 additional_data,
3605 None,
3606 ),
3607 self.encode_ui_account(
3608 pubkey,
3609 &account_after,
3610 *encoding,
3611 additional_data,
3612 None,
3613 ),
3614 )
3615 }
3616 AccountChange::Delete(account) => UiAccountChange::Delete(
3617 self.encode_ui_account(pubkey, &account, *encoding, additional_data, None),
3618 ),
3619 AccountChange::Unchanged(account) => {
3620 UiAccountChange::Unchanged(account.map(|account| {
3621 self.encode_ui_account(
3622 pubkey,
3623 &account,
3624 *encoding,
3625 additional_data,
3626 None,
3627 )
3628 }))
3629 }
3630 };
3631 UiAccountProfileState::Writable(change)
3632 }
3633 }
3634 }
3635
3636 fn encode_ui_profile_result(
3637 &self,
3638 profile_result: ProfileResult,
3639 readonly_accounts: &[Pubkey],
3640 encoding: &UiAccountEncoding,
3641 ) -> UiProfileResult {
3642 let ProfileResult {
3643 pre_execution_capture,
3644 post_execution_capture,
3645 compute_units_consumed,
3646 log_messages,
3647 error_message,
3648 } = profile_result;
3649
3650 let account_states = pre_execution_capture
3651 .into_iter()
3652 .zip(post_execution_capture)
3653 .map(|((pubkey, pre_account), (_, post_account))| {
3654 let state =
3661 AccountProfileState::new(pubkey, pre_account, post_account, readonly_accounts);
3662 (
3663 pubkey,
3664 self.encode_ui_account_profile_state(&pubkey, state, encoding),
3665 )
3666 })
3667 .collect::<IndexMap<Pubkey, UiAccountProfileState>>();
3668
3669 UiProfileResult {
3670 account_states,
3671 compute_units_consumed,
3672 log_messages,
3673 error_message,
3674 }
3675 }
3676
3677 pub fn encode_ui_keyed_profile_result(
3678 &self,
3679 keyed_profile_result: KeyedProfileResult,
3680 config: &RpcProfileResultConfig,
3681 ) -> UiKeyedProfileResult {
3682 let KeyedProfileResult {
3683 slot,
3684 key,
3685 instruction_profiles,
3686 transaction_profile,
3687 readonly_account_states,
3688 } = keyed_profile_result;
3689
3690 let encoding = config.encoding.unwrap_or(UiAccountEncoding::JsonParsed);
3691
3692 let readonly_accounts = readonly_account_states.keys().cloned().collect::<Vec<_>>();
3693
3694 let default = RpcProfileDepth::default();
3695 let instruction_profiles = match *config.depth.as_ref().unwrap_or(&default) {
3696 RpcProfileDepth::Transaction => None,
3697 RpcProfileDepth::Instruction => instruction_profiles.map(|instruction_profiles| {
3698 instruction_profiles
3699 .into_iter()
3700 .map(|p| self.encode_ui_profile_result(p, &readonly_accounts, &encoding))
3701 .collect()
3702 }),
3703 };
3704
3705 let transaction_profile =
3706 self.encode_ui_profile_result(transaction_profile, &readonly_accounts, &encoding);
3707
3708 let readonly_account_states = readonly_account_states
3709 .into_iter()
3710 .map(|(pubkey, account)| {
3711 let account = self.encode_ui_account(&pubkey, &account, encoding, None, None);
3712 (pubkey, account)
3713 })
3714 .collect();
3715
3716 UiKeyedProfileResult {
3717 slot,
3718 key,
3719 instruction_profiles,
3720 transaction_profile,
3721 readonly_account_states,
3722 }
3723 }
3724
3725 pub fn encode_ui_account<T: ReadableAccount>(
3726 &self,
3727 pubkey: &Pubkey,
3728 account: &T,
3729 encoding: UiAccountEncoding,
3730 additional_data: Option<AccountAdditionalDataV3>,
3731 data_slice_config: Option<UiDataSliceConfig>,
3732 ) -> UiAccount {
3733 let owner_program_id = account.owner();
3734 let data = account.data();
3735
3736 if encoding == UiAccountEncoding::JsonParsed {
3737 if let Ok(Some(registered_idls)) =
3738 self.registered_idls.get(&owner_program_id.to_string())
3739 {
3740 let filter_slot = self.latest_epoch_info.absolute_slot;
3741 let ordered_available_idls = registered_idls
3743 .iter()
3744 .filter_map(|VersionedIdl(slot, idl)| {
3746 if *slot <= filter_slot {
3747 Some(idl)
3748 } else {
3749 None
3750 }
3751 })
3752 .collect::<Vec<_>>();
3753
3754 for idl in &ordered_available_idls {
3758 let discriminator = &data[..8];
3760 if let Some(matching_account) = idl
3761 .accounts
3762 .iter()
3763 .find(|a| a.discriminator.eq(&discriminator))
3764 {
3765 if let Some(account_type) =
3767 idl.types.iter().find(|t| t.name == matching_account.name)
3768 {
3769 let empty_vec = vec![];
3770 let idl_type_def_generics = idl
3771 .types
3772 .iter()
3773 .find(|t| t.name == account_type.name)
3774 .map(|t| &t.generics);
3775
3776 let rest = data[8..].as_ref();
3778 if let Ok(parsed_value) =
3779 parse_bytes_to_value_with_expected_idl_type_def_ty(
3780 rest,
3781 &account_type.ty,
3782 &idl.types,
3783 &vec![],
3784 idl_type_def_generics.unwrap_or(&empty_vec),
3785 )
3786 {
3787 return UiAccount {
3788 lamports: account.lamports(),
3789 data: UiAccountData::Json(ParsedAccount {
3790 program: idl
3791 .metadata
3792 .name
3793 .to_string()
3794 .to_case(convert_case::Case::Kebab),
3795 parsed: parsed_value
3796 .to_json(Some(&get_txtx_value_json_converters())),
3797 space: data.len() as u64,
3798 }),
3799 owner: owner_program_id.to_string(),
3800 executable: account.executable(),
3801 rent_epoch: account.rent_epoch(),
3802 space: Some(data.len() as u64),
3803 };
3804 }
3805 }
3806 }
3807 }
3808 }
3809 }
3810
3811 encode_ui_account(
3813 pubkey,
3814 account,
3815 encoding,
3816 additional_data,
3817 data_slice_config,
3818 )
3819 }
3820
3821 pub fn get_account(&self, pubkey: &Pubkey) -> SurfpoolResult<Option<Account>> {
3822 self.inner.get_account(pubkey)
3823 }
3824
3825 pub fn get_all_accounts(&self) -> SurfpoolResult<Vec<(Pubkey, AccountSharedData)>> {
3826 self.inner.get_all_accounts()
3827 }
3828
3829 pub fn get_transaction(
3830 &self,
3831 signature: &Signature,
3832 ) -> SurfpoolResult<Option<SurfnetTransactionStatus>> {
3833 Ok(self.transactions.get(&signature.to_string())?)
3834 }
3835
3836 pub fn start_runbook_execution(&mut self, runbook_id: String) {
3837 self.runbook_executions
3838 .push(RunbookExecutionStatusReport::new(runbook_id));
3839 }
3840
3841 pub fn complete_runbook_execution(&mut self, runbook_id: &str, error: Option<Vec<String>>) {
3842 if let Some(execution) = self
3843 .runbook_executions
3844 .iter_mut()
3845 .find(|e| e.runbook_id.eq(runbook_id) && e.completed_at.is_none())
3846 {
3847 execution.mark_completed(error);
3848 }
3849 }
3850
3851 pub fn export_snapshot(
3859 &self,
3860 config: ExportSnapshotConfig,
3861 ) -> SurfpoolResult<BTreeMap<String, AccountSnapshot>> {
3862 let mut fixtures = BTreeMap::new();
3863 let encoding = if config.include_parsed_accounts.unwrap_or_default() {
3864 UiAccountEncoding::JsonParsed
3865 } else {
3866 UiAccountEncoding::Base64
3867 };
3868 let filter = config.filter.unwrap_or_default();
3869 let include_program_accounts = filter.include_program_accounts.unwrap_or(false);
3870 let include_accounts = filter.include_accounts.unwrap_or_default();
3871 let exclude_accounts = filter.exclude_accounts.unwrap_or_default();
3872 let exclude_sysvars = filter.exclude_sysvars.unwrap_or(false);
3873 let exclude_feature_gates = filter.exclude_feature_gates.unwrap_or(false);
3874
3875 fn is_program_account(pubkey: &Pubkey) -> bool {
3876 pubkey == &bpf_loader::id()
3877 || pubkey == &solana_sdk_ids::bpf_loader_deprecated::id()
3878 || pubkey == &solana_sdk_ids::bpf_loader_upgradeable::id()
3879 }
3880
3881 let mut process_account = |pubkey: &Pubkey, account: &Account| {
3883 let is_include_account = include_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3884 let is_exclude_account = exclude_accounts.iter().any(|k| k.eq(&pubkey.to_string()));
3885 let is_program_account = is_program_account(&account.owner);
3886 if is_exclude_account
3887 || ((is_program_account && !include_program_accounts) && !is_include_account)
3888 {
3889 return;
3890 }
3891 if !is_include_account {
3892 if exclude_sysvars && account.owner == solana_sdk_ids::sysvar::id() {
3893 return;
3894 }
3895 if exclude_feature_gates && agave_feature_set::FEATURE_NAMES.contains_key(pubkey) {
3896 return;
3897 }
3898 }
3899
3900 let additional_data: Option<AccountAdditionalDataV3> = if account.owner
3902 == spl_token_interface::id()
3903 || account.owner == spl_token_2022_interface::id()
3904 {
3905 if let Ok(token_account) = TokenAccount::unpack(&account.data) {
3906 self.account_associated_data
3907 .get(&token_account.mint().to_string())
3908 .ok()
3909 .flatten()
3910 .and_then(|data| data.try_into().ok())
3911 } else {
3912 self.account_associated_data
3913 .get(&pubkey.to_string())
3914 .ok()
3915 .flatten()
3916 .and_then(|data| data.try_into().ok())
3917 }
3918 } else {
3919 self.account_associated_data
3920 .get(&pubkey.to_string())
3921 .ok()
3922 .flatten()
3923 .and_then(|data| data.try_into().ok())
3924 };
3925
3926 let ui_account =
3927 self.encode_ui_account(pubkey, account, encoding, additional_data, None);
3928
3929 let (base64, parsed_data) = match ui_account.data {
3930 UiAccountData::Json(parsed_account) => {
3931 (BASE64_STANDARD.encode(account.data()), Some(parsed_account))
3932 }
3933 UiAccountData::Binary(base64, _) => (base64, None),
3934 UiAccountData::LegacyBinary(_) => unreachable!(),
3935 };
3936
3937 let account_snapshot = AccountSnapshot::new(
3938 account.lamports,
3939 account.owner.to_string(),
3940 account.executable,
3941 account.rent_epoch,
3942 base64,
3943 parsed_data,
3944 );
3945
3946 fixtures.insert(pubkey.to_string(), account_snapshot);
3947 };
3948
3949 match &config.scope {
3950 ExportSnapshotScope::Network => {
3951 for (pubkey, account_shared_data) in self.get_all_accounts()? {
3953 let account = Account::from(account_shared_data.clone());
3954 process_account(&pubkey, &account);
3955 }
3956 }
3957 ExportSnapshotScope::PreTransaction(signature_str) => {
3958 if let Ok(signature) = Signature::from_str(signature_str) {
3960 if let Ok(Some(profile)) = self
3961 .executed_transaction_profiles
3962 .get(&signature.to_string())
3963 {
3964 for (pubkey, account_opt) in
3967 &profile.transaction_profile.pre_execution_capture
3968 {
3969 if let Some(account) = account_opt {
3970 process_account(pubkey, account);
3971 }
3972 }
3973
3974 for (pubkey, account) in &profile.readonly_account_states {
3976 process_account(pubkey, account);
3977 }
3978 }
3979 }
3980 }
3981 }
3982
3983 Ok(fixtures)
3984 }
3985
3986 pub fn register_scenario(
3991 &mut self,
3992 scenario: surfpool_types::Scenario,
3993 slot: Option<Slot>,
3994 ) -> SurfpoolResult<()> {
3995 let base_slot = slot.unwrap_or(self.latest_epoch_info.absolute_slot);
3997
3998 info!(
3999 "Registering scenario: {} ({}) with {} overrides at base slot {}",
4000 scenario.name,
4001 scenario.id,
4002 scenario.overrides.len(),
4003 base_slot
4004 );
4005
4006 for override_instance in scenario.overrides {
4008 let scenario_relative_slot = override_instance.scenario_relative_slot;
4009 let absolute_slot = base_slot + scenario_relative_slot;
4010
4011 debug!(
4012 "Scheduling override at absolute slot {} (base {} + relative {})",
4013 absolute_slot, base_slot, scenario_relative_slot
4014 );
4015
4016 let mut slot_overrides = self
4017 .scheduled_overrides
4018 .get(&absolute_slot)
4019 .ok()
4020 .flatten()
4021 .unwrap_or_default();
4022 slot_overrides.push(override_instance);
4023 self.scheduled_overrides
4024 .store(absolute_slot, slot_overrides)?;
4025 }
4026
4027 Ok(())
4028 }
4029}
4030
4031#[cfg(test)]
4032mod tests {
4033 use agave_feature_set::{
4034 blake3_syscall_enabled, curve25519_syscall_enabled, disable_fees_sysvar,
4035 enable_extend_program_checked, enable_loader_v4, enable_sbpf_v1_deployment_and_execution,
4036 enable_sbpf_v2_deployment_and_execution, enable_sbpf_v3_deployment_and_execution,
4037 formalize_loaded_transaction_data_size, move_precompile_verification_to_svm,
4038 raise_cpi_nesting_limit_to_8, stake_raise_minimum_delegation_to_1_sol,
4039 };
4040 use base64::{Engine, engine::general_purpose};
4041 use borsh::BorshSerialize;
4042 use solana_account::Account;
4044 use solana_hash::Hash;
4045 use solana_keypair::Keypair;
4046 use solana_loader_v3_interface::get_program_data_address;
4047 use solana_message::VersionedMessage;
4048 use solana_program_pack::Pack;
4049 use solana_signer::Signer;
4050 use solana_system_interface::instruction as system_instruction;
4051 use solana_transaction::Transaction;
4052 use solana_transaction_error::TransactionError;
4053 use spl_token_interface::state::{Account as TokenAccount, AccountState};
4054 use surfpool_types::ExportSnapshotFilter;
4055 use test_case::test_case;
4056
4057 use super::*;
4058 use crate::storage::tests::TestType;
4059
4060 fn build_transfer_transaction(
4061 payer: &Keypair,
4062 recipient: &Pubkey,
4063 lamports: u64,
4064 recent_blockhash: Hash,
4065 ) -> VersionedTransaction {
4066 let tx = Transaction::new_signed_with_payer(
4067 &[system_instruction::transfer(
4068 &payer.pubkey(),
4069 recipient,
4070 lamports,
4071 )],
4072 Some(&payer.pubkey()),
4073 &[payer],
4074 recent_blockhash,
4075 );
4076
4077 VersionedTransaction {
4078 signatures: tx.signatures,
4079 message: VersionedMessage::Legacy(tx.message),
4080 }
4081 }
4082
4083 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4084 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4085 #[test_case(TestType::no_db(); "with no db")]
4086 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4087 fn test_synthetic_blockhash_generation(test_type: TestType) {
4088 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4089
4090 let test_cases = vec![0, 1, 42, 255, 1000, 0x12345678];
4092
4093 for index in test_cases {
4094 svm.chain_tip = BlockIdentifier::new(index, "test_hash");
4095
4096 let new_blockhash = svm.new_blockhash();
4098
4099 let blockhash_str = new_blockhash.hash.clone();
4101 println!("Index {} -> Blockhash: {}", index, blockhash_str);
4102
4103 assert!(!blockhash_str.is_empty());
4105 assert!(blockhash_str.len() > 20); svm.chain_tip = BlockIdentifier::new(index, "test_hash");
4109 let new_blockhash2 = svm.new_blockhash();
4110 assert_eq!(new_blockhash.hash, new_blockhash2.hash);
4111 }
4112 }
4113
4114 #[test]
4115 fn test_synthetic_blockhash_base58_encoding() {
4116 let test_index = 42u64;
4118 let index_hex = format!("{:08x}", test_index)
4119 .replace('0', "x")
4120 .replace('O', "x");
4121
4122 let target_length = 43;
4123 let padding_needed = target_length - SyntheticBlockhash::PREFIX.len() - index_hex.len();
4124 let padding = "x".repeat(padding_needed.max(0));
4125 let target_string = format!("{}{}{}", SyntheticBlockhash::PREFIX, padding, index_hex);
4126
4127 println!("Target string: {}", target_string);
4128
4129 let decoded_bytes = bs58::decode(&target_string).into_vec();
4131 assert!(decoded_bytes.is_ok(), "String should be valid base58");
4132
4133 let bytes = decoded_bytes.unwrap();
4134 assert!(bytes.len() <= 32, "Decoded bytes should fit in 32 bytes");
4135
4136 let mut blockhash_bytes = [0u8; 32];
4138 blockhash_bytes[..bytes.len().min(32)].copy_from_slice(&bytes[..bytes.len().min(32)]);
4139 let hash = Hash::new_from_array(blockhash_bytes);
4140
4141 let hash_str = hash.to_string();
4143 assert!(!hash_str.is_empty());
4144 println!("Generated hash: {}", hash_str);
4145 }
4146
4147 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4148 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4149 #[test_case(TestType::no_db(); "with no db")]
4150 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4151 fn test_blockhash_consistency_across_calls(test_type: TestType) {
4152 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4153
4154 svm.chain_tip = BlockIdentifier::new(123, "initial_hash");
4156
4157 let mut previous_hash: Option<BlockIdentifier> = None;
4159 for i in 0..5 {
4160 let new_blockhash = svm.new_blockhash();
4161 println!(
4162 "Call {}: index={}, hash={}",
4163 i, new_blockhash.index, new_blockhash.hash
4164 );
4165
4166 if let Some(prev) = previous_hash {
4167 assert_eq!(new_blockhash.index, prev.index + 1);
4169 assert_ne!(new_blockhash.hash, prev.hash);
4171 } else {
4172 assert_eq!(new_blockhash.index, svm.chain_tip.index + 1);
4174 }
4175
4176 previous_hash = Some(new_blockhash.clone());
4177 svm.chain_tip = new_blockhash;
4179 }
4180 }
4181
4182 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4183 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4184 #[test_case(TestType::no_db(); "with no db")]
4185 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4186 fn test_token_account_indexing(test_type: TestType) {
4187 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4188
4189 let owner = Pubkey::new_unique();
4190 let delegate = Pubkey::new_unique();
4191 let mint = Pubkey::new_unique();
4192 let token_account_pubkey = Pubkey::new_unique();
4193
4194 let mut token_account_data = [0u8; TokenAccount::LEN];
4196 let token_account = TokenAccount {
4197 mint,
4198 owner,
4199 amount: 1000,
4200 delegate: COption::Some(delegate),
4201 state: AccountState::Initialized,
4202 is_native: COption::None,
4203 delegated_amount: 500,
4204 close_authority: COption::None,
4205 };
4206 token_account.pack_into_slice(&mut token_account_data);
4207
4208 let account = Account {
4209 lamports: 1000000,
4210 data: token_account_data.to_vec(),
4211 owner: spl_token_interface::id(),
4212 executable: false,
4213 rent_epoch: 0,
4214 };
4215
4216 svm.set_account(&token_account_pubkey, account).unwrap();
4217
4218 assert_eq!(svm.token_accounts.keys().unwrap().len(), 1);
4220
4221 let owner_accounts = svm.get_parsed_token_accounts_by_owner(&owner);
4223 assert_eq!(owner_accounts.len(), 1);
4224 assert_eq!(owner_accounts[0].0, token_account_pubkey);
4225
4226 let delegate_accounts = svm.get_token_accounts_by_delegate(&delegate);
4228 assert_eq!(delegate_accounts.len(), 1);
4229 assert_eq!(delegate_accounts[0].0, token_account_pubkey);
4230
4231 let mint_accounts = svm.get_token_accounts_by_mint(&mint);
4233 assert_eq!(mint_accounts.len(), 1);
4234 assert_eq!(mint_accounts[0].0, token_account_pubkey);
4235 }
4236
4237 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4238 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4239 #[test_case(TestType::no_db(); "with no db")]
4240 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4241 fn test_account_update_removes_old_indexes(test_type: TestType) {
4242 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4243
4244 let owner = Pubkey::new_unique();
4245 let old_delegate = Pubkey::new_unique();
4246 let new_delegate = Pubkey::new_unique();
4247 let mint = Pubkey::new_unique();
4248 let token_account_pubkey = Pubkey::new_unique();
4249
4250 let mut token_account_data = [0u8; TokenAccount::LEN];
4252 let token_account = TokenAccount {
4253 mint,
4254 owner,
4255 amount: 1000,
4256 delegate: COption::Some(old_delegate),
4257 state: AccountState::Initialized,
4258 is_native: COption::None,
4259 delegated_amount: 500,
4260 close_authority: COption::None,
4261 };
4262 token_account.pack_into_slice(&mut token_account_data);
4263
4264 let account = Account {
4265 lamports: 1000000,
4266 data: token_account_data.to_vec(),
4267 owner: spl_token_interface::id(),
4268 executable: false,
4269 rent_epoch: 0,
4270 };
4271
4272 svm.set_account(&token_account_pubkey, account).unwrap();
4274
4275 assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 1);
4277 assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 0);
4278
4279 let updated_token_account = TokenAccount {
4281 mint,
4282 owner,
4283 amount: 1000,
4284 delegate: COption::Some(new_delegate),
4285 state: AccountState::Initialized,
4286 is_native: COption::None,
4287 delegated_amount: 500,
4288 close_authority: COption::None,
4289 };
4290 updated_token_account.pack_into_slice(&mut token_account_data);
4291
4292 let updated_account = Account {
4293 lamports: 1000000,
4294 data: token_account_data.to_vec(),
4295 owner: spl_token_interface::id(),
4296 executable: false,
4297 rent_epoch: 0,
4298 };
4299
4300 svm.set_account(&token_account_pubkey, updated_account)
4302 .unwrap();
4303
4304 assert_eq!(svm.get_token_accounts_by_delegate(&old_delegate).len(), 0);
4306 assert_eq!(svm.get_token_accounts_by_delegate(&new_delegate).len(), 1);
4307 assert_eq!(svm.get_parsed_token_accounts_by_owner(&owner).len(), 1);
4308 }
4309
4310 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4311 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4312 #[test_case(TestType::no_db(); "with no db")]
4313 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4314 fn test_non_token_accounts_not_indexed(test_type: TestType) {
4315 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4316
4317 let system_account_pubkey = Pubkey::new_unique();
4318 let account = Account {
4319 lamports: 1000000,
4320 data: vec![],
4321 owner: solana_system_interface::program::id(), executable: false,
4323 rent_epoch: 0,
4324 };
4325
4326 svm.set_account(&system_account_pubkey, account).unwrap();
4327
4328 assert_eq!(svm.token_accounts.keys().unwrap().len(), 0);
4330 assert_eq!(svm.token_accounts_by_owner.keys().unwrap().len(), 0);
4331 assert_eq!(svm.token_accounts_by_delegate.keys().unwrap().len(), 0);
4332 assert_eq!(svm.token_accounts_by_mint.keys().unwrap().len(), 0);
4333 }
4334
4335 fn expect_account_update_event(
4336 events_rx: &Receiver<SimnetEvent>,
4337 svm: &SurfnetSvm,
4338 pubkey: &Pubkey,
4339 expected_account: &Account,
4340 ) -> bool {
4341 match events_rx.recv() {
4342 Ok(event) => match event {
4343 SimnetEvent::AccountUpdate(_, account_pubkey) => {
4344 assert_eq!(pubkey, &account_pubkey);
4345 assert_eq!(
4346 svm.get_account(&pubkey).unwrap().as_ref(),
4347 Some(expected_account)
4348 );
4349 true
4350 }
4351 event => {
4352 println!("unexpected simnet event: {:?}", event);
4353 false
4354 }
4355 },
4356 Err(_) => false,
4357 }
4358 }
4359
4360 fn _expect_error_event(events_rx: &Receiver<SimnetEvent>, expected_error: &str) -> bool {
4361 match events_rx.recv() {
4362 Ok(event) => match event {
4363 SimnetEvent::ErrorLog(_, err) => {
4364 assert_eq!(err, expected_error);
4365
4366 true
4367 }
4368 event => {
4369 println!("unexpected simnet event: {:?}", event);
4370 false
4371 }
4372 },
4373 Err(_) => false,
4374 }
4375 }
4376
4377 fn create_program_accounts() -> (Pubkey, Account, Pubkey, Account) {
4378 let program_pubkey = Pubkey::new_unique();
4379 let program_data_address = get_program_data_address(&program_pubkey);
4380 let program_account = Account {
4381 lamports: 1000000000000,
4382 data: bincode::serialize(
4383 &solana_loader_v3_interface::state::UpgradeableLoaderState::Program {
4384 programdata_address: program_data_address,
4385 },
4386 )
4387 .unwrap(),
4388 owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4389 executable: true,
4390 rent_epoch: 10000000000000,
4391 };
4392
4393 let mut bin = include_bytes!("../tests/assets/metaplex_program.bin").to_vec();
4394 let mut data = bincode::serialize(
4395 &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4396 slot: 0,
4397 upgrade_authority_address: Some(Pubkey::new_unique()),
4398 },
4399 )
4400 .unwrap();
4401 data.append(&mut bin); let program_data_account = Account {
4403 lamports: 10000000000000,
4404 data,
4405 owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4406 executable: false,
4407 rent_epoch: 10000000000000,
4408 };
4409 (
4410 program_pubkey,
4411 program_account,
4412 program_data_address,
4413 program_data_account,
4414 )
4415 }
4416
4417 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4418 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4419 #[test_case(TestType::no_db(); "with no db")]
4420 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4421 fn test_inserting_account_updates(test_type: TestType) {
4422 let (mut svm, events_rx, _geyser_rx) = test_type.initialize_svm();
4423
4424 let pubkey = Pubkey::new_unique();
4425 let account = Account {
4426 lamports: 1000,
4427 data: vec![1, 2, 3],
4428 owner: Pubkey::new_unique(),
4429 executable: false,
4430 rent_epoch: 0,
4431 };
4432
4433 {
4435 let index_before = svm.get_all_accounts().unwrap();
4436 let empty_update = GetAccountResult::None(pubkey);
4437 svm.write_account_update(empty_update);
4438 assert_eq!(svm.get_all_accounts().unwrap(), index_before);
4439 }
4440
4441 {
4443 let index_before = svm.get_all_accounts().unwrap();
4444 let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), false);
4445 svm.write_account_update(found_update);
4446 assert_eq!(svm.get_all_accounts().unwrap(), index_before);
4447 }
4448
4449 {
4451 let index_before = svm.get_all_accounts().unwrap();
4452 let found_update = GetAccountResult::FoundAccount(pubkey, account.clone(), true);
4453 svm.write_account_update(found_update);
4454 assert_eq!(
4455 svm.get_all_accounts().unwrap().len(),
4456 index_before.len() + 1
4457 );
4458 if !expect_account_update_event(&events_rx, &svm, &pubkey, &account) {
4459 panic!(
4460 "Expected account update event not received after GetAccountResult::FoundAccount update"
4461 );
4462 }
4463 }
4464
4465 {
4467 let (program_address, program_account, program_data_address, _) =
4468 create_program_accounts();
4469
4470 let mut data = bincode::serialize(
4471 &solana_loader_v3_interface::state::UpgradeableLoaderState::ProgramData {
4472 slot: svm.get_latest_absolute_slot(),
4473 upgrade_authority_address: Some(system_program::id()),
4474 },
4475 )
4476 .unwrap();
4477
4478 let mut bin = crate::surfnet::noop_program::NOOP_PROGRAM_ELF.to_vec();
4479 data.append(&mut bin); let lamports = svm.inner.minimum_balance_for_rent_exemption(data.len());
4481 let default_program_data_account = Account {
4482 lamports,
4483 data,
4484 owner: solana_sdk_ids::bpf_loader_upgradeable::ID,
4485 executable: false,
4486 rent_epoch: 0,
4487 };
4488
4489 let index_before = svm.get_all_accounts().unwrap();
4490 let found_program_account_update = GetAccountResult::FoundProgramAccount(
4491 (program_address, program_account.clone()),
4492 (program_data_address, None),
4493 );
4494 svm.write_account_update(found_program_account_update);
4495
4496 if !expect_account_update_event(
4497 &events_rx,
4498 &svm,
4499 &program_data_address,
4500 &default_program_data_account,
4501 ) {
4502 panic!(
4503 "Expected account update event not received after inserting default program data account"
4504 );
4505 }
4506
4507 if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4508 panic!(
4509 "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
4510 );
4511 }
4512 assert_eq!(
4513 svm.get_all_accounts().unwrap().len(),
4514 index_before.len() + 2
4515 );
4516 }
4517
4518 {
4520 let (program_address, program_account, program_data_address, program_data_account) =
4521 create_program_accounts();
4522
4523 let index_before = svm.get_all_accounts().unwrap();
4524 let found_program_account_update = GetAccountResult::FoundProgramAccount(
4525 (program_address, program_account.clone()),
4526 (program_data_address, Some(program_data_account.clone())),
4527 );
4528 svm.write_account_update(found_program_account_update);
4529 assert_eq!(
4530 svm.get_all_accounts().unwrap().len(),
4531 index_before.len() + 2
4532 );
4533 if !expect_account_update_event(
4534 &events_rx,
4535 &svm,
4536 &program_data_address,
4537 &program_data_account,
4538 ) {
4539 panic!(
4540 "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program data pubkey"
4541 );
4542 }
4543
4544 if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4545 panic!(
4546 "Expected account update event not received after GetAccountResult::FoundProgramAccount update for program pubkey"
4547 );
4548 }
4549 }
4550
4551 {
4554 let (program_address, program_account, program_data_address, program_data_account) =
4555 create_program_accounts();
4556
4557 let index_before = svm.get_all_accounts().unwrap();
4558 let found_update = GetAccountResult::FoundAccount(
4559 program_data_address,
4560 program_data_account.clone(),
4561 true,
4562 );
4563 svm.write_account_update(found_update);
4564 assert_eq!(
4565 svm.get_all_accounts().unwrap().len(),
4566 index_before.len() + 1
4567 );
4568 if !expect_account_update_event(
4569 &events_rx,
4570 &svm,
4571 &program_data_address,
4572 &program_data_account,
4573 ) {
4574 panic!(
4575 "Expected account update event not received after GetAccountResult::FoundAccount update"
4576 );
4577 }
4578
4579 let index_before = svm.get_all_accounts().unwrap();
4580 let program_account_found_update = GetAccountResult::FoundProgramAccount(
4581 (program_address, program_account.clone()),
4582 (program_data_address, None),
4583 );
4584 svm.write_account_update(program_account_found_update);
4585 assert_eq!(
4586 svm.get_all_accounts().unwrap().len(),
4587 index_before.len() + 1
4588 );
4589 if !expect_account_update_event(&events_rx, &svm, &program_address, &program_account) {
4590 panic!(
4591 "Expected account update event not received after GetAccountResult::FoundAccount update"
4592 );
4593 }
4594 }
4595 }
4596
4597 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4598 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4599 #[test_case(TestType::no_db(); "with no db")]
4600 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4601 fn test_encode_ui_account(test_type: TestType) {
4602 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4603
4604 let idl_v1: Idl =
4605 serde_json::from_slice(&include_bytes!("../tests/assets/idl_v1.json").to_vec())
4606 .unwrap();
4607
4608 svm.register_idl(idl_v1.clone(), Some(0)).unwrap();
4609
4610 let account_pubkey = Pubkey::new_unique();
4611
4612 #[derive(borsh::BorshSerialize)]
4613 pub struct CustomAccount {
4614 pub my_custom_data: u64,
4615 pub another_field: String,
4616 pub bool: bool,
4617 pub pubkey: Pubkey,
4618 }
4619
4620 {
4622 let account_data = vec![0; 100];
4623 let base64_data = general_purpose::STANDARD.encode(&account_data);
4624 let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4625 let account = Account {
4626 lamports: 1000,
4627 data: account_data,
4628 owner: idl_v1.address.parse().unwrap(),
4629 executable: false,
4630 rent_epoch: 0,
4631 };
4632
4633 let ui_account = svm.encode_ui_account(
4634 &account_pubkey,
4635 &account,
4636 UiAccountEncoding::JsonParsed,
4637 None,
4638 None,
4639 );
4640 let expected_account = UiAccount {
4641 lamports: 1000,
4642 data: expected_data,
4643 owner: idl_v1.address.clone(),
4644 executable: false,
4645 rent_epoch: 0,
4646 space: Some(account.data.len() as u64),
4647 };
4648 assert_eq!(ui_account, expected_account);
4649 }
4650
4651 {
4653 let mut account_data = idl_v1.accounts[0].discriminator.clone();
4654 let pubkey = Pubkey::new_unique();
4655 CustomAccount {
4656 my_custom_data: 42,
4657 another_field: "test".to_string(),
4658 bool: true,
4659 pubkey,
4660 }
4661 .serialize(&mut account_data)
4662 .unwrap();
4663
4664 let account = Account {
4665 lamports: 1000,
4666 data: account_data,
4667 owner: idl_v1.address.parse().unwrap(),
4668 executable: false,
4669 rent_epoch: 0,
4670 };
4671
4672 let ui_account = svm.encode_ui_account(
4673 &account_pubkey,
4674 &account,
4675 UiAccountEncoding::JsonParsed,
4676 None,
4677 None,
4678 );
4679 let expected_account = UiAccount {
4680 lamports: 1000,
4681 data: UiAccountData::Json(ParsedAccount {
4682 program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4683 parsed: serde_json::json!({
4684 "my_custom_data": 42,
4685 "another_field": "test",
4686 "bool": true,
4687 "pubkey": pubkey.to_string(),
4688 }),
4689 space: account.data.len() as u64,
4690 }),
4691 owner: idl_v1.address.clone(),
4692 executable: false,
4693 rent_epoch: 0,
4694 space: Some(account.data.len() as u64),
4695 };
4696 assert_eq!(ui_account, expected_account);
4697 }
4698
4699 let idl_v2: Idl =
4700 serde_json::from_slice(&include_bytes!("../tests/assets/idl_v2.json").to_vec())
4701 .unwrap();
4702
4703 svm.register_idl(idl_v2.clone(), Some(100)).unwrap();
4704
4705 {
4707 let mut account_data = idl_v1.accounts[0].discriminator.clone();
4708 let pubkey = Pubkey::new_unique();
4709 CustomAccount {
4710 my_custom_data: 42,
4711 another_field: "test".to_string(),
4712 bool: true,
4713 pubkey,
4714 }
4715 .serialize(&mut account_data)
4716 .unwrap();
4717
4718 let account = Account {
4719 lamports: 1000,
4720 data: account_data,
4721 owner: idl_v1.address.parse().unwrap(),
4722 executable: false,
4723 rent_epoch: 0,
4724 };
4725
4726 let ui_account = svm.encode_ui_account(
4727 &account_pubkey,
4728 &account,
4729 UiAccountEncoding::JsonParsed,
4730 None,
4731 None,
4732 );
4733 let expected_account = UiAccount {
4734 lamports: 1000,
4735 data: UiAccountData::Json(ParsedAccount {
4736 program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4737 parsed: serde_json::json!({
4738 "my_custom_data": 42,
4739 "another_field": "test",
4740 "bool": true,
4741 "pubkey": pubkey.to_string(),
4742 }),
4743 space: account.data.len() as u64,
4744 }),
4745 owner: idl_v1.address.clone(),
4746 executable: false,
4747 rent_epoch: 0,
4748 space: Some(account.data.len() as u64),
4749 };
4750 assert_eq!(ui_account, expected_account);
4751 }
4752
4753 {
4755 #[derive(borsh::BorshSerialize)]
4757 pub struct CustomAccount {
4758 pub my_custom_data: u64,
4759 pub another_field: String,
4760 pub pubkey: Pubkey,
4761 }
4762 let mut account_data = idl_v1.accounts[0].discriminator.clone();
4763 let pubkey = Pubkey::new_unique();
4764 CustomAccount {
4765 my_custom_data: 42,
4766 another_field: "test".to_string(),
4767 pubkey,
4768 }
4769 .serialize(&mut account_data)
4770 .unwrap();
4771
4772 let account = Account {
4773 lamports: 1000,
4774 data: account_data.clone(),
4775 owner: idl_v1.address.parse().unwrap(),
4776 executable: false,
4777 rent_epoch: 0,
4778 };
4779
4780 let ui_account = svm.encode_ui_account(
4781 &account_pubkey,
4782 &account,
4783 UiAccountEncoding::JsonParsed,
4784 None,
4785 None,
4786 );
4787 let base64_data = general_purpose::STANDARD.encode(&account_data);
4788 let expected_data = UiAccountData::Binary(base64_data, UiAccountEncoding::Base64);
4789 let expected_account = UiAccount {
4790 lamports: 1000,
4791 data: expected_data,
4792 owner: idl_v1.address.clone(),
4793 executable: false,
4794 rent_epoch: 0,
4795 space: Some(account.data.len() as u64),
4796 };
4797 assert_eq!(ui_account, expected_account);
4798
4799 svm.latest_epoch_info.absolute_slot = 100; let ui_account = svm.encode_ui_account(
4802 &account_pubkey,
4803 &account,
4804 UiAccountEncoding::JsonParsed,
4805 None,
4806 None,
4807 );
4808 let expected_account = UiAccount {
4809 lamports: 1000,
4810 data: UiAccountData::Json(ParsedAccount {
4811 program: format!("{}", idl_v1.metadata.name).to_case(convert_case::Case::Kebab),
4812 parsed: serde_json::json!({
4813 "my_custom_data": 42,
4814 "another_field": "test",
4815 "pubkey": pubkey.to_string(),
4816 }),
4817 space: account.data.len() as u64,
4818 }),
4819 owner: idl_v1.address.clone(),
4820 executable: false,
4821 rent_epoch: 0,
4822 space: Some(account.data.len() as u64),
4823 };
4824 assert_eq!(ui_account, expected_account);
4825 }
4826 }
4827
4828 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4829 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
4830 #[test_case(TestType::no_db(); "with no db")]
4831 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4832 fn test_profiling_map_capacity_default(test_type: TestType) {
4833 let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
4834 assert_eq!(svm.max_profiles, DEFAULT_PROFILING_MAP_CAPACITY);
4835 }
4836
4837 #[test]
4838 fn test_default_uses_no_db_storage() {
4839 let (svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
4840 assert!(svm.inner.db.is_none());
4841 }
4842
4843 #[cfg(feature = "sqlite")]
4844 #[test]
4845 fn test_new_with_db_uses_sqlite_storage() {
4846 let (svm, _events_rx, _geyser_rx) =
4847 SurfnetSvm::new_with_db(Some(":memory:"), SurfnetSvmConfig::default()).unwrap();
4848 assert!(svm.inner.db.is_some());
4849 }
4850
4851 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4852 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4853 fn test_new_with_db_restores_slot_checkpoint(test_type: TestType) {
4854 let (database_url, surfnet_id) = match &test_type {
4855 TestType::OnDiskSqlite(db_path) => {
4856 (db_path.as_str(), "slot-checkpoint-recovery".to_string())
4857 }
4858 #[cfg(feature = "postgres")]
4859 TestType::Postgres { url, surfnet_id } => (url.as_str(), surfnet_id.clone()),
4860 _ => unreachable!("test case must provide persistent database storage"),
4861 };
4862 let config = SurfnetSvmConfig {
4863 surfnet_id,
4864 ..SurfnetSvmConfig::default()
4865 };
4866 let target_slot = (*CHECKPOINT_INTERVAL_SLOTS).max(FINALIZATION_SLOT_THRESHOLD);
4867
4868 let checkpoint_slot = {
4869 let (mut svm, _events_rx, _geyser_rx) =
4870 SurfnetSvm::new_with_db(Some(database_url), config.clone()).unwrap();
4871 while svm.get_latest_absolute_slot() <= target_slot {
4872 svm.confirm_current_block().unwrap();
4873 }
4874
4875 let checkpoint_slot = svm
4876 .slot_checkpoint
4877 .get(&"latest_slot".to_string())
4878 .unwrap()
4879 .expect("checkpoint slot should be persisted");
4880 svm.shutdown();
4881 checkpoint_slot
4882 };
4883
4884 let (mut svm, _events_rx, _geyser_rx) =
4885 SurfnetSvm::new_with_db(Some(database_url), config).unwrap();
4886 assert_eq!(svm.get_latest_absolute_slot(), checkpoint_slot);
4887 assert_eq!(svm.latest_epoch_info.block_height, checkpoint_slot);
4888 assert_eq!(svm.chain_tip.index, checkpoint_slot);
4889 assert_eq!(
4890 svm.chain_tip.hash,
4891 SyntheticBlockhash::new(checkpoint_slot - 1).to_string()
4892 );
4893
4894 let clock = svm.inner.get_sysvar::<Clock>();
4895 assert_eq!(clock.slot, checkpoint_slot);
4896
4897 let recovered_blockhash = svm.chain_tip.hash.clone();
4898 svm.confirm_current_block().unwrap();
4899 assert_eq!(
4900 svm.chain_tip.hash,
4901 SyntheticBlockhash::new(checkpoint_slot).to_string()
4902 );
4903 assert_ne!(svm.chain_tip.hash, recovered_blockhash);
4904 }
4905
4906 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
4907 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
4908 fn test_new_with_db_restores_last_checkpoint_slot_from_block_only_recovery(
4909 test_type: TestType,
4910 ) {
4911 let (database_url, surfnet_id) = match &test_type {
4912 TestType::OnDiskSqlite(db_path) => {
4913 (db_path.as_str(), "block-only-recovery".to_string())
4914 }
4915 #[cfg(feature = "postgres")]
4916 TestType::Postgres { url, surfnet_id } => (url.as_str(), surfnet_id.clone()),
4917 _ => unreachable!("test case must provide persistent database storage"),
4918 };
4919 let config = SurfnetSvmConfig {
4920 surfnet_id,
4921 ..SurfnetSvmConfig::default()
4922 };
4923 let block_slot = (*CHECKPOINT_INTERVAL_SLOTS)
4924 .max(FINALIZATION_SLOT_THRESHOLD)
4925 .saturating_add(7);
4926
4927 {
4928 let (mut svm, _events_rx, _geyser_rx) =
4929 SurfnetSvm::new_with_db(Some(database_url), config.clone()).unwrap();
4930 svm.blocks
4931 .store(
4932 block_slot,
4933 BlockHeader {
4934 hash: SyntheticBlockhash::new(block_slot - 1).to_string(),
4935 previous_blockhash: SyntheticBlockhash::new(block_slot - 2).to_string(),
4936 parent_slot: block_slot - 1,
4937 block_time: 0,
4938 block_height: block_slot,
4939 signatures: vec![Signature::new_unique()],
4940 },
4941 )
4942 .unwrap();
4943 assert!(
4944 svm.slot_checkpoint
4945 .get(&"latest_slot".to_string())
4946 .unwrap()
4947 .is_none()
4948 );
4949 svm.shutdown();
4950 }
4951
4952 let (svm, _events_rx, _geyser_rx) =
4953 SurfnetSvm::new_with_db(Some(database_url), config).unwrap();
4954 assert_eq!(svm.get_latest_absolute_slot(), block_slot);
4955 assert_eq!(svm.latest_epoch_info.block_height, block_slot);
4956 assert_eq!(svm.last_checkpoint_slot, block_slot);
4957 }
4958
4959 #[test]
4960 fn test_constructor_applies_startup_config() {
4961 let config = SurfnetSvmConfig {
4962 surfnet_id: "constructor-test".to_string(),
4963 feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4964 slot_time: 123,
4965 instruction_profiling_enabled: false,
4966 max_profiles: 17,
4967 log_bytes_limit: None,
4968 skip_blockhash_check: true,
4969 };
4970 let (svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
4971
4972 assert_eq!(svm.slot_time, 123);
4973 assert!(!svm.instruction_profiling_enabled);
4974 assert_eq!(svm.max_profiles, 17);
4975 assert_eq!(
4976 svm.latest_epoch_info.absolute_slot,
4977 FINALIZATION_SLOT_THRESHOLD
4978 );
4979 assert_eq!(svm.genesis_slot, FINALIZATION_SLOT_THRESHOLD);
4980 assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
4981
4982 let epoch_schedule = svm.inner.get_sysvar::<EpochSchedule>();
4983 assert!(!epoch_schedule.warmup);
4984
4985 let registry = TemplateRegistry::new();
4986 for (_, template) in registry.templates {
4987 let program_id = template.idl.address.clone();
4988 assert!(svm.registered_idls.get(&program_id).unwrap().is_some());
4989 }
4990 assert!(svm.skip_blockhash_check);
4991 }
4992
4993 #[test]
4994 fn test_initialize_only_updates_remote_state() {
4995 let config = SurfnetSvmConfig {
4996 surfnet_id: "remote-init-test".to_string(),
4997 feature_config: SvmFeatureConfig::new().disable(disable_fees_sysvar::id()),
4998 slot_time: 321,
4999 instruction_profiling_enabled: false,
5000 max_profiles: 23,
5001 log_bytes_limit: None,
5002 skip_blockhash_check: false,
5003 };
5004 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::new(config).unwrap();
5005 let epoch_info = EpochInfo {
5006 epoch: 7,
5007 slot_index: 4,
5008 slots_in_epoch: crate::surfnet::SLOTS_PER_EPOCH,
5009 absolute_slot: 777,
5010 block_height: 777,
5011 transaction_count: None,
5012 };
5013
5014 svm.initialize(epoch_info.clone(), EpochSchedule::without_warmup());
5015
5016 assert_eq!(svm.slot_time, 321);
5017 assert!(!svm.instruction_profiling_enabled);
5018 assert_eq!(svm.max_profiles, 23);
5019 assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
5020 assert_eq!(svm.latest_epoch_info, epoch_info);
5021 assert_eq!(svm.genesis_slot, 777);
5022 }
5023
5024 #[test]
5025 fn test_clone_for_profiling_preserves_skip_blockhash_check() {
5026 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5027 svm.skip_blockhash_check = true;
5028
5029 let profiling_clone = svm.clone_for_profiling();
5030 assert!(profiling_clone.skip_blockhash_check);
5031 }
5032
5033 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5034 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5035 #[test_case(TestType::no_db(); "with no db")]
5036 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5037 fn test_send_transaction_rejects_invalid_blockhash_by_default(test_type: TestType) {
5038 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5039 let payer = Keypair::new();
5040 let recipient = Pubkey::new_unique();
5041 let lamports = 1_000_000_000;
5042 let invalid_blockhash = Hash::new_unique();
5043
5044 svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
5045 assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
5046
5047 let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
5048 let err = svm.send_transaction(tx, false, false).unwrap_err().err;
5049
5050 assert_eq!(err, TransactionError::BlockhashNotFound);
5051 }
5052
5053 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5054 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5055 #[test_case(TestType::no_db(); "with no db")]
5056 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5057 fn test_skip_blockhash_check_bypasses_send_simulate_and_estimate(test_type: TestType) {
5058 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5059 let payer = Keypair::new();
5060 let recipient = Pubkey::new_unique();
5061 let lamports = 1_000_000_000;
5062 let invalid_blockhash = Hash::new_unique();
5063
5064 svm.skip_blockhash_check = true;
5065 svm.airdrop(&payer.pubkey(), 2 * lamports).unwrap().unwrap();
5066 assert!(!svm.check_blockhash_is_recent(&invalid_blockhash));
5067
5068 let tx = build_transfer_transaction(&payer, &recipient, lamports, invalid_blockhash);
5069
5070 let estimate = svm.estimate_compute_units(&tx);
5071 assert!(
5072 estimate.success,
5073 "estimate should succeed when skip_blockhash_check is enabled: {:?}",
5074 estimate.error_message
5075 );
5076
5077 let simulation = svm.simulate_transaction(tx.clone(), false);
5078 assert!(
5079 simulation.is_ok(),
5080 "simulation should succeed when skip_blockhash_check is enabled: {:?}",
5081 simulation.err()
5082 );
5083
5084 let send_result = svm.send_transaction(tx, false, false);
5085 assert!(
5086 send_result.is_ok(),
5087 "send should succeed when skip_blockhash_check is enabled: {:?}",
5088 send_result.err().map(|err| err.err)
5089 );
5090 }
5091
5092 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5100 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5101 #[test_case(TestType::no_db(); "with no db")]
5102 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5103 fn test_feature_set_empty_config_uses_mainnet_baseline(test_type: TestType) {
5104 let (svm, _events_rx, _geyser_rx) =
5108 test_type.initialize_svm_with_features(SvmFeatureConfig::new());
5109
5110 assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
5111 assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
5112 }
5113
5114 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5115 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5116 #[test_case(TestType::no_db(); "with no db")]
5117 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5118 fn test_feature_set_enable_feature(test_type: TestType) {
5119 let feature_id = enable_loader_v4::id();
5122 let (svm, _events_rx, _geyser_rx) =
5123 test_type.initialize_svm_with_features(SvmFeatureConfig::new().enable(feature_id));
5124
5125 assert!(svm.feature_set.is_active(&feature_id));
5126 }
5127
5128 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5129 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5130 #[test_case(TestType::no_db(); "with no db")]
5131 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5132 fn test_feature_set_disable_feature(test_type: TestType) {
5133 let feature_id = disable_fees_sysvar::id();
5136 let (svm, _events_rx, _geyser_rx) =
5137 test_type.initialize_svm_with_features(SvmFeatureConfig::new().disable(feature_id));
5138
5139 assert!(!svm.feature_set.is_active(&feature_id));
5140 }
5141
5142 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5143 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5144 #[test_case(TestType::no_db(); "with no db")]
5145 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5146 fn test_feature_set_mainnet_baseline(test_type: TestType) {
5147 let (svm, _events_rx, _geyser_rx) =
5150 test_type.initialize_svm_with_features(SvmFeatureConfig::default());
5151
5152 assert!(!svm.feature_set.is_active(&enable_loader_v4::id()));
5154 assert!(
5155 !svm.feature_set
5156 .is_active(&enable_extend_program_checked::id())
5157 );
5158 assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5159 assert!(
5160 !svm.feature_set
5161 .is_active(&raise_cpi_nesting_limit_to_8::id())
5162 );
5163 assert!(
5164 !svm.feature_set
5165 .is_active(&stake_raise_minimum_delegation_to_1_sol::id())
5166 );
5167
5168 assert!(svm.feature_set.is_active(&disable_fees_sysvar::id()));
5170 assert!(svm.feature_set.is_active(&curve25519_syscall_enabled::id()));
5171 assert!(
5172 svm.feature_set
5173 .is_active(&enable_sbpf_v1_deployment_and_execution::id())
5174 );
5175 assert!(
5176 svm.feature_set
5177 .is_active(&enable_sbpf_v2_deployment_and_execution::id())
5178 );
5179 assert!(
5180 svm.feature_set
5181 .is_active(&enable_sbpf_v3_deployment_and_execution::id())
5182 );
5183 assert!(
5184 svm.feature_set
5185 .is_active(&formalize_loaded_transaction_data_size::id())
5186 );
5187 assert!(
5188 svm.feature_set
5189 .is_active(&move_precompile_verification_to_svm::id())
5190 );
5191 }
5192
5193 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5194 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5195 #[test_case(TestType::no_db(); "with no db")]
5196 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5197 fn test_feature_set_mainnet_with_override(test_type: TestType) {
5198 let config = SvmFeatureConfig::default().enable(enable_loader_v4::id());
5201 let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5202
5203 assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
5204 assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5205 assert!(
5206 !svm.feature_set
5207 .is_active(&enable_extend_program_checked::id())
5208 );
5209 }
5210
5211 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5212 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5213 #[test_case(TestType::no_db(); "with no db")]
5214 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5215 fn test_feature_set_multiple_changes(test_type: TestType) {
5216 let config = SvmFeatureConfig::new()
5217 .enable(enable_loader_v4::id())
5218 .enable(enable_sbpf_v2_deployment_and_execution::id())
5219 .disable(disable_fees_sysvar::id())
5220 .disable(blake3_syscall_enabled::id());
5221
5222 let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5223
5224 assert!(svm.feature_set.is_active(&enable_loader_v4::id()));
5225 assert!(
5226 svm.feature_set
5227 .is_active(&enable_sbpf_v2_deployment_and_execution::id())
5228 );
5229 assert!(!svm.feature_set.is_active(&disable_fees_sysvar::id()));
5230 assert!(!svm.feature_set.is_active(&blake3_syscall_enabled::id()));
5231 }
5232
5233 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5234 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5235 #[test_case(TestType::no_db(); "with no db")]
5236 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5237 fn test_feature_set_native_mint_present(test_type: TestType) {
5238 let config = SvmFeatureConfig::new().disable(disable_fees_sysvar::id());
5241 let (svm, _events_rx, _geyser_rx) = test_type.initialize_svm_with_features(config);
5242
5243 assert!(
5244 svm.inner
5245 .get_account(&spl_token_interface::native_mint::ID)
5246 .unwrap()
5247 .is_some()
5248 );
5249 }
5250
5251 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5254 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5255 #[test_case(TestType::no_db(); "with no db")]
5256 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5257 fn test_garbage_collected_account_tracking(test_type: TestType) {
5258 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5259
5260 let owner = Pubkey::new_unique();
5261 let account_pubkey = Pubkey::new_unique();
5262
5263 let account = Account {
5264 lamports: 1000000,
5265 data: vec![1, 2, 3, 4, 5],
5266 owner,
5267 executable: false,
5268 rent_epoch: 0,
5269 };
5270
5271 svm.set_account(&account_pubkey, account.clone()).unwrap();
5272
5273 assert!(svm.get_account(&account_pubkey).unwrap().is_some());
5274 assert!(
5275 !svm.offline_accounts
5276 .contains_key(&account_pubkey.to_string())
5277 .unwrap()
5278 );
5279 assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 1);
5280
5281 let empty_account = Account::default();
5282 svm.update_account_registries(&account_pubkey, &empty_account)
5283 .unwrap();
5284
5285 assert!(
5286 svm.offline_accounts
5287 .contains_key(&account_pubkey.to_string())
5288 .unwrap()
5289 );
5290
5291 assert_eq!(svm.get_account_owned_by(&owner).unwrap().len(), 0);
5292
5293 let owned_accounts = svm.get_account_owned_by(&owner).unwrap();
5294 assert!(!owned_accounts.iter().any(|(pk, _)| *pk == account_pubkey));
5295 }
5296
5297 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5298 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5299 #[test_case(TestType::no_db(); "with no db")]
5300 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5301 fn test_garbage_collected_token_account_cleanup(test_type: TestType) {
5302 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5303
5304 let token_owner = Pubkey::new_unique();
5305 let delegate = Pubkey::new_unique();
5306 let mint = Pubkey::new_unique();
5307 let token_account_pubkey = Pubkey::new_unique();
5308
5309 let mut token_account_data = [0u8; TokenAccount::LEN];
5310 let token_account = TokenAccount {
5311 mint,
5312 owner: token_owner,
5313 amount: 1000,
5314 delegate: COption::Some(delegate),
5315 state: AccountState::Initialized,
5316 is_native: COption::None,
5317 delegated_amount: 500,
5318 close_authority: COption::None,
5319 };
5320 token_account.pack_into_slice(&mut token_account_data);
5321
5322 let account = Account {
5323 lamports: 2000000,
5324 data: token_account_data.to_vec(),
5325 owner: spl_token_interface::id(),
5326 executable: false,
5327 rent_epoch: 0,
5328 };
5329
5330 svm.set_account(&token_account_pubkey, account).unwrap();
5331
5332 assert_eq!(
5333 svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
5334 1
5335 );
5336 assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 1);
5337 assert!(
5338 !svm.offline_accounts
5339 .contains_key(&token_account_pubkey.to_string())
5340 .unwrap()
5341 );
5342
5343 let empty_account = Account::default();
5344 svm.update_account_registries(&token_account_pubkey, &empty_account)
5345 .unwrap();
5346
5347 assert!(
5348 svm.offline_accounts
5349 .contains_key(&token_account_pubkey.to_string())
5350 .unwrap()
5351 );
5352
5353 assert_eq!(
5354 svm.get_token_accounts_by_owner(&token_owner).unwrap().len(),
5355 0
5356 );
5357 assert_eq!(svm.get_token_accounts_by_delegate(&delegate).len(), 0);
5358 assert!(
5359 svm.token_accounts
5360 .get(&token_account_pubkey.to_string())
5361 .unwrap()
5362 .is_none()
5363 );
5364 }
5365
5366 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5367 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5368 #[test_case(TestType::no_db(); "with no db")]
5369 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5370 fn test_is_slot_in_valid_range(test_type: TestType) {
5371 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5372
5373 svm.genesis_slot = 100;
5375 svm.latest_epoch_info.absolute_slot = 110;
5376
5377 assert!(
5379 svm.is_slot_in_valid_range(100),
5380 "genesis_slot should be valid"
5381 );
5382 assert!(
5383 svm.is_slot_in_valid_range(105),
5384 "middle slot should be valid"
5385 );
5386 assert!(
5387 svm.is_slot_in_valid_range(110),
5388 "latest slot should be valid"
5389 );
5390
5391 assert!(
5393 !svm.is_slot_in_valid_range(99),
5394 "slot before genesis should be invalid"
5395 );
5396 assert!(
5397 !svm.is_slot_in_valid_range(111),
5398 "slot after latest should be invalid"
5399 );
5400 assert!(
5401 !svm.is_slot_in_valid_range(0),
5402 "slot 0 should be invalid when genesis > 0"
5403 );
5404 assert!(
5405 !svm.is_slot_in_valid_range(1000),
5406 "far future slot should be invalid"
5407 );
5408 }
5409
5410 #[test]
5411 fn test_is_slot_in_valid_range_genesis_zero() {
5412 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5413
5414 svm.genesis_slot = 0;
5416 svm.latest_epoch_info.absolute_slot = 50;
5417
5418 assert!(
5420 svm.is_slot_in_valid_range(0),
5421 "slot 0 should be valid when genesis = 0"
5422 );
5423 assert!(
5424 svm.is_slot_in_valid_range(25),
5425 "middle slot should be valid"
5426 );
5427 assert!(
5428 svm.is_slot_in_valid_range(50),
5429 "latest slot should be valid"
5430 );
5431 assert!(
5432 !svm.is_slot_in_valid_range(51),
5433 "slot after latest should be invalid"
5434 );
5435 }
5436
5437 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5438 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5439 #[test_case(TestType::no_db(); "with no db")]
5440 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5441 fn test_get_block_or_reconstruct_stored_block(test_type: TestType) {
5442 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5443
5444 svm.genesis_slot = 0;
5446 svm.latest_epoch_info.absolute_slot = 100;
5447
5448 let stored_block = BlockHeader {
5450 hash: "stored_block_hash".to_string(),
5451 previous_blockhash: "prev_hash".to_string(),
5452 parent_slot: 49,
5453 block_time: 1234567890,
5454 block_height: 50,
5455 signatures: vec![Signature::new_unique()],
5456 };
5457 svm.blocks.store(50, stored_block.clone()).unwrap();
5458
5459 let result = svm.get_block_or_reconstruct(50).unwrap();
5461 assert!(result.is_some(), "should return stored block");
5462 let block = result.unwrap();
5463 assert_eq!(block.hash, "stored_block_hash");
5464 assert_eq!(block.signatures.len(), 1);
5465 }
5466
5467 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5468 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5469 #[test_case(TestType::no_db(); "with no db")]
5470 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5471 fn test_get_block_or_reconstruct_empty_block(test_type: TestType) {
5472 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5473
5474 svm.genesis_slot = 0;
5476 svm.latest_epoch_info.absolute_slot = 100;
5477 svm.genesis_updated_at = 1000000; svm.slot_time = 400; let result = svm.get_block_or_reconstruct(50).unwrap();
5482 assert!(
5483 result.is_some(),
5484 "should reconstruct empty block for valid slot"
5485 );
5486
5487 let block = result.unwrap();
5488 assert!(
5490 block.signatures.is_empty(),
5491 "reconstructed block should have no signatures"
5492 );
5493 assert_eq!(block.block_height, 50);
5494 assert_eq!(block.parent_slot, 49);
5495
5496 assert_eq!(block.block_time, 1020);
5499 }
5500
5501 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5502 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5503 #[test_case(TestType::no_db(); "with no db")]
5504 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5505 fn test_get_block_or_reconstruct_out_of_range(test_type: TestType) {
5506 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5507
5508 svm.genesis_slot = 100;
5510 svm.latest_epoch_info.absolute_slot = 110;
5511
5512 let result = svm.get_block_or_reconstruct(50).unwrap();
5514 assert!(
5515 result.is_none(),
5516 "should return None for slot before genesis"
5517 );
5518
5519 let result = svm.get_block_or_reconstruct(200).unwrap();
5521 assert!(result.is_none(), "should return None for slot after latest");
5522 }
5523
5524 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5525 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5526 #[test_case(TestType::no_db(); "with no db")]
5527 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5528 #[allow(deprecated)]
5529 fn test_reconstruct_sysvars_recent_blockhashes(test_type: TestType) {
5530 use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5531
5532 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5533
5534 svm.chain_tip = BlockIdentifier::new(10, "test_hash");
5536 svm.genesis_slot = 0;
5537 svm.latest_epoch_info.absolute_slot = 10;
5538
5539 svm.reconstruct_sysvars();
5540
5541 let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
5543
5544 assert_eq!(recent_blockhashes.len(), 11);
5546
5547 let expected_hash = SyntheticBlockhash::new(10);
5549 assert_eq!(
5550 recent_blockhashes.first().unwrap().blockhash,
5551 *expected_hash.hash(),
5552 "First blockhash should match SyntheticBlockhash for chain_tip.index"
5553 );
5554
5555 let expected_last_hash = SyntheticBlockhash::new(0);
5557 assert_eq!(
5558 recent_blockhashes.last().unwrap().blockhash,
5559 *expected_last_hash.hash(),
5560 "Last blockhash should match SyntheticBlockhash for index 0"
5561 );
5562 }
5563
5564 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5565 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5566 #[test_case(TestType::no_db(); "with no db")]
5567 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5568 #[allow(deprecated)]
5569 fn test_reconstruct_sysvars_slot_hashes(test_type: TestType) {
5570 use solana_slot_hashes::SlotHashes;
5571
5572 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5573
5574 svm.chain_tip = BlockIdentifier::new(5, "test_hash");
5576 svm.genesis_slot = 100;
5577 svm.latest_epoch_info.absolute_slot = 105;
5578
5579 svm.reconstruct_sysvars();
5580
5581 let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5583
5584 assert_eq!(slot_hashes.len(), 32);
5587 assert!(
5588 slot_hashes.get(&74).is_some(),
5589 "SlotHashes should contain the finalized warmup floor"
5590 );
5591
5592 let expected_hash_105 = SyntheticBlockhash::new(5);
5594 let hash_for_105 = slot_hashes.get(&105);
5595 assert!(hash_for_105.is_some(), "SlotHashes should contain slot 105");
5596 assert_eq!(
5597 hash_for_105.unwrap(),
5598 expected_hash_105.hash(),
5599 "Hash for slot 105 should match SyntheticBlockhash for index 5"
5600 );
5601
5602 let expected_hash_100 = SyntheticBlockhash::new(0);
5604 let hash_for_100 = slot_hashes.get(&100);
5605 assert!(hash_for_100.is_some(), "SlotHashes should contain slot 100");
5606 assert_eq!(
5607 hash_for_100.unwrap(),
5608 expected_hash_100.hash(),
5609 "Hash for slot 100 should match SyntheticBlockhash for index 0"
5610 );
5611 }
5612
5613 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5614 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5615 #[test_case(TestType::no_db(); "with no db")]
5616 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5617 fn test_reconstruct_sysvars_slot_hashes_cover_finalized_warmup(test_type: TestType) {
5618 use solana_slot_hashes::SlotHashes;
5619
5620 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5621
5622 svm.chain_tip = BlockIdentifier::new(9, "test_hash");
5623 svm.genesis_slot = FINALIZATION_SLOT_THRESHOLD;
5624 svm.latest_epoch_info.absolute_slot = 40;
5625
5626 svm.reconstruct_sysvars();
5627
5628 let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5629
5630 assert_eq!(
5631 slot_hashes.len(),
5632 (FINALIZATION_SLOT_THRESHOLD + 1) as usize
5633 );
5634 assert!(
5635 slot_hashes.get(&9).is_some(),
5636 "SlotHashes should include finalized slot 9 during warmup"
5637 );
5638 assert!(
5639 slot_hashes.get(&40).is_some(),
5640 "SlotHashes should include the processed slot"
5641 );
5642 }
5643
5644 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5645 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5646 #[test_case(TestType::no_db(); "with no db")]
5647 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5648 fn test_reconstruct_sysvars_clock(test_type: TestType) {
5649 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5650
5651 svm.chain_tip = BlockIdentifier::new(50, "test_hash");
5653 svm.genesis_slot = 1000;
5654 svm.latest_epoch_info.absolute_slot = 1050;
5655 svm.latest_epoch_info.epoch = 5;
5656 svm.genesis_updated_at = 2_000_000; svm.slot_time = 400; svm.reconstruct_sysvars();
5660
5661 let clock = svm.inner.get_sysvar::<Clock>();
5663
5664 assert_eq!(clock.slot, 1050, "Clock slot should be absolute slot");
5665 assert_eq!(clock.epoch, 5, "Clock epoch should match latest_epoch_info");
5666
5667 assert_eq!(
5669 clock.unix_timestamp, 2020,
5670 "Clock unix_timestamp should be calculated correctly"
5671 );
5672 }
5673
5674 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5675 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5676 #[test_case(TestType::no_db(); "with no db")]
5677 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5678 #[allow(deprecated)]
5679 fn test_reconstruct_sysvars_max_blockhashes(test_type: TestType) {
5680 use solana_slot_hashes::SlotHashes;
5681 use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5682
5683 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5684
5685 svm.chain_tip = BlockIdentifier::new(600, "test_hash");
5687 svm.genesis_slot = 0;
5688 svm.latest_epoch_info.absolute_slot = 600;
5689
5690 svm.reconstruct_sysvars();
5691
5692 let recent_blockhashes = svm.inner.get_sysvar::<RecentBlockhashes>();
5694
5695 assert_eq!(
5696 recent_blockhashes.len(),
5697 MAX_RECENT_BLOCKHASHES_STANDARD,
5698 "RecentBlockhashes should be capped at MAX_RECENT_BLOCKHASHES_STANDARD"
5699 );
5700
5701 let slot_hashes = svm.inner.get_sysvar::<SlotHashes>();
5702 assert_eq!(
5703 slot_hashes.len(),
5704 MAX_SLOT_HASHES_ENTRIES,
5705 "SlotHashes should be capped at MAX_SLOT_HASHES_ENTRIES"
5706 );
5707 assert!(
5708 slot_hashes.get(&89).is_some(),
5709 "SlotHashes should retain the 512-slot floor"
5710 );
5711 assert!(
5712 slot_hashes.get(&88).is_none(),
5713 "SlotHashes should evict slots older than the 512-slot floor"
5714 );
5715
5716 let expected_hash = SyntheticBlockhash::new(600);
5718 assert_eq!(
5719 recent_blockhashes.first().unwrap().blockhash,
5720 *expected_hash.hash(),
5721 "First blockhash should match SyntheticBlockhash for chain_tip.index"
5722 );
5723
5724 let expected_last_hash =
5726 SyntheticBlockhash::new(600 - MAX_RECENT_BLOCKHASHES_STANDARD as u64 + 1);
5727 assert_eq!(
5728 recent_blockhashes.last().unwrap().blockhash,
5729 *expected_last_hash.hash(),
5730 "Last blockhash should match SyntheticBlockhash for start_index"
5731 );
5732 }
5733
5734 #[test_case(TestType::sqlite(); "with on-disk sqlite db")]
5735 #[test_case(TestType::in_memory(); "with in-memory sqlite db")]
5736 #[test_case(TestType::no_db(); "with no db")]
5737 #[cfg_attr(feature = "postgres", test_case(TestType::postgres(); "with postgres db"))]
5738 #[allow(deprecated)]
5739 fn test_reconstruct_sysvars_deterministic(test_type: TestType) {
5740 use solana_slot_hashes::SlotHashes;
5741 use solana_sysvar::recent_blockhashes::RecentBlockhashes;
5742
5743 let (mut svm, _events_rx, _geyser_rx) = test_type.initialize_svm();
5744
5745 svm.chain_tip = BlockIdentifier::new(25, "test_hash");
5747 svm.genesis_slot = 50;
5748 svm.latest_epoch_info.absolute_slot = 75;
5749 svm.latest_epoch_info.epoch = 2;
5750 svm.genesis_updated_at = 1_000_000;
5751 svm.slot_time = 400;
5752
5753 svm.reconstruct_sysvars();
5755 let blockhashes_1 = svm.inner.get_sysvar::<RecentBlockhashes>();
5756 let slot_hashes_1 = svm.inner.get_sysvar::<SlotHashes>();
5757 let clock_1 = svm.inner.get_sysvar::<Clock>();
5758
5759 svm.reconstruct_sysvars();
5761 let blockhashes_2 = svm.inner.get_sysvar::<RecentBlockhashes>();
5762 let slot_hashes_2 = svm.inner.get_sysvar::<SlotHashes>();
5763 let clock_2 = svm.inner.get_sysvar::<Clock>();
5764
5765 assert_eq!(blockhashes_1.len(), blockhashes_2.len());
5767 for (b1, b2) in blockhashes_1.iter().zip(blockhashes_2.iter()) {
5768 assert_eq!(
5769 b1.blockhash, b2.blockhash,
5770 "RecentBlockhashes should be deterministic"
5771 );
5772 }
5773
5774 assert_eq!(slot_hashes_1.len(), slot_hashes_2.len());
5775 assert_eq!(clock_1.slot, clock_2.slot);
5776 assert_eq!(clock_1.epoch, clock_2.epoch);
5777 assert_eq!(clock_1.unix_timestamp, clock_2.unix_timestamp);
5778 }
5779
5780 fn pick_known_feature_gate() -> Pubkey {
5781 *agave_feature_set::FEATURE_NAMES
5782 .keys()
5783 .next()
5784 .expect("agave_feature_set::FEATURE_NAMES is non-empty")
5785 }
5786
5787 fn seed_filterable_accounts(svm: &mut SurfnetSvm) -> (Pubkey, Pubkey, Pubkey) {
5788 let user_pubkey = Pubkey::new_unique();
5789 let user_account = Account {
5790 lamports: 1_000_000,
5791 data: vec![],
5792 owner: system_program::id(),
5793 executable: false,
5794 rent_epoch: 0,
5795 };
5796 svm.set_account(&user_pubkey, user_account).unwrap();
5797
5798 let sysvar_pubkey = Pubkey::new_unique();
5799 let sysvar_account = Account {
5800 lamports: 1,
5801 data: vec![1, 2, 3, 4],
5802 owner: solana_sdk_ids::sysvar::id(),
5803 executable: false,
5804 rent_epoch: 0,
5805 };
5806 svm.set_account(&sysvar_pubkey, sysvar_account).unwrap();
5807
5808 let feature_pubkey = pick_known_feature_gate();
5809 let feature_account = Account {
5810 lamports: 1,
5811 data: vec![],
5812 owner: solana_sdk_ids::feature::id(),
5813 executable: false,
5814 rent_epoch: 0,
5815 };
5816 svm.set_account(&feature_pubkey, feature_account).unwrap();
5817
5818 (user_pubkey, sysvar_pubkey, feature_pubkey)
5819 }
5820
5821 #[test]
5822 fn test_export_snapshot_default_includes_sysvars_and_feature_gates() {
5823 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5824 let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5825
5826 let snapshot = svm
5827 .export_snapshot(ExportSnapshotConfig::default())
5828 .unwrap();
5829
5830 assert!(snapshot.contains_key(&user.to_string()));
5831 assert!(snapshot.contains_key(&sysvar.to_string()));
5832 assert!(snapshot.contains_key(&feature.to_string()));
5833 }
5834
5835 #[test]
5836 fn test_export_snapshot_exclude_sysvars() {
5837 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5838 let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5839
5840 let snapshot = svm
5841 .export_snapshot(ExportSnapshotConfig {
5842 filter: Some(ExportSnapshotFilter {
5843 exclude_sysvars: Some(true),
5844 ..Default::default()
5845 }),
5846 ..Default::default()
5847 })
5848 .unwrap();
5849
5850 assert!(snapshot.contains_key(&user.to_string()));
5851 assert!(!snapshot.contains_key(&sysvar.to_string()));
5852 assert!(snapshot.contains_key(&feature.to_string()));
5853 }
5854
5855 #[test]
5856 fn test_export_snapshot_exclude_feature_gates() {
5857 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5858 let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5859
5860 let snapshot = svm
5861 .export_snapshot(ExportSnapshotConfig {
5862 filter: Some(ExportSnapshotFilter {
5863 exclude_feature_gates: Some(true),
5864 ..Default::default()
5865 }),
5866 ..Default::default()
5867 })
5868 .unwrap();
5869
5870 assert!(snapshot.contains_key(&user.to_string()));
5871 assert!(snapshot.contains_key(&sysvar.to_string()));
5872 assert!(!snapshot.contains_key(&feature.to_string()));
5873 }
5874
5875 #[test]
5876 fn test_export_snapshot_exclude_sysvars_and_feature_gates() {
5877 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5878 let (user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5879
5880 let snapshot = svm
5881 .export_snapshot(ExportSnapshotConfig {
5882 filter: Some(ExportSnapshotFilter {
5883 exclude_sysvars: Some(true),
5884 exclude_feature_gates: Some(true),
5885 ..Default::default()
5886 }),
5887 ..Default::default()
5888 })
5889 .unwrap();
5890
5891 assert!(snapshot.contains_key(&user.to_string()));
5892 assert!(!snapshot.contains_key(&sysvar.to_string()));
5893 assert!(!snapshot.contains_key(&feature.to_string()));
5894 }
5895
5896 #[test]
5897 fn test_export_snapshot_include_accounts_overrides_exclusions() {
5898 let (mut svm, _events_rx, _geyser_rx) = SurfnetSvm::default();
5899 let (_user, sysvar, feature) = seed_filterable_accounts(&mut svm);
5900
5901 let snapshot = svm
5902 .export_snapshot(ExportSnapshotConfig {
5903 filter: Some(ExportSnapshotFilter {
5904 exclude_sysvars: Some(true),
5905 exclude_feature_gates: Some(true),
5906 include_accounts: Some(vec![sysvar.to_string(), feature.to_string()]),
5907 ..Default::default()
5908 }),
5909 ..Default::default()
5910 })
5911 .unwrap();
5912
5913 assert!(snapshot.contains_key(&sysvar.to_string()));
5914 assert!(snapshot.contains_key(&feature.to_string()));
5915 }
5916
5917 #[test]
5923 fn test_pda_derivation_with_string_seed() {
5924 let program_id = "11111111111111111111111111111111"; let address = surfpool_types::AccountAddress::Pda {
5927 program_id: program_id.to_string(),
5928 seeds: vec![surfpool_types::PdaSeed::String("test_seed".to_string())],
5929 };
5930
5931 let result = address.resolve_simple();
5932 assert!(result.is_some(), "Should derive PDA with string seed");
5933
5934 let program_pubkey = Pubkey::from_str(program_id).unwrap();
5936 let (expected_pda, _) = Pubkey::find_program_address(&[b"test_seed"], &program_pubkey);
5937 assert_eq!(result.unwrap(), expected_pda);
5938 }
5939
5940 #[test]
5941 fn test_pda_derivation_with_pubkey_seed() {
5942 let program_id = "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA";
5944 let seed_pubkey = "So11111111111111111111111111111111111111112"; let address = surfpool_types::AccountAddress::Pda {
5947 program_id: program_id.to_string(),
5948 seeds: vec![surfpool_types::PdaSeed::Pubkey(seed_pubkey.to_string())],
5949 };
5950
5951 let result = address.resolve_simple();
5952 assert!(result.is_some(), "Should derive PDA with pubkey seed");
5953
5954 let program_pubkey = Pubkey::from_str(program_id).unwrap();
5956 let seed_pk = Pubkey::from_str(seed_pubkey).unwrap();
5957 let (expected_pda, _) = Pubkey::find_program_address(&[seed_pk.as_ref()], &program_pubkey);
5958 assert_eq!(result.unwrap(), expected_pda);
5959 }
5960
5961 #[test]
5962 fn test_pda_derivation_with_multiple_seeds() {
5963 let program_id = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD"; let lending_market = "ByYiZxp8QrdN9qbdtaAiePN8AAr3qvTPppNJDpf5DVJ5";
5966 let owner = "81BgcfZuZf9bESLvw3zDkh7cZmMtDwTPgkCvYu7zx26o";
5967
5968 let address = surfpool_types::AccountAddress::Pda {
5969 program_id: program_id.to_string(),
5970 seeds: vec![
5971 surfpool_types::PdaSeed::String("obligation".to_string()),
5972 surfpool_types::PdaSeed::Pubkey(lending_market.to_string()),
5973 surfpool_types::PdaSeed::Pubkey(owner.to_string()),
5974 ],
5975 };
5976
5977 let result = address.resolve_simple();
5978 assert!(result.is_some(), "Should derive PDA with multiple seeds");
5979
5980 let program_pubkey = Pubkey::from_str(program_id).unwrap();
5982 let market_pk = Pubkey::from_str(lending_market).unwrap();
5983 let owner_pk = Pubkey::from_str(owner).unwrap();
5984 let (expected_pda, _) = Pubkey::find_program_address(
5985 &[b"obligation", market_pk.as_ref(), owner_pk.as_ref()],
5986 &program_pubkey,
5987 );
5988 assert_eq!(result.unwrap(), expected_pda);
5989 }
5990
5991 #[test]
5992 fn test_pda_derivation_with_bytes_seed() {
5993 let program_id = "11111111111111111111111111111111";
5995 let bytes_seed = vec![1u8, 2, 3, 4, 5, 6, 7, 8];
5996
5997 let address = surfpool_types::AccountAddress::Pda {
5998 program_id: program_id.to_string(),
5999 seeds: vec![surfpool_types::PdaSeed::Bytes(bytes_seed.clone())],
6000 };
6001
6002 let result = address.resolve_simple();
6003 assert!(result.is_some(), "Should derive PDA with bytes seed");
6004
6005 let program_pubkey = Pubkey::from_str(program_id).unwrap();
6007 let (expected_pda, _) = Pubkey::find_program_address(&[&bytes_seed], &program_pubkey);
6008 assert_eq!(result.unwrap(), expected_pda);
6009 }
6010
6011 #[test]
6012 fn test_pda_derivation_with_property_ref_pubkey() {
6013 let program_id = "11111111111111111111111111111111";
6015 let ref_pubkey = "So11111111111111111111111111111111111111112";
6016
6017 let address = surfpool_types::AccountAddress::Pda {
6018 program_id: program_id.to_string(),
6019 seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6020 "my_pubkey".to_string(),
6021 )],
6022 };
6023
6024 let mut values = HashMap::new();
6025 values.insert(
6026 "my_pubkey".to_string(),
6027 serde_json::Value::String(ref_pubkey.to_string()),
6028 );
6029
6030 let result = address.resolve(Some(&values));
6031 assert!(
6032 result.is_some(),
6033 "Should derive PDA with property ref pubkey"
6034 );
6035
6036 let program_pubkey = Pubkey::from_str(program_id).unwrap();
6038 let seed_pk = Pubkey::from_str(ref_pubkey).unwrap();
6039 let (expected_pda, _) = Pubkey::find_program_address(&[seed_pk.as_ref()], &program_pubkey);
6040 assert_eq!(result.unwrap(), expected_pda);
6041 }
6042
6043 #[test]
6044 fn test_pda_derivation_with_property_ref_u64() {
6045 let program_id = "11111111111111111111111111111111";
6047 let ref_value: u64 = 12345;
6048
6049 let address = surfpool_types::AccountAddress::Pda {
6050 program_id: program_id.to_string(),
6051 seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6052 "my_number".to_string(),
6053 )],
6054 };
6055
6056 let mut values = HashMap::new();
6057 values.insert(
6058 "my_number".to_string(),
6059 serde_json::Value::Number(ref_value.into()),
6060 );
6061
6062 let result = address.resolve(Some(&values));
6063 assert!(result.is_some(), "Should derive PDA with property ref u64");
6064
6065 let program_pubkey = Pubkey::from_str(program_id).unwrap();
6067 let (expected_pda, _) =
6068 Pubkey::find_program_address(&[&ref_value.to_le_bytes()], &program_pubkey);
6069 assert_eq!(result.unwrap(), expected_pda);
6070 }
6071
6072 #[test]
6073 fn test_pda_derivation_invalid_program_id() {
6074 let address = surfpool_types::AccountAddress::Pda {
6076 program_id: "invalid_program_id".to_string(),
6077 seeds: vec![surfpool_types::PdaSeed::String("test".to_string())],
6078 };
6079
6080 let result = address.resolve_simple();
6081 assert!(result.is_none(), "Should fail with invalid program ID");
6082 }
6083
6084 #[test]
6085 fn test_pda_derivation_invalid_pubkey_seed() {
6086 let program_id = "11111111111111111111111111111111";
6088 let address = surfpool_types::AccountAddress::Pda {
6089 program_id: program_id.to_string(),
6090 seeds: vec![surfpool_types::PdaSeed::Pubkey(
6091 "not_a_valid_pubkey".to_string(),
6092 )],
6093 };
6094
6095 let result = address.resolve_simple();
6096 assert!(result.is_none(), "Should fail with invalid pubkey seed");
6097 }
6098
6099 #[test]
6100 fn test_pda_derivation_missing_property_ref() {
6101 let program_id = "11111111111111111111111111111111";
6103 let address = surfpool_types::AccountAddress::Pda {
6104 program_id: program_id.to_string(),
6105 seeds: vec![surfpool_types::PdaSeed::PropertyRef(
6106 "nonexistent_property".to_string(),
6107 )],
6108 };
6109
6110 let result = address.resolve_simple(); assert!(result.is_none(), "Should fail with missing property ref");
6112 }
6113
6114 #[test]
6115 fn test_pubkey_address_resolution() {
6116 let pubkey_str = "So11111111111111111111111111111111111111112";
6118 let address = surfpool_types::AccountAddress::Pubkey(pubkey_str.to_string());
6119
6120 let result = address.resolve_simple();
6121 assert!(result.is_some(), "Should resolve pubkey address");
6122 assert_eq!(result.unwrap(), Pubkey::from_str(pubkey_str).unwrap());
6123 }
6124
6125 #[test]
6126 fn test_pda_deterministic() {
6127 let program_id = "KLend2g3cP87fffoy8q1mQqGKjrxjC8boSyAYavgmjD";
6129 let address = surfpool_types::AccountAddress::Pda {
6130 program_id: program_id.to_string(),
6131 seeds: vec![
6132 surfpool_types::PdaSeed::String("reserve".to_string()),
6133 surfpool_types::PdaSeed::Pubkey(
6134 "ByYiZxp8QrdN9qbdtaAiePN8AAr3qvTPppNJDpf5DVJ5".to_string(),
6135 ),
6136 surfpool_types::PdaSeed::Pubkey(
6137 "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string(),
6138 ),
6139 ],
6140 };
6141
6142 let result1 = address.resolve_simple();
6144 let result2 = address.resolve_simple();
6145 let result3 = address.resolve_simple();
6146
6147 assert!(result1.is_some());
6148 assert_eq!(result1, result2, "PDA derivation should be deterministic");
6149 assert_eq!(result2, result3, "PDA derivation should be deterministic");
6150 }
6151
6152 #[test]
6160 fn test_raydium_clmm_sol_usdc_pool_derivation() {
6161 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6166
6167 let amm_config = "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL";
6170
6171 let token_mint_0 = "So11111111111111111111111111111111111111112"; let token_mint_1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; let address = surfpool_types::AccountAddress::Pda {
6177 program_id: raydium_clmm_program.to_string(),
6178 seeds: vec![
6179 surfpool_types::PdaSeed::String("pool".to_string()),
6180 surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6181 surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6182 surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6183 ],
6184 };
6185
6186 let result = address.resolve_simple();
6187 assert!(result.is_some(), "Should derive Raydium CLMM pool PDA");
6188
6189 let expected_pool =
6191 Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6192 assert_eq!(
6193 result.unwrap(),
6194 expected_pool,
6195 "Derived pool address should match the known SOL/USDC CLMM pool"
6196 );
6197 }
6198
6199 #[test]
6200 fn test_raydium_clmm_pool_with_property_refs() {
6201 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6203
6204 let address = surfpool_types::AccountAddress::Pda {
6205 program_id: raydium_clmm_program.to_string(),
6206 seeds: vec![
6207 surfpool_types::PdaSeed::String("pool".to_string()),
6208 surfpool_types::PdaSeed::PropertyRef("amm_config".to_string()),
6209 surfpool_types::PdaSeed::PropertyRef("token_mint_0".to_string()),
6210 surfpool_types::PdaSeed::PropertyRef("token_mint_1".to_string()),
6211 ],
6212 };
6213
6214 let mut values = HashMap::new();
6217 values.insert(
6218 "amm_config".to_string(),
6219 serde_json::Value::String("3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL".to_string()),
6220 );
6221 values.insert(
6222 "token_mint_0".to_string(),
6223 serde_json::Value::String("So11111111111111111111111111111111111111112".to_string()),
6224 );
6225 values.insert(
6226 "token_mint_1".to_string(),
6227 serde_json::Value::String("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
6228 );
6229
6230 let result = address.resolve(Some(&values));
6231 assert!(
6232 result.is_some(),
6233 "Should derive pool PDA with property refs"
6234 );
6235
6236 let expected_pool =
6237 Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6238 assert_eq!(
6239 result.unwrap(),
6240 expected_pool,
6241 "Property ref derived pool should match known pool"
6242 );
6243 }
6244
6245 #[test]
6246 fn test_raydium_clmm_different_fee_tiers() {
6247 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6249
6250 let token_mint_0 = "So11111111111111111111111111111111111111112";
6252 let token_mint_1 = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6253
6254 let fee_tiers = [
6256 (
6257 "ultra_tight",
6258 "4BLNHtVe942GSs4teSZqGX24xwKNkqU7bGgNn3iUiUpw",
6259 ), ("tight", "HfERMT5DRA6C1TAqecrJQFpmkf3wsWTMncqnj3RDg5aw"), ("standard", "E64NGkDLLCdQ2yFNPcavaKptrEgmiQaNykUuLC1Qgwyp"), ("wide", "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x"), ];
6264
6265 let mut derived_pools = Vec::new();
6266
6267 for (tier_name, amm_config) in &fee_tiers {
6268 let address = surfpool_types::AccountAddress::Pda {
6269 program_id: raydium_clmm_program.to_string(),
6270 seeds: vec![
6271 surfpool_types::PdaSeed::String("pool".to_string()),
6272 surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6273 surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6274 surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6275 ],
6276 };
6277
6278 let result = address.resolve_simple();
6279 assert!(
6280 result.is_some(),
6281 "Should derive pool for {} fee tier",
6282 tier_name
6283 );
6284 derived_pools.push((tier_name, result.unwrap()));
6285 }
6286
6287 for i in 0..derived_pools.len() {
6289 for j in (i + 1)..derived_pools.len() {
6290 assert_ne!(
6291 derived_pools[i].1, derived_pools[j].1,
6292 "Pool for {} should differ from pool for {}",
6293 derived_pools[i].0, derived_pools[j].0
6294 );
6295 }
6296 }
6297
6298 for (tier_name, amm_config) in &fee_tiers {
6300 let address = surfpool_types::AccountAddress::Pda {
6301 program_id: raydium_clmm_program.to_string(),
6302 seeds: vec![
6303 surfpool_types::PdaSeed::String("pool".to_string()),
6304 surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6305 surfpool_types::PdaSeed::Pubkey(token_mint_0.to_string()),
6306 surfpool_types::PdaSeed::Pubkey(token_mint_1.to_string()),
6307 ],
6308 };
6309 let result2 = address.resolve_simple().unwrap();
6310 let original = derived_pools
6311 .iter()
6312 .find(|(name, _)| *name == tier_name)
6313 .unwrap();
6314 assert_eq!(
6315 original.1, result2,
6316 "PDA derivation should be deterministic for {} tier",
6317 tier_name
6318 );
6319 }
6320 }
6321
6322 #[test]
6323 fn test_raydium_clmm_token_order_matters() {
6324 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6327 let amm_config = "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL";
6329
6330 let usdc = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6331 let sol = "So11111111111111111111111111111111111111112";
6332
6333 let address_sol_usdc = surfpool_types::AccountAddress::Pda {
6335 program_id: raydium_clmm_program.to_string(),
6336 seeds: vec![
6337 surfpool_types::PdaSeed::String("pool".to_string()),
6338 surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6339 surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6340 surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6341 ],
6342 };
6343
6344 let address_usdc_sol = surfpool_types::AccountAddress::Pda {
6346 program_id: raydium_clmm_program.to_string(),
6347 seeds: vec![
6348 surfpool_types::PdaSeed::String("pool".to_string()),
6349 surfpool_types::PdaSeed::Pubkey(amm_config.to_string()),
6350 surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6351 surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6352 ],
6353 };
6354
6355 let result_sol_usdc = address_sol_usdc.resolve_simple().unwrap();
6356 let result_usdc_sol = address_usdc_sol.resolve_simple().unwrap();
6357
6358 assert_ne!(
6360 result_sol_usdc, result_usdc_sol,
6361 "Different token order should produce different PDA"
6362 );
6363
6364 let expected_pool =
6366 Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6367 assert_eq!(
6368 result_sol_usdc, expected_pool,
6369 "SOL/USDC order should match known pool"
6370 );
6371 assert_ne!(
6372 result_usdc_sol, expected_pool,
6373 "USDC/SOL order should NOT match known pool"
6374 );
6375 }
6376
6377 #[test]
6378 fn test_raydium_clmm_amm_config_derivation() {
6379 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6382
6383 let test_cases = [
6385 (0u16, "4BLNHtVe942GSs4teSZqGX24xwKNkqU7bGgNn3iUiUpw"), (1u16, "E64NGkDLLCdQ2yFNPcavaKptrEgmiQaNykUuLC1Qgwyp"), (2u16, "HfERMT5DRA6C1TAqecrJQFpmkf3wsWTMncqnj3RDg5aw"), (3u16, "A1BBtTYJd4i3xU8D6Tc2FzU6ZN4oXZWXKZnCxwbHXr8x"), (8u16, "3h2e43PunVA5K34vwKCLHWhZF4aZpyaC9RmxvshGAQpL"), ];
6391
6392 for (index, expected_address) in &test_cases {
6393 let address = surfpool_types::AccountAddress::Pda {
6394 program_id: raydium_clmm_program.to_string(),
6395 seeds: vec![
6396 surfpool_types::PdaSeed::String("amm_config".to_string()),
6397 surfpool_types::PdaSeed::U16Be(*index),
6398 ],
6399 };
6400
6401 let result = address.resolve_simple();
6402 assert!(
6403 result.is_some(),
6404 "Should derive AMM config for index {}",
6405 index
6406 );
6407
6408 let expected = Pubkey::from_str(expected_address).unwrap();
6409 assert_eq!(
6410 result.unwrap(),
6411 expected,
6412 "AMM config for index {} should match",
6413 index
6414 );
6415 }
6416 }
6417
6418 #[test]
6419 fn test_raydium_clmm_dynamic_pool_derivation() {
6420 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6424
6425 let config_index = 8u16;
6427 let sol = "So11111111111111111111111111111111111111112";
6428 let usdc = "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v";
6429
6430 let address = surfpool_types::AccountAddress::Pda {
6432 program_id: raydium_clmm_program.to_string(),
6433 seeds: vec![
6434 surfpool_types::PdaSeed::String("pool".to_string()),
6435 surfpool_types::PdaSeed::DerivedPda {
6437 program_id: raydium_clmm_program.to_string(),
6438 seeds: vec![
6439 surfpool_types::PdaSeed::String("amm_config".to_string()),
6440 surfpool_types::PdaSeed::U16Be(config_index),
6441 ],
6442 },
6443 surfpool_types::PdaSeed::Pubkey(sol.to_string()),
6444 surfpool_types::PdaSeed::Pubkey(usdc.to_string()),
6445 ],
6446 };
6447
6448 let result = address.resolve_simple();
6449 assert!(result.is_some(), "Should derive pool with nested PDA");
6450
6451 let expected_pool =
6453 Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6454 assert_eq!(
6455 result.unwrap(),
6456 expected_pool,
6457 "Dynamic derivation should match known SOL/USDC pool"
6458 );
6459 }
6460
6461 #[test]
6462 fn test_raydium_clmm_dynamic_pool_with_property_refs() {
6463 let raydium_clmm_program = "CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK";
6465
6466 let address = surfpool_types::AccountAddress::Pda {
6467 program_id: raydium_clmm_program.to_string(),
6468 seeds: vec![
6469 surfpool_types::PdaSeed::String("pool".to_string()),
6470 surfpool_types::PdaSeed::DerivedPda {
6472 program_id: raydium_clmm_program.to_string(),
6473 seeds: vec![
6474 surfpool_types::PdaSeed::String("amm_config".to_string()),
6475 surfpool_types::PdaSeed::U16BeRef("config_index".to_string()),
6476 ],
6477 },
6478 surfpool_types::PdaSeed::PropertyRef("token_mint_0".to_string()),
6479 surfpool_types::PdaSeed::PropertyRef("token_mint_1".to_string()),
6480 ],
6481 };
6482
6483 let mut values = HashMap::new();
6485 values.insert(
6486 "config_index".to_string(),
6487 serde_json::Value::Number(8.into()), );
6489 values.insert(
6490 "token_mint_0".to_string(),
6491 serde_json::Value::String("So11111111111111111111111111111111111111112".to_string()),
6492 );
6493 values.insert(
6494 "token_mint_1".to_string(),
6495 serde_json::Value::String("EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v".to_string()),
6496 );
6497
6498 let result = address.resolve(Some(&values));
6499 assert!(result.is_some(), "Should derive pool with property refs");
6500
6501 let expected_pool =
6502 Pubkey::from_str("3ucNos4NbumPLZNWztqGHNFFgkHeRMBQAVemeeomsUxv").unwrap();
6503 assert_eq!(
6504 result.unwrap(),
6505 expected_pool,
6506 "Property ref dynamic derivation should match known SOL/USDC pool"
6507 );
6508 }
6509
6510 #[test]
6511 fn test_snapshot_export_restore_round_trip() {
6512 use std::{collections::HashMap, io::Write};
6513
6514 use surfpool_types::AccountSnapshot;
6515 use tempfile::NamedTempFile;
6516
6517 let (mut svm, _events_rx, _geyser_rx) =
6518 SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6519
6520 let test_accounts: Vec<(Pubkey, Account)> = vec![
6522 (
6524 Pubkey::new_unique(),
6525 Account {
6526 lamports: 1_000_000_000,
6527 data: vec![],
6528 owner: solana_sdk_ids::system_program::id(),
6529 executable: false,
6530 rent_epoch: 0,
6531 },
6532 ),
6533 (
6535 Pubkey::new_unique(),
6536 Account {
6537 lamports: 500_000,
6538 data: vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
6539 owner: Pubkey::new_unique(),
6540 executable: false,
6541 rent_epoch: 100,
6542 },
6543 ),
6544 (
6546 Pubkey::new_unique(),
6547 Account {
6548 lamports: 2_000_000,
6549 data: (0..=255u8).collect::<Vec<u8>>(),
6550 owner: Pubkey::new_unique(),
6551 executable: false,
6552 rent_epoch: 200,
6553 },
6554 ),
6555 ];
6556
6557 for (pubkey, account) in &test_accounts {
6559 svm.set_account(pubkey, account.clone())
6560 .expect("Failed to set account");
6561 }
6562
6563 for (pubkey, expected_account) in &test_accounts {
6565 let actual_account = svm
6566 .get_account(pubkey)
6567 .expect("get_account should not error")
6568 .expect("Account should exist");
6569 assert_eq!(
6570 actual_account.lamports, expected_account.lamports,
6571 "Lamports should match for {}",
6572 pubkey
6573 );
6574 assert_eq!(
6575 actual_account.data, expected_account.data,
6576 "Data should match for {}",
6577 pubkey
6578 );
6579 assert_eq!(
6580 actual_account.owner, expected_account.owner,
6581 "Owner should match for {}",
6582 pubkey
6583 );
6584 }
6585
6586 let mut snapshot: HashMap<String, AccountSnapshot> = HashMap::new();
6588 for (pubkey, account) in &test_accounts {
6589 let account_snapshot = AccountSnapshot::new(
6590 account.lamports,
6591 account.owner.to_string(),
6592 account.executable,
6593 account.rent_epoch,
6594 general_purpose::STANDARD.encode(&account.data),
6595 None,
6596 );
6597 snapshot.insert(pubkey.to_string(), account_snapshot);
6598 }
6599
6600 let snapshot_json =
6602 serde_json::to_string_pretty(&snapshot).expect("Failed to serialize snapshot");
6603
6604 let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6606 temp_file
6607 .write_all(snapshot_json.as_bytes())
6608 .expect("Failed to write snapshot");
6609 let snapshot_path = temp_file.path().to_str().unwrap();
6610
6611 let (mut svm2, _events_rx2, _geyser_rx2) =
6613 SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6614
6615 for (pubkey, _) in &test_accounts {
6617 assert!(
6618 svm2.get_account(pubkey)
6619 .expect("get_account should not error")
6620 .is_none(),
6621 "Account {} should not exist in new SVM before restore",
6622 pubkey
6623 );
6624 }
6625
6626 let restored_count = svm2
6628 .restore_from_snapshot(snapshot_path)
6629 .expect("Failed to restore from snapshot");
6630
6631 assert_eq!(
6632 restored_count,
6633 test_accounts.len(),
6634 "Should restore all accounts"
6635 );
6636
6637 for (pubkey, expected_account) in &test_accounts {
6639 let restored_account = svm2
6640 .get_account(pubkey)
6641 .expect("get_account should not error")
6642 .unwrap_or_else(|| panic!("Account {} should exist after restore", pubkey));
6643
6644 assert_eq!(
6645 restored_account.lamports, expected_account.lamports,
6646 "Lamports should match after restore for {}",
6647 pubkey
6648 );
6649 assert_eq!(
6650 restored_account.data, expected_account.data,
6651 "Data should match after restore for {}",
6652 pubkey
6653 );
6654 assert_eq!(
6655 restored_account.owner, expected_account.owner,
6656 "Owner should match after restore for {}",
6657 pubkey
6658 );
6659 assert_eq!(
6660 restored_account.executable, expected_account.executable,
6661 "Executable flag should match after restore for {}",
6662 pubkey
6663 );
6664 assert_eq!(
6665 restored_account.rent_epoch, expected_account.rent_epoch,
6666 "Rent epoch should match after restore for {}",
6667 pubkey
6668 );
6669 }
6670 }
6671
6672 #[test]
6673 fn test_snapshot_restore_invalid_file() {
6674 let (mut svm, _events_rx, _geyser_rx) =
6675 SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6676
6677 let result = svm.restore_from_snapshot("/nonexistent/path/to/snapshot.json");
6679 assert!(result.is_err(), "Should fail with non-existent file");
6680
6681 use std::io::Write;
6683
6684 use tempfile::NamedTempFile;
6685
6686 let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6687 temp_file
6688 .write_all(b"not valid json")
6689 .expect("Failed to write");
6690 let invalid_json_path = temp_file.path().to_str().unwrap();
6691
6692 let result = svm.restore_from_snapshot(invalid_json_path);
6693 assert!(result.is_err(), "Should fail with invalid JSON");
6694 }
6695
6696 #[test]
6697 fn test_snapshot_restore_partial_failure() {
6698 use std::{collections::HashMap, io::Write};
6699
6700 use surfpool_types::AccountSnapshot;
6701 use tempfile::NamedTempFile;
6702
6703 let (mut svm, _events_rx, _geyser_rx) =
6704 SurfnetSvm::new(SurfnetSvmConfig::default()).unwrap();
6705
6706 let mut snapshot: HashMap<String, AccountSnapshot> = HashMap::new();
6708
6709 let valid_pubkey = Pubkey::new_unique();
6711 snapshot.insert(
6712 valid_pubkey.to_string(),
6713 AccountSnapshot::new(
6714 1_000_000,
6715 solana_sdk_ids::system_program::id().to_string(),
6716 false,
6717 0,
6718 general_purpose::STANDARD.encode(&[1, 2, 3]),
6719 None,
6720 ),
6721 );
6722
6723 snapshot.insert(
6725 Pubkey::new_unique().to_string(),
6726 AccountSnapshot::new(
6727 500_000,
6728 "invalid_pubkey_string".to_string(), false,
6730 0,
6731 general_purpose::STANDARD.encode(&[4, 5, 6]),
6732 None,
6733 ),
6734 );
6735
6736 let snapshot_json =
6738 serde_json::to_string_pretty(&snapshot).expect("Failed to serialize snapshot");
6739 let mut temp_file = NamedTempFile::new().expect("Failed to create temp file");
6740 temp_file
6741 .write_all(snapshot_json.as_bytes())
6742 .expect("Failed to write snapshot");
6743 let snapshot_path = temp_file.path().to_str().unwrap();
6744
6745 let restored_count = svm
6747 .restore_from_snapshot(snapshot_path)
6748 .expect("Restore should succeed even with partial failures");
6749
6750 assert_eq!(restored_count, 1, "Should restore only the valid account");
6751
6752 let restored_account = svm
6754 .get_account(&valid_pubkey)
6755 .expect("get_account should not error")
6756 .expect("Valid account should be restored");
6757 assert_eq!(restored_account.lamports, 1_000_000);
6758 }
6759}