Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Module Development

The BTCDecoded blvm-node includes a process-isolated module system that enables optional features (Lightning, merge mining, privacy enhancements) without affecting consensus or base node stability. Modules run in separate processes with IPC communication, providing security through isolation.

Core Principles

  1. Process Isolation: Each module runs in a separate process with isolated memory
  2. API Boundaries: Modules communicate only through well-defined APIs
  3. Crash Containment: Module failures don’t propagate to the base node
  4. Consensus Isolation: Modules cannot modify consensus rules, UTXO set, or block validation
  5. State Separation: Module state is completely separate from consensus state

Communication

Modules communicate with the node via Inter-Process Communication (IPC) using Unix domain sockets. Protocol uses length-delimited binary messages (bincode serialization) with message types: Requests, Responses, Events. Connection is persistent for request/response pattern; events use pub/sub pattern for real-time notifications.

Module Structure

Directory Layout

Each module should be placed in a subdirectory within the modules/ directory:

modules/
└── my-module/
    ├── Cargo.toml
    ├── src/
    │   └── main.rs
    └── module.toml          # Module manifest (required)

Module Manifest (module.toml)

Every module must include a module.toml manifest file:

# ============================================================================
# Module Manifest
# ============================================================================

# ----------------------------------------------------------------------------
# Core Identity (Required)
# ----------------------------------------------------------------------------
name = "my-module"
version = "1.0.0"
entry_point = "my-module"

# ----------------------------------------------------------------------------
# Metadata (Optional)
# ----------------------------------------------------------------------------
description = "Description of what this module does"
author = "Your Name <your.email@example.com>"

# ----------------------------------------------------------------------------
# Capabilities
# ----------------------------------------------------------------------------
# Permissions this module requires to function
capabilities = [
    "read_blockchain",    # Query blockchain data
    "subscribe_events",   # Receive node events
]

# ----------------------------------------------------------------------------
# Dependencies
# ----------------------------------------------------------------------------
# Required dependencies (module cannot load without these)
[dependencies]
"blvm-lightning" = ">=1.0.0"

# Optional dependencies (module can work without these)
[optional_dependencies]
"blvm-mesh" = ">=0.5.0"

# ----------------------------------------------------------------------------
# Configuration Schema (Optional)
# ----------------------------------------------------------------------------
[config_schema]
poll_interval = "Polling interval in seconds (default: 5)"

Required Fields:

  • name: Module identifier (alphanumeric with dashes/underscores)
  • version: Semantic version (e.g., “1.0.0”)
  • entry_point: Binary name or path

Optional Fields:

  • description: Human-readable description
  • author: Module author
  • capabilities: List of required permissions
  • dependencies: Required (hard) dependencies - module cannot load without them
  • optional_dependencies: Optional (soft) dependencies - module can work without them

Dependency Version Constraints:

  • >=1.0.0 - Greater than or equal to version
  • <=2.0.0 - Less than or equal to version
  • =1.2.3 - Exact version match
  • ^1.0.0 - Compatible version (>=1.0.0 and <2.0.0)
  • ~1.2.0 - Patch updates only (>=1.2.0 and <1.3.0)

Module Development

The blvm-sdk crate provides attribute macros and a run_module! macro so you can define CLI, RPC, and event handling in one place without manual IPC or event loops. This is the recommended way to build new modules.

Dependency: Add blvm-sdk with the node feature. Use the prelude:

#![allow(unused)]
fn main() {
use blvm_sdk::module::prelude::*;
}

Module struct and config:

  • #[blvm_module] / #[module] on the struct: #[module(name = "my-module", config = MyConfig)]. Optional migrations = ((1, up_initial), (2, up_add_cache)) generates ModuleMeta for run_module_main!.
  • #[module_config(name = "my-module")] / #[config(name = "my-module")] on a config struct: generates CONFIG_SECTION_NAME (matches node [modules.my-module]), apply_env_overrides(), and load(path). Field-level #[config_env] or #[config_env("ENV_NAME")] uses env vars to override (default: MODULE_CONFIG_<FIELD>).

