1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
use std::time::Duration;
use jsonrpc_core::{BoxFuture, Result};
use jsonrpc_derive::rpc;
use solana_client::rpc_custom_error::RpcCustomError;
use surfpool_types::{SimnetCommand, SimnetEvent};
use txtx_addon_network_svm_types::subgraph::PluginConfig;
use uuid::Uuid;
use super::RunloopContext;
use crate::{PluginInfo, PluginManagerCommand, rpc::State};
#[rpc]
pub trait AdminRpc {
type Metadata;
/// Immediately shuts down the RPC server.
///
/// This administrative endpoint is typically used during controlled shutdowns of the validator
/// or service exposing the RPC interface. It allows remote administrators to gracefully terminate
/// the process, stopping all RPC activity.
///
/// ## Returns
/// - [`Result<()>`] — A unit result indicating successful shutdown, or an error if the call fails.
///
/// ## Example Request (JSON-RPC)
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "id": 42,
/// "method": "exit",
/// "params": []
/// }
/// ```
///
/// # Notes
/// - This method is privileged and should only be accessible to trusted clients.
/// - Use with extreme caution in production environments.
/// - If successful, the RPC server process will terminate immediately after processing this call.
///
/// # Security
/// Access to this method should be tightly restricted. Implement proper authorization mechanisms
/// via the RPC metadata to prevent accidental or malicious use.
#[rpc(meta, name = "exit")]
fn exit(&self, meta: Self::Metadata) -> Result<()>;
#[rpc(meta, name = "reloadPlugin")]
fn reload_plugin(
&self,
meta: Self::Metadata,
name: String,
config_file: String,
) -> BoxFuture<Result<()>>;
/// Reloads a runtime plugin with new configuration.
///
/// This administrative endpoint is used to dynamically reload a plugin without restarting
/// the entire RPC server or validator. It is useful for applying updated configurations
/// to a plugin that supports hot-reloading.
///
/// ## Parameters
/// - `name`: The identifier of the plugin to reload.
/// - `config_file`: Path to the new configuration file to load for the plugin.
///
/// ## Returns
/// - [`BoxFuture<Result<()>>`] — A future resolving to a unit result on success, or an error if
/// reloading fails.
///
/// ## Example Request (JSON-RPC)
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "id": 101,
/// "method": "reloadPlugin",
/// "params": ["myPlugin", "/etc/plugins/my_plugin_config.toml"]
/// }
/// ```
///
/// # Notes
/// - The plugin must support reloading in order for this to succeed.
/// - A failed reload will leave the plugin in its previous state.
/// - This method is intended for administrators and should be properly secured.
///
/// # Security
/// Ensure only trusted clients can invoke this method. Use metadata-based access control to limit exposure.
#[rpc(meta, name = "unloadPlugin")]
fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>>;
/// Dynamically loads a new plugin into the runtime from a configuration file.
///
/// This administrative endpoint is used to add a new plugin to the system at runtime,
/// based on the configuration provided. It enables extensibility without restarting
/// the validator or RPC server.
///
/// ## Parameters
/// - `config_file`: Path to the plugin's configuration file, which defines its behavior and settings.
///
/// ## Returns
/// - [`BoxFuture<Result<String>>`] — A future resolving to the name or identifier of the loaded plugin,
/// or an error if the plugin could not be loaded.
///
/// ## Example Request (JSON-RPC)
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "id": 102,
/// "method": "loadPlugin",
/// "params": ["/etc/plugins/my_plugin_config.toml"]
/// }
/// ```
///
/// # Notes
/// - The plugin system must be initialized and support runtime loading.
/// - The config file should be well-formed and point to a valid plugin implementation.
/// - Duplicate plugin names may lead to conflicts or errors.
///
/// # Security
/// This method should be restricted to administrators only. Validate inputs and use access control.
#[rpc(meta, name = "loadPlugin")]
fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>>;
/// Returns a list of all currently loaded plugin names.
///
/// This administrative RPC method is used to inspect which plugins have been successfully
/// loaded into the runtime. It can be useful for debugging or operational monitoring.
///
/// ## Returns
/// - `Vec<PluginInfo>` — A list of plugin information objects, each containing:
/// - `plugin_name`: The name of the plugin (e.g., "surfpool-subgraph")
/// - `uuid`: The unique identifier of the plugin instance
///
/// ## Example Request (JSON-RPC)
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "id": 103,
/// "method": "listPlugins",
/// "params": []
/// }
/// ```
///
/// ## Example Response
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "result": [
/// {
/// "plugin_name": "surfpool-subgraph",
/// "uuid": "550e8400-e29b-41d4-a716-446655440000"
/// }
/// ],
/// "id": 103
/// }
/// ```
///
/// # Notes
/// - Only plugins that have been successfully loaded will appear in this list.
/// - This method is read-only and safe to call frequently.
#[rpc(meta, name = "listPlugins")]
fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>>;
/// Returns the system start time.
///
/// This RPC method retrieves the timestamp of when the system was started, represented as
/// a `SystemTime`. It can be useful for measuring uptime or for tracking the system's runtime
/// in logs or monitoring systems.
///
/// ## Returns
/// - `SystemTime` — The timestamp representing when the system was started.
///
/// ## Example Request (JSON-RPC)
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "id": 106,
/// "method": "startTime",
/// "params": []
/// }
/// ```
///
/// ## Example Response
/// ```json
/// {
/// "jsonrpc": "2.0",
/// "result": "2025-04-24T12:34:56Z",
/// "id": 106
/// }
/// ```
///
/// # Notes
/// - The result is a `String` in UTC, reflecting the moment the system was initialized.
/// - This method is useful for monitoring system uptime and verifying system health.
#[rpc(meta, name = "startTime")]
fn start_time(&self, meta: Self::Metadata) -> Result<String>;
}
pub struct SurfpoolAdminRpc;
impl AdminRpc for SurfpoolAdminRpc {
type Metadata = Option<RunloopContext>;
fn exit(&self, meta: Self::Metadata) -> Result<()> {
let Some(ctx) = meta else {
return Err(RpcCustomError::NodeUnhealthy {
num_slots_behind: None,
}
.into());
};
let _ = ctx
.simnet_commands_tx
.send(SimnetCommand::Terminate(ctx.id));
Ok(())
}
fn reload_plugin(
&self,
meta: Self::Metadata,
name: String,
config_file: String,
) -> BoxFuture<Result<()>> {
// Parse the UUID from the name parameter
let uuid = match Uuid::parse_str(&name) {
Ok(uuid) => uuid,
Err(e) => {
return Box::pin(async move {
Err(jsonrpc_core::Error::invalid_params(format!(
"Invalid UUID: {}",
e
)))
});
}
};
// Parse the new configuration
let config = match serde_json::from_str::<PluginConfig>(&config_file)
.map_err(|e| format!("failed to deserialize plugin config: {e}"))
{
Ok(config) => config,
Err(e) => return Box::pin(async move { Err(jsonrpc_core::Error::invalid_params(&e)) }),
};
let Some(ctx) = meta else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
let simnet_events_tx = ctx.svm_locker.simnet_events_tx();
let _ = simnet_events_tx.try_send(SimnetEvent::info(format!(
"Reloading plugin with UUID - {}",
uuid
)));
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = ctx
.plugin_manager_commands_tx
.send(PluginManagerCommand::ReloadPlugin(uuid, config, tx));
let Ok(_endpoint_url) = rx.recv_timeout(Duration::from_secs(10)) else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
Box::pin(async move {
let _ = simnet_events_tx.try_send(SimnetEvent::info(format!(
"Reloaded plugin with UUID - {}",
uuid
)));
Ok(())
})
}
fn unload_plugin(&self, meta: Self::Metadata, name: String) -> BoxFuture<Result<()>> {
// Parse the UUID from the name parameter
let uuid = match Uuid::parse_str(&name) {
Ok(uuid) => uuid,
Err(e) => {
return Box::pin(async move {
Err(jsonrpc_core::Error::invalid_params(format!(
"Invalid UUID: {}",
e
)))
});
}
};
let Some(ctx) = meta else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
let simnet_events_tx = ctx.svm_locker.simnet_events_tx();
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = ctx
.plugin_manager_commands_tx
.send(PluginManagerCommand::UnloadPlugin(uuid, tx));
let Ok(result) = rx.recv_timeout(Duration::from_secs(10)) else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
Box::pin(async move {
match result {
Ok(()) => {
let _ = simnet_events_tx.try_send(SimnetEvent::info(format!(
"Unloaded plugin with UUID - {}",
uuid
)));
Ok(())
}
Err(e) => Err(jsonrpc_core::Error::invalid_params(&e)),
}
})
}
fn load_plugin(&self, meta: Self::Metadata, config_file: String) -> BoxFuture<Result<String>> {
let config = match serde_json::from_str::<PluginConfig>(&config_file)
.map_err(|e| format!("failed to deserialize plugin config: {e}"))
{
Ok(config) => config,
Err(e) => return Box::pin(async move { Err(jsonrpc_core::Error::invalid_params(&e)) }),
};
let ctx = meta.unwrap();
let uuid = Uuid::new_v4();
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = ctx
.plugin_manager_commands_tx
.send(PluginManagerCommand::LoadConfig(uuid, config, tx));
let Ok(endpoint_url) = rx.recv_timeout(Duration::from_secs(10)) else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
let _ = ctx
.svm_locker
.simnet_events_tx()
.try_send(SimnetEvent::info(format!(
"Loaded plugin with UUID - {}",
uuid
)));
// Return only the endpoint URL
Box::pin(async move { Ok(endpoint_url) })
}
fn list_plugins(&self, meta: Self::Metadata) -> BoxFuture<Result<Vec<PluginInfo>>> {
let Some(ctx) = meta else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
let (tx, rx) = crossbeam_channel::bounded(1);
let _ = ctx
.plugin_manager_commands_tx
.send(PluginManagerCommand::ListPlugins(tx));
let Ok(plugin_list) = rx.recv_timeout(Duration::from_secs(10)) else {
return Box::pin(async move { Err(jsonrpc_core::Error::internal_error()) });
};
Box::pin(async move { Ok(plugin_list) })
}
fn start_time(&self, meta: Self::Metadata) -> Result<String> {
let svm_locker = meta.get_svm_locker()?;
let system_time = svm_locker.get_start_time();
let datetime_utc: chrono::DateTime<chrono::Utc> = system_time.into();
Ok(datetime_utc.to_rfc3339())
}
}