Skip to content

Commit 995de4d

Browse files
benthecarmanclaude
andcommitted
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) <noreply@anthropic.com>
1 parent 4cf5efb commit 995de4d

6 files changed

Lines changed: 161 additions & 29 deletions

File tree

examples/cli/src/main.rs

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,11 @@ use rustyline::error::ReadlineError;
77
use orange_sdk::bitcoin_payment_instructions::amount::Amount;
88
use orange_sdk::{
99
CashuConfig, ChainSource, CurrencyUnit, Event, ExtraConfig, LoggerType, Mnemonic, PaymentInfo,
10-
Seed, SparkWalletConfig, StorageConfig, Tunables, Wallet, WalletConfig, bitcoin::Network,
10+
Seed, SparkWalletConfig, StorageConfig, Tunables, VssAuth, VssConfig, Wallet, WalletConfig,
11+
bitcoin::Network,
1112
};
1213
use rand::RngCore;
14+
use std::collections::HashMap;
1315
use std::fs;
1416
use std::io::Write;
1517
use std::path::PathBuf;
@@ -32,10 +34,45 @@ struct Cli {
3234
/// npub.cash URL for lightning address support (requires --cashu)
3335
#[arg(long, requires = "cashu")]
3436
npubcash_url: Option<String>,
37+
/// VSS server URL (e.g. http://127.0.0.1:8080/vss). When set, VSS replaces
38+
/// local SQLite for all wallet persistence.
39+
#[arg(long)]
40+
vss_url: Option<String>,
41+
/// LNURL-auth server URL for VSS authentication. When omitted, fixed
42+
/// headers (possibly empty) are used instead.
43+
#[arg(long, requires = "vss_url")]
44+
vss_lnurl_auth_url: Option<String>,
45+
/// Fixed HTTP header to attach to every VSS request, in `Key:Value` form.
46+
/// Repeat for multiple headers. Ignored when --vss-lnurl-auth-url is set.
47+
#[arg(long = "vss-header", value_parser = parse_kv_header, requires = "vss_url")]
48+
vss_headers: Vec<(String, String)>,
3549
#[command(subcommand)]
3650
command: Option<Commands>,
3751
}
3852