Single impl for CLI, RPC, and events:

  • #[module(name = "my-module")] on the impl block generates cli_spec(), dispatch_cli(), rpc_method_names(), dispatch_rpc(), event_types(), and dispatch_event() from one set of methods:
    • Methods with ctx: &InvocationContext (and no #[rpc_method] / #[on_event]) become CLI subcommands. Use #[command] to mark them explicitly. Parameters can use #[arg(long)], #[arg(short = 'n')], #[arg(default = "value")] for CLI parsing.
    • #[rpc_method] / #[rpc_method(name = "method_name")] marks RPC endpoints.
    • #[on_event(NewBlock, NewTransaction)] marks event handlers; use with #[event_handlers] on the impl to generate event_types() and dispatch_event().
    • Payload injection: For event types listed in blvm-sdk-macros event_payload_map (same field names as EventPayload in blvm-node), a handler can take payload fields by name plus optional ctx: &InvocationContext instead of only &EventMessage. The match is on &event.payload, so use reference types (e.g. packet_data: &[u8], peer_addr: &str). Example: #[on_event(MeshPacketReceived)] with (packet_data: &[u8], peer_addr: &str, ctx: &InvocationContext). The _ctx / _context names are also recognized for the legacy (&event, ctx) style.

Migrations: #[migration(version = N)] on a function; use with db.run_migrations(&[(1, up_initial), ...]) or via #[module(migrations = (...))].

Entry point:

  • ModuleBootstrap::from_env() reads MODULE_ID, SOCKET_PATH, DATA_DIR when the node spawns the module; for manual runs you can use ModuleBootstrap::init_module("my-module") or parse CLI.
  • ModuleDb::open(&bootstrap.data_dir) opens the module DB; then run_module! { bootstrap, module_name, module, module_type, db } runs the main loop (IPC connect, CLI/RPC/event dispatch, no manual event loop).
  • run_module_main!(MyModule) — when your struct has #[module(config = MyConfig, migrations = (...))] and implements ModuleMeta, this macro expands to a full main that does bootstrap, migrations, config load, and run_module!.

Example (skeleton):

use blvm_sdk::module::prelude::*;
use blvm_sdk::module::{ModuleBootstrap, ModuleDb};

#[derive(Clone, Default, serde::Serialize, serde::Deserialize)]
#[config(name = "my-module")]
pub struct MyConfig { #[config_env] pub setting: String }

#[derive(Clone)]
#[module(name = "my-module", config = MyConfig)]
pub struct MyModule { config: MyConfig }

#[module(name = "my-module")]
impl MyModule {
    #[command]
    fn status(&self, _ctx: &InvocationContext) -> Result<String, ModuleError> {
        Ok("ok".into())
    }
    #[rpc_method(name = "my_method")]
    fn my_method(&self, params: &serde_json::Value, _db: &std::sync::Arc<dyn blvm_node::storage::database::Database>) -> Result<serde_json::Value, ModuleError> {
        Ok(serde_json::json!({}))
    }
}

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    let bootstrap = ModuleBootstrap::from_env().unwrap_or_else(|_| ModuleBootstrap::init_module("my-module"));
    let db = ModuleDb::open(&bootstrap.data_dir)?;
    let config = MyConfig::load(bootstrap.data_dir.join("config.toml")).unwrap_or_default();
    let module = MyModule { config };
    blvm_sdk::run_module! {
        bootstrap: &bootstrap,
        module_name: "my-module",
        module: module,
        module_type: MyModule,
        db: db.as_db(),
    }?;
    Ok(())
}

Code: blvm-sdk-macros (attribute definitions), hello-module example, selective-sync (real module using this style).

Module CLI under the blvm binary

Modules that expose CLI handlers (methods with InvocationContext / #[command]) register a CLI spec with the node when they connect over IPC. The main blvm binary discovers registered specs and dispatches invocations to the running module process (node RPC: e.g. listing specs and forwarding runmodulecli-style calls). Users run blvm <command-group> <subcommand> (e.g. blvm sync-policy list for selective-sync). The module must be loaded; otherwise those top-level commands are unavailable. See blvm-node module docs for the full CLI flow.

Basic module structure (integration API)

If you need more control than the SDK declarative style (e.g. custom bootstrap or no macros), you can implement the lifecycle and connect via IPC directly. Two approaches:

Using ModuleIntegration

use blvm_node::module::integration::ModuleIntegration;
use blvm_node::module::EventType;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse command-line arguments
    let args = Args::parse();
    
    // Connect to node using ModuleIntegration
    // Note: socket_path must be PathBuf (convert from String if needed)
    let socket_path = std::path::PathBuf::from(&args.socket_path);
    let mut integration = ModuleIntegration::connect(
        socket_path,
        args.module_id.unwrap_or_else(|| "my-module".to_string()),
        "my-module".to_string(),
        env!("CARGO_PKG_VERSION").to_string(),
    ).await?;
    
    // Subscribe to events
    let event_types = vec![EventType::NewBlock, EventType::NewTransaction];
    integration.subscribe_events(event_types).await?;
    
    // Get NodeAPI
    let node_api = integration.node_api();
    
    // Get event receiver (broadcast::Receiver returns Result, not Option)
    let mut event_receiver = integration.event_receiver();
    
    // Main module loop
    loop {
        match event_receiver.recv().await {
            Ok(ModuleMessage::Event(event_msg)) => {
                // Process event
                match event_msg.payload {
                    // Handle specific event types
                    _ => {}
                }
            }
            Ok(_) => {} // Other message types
            Err(tokio::sync::broadcast::error::RecvError::Lagged(skipped)) => {
                warn!("Event receiver lagged, skipped {} messages", skipped);
            }
            Err(tokio::sync::broadcast::error::RecvError::Closed) => {
                break; // Channel closed, exit loop
            }
        }
    }
    
    Ok(())
}

Using ModuleIpcClient + NodeApiIpc (Legacy)

use blvm_node::module::ipc::client::ModuleIpcClient;
use blvm_node::module::api::node_api::NodeApiIpc;
use blvm_node::module::ipc::protocol::{RequestMessage, RequestPayload, MessageType};
use std::sync::Arc;
use std::path::PathBuf;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
    // Parse command-line arguments
    let args = Args::parse();
    
    // Connect to node IPC socket (PathBuf required)
    let socket_path = PathBuf::from(&args.socket_path);
    let mut ipc_client = ModuleIpcClient::connect(&socket_path).await?;
    
    // Perform handshake
    let correlation_id = ipc_client.next_correlation_id();
    let handshake_request = RequestMessage {
        correlation_id,
        request_type: MessageType::Handshake,
        payload: RequestPayload::Handshake {
            module_id: "my-module".to_string(),
            module_name: "my-module".to_string(),
            version: env!("CARGO_PKG_VERSION").to_string(),
        },
    };
    let response = ipc_client.request(handshake_request).await?;
    // Verify handshake response...
    
    // Create NodeAPI wrapper (requires Arc<Mutex<ModuleIpcClient>> and module_id)
    let ipc_client_arc = Arc::new(tokio::sync::Mutex::new(ipc_client));
    let node_api = Arc::new(NodeApiIpc::new(ipc_client_arc.clone(), "my-module".to_string()));
    
    // Subscribe to events using NodeAPI
    let event_types = vec![EventType::NewBlock, EventType::NewTransaction];
    let mut event_receiver = node_api.subscribe_events(event_types).await?;
    
    // Main module loop (mpsc::Receiver returns Option)
    while let Some(event) = event_receiver.recv().await {
        match event {
            ModuleMessage::Event(event_msg) => {
                // Process event
            }
            _ => {}
        }
    }
    
    Ok(())
}

