diff --git a/crates/cli/src/cli/simnet/mod.rs b/crates/cli/src/cli/simnet/mod.rs index c6719ac2..15453026 100644 --- a/crates/cli/src/cli/simnet/mod.rs +++ b/crates/cli/src/cli/simnet/mod.rs @@ -360,14 +360,14 @@ 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::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::SetInstructionProfiling(true)); + let _ = simnet_commands_tx + .send(SimnetCommand::CompleteRunbookExecution(runbook_id, errors)); } }, Err(_e) => { @@ -412,7 +412,6 @@ fn log_events( }, Err(_e) => { deployment_completed = true; - let _ = simnet_commands_tx.send(SimnetCommand::SetInstructionProfiling(true)); } }, } 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 bdc714d4..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::SetInstructionProfiling(false)); + .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::SetInstructionProfiling(true)); + 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::SetInstructionProfiling(true)); break; } }, diff --git a/crates/core/src/rpc/surfnet_cheatcodes.rs b/crates/core/src/rpc/surfnet_cheatcodes.rs index 0e2f6bd5..a602e319 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}, }; @@ -754,6 +754,50 @@ pub trait SurfnetCheatcodes { pubkey_str: String, 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>; } #[derive(Clone)] @@ -1281,6 +1325,18 @@ impl SurfnetCheatcodes for SurfnetCheatcodesRpc { value: (), }) } + + fn get_surfnet_info( + &self, + meta: Self::Metadata, + ) -> Result> { + let svm_locker = meta.get_svm_locker()?; + let runbook_executions = svm_locker.runbook_executions(); + Ok(RpcResponse { + context: RpcResponseContext::new(svm_locker.get_latest_absolute_slot()), + value: GetSurfnetInfoResponse::new(runbook_executions), + }) + } } #[cfg(test)] diff --git a/crates/core/src/runloops/mod.rs b/crates/core/src/runloops/mod.rs index 6848c0b5..e301db61 100644 --- a/crates/core/src/runloops/mod.rs +++ b/crates/core/src/runloops/mod.rs @@ -221,10 +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) => { - svm_locker.with_svm_writer(|svm_writer| { - svm_writer.instruction_profiling_enabled = enabled; - }); + 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 fc05e5f1..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; @@ -2940,6 +2940,30 @@ impl SurfnetSvmLocker { svm_writer.subscribe_for_logs_updates(commitment_level, filter) }) } + + 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; + } + }); + } } // Helper function to apply filters diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index 248f6897..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,6 +139,7 @@ pub struct SurfnetSvm { pub feature_set: FeatureSet, pub instruction_profiling_enabled: bool, pub max_profiles: usize, + pub runbook_executions: Vec, } pub const FEATURE: Feature = Feature { @@ -211,6 +212,7 @@ impl SurfnetSvm { feature_set, instruction_profiling_enabled: true, max_profiles: DEFAULT_PROFILING_MAP_CAPACITY, + runbook_executions: Vec::new(), }; // Generate the initial synthetic blockhash @@ -1663,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 71fdb6f2..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)>), - SetInstructionProfiling(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;