diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d40f72f4a..b0234a4b1 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -135,7 +135,9 @@ interface Builder { [Throws=BuildError] Node build_with_fs_store(NodeEntropy node_entropy); [Throws=BuildError] - Node build_with_vss_store(NodeEntropy node_entropy, string vss_url, string store_id, string lnurl_auth_server_url, record fixed_headers); + Node build_with_vss_store(NodeEntropy node_entropy, string vss_url, string store_id, record fixed_headers); + [Throws=BuildError] + Node build_with_vss_store_and_lnurl_auth(NodeEntropy node_entropy, string vss_url, string store_id, string lnurl_auth_server_url, record fixed_headers); [Throws=BuildError] Node build_with_vss_store_and_fixed_headers(NodeEntropy node_entropy, string vss_url, string store_id, record fixed_headers); [Throws=BuildError] diff --git a/src/builder.rs b/src/builder.rs index 7a285876f..7fba4e880 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -584,6 +584,37 @@ impl NodeBuilder { self.build_with_store(node_entropy, kv_store) } + /// Builds a [`Node`] instance with a [VSS] backend and according to the options + /// previously configured. + /// + /// Uses a simple authentication scheme proving knowledge of a secret key. + /// + /// `fixed_headers` are included as it is in all the requests made to VSS. + /// + /// `store_id` allows you to segment LDK Node storage from other storage accessed with + /// [`VssStoreBuilder`] using the same [`NodeEntropy`] (as storage with different keys is + /// obviously segmented to prevent wallets from reading data for unrelated wallets). It can be + /// any value. + /// + /// **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_vss_store( + &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, + fixed_headers: HashMap, + ) -> Result { + 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_sigs_auth(fixed_headers).map_err(|e| { + log_error!(logger, "Failed to setup VSS store: {}", e); + BuildError::KVStoreSetupFailed + })?; + + self.build_with_store(node_entropy, vss_store) + } + /// Builds a [`Node`] instance with a [VSS] backend and according to the options /// previously configured. /// @@ -593,6 +624,11 @@ impl NodeBuilder { /// The returned JWT token in response to the signed LNURL request, will be used for /// authentication/authorization of all the requests made to VSS. /// + /// `store_id` allows you to segment LDK Node storage from other storage accessed with + /// [`VssStoreBuilder`] using the same authentication (as storage with different keys is + /// obviously segmented to prevent wallets from reading data for unrelated wallets). It can be + /// any value. + /// /// `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. @@ -601,16 +637,17 @@ impl NodeBuilder { /// /// [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_with_vss_store( + pub fn build_with_vss_store_and_lnurl_auth( &self, node_entropy: NodeEntropy, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result { 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 vss_store = + builder.build_with_lnurl(lnurl_auth_server_url, fixed_headers).map_err(|e| { + log_error!(logger, "Failed to setup VSS store: {}", e); + BuildError::KVStoreSetupFailed + })?; self.build_with_store(node_entropy, vss_store) } @@ -956,6 +993,34 @@ impl ArcedNodeBuilder { self.inner.read().unwrap().build_with_fs_store(*node_entropy).map(Arc::new) } + /// Builds a [`Node`] instance with a [VSS] backend and according to the options + /// previously configured. + /// + /// Uses a simple authentication scheme proving knowledge of a secret key. + /// + /// `fixed_headers` are included as it is in all the requests made to VSS and LNURL auth server. + /// + /// `store_id` allows you to segment LDK Node storage from other storage accessed with + /// [`VssStoreBuilder`] using the same [`NodeEntropy`] (as storage with different keys is + /// obviously segmented to prevent wallets from reading data for unrelated wallets). It can be + /// any value. + /// + /// **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_vss_store( + &self, node_entropy: Arc, vss_url: String, store_id: String, + fixed_headers: HashMap, + ) -> Result, BuildError> { + self.inner + .read() + .unwrap() + .build_with_vss_store(*node_entropy, vss_url, store_id, fixed_headers) + .map(Arc::new) + } + /// Builds a [`Node`] instance with a [VSS] backend and according to the options /// previously configured. /// @@ -967,20 +1032,25 @@ impl ArcedNodeBuilder { /// /// `fixed_headers` are included as it is in all the requests made to VSS and LNURL auth server. /// + /// `store_id` allows you to segment LDK Node storage from other storage accessed with + /// [`VssStoreBuilder`] using the same authentication (as storage with different keys is + /// obviously segmented to prevent wallets from reading data for unrelated wallets). It can be + /// any value. + /// /// **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_with_vss_store( + pub fn build_with_vss_store_and_lnurl_auth( &self, node_entropy: Arc, vss_url: String, store_id: String, lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result, BuildError> { self.inner .read() .unwrap() - .build_with_vss_store( + .build_with_vss_store_and_lnurl_auth( *node_entropy, vss_url, store_id, diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index a7af8ecc2..7bee47d6a 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -29,6 +29,7 @@ use prost::Message; use rand::RngCore; use vss_client::client::VssClient; use vss_client::error::VssError; +use vss_client::headers::sigs_auth::SigsAuthProvider; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; use vss_client::types::{ DeleteObjectRequest, GetObjectRequest, KeyValue, ListKeyVersionsRequest, PutObjectRequest, @@ -69,6 +70,7 @@ impl_writeable_tlv_based_enum!(VssSchemaVersion, const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const VSS_SIGS_AUTH_HARDENED_CHILD_INDEX: u32 = 139; 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 @@ -853,6 +855,44 @@ impl VssStoreBuilder { Self { node_entropy, vss_url, store_id, network } } + /// Builds a [`VssStore`] with the simple signature-based authentication scheme. + /// + /// `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_with_sigs_auth( + &self, 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 sigs_auth_xprv = vss_xprv + .derive_priv( + &secp_ctx, + &[ChildNumber::Hardened { index: VSS_SIGS_AUTH_HARDENED_CHILD_INDEX }], + ) + .map_err(|_| VssStoreBuildError::KeyDerivationFailed)?; + + let auth_provider = SigsAuthProvider::new(sigs_auth_xprv.private_key, fixed_headers); + self.build_with_header_provider(Arc::new(auth_provider)) + } + /// Builds a [`VssStore`] with [LNURL-auth] based authentication scheme as default method for /// authentication/authorization. /// @@ -869,7 +909,7 @@ impl VssStoreBuilder { /// /// [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( + pub fn build_with_lnurl( &self, lnurl_auth_server_url: String, fixed_headers: HashMap, ) -> Result { let secp_ctx = Secp256k1::new(); @@ -962,7 +1002,6 @@ mod tests { use rand::distr::Alphanumeric; use rand::{rng, Rng, RngCore}; - use vss_client::headers::FixedHeaders; use super::*; use crate::io::test_utils::do_read_write_remove_list_persist; @@ -972,11 +1011,13 @@ mod tests { let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let mut rng = rng(); let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let mut vss_seed = [0u8; 32]; - rng.fill_bytes(&mut vss_seed); - let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); + let mut node_seed = [0u8; 64]; + rng.fill_bytes(&mut node_seed); + let entropy = NodeEntropy::from_seed_bytes(vss_seed); let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + VssStoreBuilder::new(entropy, vss_base_url, rand_store_id, Network::Testnet) + .build_with_sigs_auth(HashMap::new()) + .unwrap(); do_read_write_remove_list_persist(&vss_store); } @@ -985,11 +1026,13 @@ mod tests { let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let mut rng = rng(); let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); - let mut vss_seed = [0u8; 32]; - rng.fill_bytes(&mut vss_seed); - let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); + let mut node_seed = [0u8; 64]; + rng.fill_bytes(&mut node_seed); + let entropy = NodeEntropy::from_seed_bytes(vss_seed); let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + VssStoreBuilder::new(entropy, vss_base_url, rand_store_id, Network::Testnet) + .build_with_sigs_auth(HashMap::new()) + .unwrap(); do_read_write_remove_list_persist(&vss_store); drop(vss_store) diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 54912b358..a1b587758 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -13,7 +13,7 @@ use std::collections::HashMap; use ldk_node::entropy::NodeEntropy; use ldk_node::Builder; -use rand::{rng, Rng}; +use rand::RngCore; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn channel_full_cycle_with_vss_store() { @@ -25,10 +25,10 @@ async fn channel_full_cycle_with_vss_store() { builder_a.set_chain_source_esplora(esplora_url.clone(), None); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let node_a = builder_a - .build_with_vss_store_and_fixed_headers( + .build_with_vss_store( config_a.node_entropy, vss_base_url.clone(), - "node_1_store".to_string(), + "".to_owned(), HashMap::new(), ) .unwrap(); @@ -39,12 +39,7 @@ async fn channel_full_cycle_with_vss_store() { let mut builder_b = Builder::from_config(config_b.node_config); builder_b.set_chain_source_esplora(esplora_url.clone(), None); let node_b = builder_b - .build_with_vss_store_and_fixed_headers( - config_b.node_entropy, - vss_base_url, - "node_2_store".to_string(), - HashMap::new(), - ) + .build_with_vss_store(config_b.node_entropy, vss_base_url, "".to_owned(), HashMap::new()) .unwrap(); node_b.start().unwrap(); @@ -60,96 +55,15 @@ async fn channel_full_cycle_with_vss_store() { .await; } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] -async fn vss_v0_schema_backwards_compatibility() { - let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); - let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); - - let rand_suffix: String = - (0..7).map(|_| rng().sample(rand::distr::Alphanumeric) as char).collect(); - let store_id = format!("v0_compat_test_{}", rand_suffix); - let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); - let seed_bytes = [42u8; 64]; - let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); - - // Setup a v0.6.2 `Node` persisted with the v0 scheme. - let (old_balance, old_node_id) = { - let mut builder_old = ldk_node_062::Builder::new(); - builder_old.set_network(bitcoin::Network::Regtest); - builder_old.set_storage_dir_path(storage_path.clone()); - builder_old.set_entropy_seed_bytes(seed_bytes); - builder_old.set_chain_source_esplora(esplora_url.clone(), None); - let node_old = builder_old - .build_with_vss_store_and_fixed_headers( - vss_base_url.clone(), - store_id.clone(), - HashMap::new(), - ) - .unwrap(); - - node_old.start().unwrap(); - let addr_old = node_old.onchain_payment().new_address().unwrap(); - common::premine_and_distribute_funds( - &bitcoind.client, - &electrsd.client, - vec![addr_old], - bitcoin::Amount::from_sat(100_000), - ) - .await; - node_old.sync_wallets().unwrap(); - - let balance = node_old.list_balances().spendable_onchain_balance_sats; - assert!(balance > 0); - let node_id = node_old.node_id(); - - // Workaround necessary as v0.6.2's VSS runtime wasn't dropsafe in a tokio context. - tokio::task::block_in_place(move || { - node_old.stop().unwrap(); - drop(node_old); - }); - - (balance, node_id) - }; - - // Now ensure we can still reinit from the same backend. - let mut builder_new = Builder::new(); - builder_new.set_network(bitcoin::Network::Regtest); - builder_new.set_storage_dir_path(storage_path); - builder_new.set_chain_source_esplora(esplora_url, None); - - let node_new = builder_new - .build_with_vss_store_and_fixed_headers( - node_entropy, - vss_base_url, - store_id, - HashMap::new(), - ) - .unwrap(); - - node_new.start().unwrap(); - node_new.sync_wallets().unwrap(); - - let new_balance = node_new.list_balances().spendable_onchain_balance_sats; - let new_node_id = node_new.node_id(); - - assert_eq!(old_node_id, new_node_id); - assert_eq!(old_balance, new_balance); - - node_new.stop().unwrap(); -} - #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn vss_node_restart() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); - let rand_suffix: String = - (0..7).map(|_| rng().sample(rand::distr::Alphanumeric) as char).collect(); - let store_id = format!("restart_test_{}", rand_suffix); let storage_path = common::random_storage_path().to_str().unwrap().to_owned(); - let seed_bytes = [42u8; 64]; + let mut seed_bytes = [42u8; 64]; + rand::rng().fill_bytes(&mut seed_bytes); let node_entropy = NodeEntropy::from_seed_bytes(seed_bytes); // Setup initial node and fund it. @@ -159,12 +73,7 @@ async fn vss_node_restart() { builder.set_storage_dir_path(storage_path.clone()); builder.set_chain_source_esplora(esplora_url.clone(), None); let node = builder - .build_with_vss_store_and_fixed_headers( - node_entropy, - vss_base_url.clone(), - store_id.clone(), - HashMap::new(), - ) + .build_with_vss_store(node_entropy, vss_base_url.clone(), "".to_owned(), HashMap::new()) .unwrap(); node.start().unwrap(); @@ -193,12 +102,7 @@ async fn vss_node_restart() { builder.set_chain_source_esplora(esplora_url, None); let node = builder - .build_with_vss_store_and_fixed_headers( - node_entropy, - vss_base_url, - store_id, - HashMap::new(), - ) + .build_with_vss_store(node_entropy, vss_base_url, "".to_owned(), HashMap::new()) .unwrap(); node.start().unwrap();