diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index 04c0f2bc..7b88c996 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -236,6 +236,60 @@ impl SurfnetSvmLocker { }; epoch_info.transaction_count = None; + // Fetch stake-related accounts from remote if available. + // - StakeHistory: accessed via get_sysvar syscall, not as a transaction account + // - StakeConfig: LiteSVM creates a minimal default that the Stake program can't + // deserialize, so we overwrite it with the real mainnet data + // - Stake program + programdata: LiteSVM bundles an outdated stake ELF that + // can't deserialize V4 vote accounts used on mainnet + #[allow(deprecated)] + let (stake_history_account, stake_config_account, stake_program_accounts) = + if let Some(remote_client) = remote_ctx { + let history = remote_client + .get_raw_account(&solana_sdk_ids::sysvar::stake_history::id()) + .await + .ok() + .flatten(); + let config = remote_client + .get_raw_account(&solana_sdk_ids::stake::config::id()) + .await + .ok() + .flatten(); + let stake_program = remote_client + .get_raw_account(&solana_sdk_ids::stake::id()) + .await + .ok() + .flatten(); + let stake_programdata = remote_client + .get_raw_account( + &solana_loader_v3_interface::get_program_data_address( + &solana_sdk_ids::stake::id(), + ), + ) + .await + .ok() + .flatten(); + let stake_prog = match (stake_program, stake_programdata) { + (Some(prog), Some(data)) => { + info!( + "Fetched Stake program from remote: program={}, programdata={}", + prog.data.len(), + data.data.len(), + ); + Some((prog, data)) + } + _ => None, + }; + info!( + "Fetched stake accounts from remote: StakeHistory={}, StakeConfig={}", + history.as_ref().map_or(0, |a| a.data.len()), + config.as_ref().map_or(0, |a| a.data.len()), + ); + (history, config, stake_prog) + } else { + (None, None, None) + }; + self.with_svm_writer(|svm_writer| { svm_writer.initialize( epoch_info.clone(), @@ -244,6 +298,9 @@ impl SurfnetSvmLocker { remote_ctx, do_profile_instructions, log_bytes_limit, + stake_history_account, + stake_config_account, + stake_program_accounts, ); }); Ok(epoch_info) @@ -2010,9 +2067,74 @@ impl SurfnetSvmLocker { }; epoch_info.transaction_count = None; + // Re-fetch stake-related accounts from remote after reset (same as initialize) + #[allow(deprecated)] + let (stake_history_account, stake_config_account, stake_program_accounts) = + if let Some(remote_client) = remote_ctx { + let history = remote_client + .get_raw_account(&solana_sdk_ids::sysvar::stake_history::id()) + .await + .ok() + .flatten(); + let config = remote_client + .get_raw_account(&solana_sdk_ids::stake::config::id()) + .await + .ok() + .flatten(); + let stake_program = remote_client + .get_raw_account(&solana_sdk_ids::stake::id()) + .await + .ok() + .flatten(); + let stake_programdata = remote_client + .get_raw_account( + &solana_loader_v3_interface::get_program_data_address( + &solana_sdk_ids::stake::id(), + ), + ) + .await + .ok() + .flatten(); + let stake_prog = match (stake_program, stake_programdata) { + (Some(prog), Some(data)) => Some((prog, data)), + _ => None, + }; + (history, config, stake_prog) + } else { + (None, None, None) + }; + self.with_svm_writer(move |svm_writer| { let _ = svm_writer.reset_network(epoch_info); let _ = svm_writer.offline_accounts.clear(); + // Restore StakeHistory, StakeConfig, and Stake program after reset. + if let Some(account) = stake_history_account { + let _ = svm_writer.inner.svm.set_account( + solana_sdk_ids::sysvar::stake_history::id(), + account, + ); + } + #[allow(deprecated)] + if let Some(account) = stake_config_account { + let _ = svm_writer.inner.svm.set_account( + solana_sdk_ids::stake::config::id(), + account, + ); + } + if let Some((program_account, programdata_account)) = stake_program_accounts { + let programdata_address = + solana_loader_v3_interface::get_program_data_address( + &solana_sdk_ids::stake::id(), + ); + let _ = svm_writer + .inner + .svm + .set_account(programdata_address, programdata_account); + let _ = svm_writer + .inner + .svm + .set_account(solana_sdk_ids::stake::id(), program_account); + } }); Ok(()) } diff --git a/crates/core/src/surfnet/remote.rs b/crates/core/src/surfnet/remote.rs index 99aa44fc..f2d9f5be 100644 --- a/crates/core/src/surfnet/remote.rs +++ b/crates/core/src/surfnet/remote.rs @@ -96,6 +96,21 @@ impl SurfnetRemoteClient { self.client.get_epoch_schedule().await.map_err(Into::into) } + /// Fetches a raw account by pubkey without any classification logic (token, program, etc.). + /// Useful for fetching sysvar accounts that are accessed via syscalls rather than as + /// transaction accounts. + pub async fn get_raw_account( + &self, + pubkey: &Pubkey, + ) -> SurfpoolResult> { + let res = self + .client + .get_account_with_commitment(pubkey, CommitmentConfig::finalized()) + .await + .map_err(|e| SurfpoolError::get_account(*pubkey, e))?; + Ok(res.value) + } + pub async fn get_account( &self, pubkey: &Pubkey, diff --git a/crates/core/src/surfnet/surfnet_lite_svm.rs b/crates/core/src/surfnet/surfnet_lite_svm.rs index 72bfb098..12fc0563 100644 --- a/crates/core/src/surfnet/surfnet_lite_svm.rs +++ b/crates/core/src/surfnet/surfnet_lite_svm.rs @@ -122,23 +122,58 @@ impl SurfnetLiteSvm { return; } - // Preserve all critical sysvars across garbage collection + // Preserve all critical sysvars/config accounts across garbage collection // - RecentBlockhashes: for blockhash validation // - SlotHashes: for ALT resolution // - Clock: for time-dependent programs + // - StakeHistory: for Stake program Split/Deactivate instructions (accessed via syscall) + // - StakeConfig: LiteSVM default is too small for the Stake program to deserialize + // - Stake program + programdata: mainnet version replaces outdated bundled ELF let recent_blockhashes = self.svm.get_sysvar::(); let slot_hashes = self.svm.get_sysvar::(); let clock = self.svm.get_sysvar::(); + let stake_history_account = + self.svm.get_account(&solana_sdk_ids::sysvar::stake_history::id()); + let stake_config_account = + self.svm.get_account(&solana_sdk_ids::stake::config::id()); + let stake_program_account = + self.svm.get_account(&solana_sdk_ids::stake::id()); + let stake_programdata_account = self.svm.get_account( + &solana_loader_v3_interface::get_program_data_address(&solana_sdk_ids::stake::id()), + ); // todo: this is also resetting the log bytes limit and airdrop keypair, would be nice to avoid self.svm = Self::full_litesvm_settings(feature_set); create_native_mint(self); - // Restore all preserved sysvars + // Restore all preserved sysvars/config accounts self.svm.set_sysvar(&recent_blockhashes); self.svm.set_sysvar(&slot_hashes); self.svm.set_sysvar(&clock); + if let Some(account) = stake_history_account { + let _ = self + .svm + .set_account(solana_sdk_ids::sysvar::stake_history::id(), account); + } + if let Some(account) = stake_config_account { + let _ = self + .svm + .set_account(solana_sdk_ids::stake::config::id(), account); + } + // Restore the mainnet Stake program (programdata first, then program account + // to trigger program cache recompilation) + if let (Some(programdata), Some(program)) = + (stake_programdata_account, stake_program_account) + { + let programdata_address = solana_loader_v3_interface::get_program_data_address( + &solana_sdk_ids::stake::id(), + ); + let _ = self.svm.set_account(programdata_address, programdata); + let _ = self + .svm + .set_account(solana_sdk_ids::stake::id(), program); + } } pub fn apply_feature_config(&mut self, feature_set: FeatureSet) -> &mut Self { diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index cf2ad003..a14e767f 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -653,6 +653,9 @@ impl SurfnetSvm { remote_ctx: &Option, do_profile_instructions: bool, log_bytes_limit: Option, + stake_history_account: Option, + stake_config_account: Option, + stake_program_accounts: Option<(Account, Account)>, ) { self.chain_tip = self.new_blockhash(); self.latest_epoch_info = epoch_info.clone(); @@ -674,6 +677,45 @@ impl SurfnetSvm { self.inner.set_sysvar(&epoch_schedule); + // Set StakeHistory from remote if available. The Stake program accesses this + // sysvar via the get_sysvar syscall (not as a transaction account), so it must + // be proactively populated. + if let Some(account) = stake_history_account { + let _ = self.inner.svm.set_account( + solana_sdk_ids::sysvar::stake_history::id(), + account, + ); + } + + // Overwrite the default StakeConfig with real mainnet data. LiteSVM creates a + // minimal default (10 bytes) that the Stake program cannot deserialize, causing + // DelegateStake to fail with InvalidAccountData. + #[allow(deprecated)] + if let Some(account) = stake_config_account { + let _ = self.inner.svm.set_account( + solana_sdk_ids::stake::config::id(), + account, + ); + } + + // Replace the bundled Stake program with the live mainnet version. + // LiteSVM ships core_bpf_stake-1.0.1.so which was compiled against an older + // VoteStateVersions enum that doesn't include V4. Mainnet vote accounts now + // use V4, so DelegateStake fails with InvalidAccountData when it tries to + // deserialize them. Loading the current mainnet Stake program resolves this. + if let Some((program_account, programdata_account)) = stake_program_accounts { + let programdata_address = + solana_loader_v3_interface::get_program_data_address(&solana_sdk_ids::stake::id()); + let _ = self + .inner + .svm + .set_account(programdata_address, programdata_account); + let _ = self + .inner + .svm + .set_account(solana_sdk_ids::stake::id(), program_account); + } + if let Some(remote_client) = remote_ctx { let _ = self .simnet_events_tx