Skip to content

Commit 12213a8

Browse files
authored
Merge pull request #57 from lightningdevkit/vss-int
Add VSS storage backend
2 parents 4cf5efb + 995de4d commit 12213a8

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)