Recommendation: Prefer the SDK declarative style for new modules. Otherwise use ModuleIntegration for simplicity. The legacy IPC client approach is still supported but requires more boilerplate.

Module Lifecycle

Modules receive command-line arguments (--module-id, --socket-path, --data-dir) and configuration via environment variables (MODULE_CONFIG_*). Lifecycle: Initialization (connect IPC) → Start (subscribe events) → Running (process events/requests) → Stop (clean shutdown).

Querying Node Data

Modules can query blockchain data through the Node API. Recommended approach: Use NodeAPI methods directly:

#![allow(unused)]
fn main() {
// Get NodeAPI from integration
let node_api = integration.node_api();

// Get current chain tip
let chain_tip = node_api.get_chain_tip().await?;

// Get a block by hash
let block = node_api.get_block(&block_hash).await?;

// Get block header
let header = node_api.get_block_header(&block_hash).await?;

// Get transaction
let tx = node_api.get_transaction(&tx_hash).await?;

// Get UTXO
let utxo = node_api.get_utxo(&outpoint).await?;

// Get chain info
let chain_info = node_api.get_chain_info().await?;
}

Alternative (Low-Level IPC): For advanced use cases, you can use the IPC protocol directly:

#![allow(unused)]
fn main() {
// Note: This requires request_type field in RequestMessage
let request = RequestMessage {
    correlation_id: client.next_correlation_id(),
    request_type: MessageType::GetChainTip,
    payload: RequestPayload::GetChainTip,
};
let response = client.send_request(request).await?;
}

