diff --git a/Cargo.lock b/Cargo.lock index 55bc7fb805..379bef9f78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -463,7 +463,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f8d983286843e49675a4b7a2d174efe136dc93a18d69130dd18198a6c167601" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "rand_core 0.10.1", ] @@ -667,15 +667,6 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae" -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - [[package]] name = "cpufeatures" version = "0.3.0" @@ -1691,6 +1682,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inventory" +version = "0.3.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4f0c30c76f2f4ccee3fe55a2435f691ca00c0e4bd87abe4f4a851b1d4dac39b" +dependencies = [ + "rustversion", +] + [[package]] name = "ipnet" version = "2.12.0" @@ -1798,17 +1798,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json5" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" -dependencies = [ - "pest", - "pest_derive", - "serde", -] - [[package]] name = "k256" version = "0.14.0-rc.9" @@ -1818,7 +1807,7 @@ dependencies = [ "cpubits", "ecdsa", "elliptic-curve", - "sha2 0.11.0", + "sha2", ] [[package]] @@ -2251,49 +2240,6 @@ version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" -[[package]] -name = "pest" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e0848c601009d37dfa3430c4666e147e49cdcf1b92ecd3e63657d8a5f19da662" -dependencies = [ - "memchr", - "ucd-trie", -] - -[[package]] -name = "pest_derive" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "11f486f1ea21e6c10ed15d5a7c77165d0ee443402f0780849d1768e7d9d6fe77" -dependencies = [ - "pest", - "pest_generator", -] - -[[package]] -name = "pest_generator" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8040c4647b13b210a963c1ed407c1ff4fdfa01c31d6d2a098218702e6664f94f" -dependencies = [ - "pest", - "pest_meta", - "proc-macro2", - "quote 1.0.45", - "syn 2.0.117", -] - -[[package]] -name = "pest_meta" -version = "2.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89815c69d36021a140146f26659a81d6c2afa33d216d736dd4be5381a7362220" -dependencies = [ - "pest", - "sha2 0.10.9", -] - [[package]] name = "pin-project-lite" version = "0.2.17" @@ -3091,17 +3037,6 @@ dependencies = [ "syn 2.0.117", ] -[[package]] -name = "sha2" -version = "0.10.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" -dependencies = [ - "cfg-if", - "cpufeatures 0.2.17", - "digest 0.10.7", -] - [[package]] name = "sha2" version = "0.11.0" @@ -3109,7 +3044,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4" dependencies = [ "cfg-if", - "cpufeatures 0.3.0", + "cpufeatures", "digest 0.11.3", ] @@ -3241,7 +3176,7 @@ dependencies = [ "serde", "serde_json", "serial_test", - "sha2 0.11.0", + "sha2", "smallvec", "snarkvm-algorithms-cuda", "snarkvm-curves", @@ -3322,7 +3257,7 @@ dependencies = [ "nom", "num-traits", "serial_test", - "sha2 0.11.0", + "sha2", "smallvec", "snarkvm-algorithms", "snarkvm-circuit", @@ -4033,7 +3968,7 @@ dependencies = [ "paste", "rand 0.10.1", "serde_json", - "sha2 0.11.0", + "sha2", "snarkvm-algorithms", "snarkvm-circuit", "snarkvm-console", @@ -4053,6 +3988,7 @@ name = "snarkvm-slipstream-plugin-interface" version = "4.7.1" dependencies = [ "anyhow", + "inventory", ] [[package]] @@ -4060,9 +3996,6 @@ name = "snarkvm-slipstream-plugin-manager" version = "4.7.1" dependencies = [ "anyhow", - "json5", - "libloading", - "serde_json", "snarkvm-slipstream-plugin-interface", "thiserror 2.0.18", "tracing", @@ -4747,12 +4680,6 @@ version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" -[[package]] -name = "ucd-trie" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" - [[package]] name = "unarray" version = "0.1.4" diff --git a/plugins/slipstream_plugin_interface/Cargo.toml b/plugins/slipstream_plugin_interface/Cargo.toml index 5d6ebf4cb6..627ae1dd63 100644 --- a/plugins/slipstream_plugin_interface/Cargo.toml +++ b/plugins/slipstream_plugin_interface/Cargo.toml @@ -10,3 +10,6 @@ edition = "2024" [dependencies.anyhow] workspace = true + +[dependencies.inventory] +version = "0.3" diff --git a/plugins/slipstream_plugin_interface/README.md b/plugins/slipstream_plugin_interface/README.md index ce098d897d..71366b4917 100644 --- a/plugins/slipstream_plugin_interface/README.md +++ b/plugins/slipstream_plugin_interface/README.md @@ -1,91 +1,191 @@ # Aleo Slipstream Plugin Interface -This crate enables a plugin to be added into a SnarkVM runtime to take actions at the time of -mapping updates at block finalization; for example, saving historical mapping state and staking -data to an external database. The plugin must implement the `SlipstreamPlugin` trait. See -`slipstream_plugin_interface.rs` for the full interface definition. +Slipstream is a plugin system that streams canonical mapping updates and staking rewards from +snarkVM's finalize stage to external services (databases, metrics pipelines, etc.) in real time, +without modifying node code. > **Feature flag:** compile with `--features slipstream-plugins` to enable plugin support. > Plugin callbacks fire only during **canonical finalize** — speculative and dry-run executions > are never observed by plugins. -# Components +--- + +## How plugins are loaded + +Plugins are **statically linked** into the `snarkos` binary at compile time. There is no dynamic +`.so`/`.dylib` loading and no C FFI boundary. Each plugin crate calls `inventory::submit!` at link +time to register a `PluginRegistration` factory. snarkOS iterates +`inventory::iter::()` at startup, matches each config file's `name` field +against the registered factories, and calls `register()` on the manager for each match. + +--- + +## Components ### `plugins/slipstream_plugin_interface` -Defines the `SlipstreamPlugin` trait — the interface all plugins must implement. + +Defines the `SlipstreamPlugin` trait and the `PluginRegistration` factory type. + +**`SlipstreamPlugin` trait:** | Method | Description | |---|---| -| `on_load` / `on_unload` | Lifecycle hooks called on startup and shutdown | -| `subscribed_events` | Returns the event types a plugin subscribes to. Defaults to `&[]` — a plugin that does not override this method receives **no callbacks**. | -| `on_broadcast` | Called once per key-value update (and once per entry in a `replace_mapping` batch). Only fires for event kinds in the subscribed list. | +| `name` | Returns the plugin's static name string. Must match the `name` field in the config file. | +| `on_load(config_file, is_reload)` | Called once at startup. Reads config, connects to external services, verifies schema, replays any WAL. Node aborts if this returns `Err`. | +| `on_unload` | Called on graceful shutdown. Flush buffers, close connections. | +| `subscribed_events` | Returns the `BroadcastEventKind`s this plugin wants to receive. Defaults to `&[]` — a plugin that does not override this receives **no callbacks** and pays no serialization cost. | +| `on_broadcast(event)` | Called inline on the finalize thread for each subscribed event. Errors are logged as warnings and never propagated to consensus. | + +**`PluginRegistration`:** + +```rust +pub struct PluginRegistration { + pub name: &'static str, + pub factory: fn() -> Box, +} +inventory::collect!(PluginRegistration); +``` + +Plugin crates register themselves at link time: + +```rust +inventory::submit! { + PluginRegistration { + name: "my-plugin", + factory: || Box::new(MyPlugin::new()), + } +} +``` + +--- ### `plugins/slipstream_plugin_manager` -Manages loaded plugins and their backing `libloading::Library` handles. -- **`LoadedSlipstreamPlugin`** — wrapper holding a boxed plugin + its name; implements `Deref`/`DerefMut` -- **`SlipstreamPluginManager`** - - `from_config_files` — takes a slice of config file paths and loads one plugin per file - - `load_plugin(path)` / `unload_plugin(name)` — load or unload a single plugin at runtime - - `unload()` — fires `on_unload()` on every plugin then drops the libraries; field declaration order guarantees all plugin code finishes executing before the backing `.so` is unmapped - - `has_subscribers()` — aggregate opt-in check; used internally to skip serialization when no plugin is interested in an event kind - - `broadcast()` — fan-out broadcast to all interested plugins - - `list_plugins()` — returns the names of all loaded plugins +Owns all active plugins and drives their lifecycle. + +**`LoadedSlipstreamPlugin`** — wrapper holding a boxed plugin and its name. `Drop` calls +`on_unload()` automatically, so removing a plugin from `self.plugins` always triggers cleanup. + +**`SlipstreamPluginManager`:** + +| Method | Description | +|---|---| +| `register(plugin, config_file)` | Calls `on_load`, then adds the plugin to the active list. Returns `Err` if a plugin with the same name is already loaded or `on_load` fails. | +| `unload()` | Clears `self.plugins`; `Drop` on each entry fires `on_unload()`. | +| `unload_plugin(name)` | Removes and drops a single plugin by name. | +| `has_subscribers(kind)` | Returns `true` if any plugin subscribes to the given event kind. Used as a pre-serialization guard to skip byte serialization when no plugin would receive the event. | +| `broadcast(event)` | Fan-out: calls `on_broadcast` on every plugin subscribed to the event's kind. Errors are logged as warnings, never propagated. | +| `list_plugins()` | Returns the names of all active plugins. | --- ## Plugin Config File (JSON5) -Each plugin requires a config file: +Each plugin requires a JSON5 config file passed to `snarkos start --slipstream-plugins`: + ```json5 { - "libpath": "/path/to/libmy_plugin.so", // required; relative paths resolve from the config file's dir - "name": "my_plugin" // optional; overrides the plugin's name() return value + // Required: must match the name registered via inventory::submit! in the plugin crate. + name: "my-plugin", + + // Plugin-specific fields — read by the plugin's own on_load implementation. + connection_string: "postgres://user:pass@localhost/aleo", + batch_size: 100, } ``` --- -## Plugin Library Convention +## Startup + +snarkOS reads each config file, looks up the matching `PluginRegistration` factory via +`inventory::iter`, and calls `register()`: -The shared library (`.so` / `.dylib` / `.dll`) must export a C function: ```rust -#[no_mangle] -pub extern "C" fn _create_plugin() -> *mut dyn SlipstreamPlugin { - Box::into_raw(Box::new(MyPlugin::new())) -} +let plugin = factory(); +manager.register(plugin, config_path)?; ``` +`on_load` is called synchronously on the startup thread before the consensus engine starts. If +`on_load` returns `Err` for any plugin, the node exits immediately — there is no fallback. + --- -## Broadcast Event Format +## Shutdown -All byte-slice fields in `BroadcastEvent` are serialized in **little-endian** format (via -`to_bytes_le()`). Plugin implementations must deserialize accordingly. +`manager.unload()` fires `on_unload()` on every plugin in reverse registration order: + +```rust +if let Some(manager) = finalize_store.slipstream_plugin_manager().write().as_mut() { + manager.unload(); +} +``` --- -## Startup +## Broadcast Event Format -`SlipstreamPluginManager::from_config_files()` takes a slice of config file paths and returns a -manager object. Install it into the `FinalizeStore` before the node begins processing blocks: +All `&[u8]` fields in `BroadcastEvent` carry **little-endian** byte representations of the +corresponding snarkVM console types (serialized via `ToBytes`). Plugin implementations must +deserialize accordingly. ```rust -let manager = SlipstreamPluginManager::from_config_files(&[ - PathBuf::from("/etc/aleo/plugins/my_plugin.json5"), -])?; -finalize_store.set_slipstream_plugin_manager(manager); +pub enum BroadcastEvent<'a> { + MappingUpdate { program_id: &'a [u8], mapping_name: &'a [u8], key: &'a [u8], value: &'a [u8], block_height: u32 }, + StakingReward { staker: &'a [u8], validator: &'a [u8], reward: u64, new_stake: u64, block_height: u32 }, +} ``` -## Shutdown +`BroadcastEvent` is `Copy`, so the same value can be fanned out to multiple plugins without cloning. + +--- -Call `manager.unload()` during graceful shutdown before aborting tasks. This fires `on_unload()` -on every plugin — the right place for flushing buffers, closing connections, etc.: +## Writing a Plugin + +1. Add dependencies: + +```toml +[dependencies] +snarkvm-slipstream-plugin-interface = { git = "https://github.com/ProvableHQ/snarkVM.git" } +inventory = "0.3" +``` + +2. Implement the trait and self-register: ```rust -if let Some(manager) = finalize_store.slipstream_plugin_manager().write().as_mut() { - manager.unload(); +use snarkvm_slipstream_plugin_interface::slipstream_plugin_interface::{ + SlipstreamPlugin, PluginRegistration, +}; + +#[derive(Debug)] +struct MyPlugin; + +impl SlipstreamPlugin for MyPlugin { + fn name(&self) -> &'static str { "my-plugin" } + // override on_load, on_broadcast, on_unload as needed +} + +inventory::submit! { + PluginRegistration { + name: "my-plugin", + factory: || Box::new(MyPlugin::new()), + } } ``` -> Errors from plugin callbacks (`on_broadcast`) are logged as warnings and never propagated — a misbehaving plugin will not crash the node. +3. Add the plugin crate as an optional dependency under the `slipstream-plugins` feature in + `snarkOS/node/Cargo.toml`: + +```toml +[dependencies.my-plugin] +path = "../../my-plugin" +optional = true + +[features] +slipstream-plugins = [ + "snarkvm/slipstream-plugins", + "dep:my-plugin", +] +``` + +See `slipstream-plugin-postgres` for a complete reference implementation. diff --git a/plugins/slipstream_plugin_interface/src/slipstream_plugin_interface.rs b/plugins/slipstream_plugin_interface/src/slipstream_plugin_interface.rs index 1ac50d40c7..2916f89752 100644 --- a/plugins/slipstream_plugin_interface/src/slipstream_plugin_interface.rs +++ b/plugins/slipstream_plugin_interface/src/slipstream_plugin_interface.rs @@ -88,3 +88,13 @@ pub trait SlipstreamPlugin: Any + Send + Sync + std::fmt::Debug { Ok(()) } } + +/// A factory record submitted by plugin crates at link time via `inventory::submit!`. +/// snarkOS uses `inventory::iter::()` to discover all statically +/// linked plugins without needing explicit per-plugin match arms. +pub struct PluginRegistration { + pub name: &'static str, + pub factory: fn() -> Box, +} + +inventory::collect!(PluginRegistration); diff --git a/plugins/slipstream_plugin_manager/Cargo.toml b/plugins/slipstream_plugin_manager/Cargo.toml index 209718a98d..adcabe43b6 100644 --- a/plugins/slipstream_plugin_manager/Cargo.toml +++ b/plugins/slipstream_plugin_manager/Cargo.toml @@ -20,11 +20,3 @@ workspace = true [dependencies.tracing] workspace = true -[dependencies.serde_json] -workspace = true - -[dependencies.libloading] -version = "0.8" - -[dependencies.json5] -version = "0.4" diff --git a/plugins/slipstream_plugin_manager/src/slipstream_manager.rs b/plugins/slipstream_plugin_manager/src/slipstream_manager.rs index e6d4261fa6..4d6fe76ce5 100644 --- a/plugins/slipstream_plugin_manager/src/slipstream_manager.rs +++ b/plugins/slipstream_plugin_manager/src/slipstream_manager.rs @@ -19,10 +19,9 @@ use snarkvm_slipstream_plugin_interface::slipstream_plugin_interface::{ SlipstreamPlugin, }; -use libloading::Library; use std::{ ops::{Deref, DerefMut}, - path::{Path, PathBuf}, + path::Path, }; use tracing::{info, warn}; @@ -59,32 +58,17 @@ impl DerefMut for LoadedSlipstreamPlugin { } } -/// A fully-loaded plugin entry: the plugin instance, its backing shared library, and the -/// resolved path used for duplicate detection. Fields are declared in drop order — `plugin` -/// is dropped before `lib` — which guarantees all plugin code finishes executing before the -/// shared library is unloaded. -#[derive(Debug)] -struct LoadedPlugin { - plugin: LoadedSlipstreamPlugin, - _lib: Library, - /// Resolved, absolute path to the `.so` file. - /// Used to detect duplicate loads before calling `dlopen`, preventing unsafe double-loading. - libpath: PathBuf, -} - -impl Drop for LoadedPlugin { +impl Drop for LoadedSlipstreamPlugin { fn drop(&mut self) { - info!("Unloading plugin '{}'", self.plugin.name()); + info!("Unloading plugin '{}'", self.name); self.plugin.on_unload(); - // `plugin` then drops before `lib` (declaration order), ensuring all plugin code - // finishes executing before the shared library is unloaded. } } // The Plugin Manager itself #[derive(Default, Debug)] pub struct SlipstreamPluginManager { - plugins: Vec, + plugins: Vec, } impl SlipstreamPluginManager { @@ -92,22 +76,9 @@ impl SlipstreamPluginManager { SlipstreamPluginManager { plugins: Vec::default() } } - /// Initializes a manager by loading one plugin per config file. - /// - /// Each config file must be a JSON5 file with a `libpath` field pointing to the - /// shared library that implements `SlipstreamPlugin`. - pub fn from_config_files(config_files: &[std::path::PathBuf]) -> Result { - let mut manager = Self::new(); - for path in config_files { - manager.load_plugin(path)?; - } - Ok(manager) - } - - /// Unload all plugins and loaded plugin libraries, making sure to fire - /// their `on_unload()` methods so they can do any necessary cleanup. + /// Unload all plugins, firing their `on_unload()` methods. pub fn unload(&mut self) { - self.plugins.clear(); // Drop impl fires on_unload and enforces plugin-before-lib drop order. + self.plugins.clear(); // Drop impl fires on_unload for each plugin. } /// Returns `true` if any loaded plugin subscribes to the given event kind. @@ -126,58 +97,48 @@ impl SlipstreamPluginManager { if entry.plugin.subscribed_events().contains(&kind) && let Err(e) = entry.plugin.on_broadcast(event) { - warn!("Slipstream plugin '{}' on_broadcast error: {e}", entry.plugin.name()); + warn!("Slipstream plugin '{}' on_broadcast error: {e}", entry.name()); } } } /// Returns the names of all loaded plugins. pub fn list_plugins(&self) -> JsonRpcResult> { - Ok(self.plugins.iter().map(|p| p.plugin.name().to_owned()).collect()) + Ok(self.plugins.iter().map(|p| p.name().to_owned()).collect()) } - /// Loads a plugin from the given config file. - /// - /// # Safety + /// Registers a statically-linked plugin. /// - /// This function loads the dynamically linked library specified in the config. The library - /// must do necessary initializations. - pub fn load_plugin(&mut self, slipstream_plugin_config_file: impl AsRef) -> JsonRpcResult { - // Resolve the library path from the config before calling dlopen. - // This lets us detect duplicates without loading the library a second time, which is - // unsafe: a second dlopen on an already-loaded .so can trigger re-execution of Rust - // .init_array startup code, corrupting global state in the running plugin instance. - let resolved_libpath = resolve_libpath_from_config(slipstream_plugin_config_file.as_ref())?; - - // Check for duplicate library path first (catches same .so before dlopen). - if let Some(entry) = self.plugins.iter().find(|p| p.libpath == resolved_libpath) { - return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(entry.plugin.name().to_string())); + /// Calls `plugin.on_load(config_file, false)` and adds the plugin to the active list. + pub fn register( + &mut self, + mut plugin: Box, + config_file: impl AsRef, + ) -> JsonRpcResult { + let name = plugin.name().to_string(); + + if self.plugins.iter().any(|p| p.name() == name) { + return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(name)); } - let (new_lib, mut new_plugin, new_config_file) = - load_plugin_from_config(slipstream_plugin_config_file.as_ref())?; + let config_file_str = config_file + .as_ref() + .to_str() + .ok_or(SlipstreamPluginManagerError::InvalidPluginPath)?; - // Also guard against a different .so that happens to expose the same plugin name. - if self.plugins.iter().any(|entry| entry.plugin.name().eq(new_plugin.name())) { - return Err(SlipstreamPluginManagerError::PluginAlreadyLoaded(new_plugin.name().to_string())); - } - - // Call on_load and push plugin. - new_plugin - .on_load(new_config_file, false) + plugin + .on_load(config_file_str, false) .map_err(|e| SlipstreamPluginManagerError::PluginStartError(e.to_string()))?; - let name = new_plugin.name().to_string(); - self.plugins.push(LoadedPlugin { plugin: new_plugin, _lib: new_lib, libpath: resolved_libpath }); - - info!("Loaded plugin: {}", name); + self.plugins.push(LoadedSlipstreamPlugin::new(plugin, None)); + info!("Registered static plugin: {}", name); Ok(name) } /// Unloads the plugin with the given name. pub fn unload_plugin(&mut self, name: &str) -> JsonRpcResult<()> { - let Some(idx) = self.plugins.iter().position(|entry| entry.plugin.name().eq(name)) else { + let Some(idx) = self.plugins.iter().position(|entry| entry.name().eq(name)) else { return Err(SlipstreamPluginManagerError::PluginNotLoaded(name.to_string())); }; @@ -185,41 +146,24 @@ impl SlipstreamPluginManager { Ok(()) } - /// Reloads the plugin with the given name from the given config file. - /// - /// # Note - /// - /// This function is not currently exposed. It was disabled due to SIGSEGV issues - /// and is a good next step to implement safely. Use `unload_plugin` + `load_plugin` - /// as a workaround in the meantime OR just stop the snarkos service and restart it with - /// the updated plugin config(s) + /// Reload is not currently implemented. pub fn reload_plugin(&mut self, _name: &str, _config_file: &str) -> JsonRpcResult<()> { - Err(SlipstreamPluginManagerError::PluginLoadError("Plugin reload is not currently implemented.".to_string())) + Err(SlipstreamPluginManagerError::PluginLoadError( + "Plugin reload is not currently implemented.".to_string(), + )) } fn _drop_plugin(&mut self, idx: usize) { - self.plugins.remove(idx); // Drop impl fires on_unload and enforces plugin-before-lib drop order. + self.plugins.remove(idx); // Drop impl fires on_unload. } } #[derive(thiserror::Error, Debug)] pub enum SlipstreamPluginManagerError { - #[error("Cannot open the plugin config file: {0}")] - CannotOpenConfigFile(String), - - #[error("Cannot read the plugin config file: {0}")] - CannotReadConfigFile(String), - - #[error("The config file is not in a valid JSON/JSON5 format: {0}")] - InvalidConfigFileFormat(String), - - #[error("Plugin library path is not specified in the config file")] - LibPathNotSet, - #[error("Invalid plugin path")] InvalidPluginPath, - #[error("Cannot load plugin shared library (error: {0})")] + #[error("Cannot load plugin (error: {0})")] PluginLoadError(String), #[error("The slipstream plugin '{0}' is already loaded")] @@ -232,189 +176,21 @@ pub enum SlipstreamPluginManagerError { PluginStartError(String), } -/// Parses a plugin config file and returns the resolved, absolute path to the `.so`. -/// -/// Does NOT open or load the library — safe to call for duplicate detection before `dlopen`. -#[cfg(not(test))] -pub(crate) fn resolve_libpath_from_config( - slipstream_plugin_config_file: &Path, -) -> Result { - use std::{fs::File, io::Read}; - - let mut file = File::open(slipstream_plugin_config_file).map_err(|e| { - SlipstreamPluginManagerError::CannotOpenConfigFile(format!( - "Failed to open the plugin config file {slipstream_plugin_config_file:?}, error: {e:?}" - )) - })?; - - let mut contents = String::new(); - file.read_to_string(&mut contents).map_err(|e| { - SlipstreamPluginManagerError::CannotReadConfigFile(format!( - "Failed to read the plugin config file {slipstream_plugin_config_file:?}, error: {e:?}" - )) - })?; - - let result: serde_json::Value = json5::from_str(&contents).map_err(|e| { - SlipstreamPluginManagerError::InvalidConfigFileFormat(format!( - "The config file {slipstream_plugin_config_file:?} is not in a valid Json5 format, error: {e:?}" - )) - })?; - - let libpath_str = result["libpath"].as_str().ok_or(SlipstreamPluginManagerError::LibPathNotSet)?; - let mut libpath = PathBuf::from(libpath_str); - if libpath.is_relative() { - let config_dir = slipstream_plugin_config_file.parent().ok_or_else(|| { - SlipstreamPluginManagerError::CannotOpenConfigFile(format!( - "Failed to resolve parent of {slipstream_plugin_config_file:?}", - )) - })?; - libpath = config_dir.join(libpath); - } - - Ok(libpath) -} - -/// # Safety -/// -/// This function loads the dynamically linked library specified in the path. The library -/// must do necessary initializations. -/// -/// Returns the slipstream plugin, the dynamic library, and the parsed config file as a `&str`. -/// (The slipstream plugin interface requires a `&str` for the `on_load` method.) -#[cfg(not(test))] -pub(crate) fn load_plugin_from_config( - slipstream_plugin_config_file: &Path, -) -> Result<(Library, LoadedSlipstreamPlugin, &str), SlipstreamPluginManagerError> { - use std::{fs::File, io::Read, path::PathBuf}; - // Trait objects have no C equivalent; the suppression is intentional — the plugin ABI - // uses raw pointers and the caller takes ownership immediately via Box::from_raw. - #[allow(improper_ctypes_definitions)] - type PluginConstructor = unsafe extern "C" fn() -> *mut dyn SlipstreamPlugin; - use libloading::Symbol; - - let mut file = match File::open(slipstream_plugin_config_file) { - Ok(file) => file, - Err(err) => { - return Err(SlipstreamPluginManagerError::CannotOpenConfigFile(format!( - "Failed to open the plugin config file {slipstream_plugin_config_file:?}, error: {err:?}" - ))); - } - }; - - let mut contents = String::new(); - if let Err(err) = file.read_to_string(&mut contents) { - return Err(SlipstreamPluginManagerError::CannotReadConfigFile(format!( - "Failed to read the plugin config file {slipstream_plugin_config_file:?}, error: {err:?}" - ))); - } - - let result: serde_json::Value = match json5::from_str(&contents) { - Ok(value) => value, - Err(err) => { - return Err(SlipstreamPluginManagerError::InvalidConfigFileFormat(format!( - "The config file {slipstream_plugin_config_file:?} is not in a valid Json5 format, error: {err:?}" - ))); - } - }; - - let libpath = result["libpath"].as_str().ok_or(SlipstreamPluginManagerError::LibPathNotSet)?; - let mut libpath = PathBuf::from(libpath); - if libpath.is_relative() { - let config_dir = slipstream_plugin_config_file.parent().ok_or_else(|| { - SlipstreamPluginManagerError::CannotOpenConfigFile(format!( - "Failed to resolve parent of {slipstream_plugin_config_file:?}", - )) - })?; - libpath = config_dir.join(libpath); - } - - let plugin_name = result["name"].as_str().map(|s| s.to_owned()); - - let config_file = - slipstream_plugin_config_file.as_os_str().to_str().ok_or(SlipstreamPluginManagerError::InvalidPluginPath)?; - - let (plugin, lib) = unsafe { - let lib = Library::new(libpath).map_err(|e| SlipstreamPluginManagerError::PluginLoadError(e.to_string()))?; - let constructor: Symbol = - lib.get(b"_create_plugin").map_err(|e| SlipstreamPluginManagerError::PluginLoadError(e.to_string()))?; - let plugin_raw = constructor(); - if plugin_raw.is_null() { - return Err(SlipstreamPluginManagerError::PluginLoadError( - "plugin constructor returned a null pointer".to_string(), - )); - } - (Box::from_raw(plugin_raw), lib) - }; - Ok((lib, LoadedSlipstreamPlugin::new(plugin, plugin_name), config_file)) -} - -#[cfg(test)] -const TESTPLUGIN_CONFIG: &str = "TESTPLUGIN_CONFIG"; -#[cfg(test)] -const TESTPLUGIN2_CONFIG: &str = "TESTPLUGIN2_CONFIG"; - -// In tests resolve_libpath_from_config returns the config path itself as a stand-in for -// the .so path. This is sufficient for duplicate detection without real file I/O. -#[cfg(test)] -pub(crate) fn resolve_libpath_from_config( - slipstream_plugin_config_file: &Path, -) -> Result { - Ok(slipstream_plugin_config_file.to_path_buf()) -} - -// This is mocked for tests to avoid having to do IO with a dynamically linked library -// across different architectures at test time. -#[cfg(test)] -pub(crate) fn load_plugin_from_config( - slipstream_plugin_config_file: &Path, -) -> Result<(Library, LoadedSlipstreamPlugin, &str), SlipstreamPluginManagerError> { - if slipstream_plugin_config_file.ends_with(TESTPLUGIN_CONFIG) { - Ok(tests::dummy_plugin_and_library(tests::TestPlugin, TESTPLUGIN_CONFIG)) - } else if slipstream_plugin_config_file.ends_with(TESTPLUGIN2_CONFIG) { - Ok(tests::dummy_plugin_and_library(tests::TestPlugin2, TESTPLUGIN2_CONFIG)) - } else { - Err(SlipstreamPluginManagerError::CannotOpenConfigFile( - slipstream_plugin_config_file.to_str().unwrap().to_string(), - )) - } -} - #[cfg(test)] mod tests { - use crate::slipstream_manager::{ - LoadedPlugin, - LoadedSlipstreamPlugin, - SlipstreamPluginManager, - TESTPLUGIN_CONFIG, - TESTPLUGIN2_CONFIG, - }; - use libloading::Library; + use crate::slipstream_manager::{LoadedSlipstreamPlugin, SlipstreamPluginManager}; use snarkvm_slipstream_plugin_interface::slipstream_plugin_interface::{ BroadcastEvent, BroadcastEventKind, SlipstreamPlugin, }; - use std::{ - path::PathBuf, - sync::{Arc, RwLock}, - }; - - pub(super) fn dummy_plugin_and_library( - plugin: P, - config_path: &'static str, - ) -> (Library, LoadedSlipstreamPlugin, &'static str) { - #[cfg(unix)] - let library = libloading::os::unix::Library::this(); - #[cfg(windows)] - let library = libloading::os::windows::Library::this().unwrap(); - (Library::from(library), LoadedSlipstreamPlugin::new(Box::new(plugin), None), config_path) - } + use std::sync::{Arc, RwLock}; const DUMMY_NAME: &str = "dummy"; const ANOTHER_DUMMY_NAME: &str = "another_dummy"; #[derive(Clone, Copy, Debug)] - pub(super) struct TestPlugin; + struct TestPlugin; impl SlipstreamPlugin for TestPlugin { fn name(&self) -> &'static str { @@ -423,7 +199,7 @@ mod tests { } #[derive(Clone, Copy, Debug)] - pub(super) struct TestPlugin2; + struct TestPlugin2; impl SlipstreamPlugin for TestPlugin2 { fn name(&self) -> &'static str { @@ -433,47 +209,35 @@ mod tests { #[test] fn test_plugin_list() { - // Initialize empty manager. let plugin_manager = Arc::new(RwLock::new(SlipstreamPluginManager::new())); - let mut plugin_manager_lock = plugin_manager.write().unwrap(); - - // Load two plugins. - let (_lib, mut plugin, config) = dummy_plugin_and_library(TestPlugin, TESTPLUGIN_CONFIG); - plugin.on_load(config, false).unwrap(); - plugin_manager_lock.plugins.push(LoadedPlugin { plugin, _lib, libpath: PathBuf::from(config) }); + let mut lock = plugin_manager.write().unwrap(); - let (_lib, mut plugin, config) = dummy_plugin_and_library(TestPlugin2, TESTPLUGIN2_CONFIG); - plugin.on_load(config, false).unwrap(); - plugin_manager_lock.plugins.push(LoadedPlugin { plugin, _lib, libpath: PathBuf::from(config) }); + lock.plugins.push(LoadedSlipstreamPlugin::new(Box::new(TestPlugin), None)); + lock.plugins.push(LoadedSlipstreamPlugin::new(Box::new(TestPlugin2), None)); - // Check that both plugins are returned in the list. - let plugins = plugin_manager_lock.list_plugins().unwrap(); + let plugins = lock.list_plugins().unwrap(); assert!(plugins.iter().any(|name| name.eq(DUMMY_NAME))); assert!(plugins.iter().any(|name| name.eq(ANOTHER_DUMMY_NAME))); } #[test] - fn test_plugin_load_unload() { - // Initialize empty manager. + fn test_plugin_register_unload() { let plugin_manager = Arc::new(RwLock::new(SlipstreamPluginManager::new())); - let mut plugin_manager_lock = plugin_manager.write().unwrap(); + let mut lock = plugin_manager.write().unwrap(); - // Load rpc call. - let load_result = plugin_manager_lock.load_plugin(TESTPLUGIN_CONFIG); - assert!(load_result.is_ok()); - assert_eq!(plugin_manager_lock.plugins.len(), 1); + let result = lock.register(Box::new(TestPlugin), "dummy_config"); + assert!(result.is_ok()); + assert_eq!(lock.plugins.len(), 1); - // Unload rpc call. - let unload_result = plugin_manager_lock.unload_plugin(DUMMY_NAME); - assert!(unload_result.is_ok()); - assert_eq!(plugin_manager_lock.plugins.len(), 0); + let result = lock.unload_plugin(DUMMY_NAME); + assert!(result.is_ok()); + assert_eq!(lock.plugins.len(), 0); } #[test] fn test_broadcast_mapping_update() { let mut manager = SlipstreamPluginManager::new(); - // Install a mock plugin that tracks calls. #[derive(Debug)] struct TrackingPlugin { calls: std::sync::atomic::AtomicU32, @@ -493,20 +257,11 @@ mod tests { } } - // Manually push the plugin (bypassing dynamic loading). - #[cfg(unix)] - let _lib = Library::from(libloading::os::unix::Library::this()); - #[cfg(windows)] - let _lib = Library::from(libloading::os::windows::Library::this().unwrap()); - - let plugin = TrackingPlugin { calls: std::sync::atomic::AtomicU32::new(0) }; - manager.plugins.push(LoadedPlugin { - plugin: LoadedSlipstreamPlugin::new(Box::new(plugin), None), - _lib, - libpath: PathBuf::new(), - }); + manager.plugins.push(LoadedSlipstreamPlugin::new( + Box::new(TrackingPlugin { calls: std::sync::atomic::AtomicU32::new(0) }), + None, + )); - // Broadcast a MappingUpdate and verify the plugin received it. manager.broadcast(BroadcastEvent::MappingUpdate { program_id: b"program_id", mapping_name: b"mapping", @@ -515,7 +270,6 @@ mod tests { block_height: 42, }); - // Verify via list_plugins that the plugin is still loaded. assert_eq!(manager.list_plugins().unwrap(), vec!["tracking"]); } }