Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions crates/core/src/surfnet/locker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand All @@ -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)
Expand Down Expand Up @@ -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(())
}
Expand Down
15 changes: 15 additions & 0 deletions crates/core/src/surfnet/remote.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Option<Account>> {
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,
Expand Down
39 changes: 37 additions & 2 deletions crates/core/src/surfnet/surfnet_lite_svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::<RecentBlockhashes>();
let slot_hashes = self.svm.get_sysvar::<SlotHashes>();
let clock = self.svm.get_sysvar::<Clock>();
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 {
Expand Down
42 changes: 42 additions & 0 deletions crates/core/src/surfnet/svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -653,6 +653,9 @@ impl SurfnetSvm {
remote_ctx: &Option<SurfnetRemoteClient>,
do_profile_instructions: bool,
log_bytes_limit: Option<usize>,
stake_history_account: Option<Account>,
stake_config_account: Option<Account>,
stake_program_accounts: Option<(Account, Account)>,
) {
self.chain_tip = self.new_blockhash();
self.latest_epoch_info = epoch_info.clone();
Expand All @@ -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
Expand Down