Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 22 additions & 64 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use std::{fmt, fs};
use bdk_wallet::template::Bip84;
use bdk_wallet::{KeychainKind, Wallet as BdkWallet};
use bitcoin::bip32::{ChildNumber, Xpriv};
use bitcoin::key::Secp256k1;
use bitcoin::secp256k1::PublicKey;
use bitcoin::{BlockHash, Network};
use lightning::chain::{chainmonitor, BestBlock, Watch};
Expand All @@ -38,7 +39,7 @@ use lightning::util::persist::{
use lightning::util::ser::ReadableArgs;
use lightning::util::sweep::OutputSweeper;
use lightning_persister::fs_store::FilesystemStore;
use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider};
use vss_client::headers::VssHeaderProvider;

use crate::chain::ChainSource;
use crate::config::{
Expand All @@ -55,7 +56,7 @@ use crate::io::sqlite_store::SqliteStore;
use crate::io::utils::{
read_external_pathfinding_scores_from_cache, read_node_metrics, write_node_metrics,
};
use crate::io::vss_store::VssStore;
use crate::io::vss_store::VssStoreBuilder;
use crate::io::{
self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
};
Expand All @@ -76,8 +77,6 @@ use crate::wallet::persist::KVStoreWalletPersister;
use crate::wallet::Wallet;
use crate::{Node, NodeMetrics};

const VSS_HARDENED_CHILD_INDEX: u32 = 877;
const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138;
const LSPS_HARDENED_CHILD_INDEX: u32 = 577;
const PERSISTER_MAX_PENDING_UPDATES: u64 = 100;

Expand Down Expand Up @@ -589,41 +588,14 @@ impl NodeBuilder {
&self, node_entropy: NodeEntropy, vss_url: String, store_id: String,
lnurl_auth_server_url: String, fixed_headers: HashMap<String, String>,
) -> Result<Node, BuildError> {
use bitcoin::key::Secp256k1;

let logger = setup_logger(&self.log_writer_config, &self.config)?;
let builder = VssStoreBuilder::new(node_entropy, vss_url, store_id, self.config.network);
let vss_store = builder.build(lnurl_auth_server_url, fixed_headers).map_err(|e| {
log_error!(logger, "Failed to setup VSS store: {}", e);
BuildError::KVStoreSetupFailed
})?;

let seed_bytes = node_entropy.to_seed_bytes();
let config = Arc::new(self.config.clone());

let vss_xprv =
derive_xprv(config, &seed_bytes, VSS_HARDENED_CHILD_INDEX, Arc::clone(&logger))?;

let lnurl_auth_xprv = vss_xprv
.derive_priv(
&Secp256k1::new(),
&[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }],
)
.map_err(|e| {
log_error!(logger, "Failed to derive VSS secret: {}", e);
BuildError::KVStoreSetupFailed
})?;

let lnurl_auth_jwt_provider =
LnurlAuthToJwtProvider::new(lnurl_auth_xprv, lnurl_auth_server_url, fixed_headers)
.map_err(|e| {
log_error!(logger, "Failed to create LnurlAuthToJwtProvider: {}", e);
BuildError::KVStoreSetupFailed
})?;

let header_provider = Arc::new(lnurl_auth_jwt_provider);

self.build_with_vss_store_and_header_provider(
node_entropy,
vss_url,
store_id,
header_provider,
)
self.build_with_store(node_entropy, Arc::new(vss_store))
}