Recommendation: Use NodeAPI methods for simplicity and type safety. Low-level IPC is only needed for custom protocols.

Available NodeAPI Methods:

Blockchain API:

  • get_block(hash) - Get block by hash
  • get_block_header(hash) - Get block header by hash
  • get_transaction(hash) - Get transaction by hash
  • has_transaction(hash) - Check if transaction exists
  • get_chain_tip() - Get current chain tip hash
  • get_block_height() - Get current block height
  • get_block_by_height(height) - Get block by height
  • get_utxo(outpoint) - Get UTXO by outpoint (read-only)
  • get_chain_info() - Get chain information (tip, height, difficulty, etc.)

Mempool API:

  • get_mempool_transactions() - Get all transaction hashes in mempool
  • get_mempool_transaction(hash) - Get transaction from mempool by hash
  • get_mempool_size() - Get mempool size information
  • check_transaction_in_mempool(hash) - Check if transaction is in mempool
  • get_fee_estimate(target_blocks) - Get fee estimate for target confirmation blocks

Network API:

  • get_network_stats() - Get network statistics
  • get_network_peers() - Get list of connected peers

P2P serve policy & sync (read + targeted writes):

These calls affect what the node serves on the Bitcoin P2P wire (getdata responses) and sync introspection. They do not change consensus validation; withheld blocks/transactions are still validated if present locally. Use with care: broad denylists or bans affect relay and peer relationships.

  • Block getdata denylist — additive merge, bounded snapshot, clear, or replace full-hash sets. Peers requesting a denied block hash get notfound instead of a full block message.
    • merge_block_serve_denylist(hashes)
    • get_block_serve_denylist_snapshot()
    • clear_block_serve_denylist()
    • replace_block_serve_denylist(hashes)
  • Transaction getdata denylist — same pattern for full tx serves on getdata.
    • merge_tx_serve_denylist(hashes)
    • get_tx_serve_denylist_snapshot()
    • clear_tx_serve_denylist()
    • replace_tx_serve_denylist(hashes)
  • Sync status — finer-grained view than get_chain_info() alone (coordinator phase / progress); see SyncStatus in the trait.
    • get_sync_status()
  • Operational maintenance — when enabled, the node refuses all full-block answers on getdata (coarse knob for degraded operation).
    • set_block_serve_maintenance_mode(enabled)
  • Peer ban — request a ban by peer address string; optional duration (None = permanent). High-impact; subject to node policy and review.
    • ban_peer(peer_addr, ban_duration_seconds)

Corresponding IPC MessageType / RequestPayload names match the NodeAPI methods (see Module IPC Protocol). Implementation: traits.rs, getdata_serve.rs.