53+
fn parse_kv_header(s: &str) -> Result<(String, String), String> {
54+
let (k, v) = s.split_once(':').ok_or_else(|| format!("expected `Key:Value`, got `{s}`"))?;
55+
Ok((k.trim().to_string(), v.trim().to_string()))
56+
}
57+
58+
fn build_storage_config(cli: &Cli, storage_path: &str) -> StorageConfig {
59+
let Some(vss_url) = cli.vss_url.clone() else {
60+
return StorageConfig::LocalSQLite(storage_path.to_string());
61+
};
62+
let store_id = "orange-cli".to_string();
63+
let headers = match cli.vss_lnurl_auth_url.clone() {
64+
Some(url) => VssAuth::LNURLAuthServer(url),
65+
None => VssAuth::FixedHeaders(cli.vss_headers.iter().cloned().collect::<HashMap<_, _>>()),
66+
};
67+
println!(
68+
"{} VSS storage: {} (store_id={})",
69+
"💾".bright_green(),
70+
vss_url.bright_cyan(),
71+
store_id.bright_cyan()
72+
);
73+
StorageConfig::Vss(VssConfig { vss_url, store_id, headers })
74+
}
75+
3976
#[derive(Subcommand)]
4077
enum Commands {
4178
/// Get wallet balance
@@ -89,6 +126,8 @@ fn get_config(network: Network, cli: &Cli) -> Result<WalletConfig> {
89126
// Generate or load seed
90127
let seed = generate_or_load_seed(&storage_path)?;
91128

129+
let storage_config = build_storage_config(cli, &storage_path);
130+
92131
let extra_config = if cli.cashu {
93132
let mint_url = cli
94133
.mint_url
@@ -113,7 +152,7 @@ fn get_config(network: Network, cli: &Cli) -> Result<WalletConfig> {
113152
.context("Failed to parse LSP public key")?;
114153

115154
Ok(WalletConfig {
116-
storage_config: StorageConfig::LocalSQLite(storage_path.to_string()),
155+
storage_config,
117156
logger_type: LoggerType::File {
118157
path: PathBuf::from(format!("{storage_path}/wallet.log")),
119158
},
@@ -140,7 +179,7 @@ fn get_config(network: Network, cli: &Cli) -> Result<WalletConfig> {
140179
let lsp_token = Some("DeveloperTestingOnly".to_string());
141180

142181
Ok(WalletConfig {
143-
storage_config: StorageConfig::LocalSQLite(storage_path.to_string()),
182+
storage_config,
144183
logger_type: LoggerType::File {
145184
path: PathBuf::from(format!("{storage_path}/wallet.log")),
146185
},

justfile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ cli-cashu *args:
2222
cli-logs:
2323
tail -n 50 -f examples/cli/wallet_data/bitcoin/wallet.log
2424

25+
# Run the CLI against a local VSS server on http://127.0.0.1:8080/vss.
26+
cli-vss:
27+
cd examples/cli && cargo run -- --vss-url http://127.0.0.1:8080/vss
28+
2529
build-android:
2630
./scripts/uniffi_bindgen_generate_kotlin_android.sh
2731
cd bindings/kotlin/orange-sdk-android/ && ./gradlew build

orange-sdk/src/ffi/orange/config.rs

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -53,12 +53,27 @@ impl TryInto<OrangeSeed> for Seed {
5353
}
5454
}
5555

56-
/// Represents the authentication method for a Versioned Storage Service (VSS).
56+
/// Authentication method used on every request to the [VSS] server.
57+
///
58+
/// **Caution**: VSS support is in **alpha** and is considered experimental.
59+
/// Using VSS (or any remote persistence) may cause LDK to panic if persistence
60+
/// failures are unrecoverable, i.e., if they remain unresolved after internal
61+
/// retries are exhausted.
62+
///
63+
/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
5764
#[derive(Debug, Clone, uniffi::Enum)]
5865
pub enum VssAuth {
59-
/// Authentication using an LNURL-auth server.
66+
/// [LNURL-auth] based authentication scheme.
67+
///
68+
/// The LNURL challenge will be retrieved by making a request to the given
69+
/// URL. The returned JWT token in response to the signed LNURL request
70+
/// will be used for authentication/authorization of all the requests made
71+
/// to VSS.
72+
///
73+
/// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md
6074
LNURLAuthServer(String),
61-
/// Authentication using a fixed set of HTTP headers.
75+
/// A fixed set of HTTP headers included as-is on every request made to
76+
/// VSS.
6277
FixedHeaders(HashMap<String, String>),
6378
}
6479

@@ -80,14 +95,23 @@ impl From<OrangeVssAuth> for VssAuth {
8095
}
8196
}
8297

83-
/// Configuration for a Versioned Storage Service (VSS).
98+
/// Configuration for a [Versioned Storage Service (VSS)] backend.
99+
///
100+
/// **Caution**: VSS support is in **alpha** and is considered experimental.
101+
/// Using VSS (or any remote persistence) may cause LDK to panic if persistence
102+
/// failures are unrecoverable, i.e., if they remain unresolved after internal
103+
/// retries are exhausted.
104+
///
105+
/// [Versioned Storage Service (VSS)]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
84106
#[derive(Debug, Clone, uniffi::Object)]
85107
pub struct VssConfig {
86-
/// The URL of the VSS.
108+
/// Base URL of the VSS server (e.g. `https://vss.example.com/vss`).
87109
vss_url: String,
88-
/// The store ID for the VSS.
110+
/// Segments storage from other storage accessed under the same seed (as
111+
/// storage keyed by different seeds is already segmented to prevent
112+
/// wallets from reading data for unrelated wallets). Can be any value.
89113
store_id: String,
90-
/// Authentication method for the VSS.
114+
/// Authentication method attached to every VSS request.
91115
headers: VssAuth,
92116
}
93117

@@ -124,14 +148,17 @@ impl From<OrangeVssConfig> for VssConfig {
124148
pub enum StorageConfig {
125149
/// Local SQLite database configuration.
126150
LocalSQLite(String),
127-
// todo VSS(VssConfig),
151+
/// Versioned Storage Service configuration.
152+
Vss(Arc<VssConfig>),
128153
}
129154

130155
impl From<StorageConfig> for OrangeStorageConfig {
131156
fn from(config: StorageConfig) -> Self {
132157
match config {
133158
StorageConfig::LocalSQLite(path) => OrangeStorageConfig::LocalSQLite(path),
134-
// todo VSS(vss_config) => OrangeStorageConfig::VSS(vss_config.into()),
159+
StorageConfig::Vss(vss_config) => {
160+
OrangeStorageConfig::Vss(vss_config.deref().clone().into())
161+
},
135162
}
136163
}
137164
}

orange-sdk/src/ffi/orange/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub enum InitFailure {
3737
LdkNodeStartFailure(String),
3838
/// Failure in the trusted wallet implementation.
3939
TrustedFailure(String),
40+
/// Failure to build the VSS-backed store.
41+
VssStoreBuildFailure(String),
4042
}
4143

4244
impl Display for InitFailure {
@@ -47,6 +49,7 @@ impl Display for InitFailure {
4749
InitFailure::LdkNodeBuildFailure(e) => write!(f, "Failed to build the LDK node: {e}"),
4850
InitFailure::LdkNodeStartFailure(e) => write!(f, "Failed to start the LDK node: {e}"),
4951
InitFailure::TrustedFailure(e) => write!(f, "Failed to create the trusted wallet: {e}"),
52+
InitFailure::VssStoreBuildFailure(e) => write!(f, "Failed to build the VSS store: {e}"),
5053
}
5154
}
5255
}
@@ -68,6 +71,9 @@ impl From<OrangeInitFailure> for InitFailure {
6871
InitFailure::LdkNodeStartFailure(e.to_string())
6972
},
7073
OrangeInitFailure::TrustedFailure(e) => InitFailure::TrustedFailure(e.to_string()),
74+
OrangeInitFailure::VssStoreBuildFailure(e) => {
75+
InitFailure::VssStoreBuildFailure(e.to_string())
76+
},
7177
}
7278
}
7379
}

orange-sdk/src/lib.rs

Lines changed: 71 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ use ldk_node::bitcoin::Network;
2121
use ldk_node::bitcoin::hashes::Hash;
2222
use ldk_node::bitcoin::io;
2323
use ldk_node::bitcoin::secp256k1::PublicKey;
24+
use ldk_node::entropy::NodeEntropy;
2425
use ldk_node::io::sqlite_store::SqliteStore;
2526
use ldk_node::lightning::ln::msgs::SocketAddress;
2627
use ldk_node::lightning::util::logger::Logger as _;
@@ -63,6 +64,7 @@ pub use cdk::nuts::nut00::CurrencyUnit;
6364
pub use event::{Event, EventQueue};
6465
pub use ldk_node::bip39::Mnemonic;
6566
pub use ldk_node::bitcoin;
67+
use ldk_node::io::vss_store::VssStore;
6668
pub use ldk_node::payment::ConfirmationStatus;
6769
pub use store::{PaymentId, PaymentType, Transaction, TxStatus};
6870
pub use trusted_wallet::ExtraConfig;
@@ -154,24 +156,58 @@ pub enum Seed {
154156
Seed64([u8; 64]),
155157
}
156158

157-
/// Represents the authentication method for a Versioned Storage Service (VSS).
159+
impl Seed {
160+
pub(crate) fn to_node_entropy(&self) -> NodeEntropy {
161+
match self {
162+
Seed::Seed64(s) => NodeEntropy::from_seed_bytes(*s),
163+
Seed::Mnemonic { mnemonic, passphrase } => {
164+
NodeEntropy::from_bip39_mnemonic(mnemonic.clone(), passphrase.clone())
165+
},
166+
}
167+
}
168+
}
169+
170+
/// Authentication method used on every request to the [VSS] server.
171+
///
172+
/// **Caution**: VSS support is in **alpha** and is considered experimental.
173+
/// Using VSS (or any remote persistence) may cause LDK to panic if persistence
174+
/// failures are unrecoverable, i.e., if they remain unresolved after internal
175+
/// retries are exhausted.
176+
///
177+
/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
158178
#[derive(Debug, Clone)]
159179
pub enum VssAuth {
160-
/// Authentication using an LNURL-auth server.
180+
/// [LNURL-auth] based authentication scheme.
181+
///
182+
/// The LNURL challenge will be retrieved by making a request to the given
183+
/// URL. The returned JWT token in response to the signed LNURL request
184+
/// will be used for authentication/authorization of all the requests made
185+
/// to VSS.
186+
///
187+
/// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md
161188
LNURLAuthServer(String),
162-
/// Authentication using a fixed set of HTTP headers.
189+
/// A fixed set of HTTP headers included as-is on every request made to
190+
/// VSS.
163191
FixedHeaders(HashMap<String, String>),
164192
}
165193

166-
/// Configuration for a Versioned Storage Service (VSS).
194+
/// Configuration for a [Versioned Storage Service (VSS)] backend.
195+
///
196+
/// **Caution**: VSS support is in **alpha** and is considered experimental.
197+
/// Using VSS (or any remote persistence) may cause LDK to panic if persistence
198+
/// failures are unrecoverable, i.e., if they remain unresolved after internal
199+
/// retries are exhausted.
200+
///
201+
/// [Versioned Storage Service (VSS)]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
167202
#[derive(Debug, Clone)]
168-
#[allow(dead_code)]
169203
pub struct VssConfig {
170-
/// The URL of the VSS.
204+
/// Base URL of the VSS server (e.g. `https://vss.example.com/vss`).
171205
pub vss_url: String,
172-
/// The store ID for the VSS.
206+
/// Segments storage from other storage accessed under the same seed (as
207+
/// storage keyed by different seeds is already segmented to prevent
208+
/// wallets from reading data for unrelated wallets). Can be any value.
173209
pub store_id: String,
174-
/// Authentication method for the VSS.
210+
/// Authentication method attached to every VSS request.
175211
pub headers: VssAuth,
176212
}
177213

@@ -180,7 +216,10 @@ pub struct VssConfig {
180216
pub enum StorageConfig {
181217
/// Local SQLite database configuration.
182218
LocalSQLite(String),
183-
// todo VSS(VssConfig),
219+
/// Versioned Storage Service configuration. The same store backs LDK
220+
/// channel state and orange-sdk metadata, so a seed-based recovery against
221+
/// the configured VSS endpoint restores both.
222+
Vss(VssConfig),
184223
}
185224

186225
/// Configuration for the blockchain data source.
@@ -411,6 +450,8 @@ pub enum InitFailure {
411450
LdkNodeStartFailure(NodeError),
412451
/// Failure in the trusted wallet implementation.
413452
TrustedFailure(TrustedError),
453+
/// Failure to build the VSS-backed store.
454+
VssStoreBuildFailure(ldk_node::io::vss_store::VssStoreBuildError),
414455
}
415456

416457
impl From<io::Error> for InitFailure {
@@ -437,6 +478,12 @@ impl From<TrustedError> for InitFailure {
437478
}
438479
}
439480

481+
impl From<ldk_node::io::vss_store::VssStoreBuildError> for InitFailure {
482+
fn from(e: ldk_node::io::vss_store::VssStoreBuildError) -> InitFailure {
483+
InitFailure::VssStoreBuildFailure(e)
484+
}
485+
}
486+
440487
/// Represents possible errors during wallet operations.
441488
#[derive(Debug)]
442489
pub enum WalletError {
@@ -526,6 +573,21 @@ impl Wallet {
526573
StorageConfig::LocalSQLite(path) => {
527574
Arc::new(SqliteStore::new(path.into(), Some("orange.sqlite".to_owned()), None)?)
528575
},
576+
StorageConfig::Vss(vss_config) => {
577+
let builder = VssStore::builder(
578+
config.seed.to_node_entropy(),
579+
vss_config.vss_url.clone(),
580+
vss_config.store_id.clone(),
581+
config.network,
582+
);
583+
let vss_store = match &vss_config.headers {
584+
VssAuth::FixedHeaders(h) => builder.build_with_fixed_headers(h.clone())?,
585+
VssAuth::LNURLAuthServer(url) => {
586+
builder.build_with_lnurl(url.clone(), HashMap::new())?
587+
},
588+
};
589+
Arc::new(vss_store)
590+
},
529591
};
530592

531593
let event_queue = Arc::new(EventQueue::new(Arc::clone(&store), Arc::clone(&logger)));

orange-sdk/src/lightning_wallet.rs

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use crate::event::{EventQueue, LdkEventHandler};
55
use crate::logging::Logger;
66
use crate::runtime::Runtime;
77
use crate::store::{TxMetadataStore, TxStatus};
8-
use crate::{ChainSource, InitFailure, PaymentType, Seed, WalletConfig, store};
8+
use crate::{ChainSource, InitFailure, PaymentType, WalletConfig, store};
99

1010
use bitcoin_payment_instructions::PaymentMethod;
1111
use bitcoin_payment_instructions::amount::Amount;
@@ -15,7 +15,6 @@ use ldk_node::bitcoin::base64::prelude::BASE64_STANDARD;
1515
use ldk_node::bitcoin::secp256k1::PublicKey;
1616
use ldk_node::bitcoin::{Address, Network};
1717
use ldk_node::config::{AsyncPaymentsRole, BackgroundSyncConfig, SyncTimeoutsConfig};
18-
use ldk_node::entropy::NodeEntropy;
1918
use ldk_node::lightning::ln::channelmanager::PaymentId;
2019
use ldk_node::lightning::ln::msgs::SocketAddress;
2120
use ldk_node::lightning::util::logger::Logger as _;
@@ -74,12 +73,7 @@ impl LightningWallet {
7473
};
7574
let mut builder = ldk_node::Builder::from_config(ldk_node_config);
7675
builder.set_network(config.network);
77-
let node_entropy = match config.seed {
78-
Seed::Seed64(seed) => NodeEntropy::from_seed_bytes(seed),
79-
Seed::Mnemonic { mnemonic, passphrase } => {
80-
NodeEntropy::from_bip39_mnemonic(mnemonic, passphrase)
81-
},
82-
};
76+
let node_entropy = config.seed.to_node_entropy();
8377

8478
match config.rgs_url {
8579
Some(url) => {

0 commit comments

Comments
 (0)