/// Builds a [`Node`] instance with a [VSS] backend and according to the options
Expand All @@ -638,18 +610,19 @@ impl NodeBuilder {
/// unrecoverable, i.e., if they remain unresolved after internal retries are exhausted.
///
/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
/// [`FixedHeaders`]: vss_client::headers::FixedHeaders
pub fn build_with_vss_store_and_fixed_headers(
&self, node_entropy: NodeEntropy, vss_url: String, store_id: String,
fixed_headers: HashMap<String, String>,
) -> Result<Node, BuildError> {
let header_provider = Arc::new(FixedHeaders::new(fixed_headers));
let logger = setup_logger(&self.log_writer_config, &self.config)?;
let builder = VssStoreBuilder::new(node_entropy, vss_url, store_id, self.config.network);
let vss_store = builder.build_with_fixed_headers(fixed_headers).map_err(|e| {
log_error!(logger, "Failed to setup VSS store: {}", e);
BuildError::KVStoreSetupFailed
})?;

self.build_with_vss_store_and_header_provider(
node_entropy,
vss_url,
store_id,
header_provider,
)
self.build_with_store(node_entropy, Arc::new(vss_store))
}

/// Builds a [`Node`] instance with a [VSS] backend and according to the options
Expand All @@ -668,24 +641,11 @@ impl NodeBuilder {
header_provider: Arc<dyn VssHeaderProvider>,
) -> Result<Node, BuildError> {
let logger = setup_logger(&self.log_writer_config, &self.config)?;

let seed_bytes = node_entropy.to_seed_bytes();
let config = Arc::new(self.config.clone());

let vss_xprv = derive_xprv(
config.clone(),
&seed_bytes,
VSS_HARDENED_CHILD_INDEX,
Arc::clone(&logger),
)?;

let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes();

let vss_store =
VssStore::new(vss_url, store_id, vss_seed_bytes, header_provider).map_err(|e| {
log_error!(logger, "Failed to setup VSS store: {}", e);
BuildError::KVStoreSetupFailed
})?;
let builder = VssStoreBuilder::new(node_entropy, vss_url, store_id, self.config.network);
let vss_store = builder.build_with_header_provider(header_provider).map_err(|e| {
log_error!(logger, "Failed to setup VSS store: {}", e);
BuildError::KVStoreSetupFailed
})?;

self.build_with_store(node_entropy, Arc::new(vss_store))
}
Expand Down Expand Up @@ -1797,8 +1757,6 @@ fn setup_logger(
fn derive_xprv(
config: Arc<Config>, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc<Logger>,
) -> Result<Xpriv, BuildError> {
use bitcoin::key::Secp256k1;

let xprv = Xpriv::new_master(config.network, seed_bytes).map_err(|e| {
log_error!(logger, "Failed to derive master secret: {}", e);
BuildError::WalletSetupFailed
Expand Down
2 changes: 1 addition & 1 deletion src/io/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ pub mod sqlite_store;
#[cfg(test)]
pub(crate) mod test_utils;
pub(crate) mod utils;
pub(crate) mod vss_store;
pub mod vss_store;

/// The event queue will be persisted under this key.
pub(crate) const EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE: &str = "";
Expand Down
163 changes: 161 additions & 2 deletions src/io/vss_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,11 @@
// http://opensource.org/licenses/MIT>, at your option. You may not use this file except in
// accordance with one or both of these licenses.

//! Objects related to [`VssStore`] live here.

use std::boxed::Box;
use std::collections::HashMap;
use std::fmt;
use std::future::Future;
#[cfg(test)]
use std::panic::RefUnwindSafe;
Expand All @@ -15,7 +18,10 @@ use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;

use bitcoin::bip32::{ChildNumber, Xpriv};
use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine};
use bitcoin::key::Secp256k1;
use bitcoin::Network;
use lightning::impl_writeable_tlv_based_enum;
use lightning::io::{self, Error, ErrorKind};
use lightning::util::persist::{KVStore, KVStoreSync};
Expand All @@ -24,7 +30,7 @@ use prost::Message;
use rand::RngCore;
use vss_client::client::VssClient;
use vss_client::error::VssError;
use vss_client::headers::VssHeaderProvider;
use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider};
use vss_client::types::{
DeleteObjectRequest, GetObjectRequest, KeyValue, ListKeyVersionsRequest, PutObjectRequest,
Storable,
Expand All @@ -36,6 +42,7 @@ use vss_client::util::retry::{
};
use vss_client::util::storable_builder::{EntropySource, StorableBuilder};

use crate::entropy::NodeEntropy;
use crate::io::utils::check_namespace_key_validity;

type CustomRetryPolicy = FilteredRetryPolicy<
Expand All @@ -61,13 +68,17 @@ impl_writeable_tlv_based_enum!(VssSchemaVersion,
(1, V1) => {},
);

const VSS_HARDENED_CHILD_INDEX: u32 = 877;
const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138;
const VSS_SCHEMA_VERSION_KEY: &str = "vss_schema_version";

// We set this to a small number of threads that would still allow to make some progress if one
// would hit a blocking case
const INTERNAL_RUNTIME_WORKERS: usize = 2;

/// A [`KVStoreSync`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend.
/// A [`KVStore`]/[`KVStoreSync`] implementation that writes to and reads from a [VSS] backend.
///
/// [VSS]: https://github.com/lightningdevkit/vss-server/blob/main/README.md
pub struct VssStore {
inner: Arc<VssStoreInner>,
// Version counter to ensure that writes are applied in the correct order. It is assumed that read and list
Expand Down Expand Up @@ -139,6 +150,12 @@ impl VssStore {

Ok(Self { inner, next_version, internal_runtime: Some(internal_runtime) })
}
/// Returns a [`VssStoreBuilder`] allowing to build a [`VssStore`].
pub fn builder(
node_entropy: NodeEntropy, vss_url: String, store_id: String, network: Network,
) -> VssStoreBuilder {
VssStoreBuilder::new(node_entropy, vss_url, store_id, network)
}

// Same logic as for the obfuscated keys below, but just for locking, using the plaintext keys
fn build_locking_key(
Expand Down Expand Up @@ -800,6 +817,148 @@ impl EntropySource for RandEntropySource {
#[cfg(test)]
impl RefUnwindSafe for VssStore {}

/// An error that could arise during [`VssStore`] building.
#[derive(Debug, Clone, PartialEq)]
pub enum VssStoreBuildError {
/// Key derivation failed
KeyDerivationFailed,
/// Authentication provider setup failed
AuthProviderSetupFailed,
/// Store setup failed
StoreSetupFailed,
}

impl fmt::Display for VssStoreBuildError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
Self::KeyDerivationFailed => write!(f, "Key derivation failed"),
Self::AuthProviderSetupFailed => write!(f, "Authentication provider setup failed"),
Self::StoreSetupFailed => write!(f, "Store setup failed"),
}
}
}

impl std::error::Error for VssStoreBuildError {}

/// A builder for a [`VssStore`] instance.
pub struct VssStoreBuilder {
node_entropy: NodeEntropy,
vss_url: String,
store_id: String,
network: Network,
}

impl VssStoreBuilder {
/// Create a new [`VssStoreBuilder`].
pub fn new(
node_entropy: NodeEntropy, vss_url: String, store_id: String, network: Network,
) -> Self {
Self { node_entropy, vss_url, store_id, network }
}

/// Builds a [`VssStore`] with [LNURL-auth] based authentication scheme as default method for
/// authentication/authorization.
///
/// The LNURL challenge will be retrieved by making a request to the given
/// `lnurl_auth_server_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.
///
/// `fixed_headers` are included as it is in all the requests made to VSS and LNURL auth
/// 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
/// [LNURL-auth]: https://github.com/lnurl/luds/blob/luds/04.md
pub fn build(
&self, lnurl_auth_server_url: String, fixed_headers: HashMap<String, String>,
Copy link
Contributor

@benthecarman benthecarman Dec 4, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason for these we using &self instead of consuming it with self? I see we do the same for the node builder but typically builders consume themselves when done, it would allow you to remove some clones

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, for the Builder we do that to be able to reuse it in the bindings variant ArcedNodeBuilder (as bindings don't support move semantics, only reference-counted Arcs). Here, we go the same route as we will at least attempt to expose VssStore/VssStoreBuilder in bidnings, though with the new async KVStore trait that could prove difficult, we'll see.

) -> Result<VssStore, VssStoreBuildError> {
let secp_ctx = Secp256k1::new();
let seed_bytes = self.node_entropy.to_seed_bytes();
let vss_xprv = Xpriv::new_master(self.network, &seed_bytes)
.map_err(|_| VssStoreBuildError::KeyDerivationFailed)
.and_then(|master| {
master
.derive_priv(
&secp_ctx,
&[ChildNumber::Hardened { index: VSS_HARDENED_CHILD_INDEX }],
)
.map_err(|_| VssStoreBuildError::KeyDerivationFailed)
})?;

let lnurl_auth_xprv = vss_xprv
.derive_priv(
&secp_ctx,
&[ChildNumber::Hardened { index: VSS_LNURL_AUTH_HARDENED_CHILD_INDEX }],
)
.map_err(|_| VssStoreBuildError::KeyDerivationFailed)?;

let lnurl_auth_jwt_provider =
LnurlAuthToJwtProvider::new(lnurl_auth_xprv, lnurl_auth_server_url, fixed_headers)
.map_err(|_| VssStoreBuildError::AuthProviderSetupFailed)?;

let header_provider = Arc::new(lnurl_auth_jwt_provider);

self.build_with_header_provider(header_provider)
}

/// Builds a [`VssStore`] with [`FixedHeaders`] as default method for
/// authentication/authorization.
///
/// Given `fixed_headers` are included as it is in all the requests made to VSS.
///
/// **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
pub fn build_with_fixed_headers(
&self, fixed_headers: HashMap<String, String>,
) -> Result<VssStore, VssStoreBuildError> {
let header_provider = Arc::new(FixedHeaders::new(fixed_headers));
self.build_with_header_provider(header_provider)
}

/// Builds a [`VssStore`] with [`VssHeaderProvider`].
///
/// Any headers provided by `header_provider` will be attached to every request made to VSS.
///
/// **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
pub fn build_with_header_provider(
&self, header_provider: Arc<dyn VssHeaderProvider>,
) -> Result<VssStore, VssStoreBuildError> {
let seed_bytes = self.node_entropy.to_seed_bytes();
let vss_xprv = Xpriv::new_master(self.network, &seed_bytes)
.map_err(|_| VssStoreBuildError::KeyDerivationFailed)
.and_then(|master| {
master
.derive_priv(
&Secp256k1::new(),
&[ChildNumber::Hardened { index: VSS_HARDENED_CHILD_INDEX }],
)
.map_err(|_| VssStoreBuildError::KeyDerivationFailed)
})?;

let vss_seed_bytes: [u8; 32] = vss_xprv.private_key.secret_bytes();

let vss_store = VssStore::new(
self.vss_url.clone(),
self.store_id.clone(),
vss_seed_bytes,
header_provider,
)
.map_err(|_| VssStoreBuildError::StoreSetupFailed)?;

Ok(vss_store)
}
}

#[cfg(test)]
#[cfg(vss_test)]
mod tests {
Expand Down