Storage API:

  • storage_open_tree(name) - Open a storage tree (isolated per module)
  • storage_insert(tree_id, key, value) - Insert a key-value pair
  • storage_get(tree_id, key) - Get a value by key
  • storage_remove(tree_id, key) - Remove a key-value pair
  • storage_contains_key(tree_id, key) - Check if key exists
  • storage_iter(tree_id) - Iterate over all key-value pairs
  • storage_transaction(tree_id, operations) - Execute atomic batch of operations

Filesystem API:

  • read_file(path) - Read a file from module’s data directory
  • write_file(path, data) - Write data to a file
  • delete_file(path) - Delete a file
  • list_directory(path) - List directory contents
  • create_directory(path) - Create a directory
  • get_file_metadata(path) - Get file metadata (size, type, timestamps)

Module Communication API:

  • call_module(target_module_id, method, params) - Call an API method on another module
  • publish_event(event_type, payload) - Publish an event to other modules
  • register_module_api(api) - Register module API for other modules to call
  • unregister_module_api() - Unregister module API
  • discover_modules() - Discover all available modules
  • get_module_info(module_id) - Get information about a specific module
  • is_module_available(module_id) - Check if a module is available

RPC API:

  • register_rpc_endpoint(method, description) - Register a JSON-RPC endpoint
  • unregister_rpc_endpoint(method) - Unregister an RPC endpoint

Timers API:

  • register_timer(interval_seconds, callback) - Register a periodic timer
  • cancel_timer(timer_id) - Cancel a registered timer
  • schedule_task(delay_seconds, callback) - Schedule a one-time task

Metrics API:

  • report_metric(metric) - Report a metric to the node
  • get_module_metrics(module_id) - Get module metrics
  • get_all_metrics() - Get aggregated metrics from all modules

Lightning & Payment API:

  • get_lightning_node_url() - Get Lightning node connection info
  • get_lightning_info() - Get Lightning node information
  • get_payment_state(payment_id) - Get payment state by payment ID

Network Integration API:

  • send_mesh_packet_to_peer(peer_addr, packet_data) — send a mesh packet to a peer (supported path for IPC modules).
  • send_mesh_packet_to_module(module_id, packet_data, peer_addr) — may be unimplemented for out-of-process modules (NodeApiIpc); use peer-targeted sends where available.

For complete API reference, see NodeAPI trait.

Subscribing to events

Modules subscribe with SubscribeEvents and receive EventType / EventPayload streams (chain, mempool, network, payments, mining, governance, maintenance, etc.). Events are notifications; changing serve policy or sync-adjacent behavior uses the NodeAPI methods above (denylists, maintenance mode, bans), not events alone.

Modules can subscribe to real-time node events. The approach depends on which integration method you’re using:

Using ModuleIntegration

#![allow(unused)]
fn main() {
// Subscribe to events
let event_types = vec![EventType::NewBlock, EventType::NewTransaction];
integration.subscribe_events(event_types).await?;

// Get event receiver
let mut event_receiver = integration.event_receiver();

// Receive events in main loop
while let Some(event) = event_receiver.recv().await {
    match event {
        ModuleMessage::Event(event_msg) => {
            // Handle event
        }
        _ => {}
    }
}
}

Using ModuleClient

#![allow(unused)]
fn main() {
// Subscribe to events
let event_types = vec![EventType::NewBlock, EventType::NewTransaction];
client.subscribe_events(event_types).await?;

// Get event receiver
let mut event_receiver = client.event_receiver();

// Receive events in main loop
while let Some(event) = event_receiver.recv().await {
    match event {
        ModuleMessage::Event(event_msg) => {
            // Handle event
        }
        _ => {}
    }
}
}

Available Event Types:

Core Blockchain Events:

  • NewBlock - New block connected to chain
  • NewTransaction - New transaction in mempool
  • BlockDisconnected - Block disconnected (chain reorg)
  • ChainReorg - Chain reorganization occurred

