From 995de4dd78cb90405a74f56dd61589e49aac7508 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Fri, 8 May 2026 13:13:23 -0500 Subject: [PATCH] Add VSS storage backend Local SQLite can't recover Lightning channel state from seed alone, so losing local data means losing the funds in the channels. VSS gives the wallet a remote, seed-derived store that can be reconstructed from the seed against the configured VSS endpoint. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/cli/src/main.rs | 45 ++++++++++++++-- justfile | 4 ++ orange-sdk/src/ffi/orange/config.rs | 45 ++++++++++++---- orange-sdk/src/ffi/orange/error.rs | 6 +++ orange-sdk/src/lib.rs | 80 +++++++++++++++++++++++++---- orange-sdk/src/lightning_wallet.rs | 10 +--- 6 files changed, 161 insertions(+), 29 deletions(-) diff --git a/examples/cli/src/main.rs b/examples/cli/src/main.rs index 9e891ad..cd86fdf 100644 --- a/examples/cli/src/main.rs +++ b/examples/cli/src/main.rs @@ -7,9 +7,11 @@ use rustyline::error::ReadlineError; use orange_sdk::bitcoin_payment_instructions::amount::Amount; use orange_sdk::{ CashuConfig, ChainSource, CurrencyUnit, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo, - Seed, SparkWalletConfig, StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network, + Seed, SparkWalletConfig, StorageConfig, Tunables, VssAuth, VssConfig, Wallet, WalletConfig, + bitcoin::Network, }; use rand::RngCore; +use std::collections::HashMap; use std::fs; use std::io::Write; use std::path::PathBuf; @@ -32,10 +34,45 @@ struct Cli { /// npub.cash URL for lightning address support (requires --cashu) #[arg(long, requires = "cashu")] npubcash_url: Option, + /// VSS server URL (e.g. http://127.0.0.1:8080/vss). When set, VSS replaces + /// local SQLite for all wallet persistence. + #[arg(long)] + vss_url: Option, + /// LNURL-auth server URL for VSS authentication. When omitted, fixed + /// headers (possibly empty) are used instead. + #[arg(long, requires = "vss_url")] + vss_lnurl_auth_url: Option, + /// Fixed HTTP header to attach to every VSS request, in `Key:Value` form. + /// Repeat for multiple headers. Ignored when --vss-lnurl-auth-url is set. + #[arg(long = "vss-header", value_parser = parse_kv_header, requires = "vss_url")] + vss_headers: Vec<(String, String)>, #[command(subcommand)] command: Option, } +fn parse_kv_header(s: &str) -> Result<(String, String), String> { + let (k, v) = s.split_once(':').ok_or_else(|| format!("expected `Key:Value`, got `{s}`"))?; + Ok((k.trim().to_string(), v.trim().to_string())) +} + +fn build_storage_config(cli: &Cli, storage_path: &str) -> StorageConfig { + let Some(vss_url) = cli.vss_url.clone() else { + return StorageConfig::LocalSQLite(storage_path.to_string()); + }; + let store_id = "orange-cli".to_string(); + let headers = match cli.vss_lnurl_auth_url.clone() { + Some(url) => VssAuth::LNURLAuthServer(url), + None => VssAuth::FixedHeaders(cli.vss_headers.iter().cloned().collect::>()), + }; + println!( + "{} VSS storage: {} (store_id={})", + "💾".bright_green(), + vss_url.bright_cyan(), + store_id.bright_cyan() + ); + StorageConfig::Vss(VssConfig { vss_url, store_id, headers }) +} + #[derive(Subcommand)] enum Commands { /// Get wallet balance @@ -89,6 +126,8 @@ fn get_config(network: Network, cli: &Cli) -> Result { // Generate or load seed let seed = generate_or_load_seed(&storage_path)?; + let storage_config = build_storage_config(cli, &storage_path); + let extra_config = if cli.cashu { let mint_url = cli .mint_url @@ -113,7 +152,7 @@ fn get_config(network: Network, cli: &Cli) -> Result { .context("Failed to parse LSP public key")?; Ok(WalletConfig { - storage_config: StorageConfig::LocalSQLite(storage_path.to_string()), + storage_config, logger_type: LoggerType::File { path: PathBuf::from(format!("{storage_path}/wallet.log")), }, @@ -140,7 +179,7 @@ fn get_config(network: Network, cli: &Cli) -> Result { let lsp_token = Some("DeveloperTestingOnly".to_string()); Ok(WalletConfig { - storage_config: StorageConfig::LocalSQLite(storage_path.to_string()), + storage_config, logger_type: LoggerType::File { path: PathBuf::from(format!("{storage_path}/wallet.log")), }, diff --git a/justfile b/justfile index 61317ac..2ea4e52 100644 --- a/justfile +++ b/justfile @@ -22,6 +22,10 @@ cli-cashu *args: cli-logs: tail -n 50 -f examples/cli/wallet_data/bitcoin/wallet.log +# Run the CLI against a local VSS server on http://127.0.0.1:8080/vss. +cli-vss: + cd examples/cli && cargo run -- --vss-url http://127.0.0.1:8080/vss + build-android: ./scripts/uniffi_bindgen_generate_kotlin_android.sh cd bindings/kotlin/orange-sdk-android/ && ./gradlew build diff --git a/orange-sdk/src/ffi/orange/config.rs b/orange-sdk/src/ffi/orange/config.rs index f7b6a47..c6eaff0 100644 --- a/orange-sdk/src/ffi/orange/config.rs +++ b/orange-sdk/src/ffi/orange/config.rs @@ -53,12 +53,27 @@ impl TryInto for Seed { } } -/// Represents the authentication method for a Versioned Storage Service (VSS). +/// Authentication method used on every request to the [VSS] server. +/// +/// **Caution**: VSS support is in **alpha** and is considered experimental. +/// Using VSS (or any remote persistence) may cause LDK to panic if persistence +/// failures are unrecoverable, i.e., if they remain unresolved after internal +/// retries are exhausted. +/// +/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md #[derive(Debug, Clone, uniffi::Enum)] pub enum VssAuth { - /// Authentication using an LNURL-auth server. + /// [LNURL-auth] based authentication scheme. + /// + /// The LNURL challenge will be retrieved by making a request to the given + /// URL. The returned JWT token in response to the signed LNURL request + /// will be used for authentication/authorization of all the requests made + /// to VSS. + /// + /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md LNURLAuthServer(String), - /// Authentication using a fixed set of HTTP headers. + /// A fixed set of HTTP headers included as-is on every request made to + /// VSS. FixedHeaders(HashMap), } @@ -80,14 +95,23 @@ impl From for VssAuth { } } -/// Configuration for a Versioned Storage Service (VSS). +/// Configuration for a [Versioned Storage Service (VSS)] backend. +/// +/// **Caution**: VSS support is in **alpha** and is considered experimental. +/// Using VSS (or any remote persistence) may cause LDK to panic if persistence +/// failures are unrecoverable, i.e., if they remain unresolved after internal +/// retries are exhausted. +/// +/// [Versioned Storage Service (VSS)]: https://github.com/lightningdevkit/vss-server/blob/main/README.md #[derive(Debug, Clone, uniffi::Object)] pub struct VssConfig { - /// The URL of the VSS. + /// Base URL of the VSS server (e.g. `https://vss.example.com/vss`). vss_url: String, - /// The store ID for the VSS. + /// Segments storage from other storage accessed under the same seed (as + /// storage keyed by different seeds is already segmented to prevent + /// wallets from reading data for unrelated wallets). Can be any value. store_id: String, - /// Authentication method for the VSS. + /// Authentication method attached to every VSS request. headers: VssAuth, } @@ -124,14 +148,17 @@ impl From for VssConfig { pub enum StorageConfig { /// Local SQLite database configuration. LocalSQLite(String), - // todo VSS(VssConfig), + /// Versioned Storage Service configuration. + Vss(Arc), } impl From for OrangeStorageConfig { fn from(config: StorageConfig) -> Self { match config { StorageConfig::LocalSQLite(path) => OrangeStorageConfig::LocalSQLite(path), - // todo VSS(vss_config) => OrangeStorageConfig::VSS(vss_config.into()), + StorageConfig::Vss(vss_config) => { + OrangeStorageConfig::Vss(vss_config.deref().clone().into()) + }, } } } diff --git a/orange-sdk/src/ffi/orange/error.rs b/orange-sdk/src/ffi/orange/error.rs index 056e96a..63e2863 100644 --- a/orange-sdk/src/ffi/orange/error.rs +++ b/orange-sdk/src/ffi/orange/error.rs @@ -37,6 +37,8 @@ pub enum InitFailure { LdkNodeStartFailure(String), /// Failure in the trusted wallet implementation. TrustedFailure(String), + /// Failure to build the VSS-backed store. + VssStoreBuildFailure(String), } impl Display for InitFailure { @@ -47,6 +49,7 @@ impl Display for InitFailure { InitFailure::LdkNodeBuildFailure(e) => write!(f, "Failed to build the LDK node: {e}"), InitFailure::LdkNodeStartFailure(e) => write!(f, "Failed to start the LDK node: {e}"), InitFailure::TrustedFailure(e) => write!(f, "Failed to create the trusted wallet: {e}"), + InitFailure::VssStoreBuildFailure(e) => write!(f, "Failed to build the VSS store: {e}"), } } } @@ -68,6 +71,9 @@ impl From for InitFailure { InitFailure::LdkNodeStartFailure(e.to_string()) }, OrangeInitFailure::TrustedFailure(e) => InitFailure::TrustedFailure(e.to_string()), + OrangeInitFailure::VssStoreBuildFailure(e) => { + InitFailure::VssStoreBuildFailure(e.to_string()) + }, } } } diff --git a/orange-sdk/src/lib.rs b/orange-sdk/src/lib.rs index aa58b74..607f932 100644 --- a/orange-sdk/src/lib.rs +++ b/orange-sdk/src/lib.rs @@ -21,6 +21,7 @@ use ldk_node::bitcoin::Network; use ldk_node::bitcoin::hashes::Hash; use ldk_node::bitcoin::io; use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::entropy::NodeEntropy; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::util::logger::Logger as _; @@ -63,6 +64,7 @@ pub use cdk::nuts::nut00::CurrencyUnit; pub use event::{Event, EventQueue}; pub use ldk_node::bip39::Mnemonic; pub use ldk_node::bitcoin; +use ldk_node::io::vss_store::VssStore; pub use ldk_node::payment::ConfirmationStatus; pub use store::{PaymentId, PaymentType, Transaction, TxStatus}; pub use trusted_wallet::ExtraConfig; @@ -154,24 +156,58 @@ pub enum Seed { Seed64([u8; 64]), } -/// Represents the authentication method for a Versioned Storage Service (VSS). +impl Seed { + pub(crate) fn to_node_entropy(&self) -> NodeEntropy { + match self { + Seed::Seed64(s) => NodeEntropy::from_seed_bytes(*s), + Seed::Mnemonic { mnemonic, passphrase } => { + NodeEntropy::from_bip39_mnemonic(mnemonic.clone(), passphrase.clone()) + }, + } + } +} + +/// Authentication method used on every request to the [VSS] server. +/// +/// **Caution**: VSS support is in **alpha** and is considered experimental. +/// Using VSS (or any remote persistence) may cause LDK to panic if persistence +/// failures are unrecoverable, i.e., if they remain unresolved after internal +/// retries are exhausted. +/// +/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md #[derive(Debug, Clone)] pub enum VssAuth { - /// Authentication using an LNURL-auth server. + /// [LNURL-auth] based authentication scheme. + /// + /// The LNURL challenge will be retrieved by making a request to the given + /// URL. The returned JWT token in response to the signed LNURL request + /// will be used for authentication/authorization of all the requests made + /// to VSS. + /// + /// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md LNURLAuthServer(String), - /// Authentication using a fixed set of HTTP headers. + /// A fixed set of HTTP headers included as-is on every request made to + /// VSS. FixedHeaders(HashMap), } -/// Configuration for a Versioned Storage Service (VSS). +/// Configuration for a [Versioned Storage Service (VSS)] backend. +/// +/// **Caution**: VSS support is in **alpha** and is considered experimental. +/// Using VSS (or any remote persistence) may cause LDK to panic if persistence +/// failures are unrecoverable, i.e., if they remain unresolved after internal +/// retries are exhausted. +/// +/// [Versioned Storage Service (VSS)]: https://github.com/lightningdevkit/vss-server/blob/main/README.md #[derive(Debug, Clone)] -#[allow(dead_code)] pub struct VssConfig { - /// The URL of the VSS. + /// Base URL of the VSS server (e.g. `https://vss.example.com/vss`). pub vss_url: String, - /// The store ID for the VSS. + /// Segments storage from other storage accessed under the same seed (as + /// storage keyed by different seeds is already segmented to prevent + /// wallets from reading data for unrelated wallets). Can be any value. pub store_id: String, - /// Authentication method for the VSS. + /// Authentication method attached to every VSS request. pub headers: VssAuth, } @@ -180,7 +216,10 @@ pub struct VssConfig { pub enum StorageConfig { /// Local SQLite database configuration. LocalSQLite(String), - // todo VSS(VssConfig), + /// Versioned Storage Service configuration. The same store backs LDK + /// channel state and orange-sdk metadata, so a seed-based recovery against + /// the configured VSS endpoint restores both. + Vss(VssConfig), } /// Configuration for the blockchain data source. @@ -411,6 +450,8 @@ pub enum InitFailure { LdkNodeStartFailure(NodeError), /// Failure in the trusted wallet implementation. TrustedFailure(TrustedError), + /// Failure to build the VSS-backed store. + VssStoreBuildFailure(ldk_node::io::vss_store::VssStoreBuildError), } impl From for InitFailure { @@ -437,6 +478,12 @@ impl From for InitFailure { } } +impl From for InitFailure { + fn from(e: ldk_node::io::vss_store::VssStoreBuildError) -> InitFailure { + InitFailure::VssStoreBuildFailure(e) + } +} + /// Represents possible errors during wallet operations. #[derive(Debug)] pub enum WalletError { @@ -526,6 +573,21 @@ impl Wallet { StorageConfig::LocalSQLite(path) => { Arc::new(SqliteStore::new(path.into(), Some("orange.sqlite".to_owned()), None)?) }, + StorageConfig::Vss(vss_config) => { + let builder = VssStore::builder( + config.seed.to_node_entropy(), + vss_config.vss_url.clone(), + vss_config.store_id.clone(), + config.network, + ); + let vss_store = match &vss_config.headers { + VssAuth::FixedHeaders(h) => builder.build_with_fixed_headers(h.clone())?, + VssAuth::LNURLAuthServer(url) => { + builder.build_with_lnurl(url.clone(), HashMap::new())? + }, + }; + Arc::new(vss_store) + }, }; let event_queue = Arc::new(EventQueue::new(Arc::clone(&store), Arc::clone(&logger))); diff --git a/orange-sdk/src/lightning_wallet.rs b/orange-sdk/src/lightning_wallet.rs index 3d7a10b..b910856 100644 --- a/orange-sdk/src/lightning_wallet.rs +++ b/orange-sdk/src/lightning_wallet.rs @@ -5,7 +5,7 @@ use crate::event::{EventQueue, LdkEventHandler}; use crate::logging::Logger; use crate::runtime::Runtime; use crate::store::{TxMetadataStore, TxStatus}; -use crate::{ChainSource, InitFailure, PaymentType, Seed, WalletConfig, store}; +use crate::{ChainSource, InitFailure, PaymentType, WalletConfig, store}; use bitcoin_payment_instructions::PaymentMethod; use bitcoin_payment_instructions::amount::Amount; @@ -15,7 +15,6 @@ use ldk_node::bitcoin::base64::prelude::BASE64_STANDARD; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::{Address, Network}; use ldk_node::config::{AsyncPaymentsRole, BackgroundSyncConfig, SyncTimeoutsConfig}; -use ldk_node::entropy::NodeEntropy; use ldk_node::lightning::ln::channelmanager::PaymentId; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::util::logger::Logger as _; @@ -74,12 +73,7 @@ impl LightningWallet { }; let mut builder = ldk_node::Builder::from_config(ldk_node_config); builder.set_network(config.network); - let node_entropy = match config.seed { - Seed::Seed64(seed) => NodeEntropy::from_seed_bytes(seed), - Seed::Mnemonic { mnemonic, passphrase } => { - NodeEntropy::from_bip39_mnemonic(mnemonic, passphrase) - }, - }; + let node_entropy = config.seed.to_node_entropy(); match config.rgs_url { Some(url) => {