diff --git a/src/builder.rs b/src/builder.rs index 13a7567b7..b5f64c617 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -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}; @@ -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::{ @@ -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, }; @@ -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; @@ -589,41 +588,14 @@ impl NodeBuilder { &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result { - 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 @@ -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, ) -> Result { - 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 @@ -668,24 +641,11 @@ impl NodeBuilder { header_provider: Arc, ) -> Result { 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)) } @@ -1797,8 +1757,6 @@ fn setup_logger( fn derive_xprv( config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { - 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 diff --git a/src/io/mod.rs b/src/io/mod.rs index 38fba5114..7afd5bd40 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -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 = ""; diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 2fd1ab2ca..05709caa7 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -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; @@ -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}; @@ -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, @@ -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< @@ -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, // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list @@ -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( @@ -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, + ) -> Result { + 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, + ) -> Result { + 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, + ) -> Result { + 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 {