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