Payment Events:

  • PaymentRequestCreated, PaymentSettled, PaymentFailed, PaymentVerified, PaymentRouteFound, PaymentRouteFailed, ChannelOpened, ChannelClosed

Mining Events:

  • BlockMined, BlockTemplateUpdated, MiningDifficultyChanged, MiningJobCreated, ShareSubmitted, MergeMiningReward, MiningPoolConnected, MiningPoolDisconnected

Network Events:

  • PeerConnected, PeerDisconnected, PeerBanned, MessageReceived, MessageSent, BroadcastStarted, BroadcastCompleted, RouteDiscovered, RouteFailed

Module Lifecycle Events:

  • ModuleLoaded, ModuleUnloaded, ModuleCrashed, ModuleDiscovered, ModuleInstalled, ModuleUpdated, ModuleRemoved

Configuration & Lifecycle Events:

  • ConfigLoaded, NodeStartupCompleted, NodeShutdown, NodeShutdownCompleted

Maintenance & Resource Events:

  • DataMaintenance, MaintenanceStarted, MaintenanceCompleted, HealthCheck, DiskSpaceLow, ResourceLimitWarning

Governance Events:

  • GovernanceProposalCreated, GovernanceProposalVoted, GovernanceProposalMerged, WebhookSent, WebhookFailed, GovernanceForkDetected

Consensus Events:

  • BlockValidationStarted, BlockValidationCompleted, ScriptVerificationStarted, ScriptVerificationCompleted, DifficultyAdjusted, SoftForkActivated

Mempool Events:

  • MempoolTransactionAdded, MempoolTransactionRemoved, FeeRateChanged

And many more. For complete list, see EventType enum and Event System.

Configuration

Module system is configured in node config (see Node Configuration):

[modules]
enabled = true
modules_dir = "modules"
data_dir = "data/modules"
enabled_modules = []  # Empty = auto-discover all

[modules.module_configs.my-module]
setting1 = "value1"

Modules can have their own config.toml files, passed via environment variables.

Security Model

Permissions

Modules operate with whitelist-only access control. Each module declares required capabilities in its manifest. Capabilities use snake_case in module.toml and map to Permission enum variants.

Core Permissions:

  • read_blockchain - Access to blockchain data
  • read_utxo - Query UTXO set (read-only)
  • read_chain_state - Query chain state (height, tip)
  • subscribe_events - Subscribe to node events
  • send_transactions - Submit transactions to mempool (future: may be restricted)

Additional Permissions:

  • read_mempool - Read mempool data
  • read_network - Read network data (peers, stats)
  • network_access - Send network packets
  • read_lightning - Read Lightning network data
  • read_payment - Read payment data
  • read_storage, write_storage, manage_storage - Storage access
  • read_filesystem, write_filesystem, manage_filesystem - Filesystem access
  • register_rpc_endpoint - Register RPC endpoints
  • manage_timers - Manage timers and scheduled tasks
  • report_metrics, read_metrics - Metrics access
  • discover_modules - Discover other modules
  • publish_events - Publish events to other modules
  • call_module - Call other modules’ APIs
  • register_module_api - Register module API for other modules to call

For complete list, see Permission enum.

Sandboxing

Modules are sandboxed to ensure security:

  1. Process Isolation: Separate process, isolated memory
  2. File System: Access limited to module data directory
  3. Network: No network access (modules can only communicate via IPC)
  4. Resource Limits: CPU, memory, and file descriptor limits (configurable via node module_resource_limits; on Linux applied via prlimit after spawn)

Request Validation

All module API requests are validated:

  • Permission checks (module has required permission)
  • Consensus protection (no consensus-modifying operations)
  • Resource limits (enforced per module); rate limiting (planned)

API Reference

NodeAPI Methods: See Querying Node Data section above for complete list of available methods.

Event Types: See Subscribing to Events section above for complete list of available event types.

Permissions: See Permissions section above for complete list of available permissions.

For detailed API reference, see:

For detailed API reference, see blvm-node/src/module/ (traits, IPC protocol, Node API, security).

See Also