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