From 010666923ae53b28787c50cac7a573d3eaa05d5e Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 24 Sep 2025 15:59:26 -0400 Subject: [PATCH 1/3] feat(core): add surfnet_readyCheck RPC endpoint --- crates/cli/src/cli/simnet/mod.rs | 7 +++---- crates/cli/src/tui/simnet.rs | 6 +++--- crates/core/src/rpc/surfnet_cheatcodes.rs | 12 ++++++++++++ crates/core/src/runloops/mod.rs | 6 ++++-- crates/core/src/surfnet/locker.rs | 4 ++++ crates/core/src/surfnet/svm.rs | 2 ++ crates/types/src/types.rs | 2 +- 7 files changed, 29 insertions(+), 10 deletions(-) diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index c6719ac2..a7c30881 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -360,14 +360,13 @@ fn log_events( SimnetEvent::RunbookStarted(runbook_id) => { deployment_completed = false; info!("Runbook '{}' execution started", runbook_id); - let _ = - simnet_commands_tx.send(SimnetCommand::SetInstructionProfiling(false)); + let _ = simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(true)); } SimnetEvent::RunbookCompleted(runbook_id) => { deployment_completed = true; info!("Runbook '{}' execution completed", runbook_id); let _ = - simnet_commands_tx.send(SimnetCommand::SetInstructionProfiling(true)); + simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(false)); } }, Err(_e) => { @@ -412,7 +411,7 @@ fn log_events( }, Err(_e) => { deployment_completed = true; - let _ = simnet_commands_tx.send(SimnetCommand::SetInstructionProfiling(true)); + let _ = simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(false)); } }, } diff --git a/crates/cli/src/tui/simnet.rs b/crates/cli/src/tui/simnet.rs index bdc714d4..30d64e99 100644 --- a/crates/cli/src/tui/simnet.rs +++ b/crates/cli/src/tui/simnet.rs @@ -550,7 +550,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( )); let _ = app .simnet_commands_tx - .send(SimnetCommand::SetInstructionProfiling(false)); + .send(SimnetCommand::SetIsExecutingRunbook(true)); } SimnetEvent::RunbookCompleted(runbook_id) => { deployment_completed = true; @@ -561,7 +561,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( )); let _ = app .simnet_commands_tx - .send(SimnetCommand::SetInstructionProfiling(true)); + .send(SimnetCommand::SetIsExecutingRunbook(false)); app.status_bar_message = None; } }, @@ -659,7 +659,7 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( deployment_completed = true; let _ = app .simnet_commands_tx - .send(SimnetCommand::SetInstructionProfiling(true)); + .send(SimnetCommand::SetIsExecutingRunbook(false)); break; } }, diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 0e2f6bd5..2b8336ae 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -754,6 +754,9 @@ pub trait SurfnetCheatcodes { pubkey_str: String, config: Option, ) -> Result>; + + #[rpc(meta, name = "surfnet_readyCheck")] + fn is_surfnet_ready(&self, meta: Self::Metadata) -> Result>; } #[derive(Clone)] @@ -1281,6 +1284,15 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { value: (), }) } + + fn is_surfnet_ready(&self, meta: Self::Metadata) -> Result> { + let svm_locker = meta.get_svm_locker()?; + let is_ready = !svm_locker.is_executing_runbook(); + Ok(RpcResponse { + context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), + value: is_ready, + }) + } } #[cfg(test)] diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 6848c0b5..c1b741f0 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -221,9 +221,11 @@ pub async fn start_block_production_runloop( let _ = svm_locker.simnet_events_tx().send(SimnetEvent::Aborted("Terminated due to inactivity.".to_string())); break; } - SimnetCommand::SetInstructionProfiling(enabled) => { + SimnetCommand::SetIsExecutingRunbook(is_executing) => { svm_locker.with_svm_writer(|svm_writer| { - svm_writer.instruction_profiling_enabled = enabled; + // only enable instruction profiling if we are not executing a runbook + svm_writer.instruction_profiling_enabled = !is_executing; + svm_writer.is_executing_runbook = is_executing; }); } } diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index fc05e5f1..249dcec9 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -2940,6 +2940,10 @@ impl SurfnetSvmLocker { svm_writer.subscribe_for_logs_updates(commitment_level, filter) }) } + + pub fn is_executing_runbook(&self) -> bool { + self.with_svm_reader(|svm_reader| svm_reader.is_executing_runbook) + } } // Helper function to apply filters diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 248f6897..4a81ae10 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -139,6 +139,7 @@ pub struct SurfnetSvm { pub feature_set: FeatureSet, pub instruction_profiling_enabled: bool, pub max_profiles: usize, + pub is_executing_runbook: bool, } pub const FEATURE: Feature = Feature { @@ -211,6 +212,7 @@ impl SurfnetSvm { feature_set, instruction_profiling_enabled: true, max_profiles: DEFAULT_PROFILING_MAP_CAPACITY, + is_executing_runbook: false, }; // Generate the initial synthetic blockhash diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index 71fdb6f2..a78226d4 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -464,7 +464,7 @@ pub enum SimnetCommand { bool, ), Terminate(Option<(Hash, String)>), - SetInstructionProfiling(bool), + SetIsExecutingRunbook(bool), } #[derive(Debug)] From 482e1f288b4ece4269ca5e0160b3d18887fd6716 Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 24 Sep 2025 17:04:41 -0400 Subject: [PATCH 2/3] change `surfnet_readyCheck` to `surfnet_getSurfnetInfo` --- crates/cli/src/cli/simnet/mod.rs | 10 +++--- crates/cli/src/runbook/mod.rs | 6 +++- crates/cli/src/tui/simnet.rs | 16 +++++----- crates/core/src/rpc/surfnet_cheatcodes.rs | 18 +++++++---- crates/core/src/runloops/mod.rs | 11 +++---- crates/core/src/surfnet/locker.rs | 30 ++++++++++++++--- crates/core/src/surfnet/svm.rs | 27 ++++++++++++---- crates/types/src/types.rs | 39 +++++++++++++++++++++-- 8 files changed, 117 insertions(+), 40 deletions(-) diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index a7c30881..15453026 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -360,13 +360,14 @@ fn log_events( SimnetEvent::RunbookStarted(runbook_id) => { deployment_completed = false; info!("Runbook '{}' execution started", runbook_id); - let _ = simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(true)); + let _ = simnet_commands_tx + .send(SimnetCommand::StartRunbookExecution(runbook_id)); } - SimnetEvent::RunbookCompleted(runbook_id) => { + SimnetEvent::RunbookCompleted(runbook_id, errors) => { deployment_completed = true; info!("Runbook '{}' execution completed", runbook_id); - let _ = - simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(false)); + let _ = simnet_commands_tx + .send(SimnetCommand::CompleteRunbookExecution(runbook_id, errors)); } }, Err(_e) => { @@ -411,7 +412,6 @@ fn log_events( }, Err(_e) => { deployment_completed = true; - let _ = simnet_commands_tx.send(SimnetCommand::SetIsExecutingRunbook(false)); } }, } diff --git a/crates/cli/src/runbook/mod.rs b/crates/cli/src/runbook/mod.rs index 2bc7ab48..f647f6f7 100644 --- a/crates/cli/src/runbook/mod.rs +++ b/crates/cli/src/runbook/mod.rs @@ -273,6 +273,10 @@ pub async fn execute_runbook( if cmd.unsupervised { let _ = simnet_events_tx.send(SimnetEvent::RunbookStarted(runbook_id.clone())); let res = start_unsupervised_runbook_runloop(&mut runbook, &progress_tx).await; + let diags = res + .as_ref() + .map_err(|ds| ds.iter().map(|d| d.message.clone()).collect()) + .err(); process_runbook_execution_output( res, &mut runbook, @@ -280,7 +284,7 @@ pub async fn execute_runbook( &simnet_events_tx, cmd.output_json, ); - let _ = simnet_events_tx.send(SimnetEvent::RunbookCompleted(runbook_id)); + let _ = simnet_events_tx.send(SimnetEvent::RunbookCompleted(runbook_id, diags)); } else { let (kill_supervised_execution_tx, block_store_handle) = configure_supervised_execution(runbook, runbook_state_location, &cmd, simnet_events_tx) diff --git a/crates/cli/src/tui/simnet.rs b/crates/cli/src/tui/simnet.rs index 30d64e99..8bcc245e 100644 --- a/crates/cli/src/tui/simnet.rs +++ b/crates/cli/src/tui/simnet.rs @@ -550,18 +550,21 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( )); let _ = app .simnet_commands_tx - .send(SimnetCommand::SetIsExecutingRunbook(true)); + .send(SimnetCommand::StartRunbookExecution(runbook_id.clone())); } - SimnetEvent::RunbookCompleted(runbook_id) => { + SimnetEvent::RunbookCompleted(runbook_id, errors) => { deployment_completed = true; new_events.push(( EventType::Success, Local::now(), format!("Runbook '{}' execution completed", runbook_id), )); - let _ = app - .simnet_commands_tx - .send(SimnetCommand::SetIsExecutingRunbook(false)); + let _ = app.simnet_commands_tx.send( + SimnetCommand::CompleteRunbookExecution( + runbook_id.clone(), + errors.clone(), + ), + ); app.status_bar_message = None; } }, @@ -657,9 +660,6 @@ fn run_app(terminal: &mut Terminal, mut app: App) -> io::Result<( }, Err(_) => { deployment_completed = true; - let _ = app - .simnet_commands_tx - .send(SimnetCommand::SetIsExecutingRunbook(false)); break; } }, diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 2b8336ae..382b894b 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -11,8 +11,8 @@ use solana_sdk::{program_option::COption, transaction::VersionedTransaction}; use solana_system_interface::program as system_program; use spl_associated_token_account::get_associated_token_address_with_program_id; use surfpool_types::{ - ClockCommand, Idl, ResetAccountConfig, RpcProfileResultConfig, SimnetCommand, SimnetEvent, - UiKeyedProfileResult, + ClockCommand, GetSurfnetInfoResponse, Idl, ResetAccountConfig, RpcProfileResultConfig, + SimnetCommand, SimnetEvent, UiKeyedProfileResult, types::{AccountUpdate, SetSomeAccount, SupplyUpdate, TokenAccountUpdate, UuidOrSignature}, }; @@ -755,8 +755,9 @@ pub trait SurfnetCheatcodes { config: Option, ) -> Result>; - #[rpc(meta, name = "surfnet_readyCheck")] - fn is_surfnet_ready(&self, meta: Self::Metadata) -> Result>; + #[rpc(meta, name = "surfnet_getSurfnetInfo")] + fn get_surfnet_info(&self, meta: Self::Metadata) + -> Result>; } #[derive(Clone)] @@ -1285,12 +1286,15 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { }) } - fn is_surfnet_ready(&self, meta: Self::Metadata) -> Result> { + fn get_surfnet_info( + &self, + meta: Self::Metadata, + ) -> Result> { let svm_locker = meta.get_svm_locker()?; - let is_ready = !svm_locker.is_executing_runbook(); + let runbook_executions = svm_locker.runbook_executions(); Ok(RpcResponse { context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), - value: is_ready, + value: GetSurfnetInfoResponse::new(runbook_executions), }) } } diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index c1b741f0..e301db61 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -221,12 +221,11 @@ pub async fn start_block_production_runloop( let _ = svm_locker.simnet_events_tx().send(SimnetEvent::Aborted("Terminated due to inactivity.".to_string())); break; } - SimnetCommand::SetIsExecutingRunbook(is_executing) => { - svm_locker.with_svm_writer(|svm_writer| { - // only enable instruction profiling if we are not executing a runbook - svm_writer.instruction_profiling_enabled = !is_executing; - svm_writer.is_executing_runbook = is_executing; - }); + SimnetCommand::StartRunbookExecution(runbook_id) => { + svm_locker.start_runbook_execution(runbook_id); + } + SimnetCommand::CompleteRunbookExecution(runbook_id, error) => { + svm_locker.complete_runbook_execution(runbook_id, error); } } }, diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index 249dcec9..4ceadd26 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -53,9 +53,9 @@ use solana_transaction_status::{ }; use surfpool_types::{ ComputeUnitsEstimationResult, ExecutionCapture, Idl, KeyedProfileResult, ProfileResult, - ResetAccountConfig, RpcProfileResultConfig, SimnetCommand, SimnetEvent, - TransactionConfirmationStatus, TransactionStatusEvent, UiKeyedProfileResult, UuidOrSignature, - VersionedIdl, + ResetAccountConfig, RpcProfileResultConfig, RunbookExecutionStatusReport, SimnetCommand, + SimnetEvent, TransactionConfirmationStatus, TransactionStatusEvent, UiKeyedProfileResult, + UuidOrSignature, VersionedIdl, }; use tokio::sync::RwLock; use txtx_addon_kit::indexmap::IndexSet; @@ -2941,8 +2941,28 @@ impl SurfnetSvmLocker { }) } - pub fn is_executing_runbook(&self) -> bool { - self.with_svm_reader(|svm_reader| svm_reader.is_executing_runbook) + pub fn runbook_executions(&self) -> Vec { + self.with_svm_reader(|svm_reader| svm_reader.runbook_executions.clone()) + } + + pub fn start_runbook_execution(&self, runbook_id: String) { + self.with_svm_writer(|svm_writer| { + svm_writer.instruction_profiling_enabled = false; + svm_writer.start_runbook_execution(runbook_id); + }); + } + + pub fn complete_runbook_execution(&self, runbook_id: String, error: Option>) { + self.with_svm_writer(|svm_writer| { + svm_writer.complete_runbook_execution(&runbook_id, error); + let some_runbook_executing = svm_writer + .runbook_executions + .iter() + .any(|e| e.completed_at.is_none()); + if !some_runbook_executing { + svm_writer.instruction_profiling_enabled = true; + } + }); } } diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 4a81ae10..2a464e19 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -9,7 +9,7 @@ use chrono::Utc; use convert_case::Casing; use crossbeam_channel::{Receiver, Sender, unbounded}; use litesvm::{ - LiteSVM, + LiteSVM, error, types::{ FailedTransactionMetadata, SimulatedTransactionInfo, TransactionMetadata, TransactionResult, }, @@ -48,9 +48,9 @@ use spl_token_2022::extension::{ }; use surfpool_types::{ AccountChange, AccountProfileState, DEFAULT_PROFILING_MAP_CAPACITY, DEFAULT_SLOT_TIME_MS, - FifoMap, Idl, ProfileResult, RpcProfileDepth, RpcProfileResultConfig, SimnetEvent, - TransactionConfirmationStatus, TransactionStatusEvent, UiAccountChange, UiAccountProfileState, - UiProfileResult, VersionedIdl, + FifoMap, Idl, ProfileResult, RpcProfileDepth, RpcProfileResultConfig, + RunbookExecutionStatusReport, SimnetEvent, TransactionConfirmationStatus, + TransactionStatusEvent, UiAccountChange, UiAccountProfileState, UiProfileResult, VersionedIdl, types::{ ComputeUnitsEstimationResult, KeyedProfileResult, UiKeyedProfileResult, UuidOrSignature, }, @@ -139,7 +139,7 @@ pub struct SurfnetSvm { pub feature_set: FeatureSet, pub instruction_profiling_enabled: bool, pub max_profiles: usize, - pub is_executing_runbook: bool, + pub runbook_executions: Vec, } pub const FEATURE: Feature = Feature { @@ -212,7 +212,7 @@ impl SurfnetSvm { feature_set, instruction_profiling_enabled: true, max_profiles: DEFAULT_PROFILING_MAP_CAPACITY, - is_executing_runbook: false, + runbook_executions: Vec::new(), }; // Generate the initial synthetic blockhash @@ -1665,6 +1665,21 @@ impl SurfnetSvm { pub fn iter_accounts(&self) -> std::collections::hash_map::Iter<'_, Pubkey, AccountSharedData> { self.inner.accounts_db().inner.iter() } + + pub fn start_runbook_execution(&mut self, runbook_id: String) { + self.runbook_executions + .push(RunbookExecutionStatusReport::new(runbook_id)); + } + + pub fn complete_runbook_execution(&mut self, runbook_id: &str, error: Option>) { + if let Some(execution) = self + .runbook_executions + .iter_mut() + .find(|e| e.runbook_id.eq(runbook_id) && e.completed_at.is_none()) + { + execution.mark_completed(error); + } + } } #[cfg(test)] diff --git a/crates/types/src/types.rs b/crates/types/src/types.rs index a78226d4..35d34d60 100644 --- a/crates/types/src/types.rs +++ b/crates/types/src/types.rs @@ -348,7 +348,7 @@ pub enum SimnetEvent { timestamp: DateTime, }, RunbookStarted(String), - RunbookCompleted(String), + RunbookCompleted(String, Option>), } impl SimnetEvent { @@ -464,7 +464,8 @@ pub enum SimnetCommand { bool, ), Terminate(Option<(Hash, String)>), - SetIsExecutingRunbook(bool), + StartRunbookExecution(String), + CompleteRunbookExecution(String, Option>), } #[derive(Debug)] @@ -919,6 +920,40 @@ impl Default for ResetAccountConfig { } } +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct GetSurfnetInfoResponse { + runbook_executions: Vec, +} +impl GetSurfnetInfoResponse { + pub fn new(runbook_executions: Vec) -> Self { + Self { runbook_executions } + } +} + +#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct RunbookExecutionStatusReport { + pub started_at: u32, + pub completed_at: Option, + pub runbook_id: String, + pub errors: Option>, +} +impl RunbookExecutionStatusReport { + pub fn new(runbook_id: String) -> Self { + Self { + started_at: Local::now().timestamp() as u32, + completed_at: None, + runbook_id, + errors: None, + } + } + pub fn mark_completed(&mut self, error: Option>) { + self.completed_at = Some(Local::now().timestamp() as u32); + self.errors = error; + } +} + #[cfg(test)] mod tests { use serde_json::json; From 71e8160a9e626ca8a76e93279d544c90a10011bf Mon Sep 17 00:00:00 2001 From: MicaiahReid Date: Wed, 24 Sep 2025 20:29:25 -0400 Subject: [PATCH 3/3] add docs --- crates/core/src/rpc/surfnet_cheatcodes.rs | 40 +++++++++++++++++++++++ 1 file changed, 40 insertions(+) diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 382b894b..a602e319 100644 --- a/crates/core/src/rpc/surfnet_cheatcodes.rs +++ b/crates/core/src/rpc/surfnet_cheatcodes.rs @@ -755,6 +755,46 @@ pub trait SurfnetCheatcodes { config: Option, ) -> Result>; + /// A cheat code to get Surfnet network information. + /// + /// ## Parameters + /// - `meta`: Metadata passed with the request, such as the client's request context. + /// + /// ## Returns + /// A `RpcResponse` containing the Surfnet network information. + /// + /// ## Example Request + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "id": 1, + /// "method": "surfnet_getSurfnetInfo" + /// } + /// ``` + /// + /// ## Example Response + /// ```json + /// { + /// "jsonrpc": "2.0", + /// "result": { + /// "context": { + /// "slot": 369027326, + /// "apiVersion": "2.3.8" + /// }, + /// "value": { + /// "runbookExecutions": [ + /// { + /// "startedAt": 1758747828, + /// "completedAt": 1758747828, + /// "runbookId": "deployment" + /// } + /// ] + /// } + /// }, + /// "id": 1 + /// } + /// ``` + /// #[rpc(meta, name = "surfnet_getSurfnetInfo")] fn get_surfnet_info(&self, meta: Self::Metadata) -> Result>;