diff --git a/Cargo.toml b/Cargo.toml index 10616e3f..9632d61b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [workspace.package] version = "0.11.0" edition = "2024" -description = "Surfpool is the best place to train before surfing Solana." +description = "Surfpool is where developers start their Solana journey." license = "Apache-2.0" readme = "README.md" repository = "https://github.com/txtx/surfpool" @@ -19,7 +19,7 @@ members = [ "crates/subgraph", "crates/types", ] -exclude = ["examples/hello-geyser"] +exclude = ["examples/*"] default-members = ["crates/cli"] resolver = "2" @@ -39,7 +39,10 @@ bincode = "1.3.3" blake3 = { version = "1.8.2", default-features = false } borsh = { version = "1.5.5", default-features = false } bs58 = { version = "0.5.0", default-features = false } -chrono = { version = "0.4.42", features = ["alloc", "clock"], default-features = false } +chrono = { version = "0.4.42", features = [ + "alloc", + "clock", +], default-features = false } clap = { version = "4.5.48", default-features = false } clap_complete = { version = "4.5.48", default-features = false } convert_case = "0.8.0" @@ -57,13 +60,17 @@ fern = { version = "0.7.1", default-features = false } fork = "0.2.0" futures = { version = "0.3.22", default-features = false } hex = { version = "0.4.3", default-features = false } -hiro-system-kit = { version = "0.3.4", default-features = false, features = ["tokio_helpers"] } +hiro-system-kit = { version = "0.3.4", default-features = false, features = [ + "tokio_helpers", +] } include_dir = "0.7.4" indicatif = { version = "0.18.0", default-features = false } ipc-channel = "0.19.0" itertools = { version = "0.14.0", default-features = false } json5 = "0.4.1" -jsonrpc-core = { version = "18.0.0", features = ["futures"], default-features = false } +jsonrpc-core = { version = "18.0.0", features = [ + "futures", +], default-features = false } jsonrpc-core-client = { version = "18.0.0", default-features = false } jsonrpc-derive = "18.0.0" jsonrpc-http-server = "18.0.0" @@ -83,12 +90,14 @@ mustache = "0.9.0" notify = { version = "8.0.0", default-features = false } npm_rs = "1.0.0" once_cell = { version = "1.19.0", default-features = false } -ratatui = { version = "0.29.0", features = ["crossterm"], default-features = false } +ratatui = { version = "0.29.0", features = [ + "crossterm", +], default-features = false } reqwest = { version = "0.12.23", default-features = false } rmcp = { git = "https://github.com/modelcontextprotocol/rust-sdk", rev = "ff71a526156e6c9409c450f71eccd6aced9bc339", package = "rmcp" } rust-embed = "8.2.0" serde = { version = "1.0.226", default-features = false } -serde_derive = { version = "1.0.226", default-features = false } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 +serde_derive = { version = "1.0.226", default-features = false } # must match the serde version, see https://github.com/serde-rs/serde/issues/2584#issuecomment-1685252251 serde_json = { version = "1.0.135", default-features = false } serde_with = { version = "3.14.0", default-features = false } solana-account = { version = "3.0.0", default-features = false } @@ -118,7 +127,9 @@ solana-rpc-client = { version = "3.0.0", default-features = false } solana-rpc-client-api = { version = "3.0.0", default-features = false } solana-runtime = { version = "3.0.0", default-features = false } solana-sdk-ids = { version = "3.0.0", default-features = false } -solana-signature = { version = "3.0.0", default-features = false, features = ["rand"] } +solana-signature = { version = "3.0.0", default-features = false, features = [ + "rand", +] } solana-signer = { version = "3.0.0", default-features = false } solana-slot-hashes = { version = "3.0.0", default-features = false } solana-system-interface = { version = "2.0.0", default-features = false } diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index e631d1b1..5806c502 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -749,6 +749,37 @@ pub trait SurfnetCheatcodes { config: Option, ) -> Result>; + /// A cheat code to reset a network. + /// + /// ## Returns + /// An `RpcResponse<()>` indicating whether the network reset was successful. + /// + /// ## Example Request + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "id": 1, + /// "method": "surfnet_resetNetwork", /// } + /// ``` + /// + /// ## Example Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": { + /// "context": { + /// "slot": 123456789, + /// "apiVersion": "2.3.8" + /// }, + /// "value": null /// }, + /// "id": 1 + /// } + /// ``` + /// + + #[rpc(meta, name = "surfnet_resetNetwork")] + fn reset_network(&self, meta: Self::Metadata) -> Result>; + /// A cheat code to export a snapshot of all accounts in the Surfnet SVM. /// /// This method retrieves the current state of all accounts stored in the Surfnet Virtual Machine (SVM) @@ -1422,6 +1453,15 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { }) } + fn reset_network(&self, meta: Self::Metadata) -> Result> { + let svm_locker = meta.get_svm_locker()?; + svm_locker.reset_network()?; + Ok(RpcResponse { + context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), + value: (), + }) + } + fn stream_account( &self, meta: Self::Metadata, diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index bcc1fce4..12884ba4 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -1675,6 +1675,15 @@ impl SurfnetSvmLocker { }) } + /// Resets SVM state + /// + /// This function coordinates the reset of accounts by calling the SVM's reset_account method. + pub fn reset_network(&self) -> SurfpoolResult<()> { + let simnet_events_tx = self.simnet_events_tx(); + let _ = simnet_events_tx.send(SimnetEvent::info("Resetting network...")); + self.with_svm_writer(move |svm_writer| svm_writer.reset_network()) + } + /// Streams an account by its pubkey. pub fn stream_account( &self, diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 9cd93ba5..e27f41fd 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -677,6 +677,51 @@ impl SurfnetSvm { } } + pub fn reset_network(&mut self) -> SurfpoolResult<()> { + // pub inner: LiteSVM, + let mut inner = LiteSVM::new() + .with_feature_set(self.feature_set.clone()) + .with_blockhash_check(false) + .with_sigverify(false); + + // Add the native mint (SOL) to the SVM + create_native_mint(&mut inner); + let native_mint_account = inner + .get_account(&spl_token_interface::native_mint::ID) + .unwrap(); + let parsed_mint_account = MintAccount::unpack(&native_mint_account.data).unwrap(); + + // Load native mint into owned account and token mint indexes + let accounts_by_owner = HashMap::from([( + native_mint_account.owner, + vec![spl_token_interface::native_mint::ID], + )]); + let token_mints = + HashMap::from([(spl_token_interface::native_mint::ID, parsed_mint_account)]); + + self.inner = inner; + self.blocks.clear(); + self.transactions.clear(); + self.transactions_queued_for_confirmation.clear(); + self.transactions_queued_for_finalization.clear(); + self.perf_samples.clear(); + self.transactions_processed = 0; + self.profile_tag_map.clear(); + self.simulated_transaction_profiles.clear(); + self.accounts_by_owner = accounts_by_owner; + self.account_associated_data.clear(); + self.token_accounts.clear(); + self.token_mints = token_mints; + self.token_accounts_by_owner.clear(); + self.token_accounts_by_delegate.clear(); + self.token_accounts_by_mint.clear(); + self.non_circulating_accounts.clear(); + self.registered_idls.clear(); + self.runbook_executions.clear(); + self.streamed_accounts.clear(); + Ok(()) + } + pub fn reset_account( &mut self, pubkey: &Pubkey, diff --git a/crates/core/src/tests/integration.rs b/crates/core/src/tests/integration.rs index 81e12e17..2a15b8a2 100644 --- a/crates/core/src/tests/integration.rs +++ b/crates/core/src/tests/integration.rs @@ -3962,3 +3962,52 @@ fn test_reset_streamed_account_cascade() { assert!(svm_locker.get_account_local(&owner).inner.is_none()); assert!(svm_locker.get_account_local(&owned).inner.is_none()); } + +#[test] +fn test_reset_network() { + let (svm_instance, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::new(); + let svm_locker = SurfnetSvmLocker::new(svm_instance); + + // Create owner account and owned account + let owner = Pubkey::new_unique(); + let owned = Pubkey::new_unique(); + + let owner_account = Account { + lamports: 10 * LAMPORTS_PER_SOL, + data: vec![0x01, 0x02], + owner: solana_sdk_ids::system_program::id(), + executable: false, + rent_epoch: 0, + }; + + let owned_account = Account { + lamports: 5 * LAMPORTS_PER_SOL, + data: vec![0x03, 0x04], + owner, // Owned by the first account + executable: false, + rent_epoch: 0, + }; + + // Insert accounts + svm_locker + .with_svm_writer(|svm_writer| { + svm_writer.set_account(&owner, owner_account).unwrap(); + svm_writer.set_account(&owned, owned_account).unwrap(); + Ok::<(), SurfpoolError>(()) + }) + .unwrap(); + + // Verify accounts exist + assert!(!svm_locker.get_account_local(&owner).inner.is_none()); + assert!(!svm_locker.get_account_local(&owned).inner.is_none()); + + // Reset with cascade=true (for regular accounts, doesn't cascade but tests the code path) + svm_locker.reset_network().unwrap(); + + // Owner is deleted, owned account is deleted + assert!(svm_locker.get_account_local(&owner).inner.is_none()); + assert!(svm_locker.get_account_local(&owned).inner.is_none()); + + // Clean up + svm_locker.reset_account(owned, false).unwrap(); +} diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 19b63a70..9dc885bd 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -917,6 +917,10 @@ impl FifoMap { self.map.len() } + pub fn clear(&mut self) { + self.map.clear(); + } + pub fn is_empty(&self) -> bool { self.map.is_empty() }