Skip to content

Commit b2256f9

Browse files
authored
feat(core): add support for loading account snapshots at startup (solana-foundation#464)
1 parent 37b0e8f commit b2256f9

7 files changed

Lines changed: 656 additions & 7 deletions

File tree

crates/cli/src/cli/mod.rs

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use std::{env, fs::File, path::PathBuf, process, str::FromStr};
1+
use std::{collections::BTreeMap, env, fs::File, path::PathBuf, process, str::FromStr};
22

33
use chrono::Local;
44
use clap::{ArgAction, CommandFactory, Parser, Subcommand};
@@ -13,7 +13,7 @@ use solana_pubkey::Pubkey;
1313
use solana_signer::{EncodableKey, Signer};
1414
use surfpool_mcp::McpOptions;
1515
use surfpool_types::{
16-
BlockProductionMode, CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED,
16+
AccountSnapshot, BlockProductionMode, CHANGE_TO_DEFAULT_STUDIO_PORT_ONCE_SUPERVISOR_MERGED,
1717
DEFAULT_NETWORK_HOST, DEFAULT_RPC_PORT, DEFAULT_SLOT_TIME_MS, DEFAULT_WS_PORT, RpcConfig,
1818
SimnetConfig, SimnetEvent, StudioConfig, SubgraphConfig, SurfpoolConfig, SvmFeature,
1919
SvmFeatureConfig,
@@ -245,6 +245,13 @@ pub struct StartSimnet {
245245
/// A set of inputs to use for the runbook (eg. surfpool start --runbook-input myInputs.json)
246246
#[arg(long = "runbook-input", short = 'i')]
247247
pub runbook_input: Vec<String>,
248+
/// Path to JSON snapshot file(s) to preload accounts from. Can be specified multiple times.
249+
/// (eg. surfpool start --snapshot ./snapshot1.json --snapshot ./snapshot2.json)
250+
/// The snapshot format matches the output of surfnet_exportSnapshot RPC method.
251+
/// Account values can be null to fetch the account from the remote RPC instead.
252+
/// When multiple files are provided, later files override earlier ones for duplicate keys.
253+
#[arg(long = "snapshot")]
254+
pub snapshot: Vec<String>,
248255
}
249256

250257
fn parse_svm_feature(s: &str) -> Result<SvmFeature, String> {
@@ -372,7 +379,11 @@ impl StartSimnet {
372379
config
373380
}
374381

375-
pub fn simnet_config(&self, airdrop_addresses: Vec<Pubkey>) -> SimnetConfig {
382+
pub fn simnet_config(
383+
&self,
384+
airdrop_addresses: Vec<Pubkey>,
385+
snapshot: BTreeMap<String, Option<AccountSnapshot>>,
386+
) -> SimnetConfig {
376387
let remote_rpc_url = if !self.offline {
377388
Some(self.datasource_rpc_url())
378389
} else {
@@ -396,6 +407,7 @@ impl StartSimnet {
396407
},
397408
feature_config: self.feature_config(),
398409
skip_signature_verification: false,
410+
snapshot,
399411
}
400412
}
401413

@@ -415,15 +427,19 @@ impl StartSimnet {
415427
SubgraphConfig {}
416428
}
417429

418-
pub fn surfpool_config(&self, airdrop_addresses: Vec<Pubkey>) -> SurfpoolConfig {
430+
pub fn surfpool_config(
431+
&self,
432+
airdrop_addresses: Vec<Pubkey>,
433+
snapshot: BTreeMap<String, Option<AccountSnapshot>>,
434+
) -> SurfpoolConfig {
419435
let plugin_config_path = self
420436
.plugin_config_path
421437
.iter()
422438
.map(PathBuf::from)
423439
.collect::<Vec<_>>();
424440

425441
SurfpoolConfig {
426-
simnets: vec![self.simnet_config(airdrop_addresses)],
442+
simnets: vec![self.simnet_config(airdrop_addresses, snapshot)],
427443
rpc: self.rpc_config(),
428444
subgraph: self.subgraph_config(),
429445
studio: self.studio_config(),

crates/cli/src/cli/simnet/mod.rs

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,8 +82,39 @@ pub async fn handle_start_local_surfnet_command(
8282
Some(keypair)
8383
};
8484

85+
// Parse and merge snapshot files (multiple files supported, later files override earlier ones)
86+
// The actual loading happens in the runloop after the locker is created
87+
let snapshot = {
88+
let mut merged_snapshot: std::collections::BTreeMap<
89+
String,
90+
Option<surfpool_types::AccountSnapshot>,
91+
> = std::collections::BTreeMap::new();
92+
93+
for snapshot_path in &cmd.snapshot {
94+
let file_location = FileLocation::from_path(std::path::PathBuf::from(snapshot_path));
95+
let content = file_location
96+
.read_content_as_utf8()
97+
.map_err(|e| format!("Failed to read snapshot file '{}': {}", snapshot_path, e))?;
98+
let snapshot_data: std::collections::BTreeMap<
99+
String,
100+
Option<surfpool_types::AccountSnapshot>,
101+
> = serde_json::from_str(&content)
102+
.map_err(|e| format!("Failed to parse snapshot JSON '{}': {}", snapshot_path, e))?;
103+
let _ = simnet_events_tx.send(SimnetEvent::info(format!(
104+
"Loaded {} accounts from snapshot file: {}",
105+
snapshot_data.len(),
106+
snapshot_path
107+
)));
108+
109+
// Merge into the combined snapshot (later files override earlier ones)
110+
merged_snapshot.extend(snapshot_data);
111+
}
112+
113+
merged_snapshot
114+
};
115+
85116
// Build config
86-
let config = cmd.surfpool_config(airdrop_addresses);
117+
let config = cmd.surfpool_config(airdrop_addresses, snapshot);
87118

88119
let studio_binding_address = config.studio.get_studio_base_url();
89120

@@ -155,6 +186,8 @@ pub async fn handle_start_local_surfnet_command(
155186
})
156187
.map_err(|e| format!("{}", e))?;
157188

189+
// Collect events that occur before Ready so we can re-send them to the TUI
190+
let mut early_events = Vec::new();
158191
loop {
159192
match simnet_events_rx.recv() {
160193
Ok(SimnetEvent::Aborted(error)) => {
@@ -163,10 +196,16 @@ pub async fn handle_start_local_surfnet_command(
163196
}
164197
Ok(SimnetEvent::Shutdown) => return Ok(()),
165198
Ok(SimnetEvent::Ready) => break,
166-
_other => continue,
199+
Ok(other) => early_events.push(other),
200+
Err(_) => continue,
167201
}
168202
}
169203

204+
// Re-send early events (like snapshot loading messages) so the TUI receives them
205+
for event in early_events {
206+
let _ = simnet_events_tx.send(event);
207+
}
208+
170209
for event in airdrop_events {
171210
let _ = simnet_events_tx.send(event);
172211
}

crates/core/src/runloops/mod.rs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,36 @@ pub async fn start_local_surfnet_runloop(
109109
.await?;
110110

111111
svm_locker.airdrop_pubkeys(simnet.airdrop_token_amount, &simnet.airdrop_addresses);
112+
113+
// Load snapshot accounts if provided
114+
if !simnet.snapshot.is_empty() {
115+
match svm_locker
116+
.load_snapshot(
117+
&simnet.snapshot,
118+
remote_rpc_client.as_ref(),
119+
CommitmentConfig::confirmed(),
120+
)
121+
.await
122+
{
123+
Ok(loaded_count) => {
124+
let _ = svm_locker.with_svm_reader(|svm| {
125+
svm.simnet_events_tx.send(SimnetEvent::info(format!(
126+
"Preloaded {} accounts from snapshot(s) into SVM",
127+
loaded_count
128+
)))
129+
});
130+
}
131+
Err(e) => {
132+
let _ = svm_locker.with_svm_reader(|svm| {
133+
svm.simnet_events_tx.send(SimnetEvent::warn(format!(
134+
"Error loading snapshot accounts: {}",
135+
e
136+
)))
137+
});
138+
}
139+
}
140+
}
141+
112142
let simnet_events_tx_cc = svm_locker.simnet_events_tx();
113143

114144
let (plugin_manager_commands_rx, _rpc_handle, _ws_handle) = start_rpc_servers_runloop(
@@ -726,6 +756,40 @@ fn start_geyser_runloop(
726756
}
727757
}
728758
}
759+
Ok(GeyserEvent::StartupAccountUpdate(account_update)) => {
760+
let GeyserAccountUpdate {
761+
pubkey,
762+
account,
763+
slot,
764+
sanitized_transaction,
765+
write_version,
766+
} = account_update;
767+
768+
let account_replica = ReplicaAccountInfoV3 {
769+
pubkey: pubkey.as_ref(),
770+
lamports: account.lamports,
771+
owner: account.owner.as_ref(),
772+
executable: account.executable,
773+
rent_epoch: account.rent_epoch,
774+
data: account.data.as_ref(),
775+
write_version,
776+
txn: sanitized_transaction.as_ref(),
777+
};
778+
779+
// Send startup account updates with is_startup=true
780+
for plugin in surfpool_plugin_manager.iter() {
781+
if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, true) {
782+
let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to send startup account update to Geyser plugin: {:?}", e)));
783+
}
784+
}
785+
786+
#[cfg(feature = "geyser_plugin")]
787+
for plugin in plugin_manager.plugins.iter() {
788+
if let Err(e) = plugin.update_account(ReplicaAccountInfoVersions::V0_0_3(&account_replica), slot, true) {
789+
let _ = simnet_events_tx.send(SimnetEvent::error(format!("Failed to send startup account update to Geyser plugin: {:?}", e)));
790+
}
791+
}
792+
}
729793
}
730794
}
731795
};

0 commit comments

Comments
 (0)