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