From a38b1803706c4650d0c5d74042d671e50f58bbd4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 13 Feb 2025 15:03:51 +0100 Subject: [PATCH 001/177] Fix amount calculation for tracked onchain payments We recently started tracking onchain payments in the store. It turns out the calculated amount was slightly off as in the outbound case it would include the paid fees. Here, we fix this minor bug. --- src/wallet/mod.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index f623cfc2c7..1b22ade17b 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -187,14 +187,21 @@ where // here to determine the `PaymentKind`, but that's not really satisfactory, so // we're punting on it until we can come up with a better solution. let kind = crate::payment::PaymentKind::Onchain { txid, status: confirmation_status }; + let fee = locked_wallet.calculate_fee(&wtx.tx_node.tx).unwrap_or(Amount::ZERO); let (sent, received) = locked_wallet.sent_and_received(&wtx.tx_node.tx); let (direction, amount_msat) = if sent > received { let direction = PaymentDirection::Outbound; - let amount_msat = Some(sent.to_sat().saturating_sub(received.to_sat()) * 1000); + let amount_msat = Some( + sent.to_sat().saturating_sub(fee.to_sat()).saturating_sub(received.to_sat()) + * 1000, + ); (direction, amount_msat) } else { let direction = PaymentDirection::Inbound; - let amount_msat = Some(received.to_sat().saturating_sub(sent.to_sat()) * 1000); + let amount_msat = Some( + received.to_sat().saturating_sub(sent.to_sat().saturating_sub(fee.to_sat())) + * 1000, + ); (direction, amount_msat) }; From ca3546b960b287a79a536c10def6396cf0944edd Mon Sep 17 00:00:00 2001 From: Artur Gontijo Date: Wed, 19 Feb 2025 18:55:12 -0300 Subject: [PATCH 002/177] Fix multi_hop_sending test. --- tests/integration_tests_rust.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2dc74cea98..dc21c8f342 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -202,7 +202,11 @@ fn multi_hop_sending() { nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap(); expect_event!(nodes[1], PaymentForwarded); - expect_event!(nodes[2], PaymentForwarded); + + // We expect that the payment goes through N2 or N3, so we check both for the PaymentForwarded event. + let node_2_fwd_event = matches!(nodes[2].next_event(), Some(Event::PaymentForwarded { .. })); + let node_3_fwd_event = matches!(nodes[3].next_event(), Some(Event::PaymentForwarded { .. })); + assert!(node_2_fwd_event || node_3_fwd_event); let payment_id = expect_payment_received_event!(&nodes[4], 2_500_000); let fee_paid_msat = Some(2000); From 0b7af80ab517630cbd6197f75a63197913de4705 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 13 Feb 2025 15:05:02 +0100 Subject: [PATCH 003/177] Track paid fees in `PaymentDetails` Since we split-out the paid fees anways, we optionally start tracking them in `PaymentDetails` for all payment types. --- bindings/ldk_node.udl | 1 + src/event.rs | 3 ++ src/payment/bolt11.rs | 6 ++++ src/payment/bolt12.rs | 6 ++++ src/payment/spontaneous.rs | 2 ++ src/payment/store.rs | 42 +++++++++++++++++++++++---- src/wallet/mod.rs | 16 ++++------- tests/common/mod.rs | 6 ++++ tests/integration_tests_rust.rs | 50 +++++++++++++++++++++++++++++++-- 9 files changed, 113 insertions(+), 19 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 5c4133c46d..5c5238e0a7 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -412,6 +412,7 @@ dictionary PaymentDetails { PaymentId id; PaymentKind kind; u64? amount_msat; + u64? fee_paid_msat; PaymentDirection direction; PaymentStatus status; u64 latest_update_timestamp; diff --git a/src/event.rs b/src/event.rs index 40e2367930..8cd01c5aff 100644 --- a/src/event.rs +++ b/src/event.rs @@ -725,6 +725,7 @@ where payment_id, kind, Some(amount_msat), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -765,6 +766,7 @@ where payment_id, kind, Some(amount_msat), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -931,6 +933,7 @@ where let update = PaymentDetailsUpdate { hash: Some(Some(payment_hash)), preimage: Some(Some(payment_preimage)), + fee_paid_msat: Some(fee_paid_msat), status: Some(PaymentStatus::Succeeded), ..PaymentDetailsUpdate::new(payment_id) }; diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 2c1b19143c..5c6ce35f82 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -160,6 +160,7 @@ impl Bolt11Payment { payment_id, kind, invoice.amount_milli_satoshis(), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -182,6 +183,7 @@ impl Bolt11Payment { payment_id, kind, invoice.amount_milli_satoshis(), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -293,6 +295,7 @@ impl Bolt11Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -315,6 +318,7 @@ impl Bolt11Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -531,6 +535,7 @@ impl Bolt11Payment { id, kind, amount_msat, + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -666,6 +671,7 @@ impl Bolt11Payment { id, kind, amount_msat, + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 1ff8739bef..dbeee0ab81 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -113,6 +113,7 @@ impl Bolt12Payment { payment_id, kind, Some(offer_amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -137,6 +138,7 @@ impl Bolt12Payment { payment_id, kind, Some(offer_amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -217,6 +219,7 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -241,6 +244,7 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); @@ -338,6 +342,7 @@ impl Bolt12Payment { payment_id, kind, Some(refund.amount_msats()), + None, PaymentDirection::Inbound, PaymentStatus::Pending, ); @@ -402,6 +407,7 @@ impl Bolt12Payment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 9846198556..f33ea15cca 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -140,6 +140,7 @@ impl SpontaneousPayment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Pending, ); @@ -161,6 +162,7 @@ impl SpontaneousPayment { payment_id, kind, Some(amount_msat), + None, PaymentDirection::Outbound, PaymentStatus::Failed, ); diff --git a/src/payment/store.rs b/src/payment/store.rs index 7e677c02d3..8a9222912f 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -42,6 +42,13 @@ pub struct PaymentDetails { pub kind: PaymentKind, /// The amount transferred. pub amount_msat: Option, + /// The fees that were paid for this payment. + /// + /// For Lightning payments, this will only be updated for outbound payments once they + /// succeeded. + /// + /// Will be `None` for Lightning payments made with LDK Node v0.4.x and earlier. + pub fee_paid_msat: Option, /// The direction of the payment. pub direction: PaymentDirection, /// The status of the payment. @@ -52,14 +59,14 @@ pub struct PaymentDetails { impl PaymentDetails { pub(crate) fn new( - id: PaymentId, kind: PaymentKind, amount_msat: Option, direction: PaymentDirection, - status: PaymentStatus, + id: PaymentId, kind: PaymentKind, amount_msat: Option, fee_paid_msat: Option, + direction: PaymentDirection, status: PaymentStatus, ) -> Self { let latest_update_timestamp = SystemTime::now() .duration_since(UNIX_EPOCH) .unwrap_or(Duration::from_secs(0)) .as_secs(); - Self { id, kind, amount_msat, direction, status, latest_update_timestamp } + Self { id, kind, amount_msat, fee_paid_msat, direction, status, latest_update_timestamp } } pub(crate) fn update(&mut self, update: &PaymentDetailsUpdate) -> bool { @@ -154,6 +161,10 @@ impl PaymentDetails { update_if_necessary!(self.amount_msat, amount_opt); } + if let Some(fee_paid_msat_opt) = update.fee_paid_msat { + update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt); + } + if let Some(status) = update.status { update_if_necessary!(self.status, status); } @@ -192,6 +203,7 @@ impl Writeable for PaymentDetails { (4, None::>, required), (5, self.latest_update_timestamp, required), (6, self.amount_msat, required), + (7, self.fee_paid_msat, option), (8, self.direction, required), (10, self.status, required) }); @@ -213,6 +225,7 @@ impl Readable for PaymentDetails { (4, secret, required), (5, latest_update_timestamp, (default_value, unix_time_secs)), (6, amount_msat, required), + (7, fee_paid_msat, option), (8, direction, required), (10, status, required) }); @@ -253,7 +266,15 @@ impl Readable for PaymentDetails { } }; - Ok(PaymentDetails { id, kind, amount_msat, direction, status, latest_update_timestamp }) + Ok(PaymentDetails { + id, + kind, + amount_msat, + fee_paid_msat, + direction, + status, + latest_update_timestamp, + }) } } @@ -479,6 +500,7 @@ pub(crate) struct PaymentDetailsUpdate { pub preimage: Option>, pub secret: Option>, pub amount_msat: Option>, + pub fee_paid_msat: Option>, pub direction: Option, pub status: Option, pub confirmation_status: Option, @@ -492,6 +514,7 @@ impl PaymentDetailsUpdate { preimage: None, secret: None, amount_msat: None, + fee_paid_msat: None, direction: None, status: None, confirmation_status: None, @@ -521,6 +544,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { preimage: Some(preimage), secret: Some(secret), amount_msat: Some(value.amount_msat), + fee_paid_msat: Some(value.fee_paid_msat), direction: Some(value.direction), status: Some(value.status), confirmation_status, @@ -708,8 +732,14 @@ mod tests { .is_err()); let kind = PaymentKind::Bolt11 { hash, preimage: None, secret: None }; - let payment = - PaymentDetails::new(id, kind, None, PaymentDirection::Inbound, PaymentStatus::Pending); + let payment = PaymentDetails::new( + id, + kind, + None, + None, + PaymentDirection::Inbound, + PaymentStatus::Pending, + ); assert_eq!(Ok(false), payment_store.insert(payment.clone())); assert!(payment_store.get(&id).is_some()); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 1b22ade17b..58a28308d4 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -49,7 +49,6 @@ use bitcoin::{ use std::ops::Deref; use std::sync::{Arc, Mutex}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub(crate) enum OnchainSendAmount { ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, @@ -146,11 +145,6 @@ where fn update_payment_store<'a>( &self, locked_wallet: &'a mut PersistedWallet, ) -> Result<(), Error> { - let latest_update_timestamp = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - for wtx in locked_wallet.transactions() { let id = PaymentId(wtx.tx_node.txid.to_byte_array()); let txid = wtx.tx_node.txid; @@ -205,14 +199,16 @@ where (direction, amount_msat) }; - let payment = PaymentDetails { + let fee_paid_msat = Some(fee.to_sat() * 1000); + + let payment = PaymentDetails::new( id, kind, amount_msat, + fee_paid_msat, direction, - status: payment_status, - latest_update_timestamp, - }; + payment_status, + ); self.payment_store.insert_or_update(&payment)?; } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0c0c24b7cd..2ad7368239 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -103,6 +103,10 @@ macro_rules! expect_payment_received_event { ref e @ Event::PaymentReceived { payment_id, amount_msat, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(amount_msat, $amount_msat); + let payment = $node.payment(&payment_id.unwrap()).unwrap(); + if !matches!(payment.kind, PaymentKind::Onchain { .. }) { + assert_eq!(payment.fee_paid_msat, None); + } $node.event_handled(); payment_id }, @@ -148,6 +152,8 @@ macro_rules! expect_payment_successful_event { if let Some(fee_msat) = $fee_paid_msat { assert_eq!(fee_paid_msat, fee_msat); } + let payment = $node.payment(&$payment_id.unwrap()).unwrap(); + assert_eq!(payment.fee_paid_msat, fee_paid_msat); assert_eq!(payment_id, $payment_id); $node.event_handled(); }, diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 2dc74cea98..19e7806d7d 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -26,6 +26,7 @@ use lightning::util::persist::KVStore; use bitcoincore_rpc::RpcApi; +use bitcoin::hashes::Hash; use bitcoin::Amount; use lightning_invoice::{Bolt11InvoiceDescription, Description}; @@ -281,7 +282,7 @@ fn start_stop_reinit() { } #[test] -fn onchain_spend_receive() { +fn onchain_send_receive() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); @@ -352,12 +353,37 @@ fn onchain_spend_receive() { node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None) ); - let amount_to_send_sats = 1000; + let amount_to_send_sats = 54321; let txid = node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); - generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); wait_for_tx(&electrsd.client, txid); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + let payment_id = PaymentId(txid.to_byte_array()); + let payment_a = node_a.payment(&payment_id).unwrap(); + assert_eq!(payment_a.status, PaymentStatus::Pending); + match payment_a.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_a.fee_paid_msat > Some(0)); + let payment_b = node_b.payment(&payment_id).unwrap(); + assert_eq!(payment_b.status, PaymentStatus::Pending); + match payment_a.kind { + PaymentKind::Onchain { status, .. } => { + assert!(matches!(status, ConfirmationStatus::Unconfirmed)); + }, + _ => panic!("Unexpected payment kind"), + } + assert!(payment_b.fee_paid_msat > Some(0)); + assert_eq!(payment_a.amount_msat, Some(amount_to_send_sats * 1000)); + assert_eq!(payment_a.amount_msat, payment_b.amount_msat); + assert_eq!(payment_a.fee_paid_msat, payment_b.fee_paid_msat); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); node_a.sync_wallets().unwrap(); node_b.sync_wallets().unwrap(); @@ -375,6 +401,24 @@ fn onchain_spend_receive() { node_b.list_payments_with_filter(|p| matches!(p.kind, PaymentKind::Onchain { .. })); assert_eq!(node_b_payments.len(), 3); + let payment_a = node_a.payment(&payment_id).unwrap(); + match payment_a.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + + let payment_b = node_a.payment(&payment_id).unwrap(); + match payment_b.kind { + PaymentKind::Onchain { txid: _txid, status } => { + assert_eq!(_txid, txid); + assert!(matches!(status, ConfirmationStatus::Confirmed { .. })); + }, + _ => panic!("Unexpected payment kind"), + } + let addr_b = node_b.onchain_payment().new_address().unwrap(); let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); From e9c435aeb139e5f5d3eca644af8689c0f917bd0b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 4 Mar 2025 00:32:08 +0100 Subject: [PATCH 004/177] Pin `idna_adapter` to fix MSRV CI (#480) Starting with v1.2, the `idna_adapter` crate switched to use `icu4x`, which requires a rust version of 1.81, conflicting with our MSRV. Here, we hence pin `idna_adapter` to v1.1 in our MSRV CI. --- .github/workflows/rust.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 0bf3ac49c1..1c4e6ed15e 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -46,6 +46,7 @@ jobs: if: matrix.msrv run: | cargo update -p home --precise "0.5.9" --verbose # home v0.5.11 requires rustc 1.81 or newer + cargo update -p idna_adapter --precise "1.1.0" --verbose # idna_adapter 1.2 switched to ICU4X, requiring 1.81 and newer - name: Set RUSTFLAGS to deny warnings if: "matrix.toolchain == 'stable'" run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" From f40a0078621b51d895c4a19d6f1dd8e6d7e2dd05 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 11:55:05 +0100 Subject: [PATCH 005/177] Prefactor: Rename fields for clarity Previously, we named internal fields/APIs `lspsX_service` as us being the client was implied. Since we're about to also add service-side functionalities, such naming would start to get confusing. We hence rename them to follow a `lspsX_client` scheme, and will add the service-side APIs using the `service` terminology. --- src/builder.rs | 50 ++++++++++----- src/liquidity.rs | 140 +++++++++++++++++++++--------------------- src/payment/bolt11.rs | 5 +- 3 files changed, 105 insertions(+), 90 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 70dc7ff7a9..baefa2ecec 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -94,18 +94,26 @@ enum GossipSourceConfig { RapidGossipSync(String), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Default)] struct LiquiditySourceConfig { - // LSPS1 service's (node_id, address, token) - lsps1_service: Option<(PublicKey, SocketAddress, Option)>, - // LSPS2 service's (node_id, address, token) - lsps2_service: Option<(PublicKey, SocketAddress, Option)>, + // Act as an LSPS1 client connecting to the given service. + lsps1_client: Option, + // Act as an LSPS2 client connecting to the given service. + lsps2_client: Option, } -impl Default for LiquiditySourceConfig { - fn default() -> Self { - Self { lsps1_service: None, lsps2_service: None } - } +#[derive(Debug, Clone)] +struct LSPS1ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, +} + +#[derive(Debug, Clone)] +struct LSPS2ClientConfig { + node_id: PublicKey, + address: SocketAddress, + token: Option, } #[derive(Clone)] @@ -319,7 +327,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps1_service = Some((node_id, address, token)); + let lsps1_client_config = LSPS1ClientConfig { node_id, address, token }; + liquidity_source_config.lsps1_client = Some(lsps1_client_config); self } @@ -339,7 +348,8 @@ impl NodeBuilder { let liquidity_source_config = self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); - liquidity_source_config.lsps2_service = Some((node_id, address, token)); + let lsps2_client_config = LSPS2ClientConfig { node_id, address, token }; + liquidity_source_config.lsps2_client = Some(lsps2_client_config); self } @@ -1039,7 +1049,7 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() { // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll // check that they don't take too much before claiming. user_config.channel_config.accept_underpaying_htlcs = true; @@ -1180,12 +1190,20 @@ fn build_with_store_internal( Arc::clone(&logger), ); - lsc.lsps1_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps1_service(*node_id, address.clone(), token.clone()) + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); - lsc.lsps2_service.as_ref().map(|(node_id, address, token)| { - liquidity_source_builder.lsps2_service(*node_id, address.clone(), token.clone()) + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) }); Arc::new(liquidity_source_builder.build()) diff --git a/src/liquidity.rs b/src/liquidity.rs index cbc19954fc..14891f81de 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -40,9 +40,9 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; -struct LSPS1Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS1Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS1ClientConfig, pending_opening_params_requests: @@ -52,9 +52,9 @@ struct LSPS1Service { Mutex>>, } -struct LSPS2Service { - node_id: PublicKey, - address: SocketAddress, +struct LSPS2Client { + lsp_node_id: PublicKey, + lsp_address: SocketAddress, token: Option, client_config: LSPS2ClientConfig, pending_fee_requests: Mutex>>, @@ -65,8 +65,8 @@ pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -82,11 +82,11 @@ where channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { - let lsps1_service = None; - let lsps2_service = None; + let lsps1_client = None; + let lsps2_client = None; Self { - lsps1_service, - lsps2_service, + lsps1_client, + lsps2_client, channel_manager, keys_manager, chain_source, @@ -95,17 +95,17 @@ where } } - pub(crate) fn lsps1_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps1_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); - self.lsps1_service = Some(LSPS1Service { - node_id, - address, + self.lsps1_client = Some(LSPS1Client { + lsp_node_id, + lsp_address, token, client_config, pending_opening_params_requests, @@ -115,15 +115,15 @@ where self } - pub(crate) fn lsps2_service( - &mut self, node_id: PublicKey, address: SocketAddress, token: Option, + pub(crate) fn lsps2_client( + &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { let client_config = LSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); - self.lsps2_service = Some(LSPS2Service { - node_id, - address, + self.lsps2_client = Some(LSPS2Client { + lsp_node_id, + lsp_address, token, client_config, pending_fee_requests, @@ -133,8 +133,8 @@ where } pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_service.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_service.as_ref().map(|s| s.client_config.clone()); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); @@ -148,8 +148,8 @@ where )); LiquiditySource { - lsps1_service: self.lsps1_service, - lsps2_service: self.lsps2_service, + lsps1_client: self.lsps1_client, + lsps2_client: self.lsps2_client, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -163,8 +163,8 @@ pub(crate) struct LiquiditySource where L::Target: LdkLogger, { - lsps1_service: Option, - lsps2_service: Option, + lsps1_client: Option, + lsps2_client: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, @@ -185,12 +185,12 @@ where self.liquidity_manager.as_ref() } - pub(crate) fn get_lsps1_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps1_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps1_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } - pub(crate) fn get_lsps2_service_details(&self) -> Option<(PublicKey, SocketAddress)> { - self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone())) + pub(crate) fn get_lsps2_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { + self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone())) } pub(crate) async fn handle_next_event(&self) { @@ -200,8 +200,8 @@ where counterparty_node_id, supported_options, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -213,7 +213,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_opening_params_requests .lock() .unwrap() @@ -256,8 +256,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -269,7 +269,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_create_order_requests .lock() .unwrap() @@ -314,8 +314,8 @@ where payment, channel, }) => { - if let Some(lsps1_service) = self.lsps1_service.as_ref() { - if counterparty_node_id != lsps1_service.node_id { + if let Some(lsps1_client) = self.lsps1_client.as_ref() { + if counterparty_node_id != lsps1_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -327,7 +327,7 @@ where return; } - if let Some(sender) = lsps1_service + if let Some(sender) = lsps1_client .pending_check_order_status_requests .lock() .unwrap() @@ -369,8 +369,8 @@ where counterparty_node_id, opening_fee_params_menu, }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -383,7 +383,7 @@ where } if let Some(sender) = - lsps2_service.pending_fee_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_fee_requests.lock().unwrap().remove(&request_id) { let response = LSPS2FeeResponse { opening_fee_params_menu }; @@ -421,8 +421,8 @@ where cltv_expiry_delta, .. }) => { - if let Some(lsps2_service) = self.lsps2_service.as_ref() { - if counterparty_node_id != lsps2_service.node_id { + if let Some(lsps2_client) = self.lsps2_client.as_ref() { + if counterparty_node_id != lsps2_client.lsp_node_id { debug_assert!( false, "Received response from unexpected LSP counterparty. This should never happen." @@ -435,7 +435,7 @@ where } if let Some(sender) = - lsps2_service.pending_buy_requests.lock().unwrap().remove(&request_id) + lsps2_client.pending_buy_requests.lock().unwrap().remove(&request_id) { let response = LSPS2BuyResponse { intercept_scid, cltv_expiry_delta }; @@ -475,7 +475,7 @@ where pub(crate) async fn lsps1_request_opening_params( &self, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); @@ -485,8 +485,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_opening_params_requests_lock = - lsps1_service.pending_opening_params_requests.lock().unwrap(); - let request_id = client_handler.request_supported_options(lsps1_service.node_id); + lsps1_client.pending_opening_params_requests.lock().unwrap(); + let request_id = client_handler.request_supported_options(lsps1_client.lsp_node_id); pending_opening_params_requests_lock.insert(request_id, request_sender); } @@ -506,7 +506,7 @@ where &self, lsp_balance_sat: u64, client_balance_sat: u64, channel_expiry_blocks: u32, announce_channel: bool, refund_address: bitcoin::Address, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -560,7 +560,7 @@ where required_channel_confirmations: lsp_limits.min_required_channel_confirmations, funding_confirms_within_blocks: lsp_limits.min_funding_confirms_within_blocks, channel_expiry_blocks, - token: lsps1_service.token.clone(), + token: lsps1_client.token.clone(), announce_channel, }; @@ -568,9 +568,9 @@ where let request_id; { let mut pending_create_order_requests_lock = - lsps1_service.pending_create_order_requests.lock().unwrap(); + lsps1_client.pending_create_order_requests.lock().unwrap(); request_id = client_handler.create_order( - &lsps1_service.node_id, + &lsps1_client.lsp_node_id, order_params.clone(), Some(refund_address), ); @@ -605,7 +605,7 @@ where pub(crate) async fn lsps1_check_order_status( &self, order_id: OrderId, ) -> Result { - let lsps1_service = self.lsps1_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { log_error!(self.logger, "LSPS1 liquidity client was not configured.",); Error::LiquiditySourceUnavailable @@ -614,8 +614,8 @@ where let (request_sender, request_receiver) = oneshot::channel(); { let mut pending_check_order_status_requests_lock = - lsps1_service.pending_check_order_status_requests.lock().unwrap(); - let request_id = client_handler.check_order_status(&lsps1_service.node_id, order_id); + lsps1_client.pending_check_order_status_requests.lock().unwrap(); + let request_id = client_handler.check_order_status(&lsps1_client.lsp_node_id, order_id); pending_check_order_status_requests_lock.insert(request_id, request_sender); } @@ -740,7 +740,7 @@ where } async fn lsps2_request_opening_fee_params(&self) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -749,9 +749,9 @@ where let (fee_request_sender, fee_request_receiver) = oneshot::channel(); { - let mut pending_fee_requests_lock = lsps2_service.pending_fee_requests.lock().unwrap(); + let mut pending_fee_requests_lock = lsps2_client.pending_fee_requests.lock().unwrap(); let request_id = client_handler - .request_opening_params(lsps2_service.node_id, lsps2_service.token.clone()); + .request_opening_params(lsps2_client.lsp_node_id, lsps2_client.token.clone()); pending_fee_requests_lock.insert(request_id, fee_request_sender); } @@ -773,7 +773,7 @@ where async fn lsps2_send_buy_request( &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps2_client_handler().ok_or_else(|| { log_error!(self.logger, "Liquidity client was not configured.",); @@ -782,9 +782,9 @@ where let (buy_request_sender, buy_request_receiver) = oneshot::channel(); { - let mut pending_buy_requests_lock = lsps2_service.pending_buy_requests.lock().unwrap(); + let mut pending_buy_requests_lock = lsps2_client.pending_buy_requests.lock().unwrap(); let request_id = client_handler - .select_opening_params(lsps2_service.node_id, amount_msat, opening_fee_params) + .select_opening_params(lsps2_client.lsp_node_id, amount_msat, opening_fee_params) .map_err(|e| { log_error!( self.logger, @@ -817,7 +817,7 @@ where &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; + let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; @@ -830,7 +830,7 @@ where })?; let route_hint = RouteHint(vec![RouteHintHop { - src_node_id: lsps2_service.node_id, + src_node_id: lsps2_client.lsp_node_id, short_channel_id: buy_response.intercept_scid, fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, cltv_expiry_delta: buy_response.cltv_expiry_delta as u16, @@ -999,9 +999,8 @@ impl LSPS1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); @@ -1045,9 +1044,8 @@ impl LSPS1Liquidity { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (lsp_node_id, lsp_address) = liquidity_source - .get_lsps1_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (lsp_node_id, lsp_address) = + liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 5c6ce35f82..49a5ecb6be 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -601,9 +601,8 @@ impl Bolt11Payment { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; - let (node_id, address) = liquidity_source - .get_lsps2_service_details() - .ok_or(Error::LiquiditySourceUnavailable)?; + let (node_id, address) = + liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; let rt_lock = self.runtime.read().unwrap(); let runtime = rt_lock.as_ref().unwrap(); From 5680fcd02f40d2607f70b242321ce115cc190992 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:01:52 +0100 Subject: [PATCH 006/177] Refactor `derive_xprv` to make it reusable .. and while we're at it we move the VSS child key indexes to constants. --- src/builder.rs | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index baefa2ecec..9e7f8d1ef6 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -75,6 +75,9 @@ use std::sync::{Arc, Mutex, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; +const VSS_HARDENED_CHILD_INDEX: u32 = 877; +const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; + #[derive(Debug, Clone)] enum ChainDataSourceConfig { Esplora { server_url: String, sync_config: Option }, @@ -481,10 +484,14 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config, &seed_bytes, Arc::clone(&logger))?; + 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: 138 }]) + .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 @@ -546,7 +553,12 @@ impl NodeBuilder { let config = Arc::new(self.config.clone()); - let vss_xprv = derive_vss_xprv(config.clone(), &seed_bytes, Arc::clone(&logger))?; + 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(); @@ -1415,8 +1427,8 @@ fn seed_bytes_from_config( } } -fn derive_vss_xprv( - config: Arc, seed_bytes: &[u8; 64], logger: Arc, +fn derive_xprv( + config: Arc, seed_bytes: &[u8; 64], hardened_child_index: u32, logger: Arc, ) -> Result { use bitcoin::key::Secp256k1; @@ -1425,10 +1437,11 @@ fn derive_vss_xprv( BuildError::InvalidSeedBytes })?; - xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: 877 }]).map_err(|e| { - log_error!(logger, "Failed to derive VSS secret: {}", e); - BuildError::KVStoreSetupFailed - }) + xprv.derive_priv(&Secp256k1::new(), &[ChildNumber::Hardened { index: hardened_child_index }]) + .map_err(|e| { + log_error!(logger, "Failed to derive hardened child secret: {}", e); + BuildError::InvalidSeedBytes + }) } /// Sanitize the user-provided node alias to ensure that it is a valid protocol-specified UTF-8 string. From 1357121c9d325ae9025c130bf249440c956134ea Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 11:35:24 +0100 Subject: [PATCH 007/177] Prefix the lightning-liqudity config types with `Ldk` for clarity .. we might eventually want to drop them anyways, but for now we rename them to make them easily discernable from their counterparts in `builder.rs`. --- src/liquidity.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/liquidity.rs b/src/liquidity.rs index 14891f81de..e13d4dab07 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -19,10 +19,10 @@ use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; -use lightning_liquidity::lsps1::client::LSPS1ClientConfig; +use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; -use lightning_liquidity::lsps2::client::LSPS2ClientConfig; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; use lightning_liquidity::lsps2::utils::compute_opening_fee; @@ -44,7 +44,7 @@ struct LSPS1Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, - client_config: LSPS1ClientConfig, + ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: Mutex>>, pending_create_order_requests: Mutex>>, @@ -56,7 +56,7 @@ struct LSPS2Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, - client_config: LSPS2ClientConfig, + ldk_client_config: LdkLSPS2ClientConfig, pending_fee_requests: Mutex>>, pending_buy_requests: Mutex>>, } @@ -99,7 +99,7 @@ where &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { // TODO: allow to set max_channel_fees_msat - let client_config = LSPS1ClientConfig { max_channel_fees_msat: None }; + let ldk_client_config = LdkLSPS1ClientConfig { max_channel_fees_msat: None }; let pending_opening_params_requests = Mutex::new(HashMap::new()); let pending_create_order_requests = Mutex::new(HashMap::new()); let pending_check_order_status_requests = Mutex::new(HashMap::new()); @@ -107,7 +107,7 @@ where lsp_node_id, lsp_address, token, - client_config, + ldk_client_config, pending_opening_params_requests, pending_create_order_requests, pending_check_order_status_requests, @@ -118,14 +118,14 @@ where pub(crate) fn lsps2_client( &mut self, lsp_node_id: PublicKey, lsp_address: SocketAddress, token: Option, ) -> &mut Self { - let client_config = LSPS2ClientConfig {}; + let ldk_client_config = LdkLSPS2ClientConfig {}; let pending_fee_requests = Mutex::new(HashMap::new()); let pending_buy_requests = Mutex::new(HashMap::new()); self.lsps2_client = Some(LSPS2Client { lsp_node_id, lsp_address, token, - client_config, + ldk_client_config, pending_fee_requests, pending_buy_requests, }); @@ -133,8 +133,8 @@ where } pub(crate) fn build(self) -> LiquiditySource { - let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.client_config.clone()); - let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.client_config.clone()); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); + let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); let liquidity_client_config = Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); From 82f28a78e29ff818be6c86eb272e2acfef9efb59 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 11:58:30 +0100 Subject: [PATCH 008/177] Move `LSPSXClientConfig` to `liquidity` .. for consistency, as we're about to add `LSPS2ServiceConfig` there, too. --- src/builder.rs | 16 +--------------- src/liquidity.rs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 9e7f8d1ef6..b631af1f99 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,7 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::LiquiditySourceBuilder; +use crate::liquidity::{LSPS1ClientConfig, LSPS2ClientConfig, LiquiditySourceBuilder}; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -105,20 +105,6 @@ struct LiquiditySourceConfig { lsps2_client: Option, } -#[derive(Debug, Clone)] -struct LSPS1ClientConfig { - node_id: PublicKey, - address: SocketAddress, - token: Option, -} - -#[derive(Debug, Clone)] -struct LSPS2ClientConfig { - node_id: PublicKey, - address: SocketAddress, - token: Option, -} - #[derive(Clone)] enum LogWriterConfig { File { log_file_path: Option, max_log_level: Option }, diff --git a/src/liquidity.rs b/src/liquidity.rs index e13d4dab07..43a76770b2 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -52,6 +52,13 @@ struct LSPS1Client { Mutex>>, } +#[derive(Debug, Clone)] +pub(crate) struct LSPS1ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + struct LSPS2Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, @@ -61,6 +68,13 @@ struct LSPS2Client { pending_buy_requests: Mutex>>, } +#[derive(Debug, Clone)] +pub(crate) struct LSPS2ClientConfig { + pub node_id: PublicKey, + pub address: SocketAddress, + pub token: Option, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, From 3e0f6ee21b0f63bb5e04153c73d721c714b1dd13 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:17:47 +0100 Subject: [PATCH 009/177] Allow configuring LSPS2 service via builders We add the capability to configure LSPS2 service mode in `Builder` and `LiquiditySourceBuilder`. --- bindings/ldk_node.udl | 12 +++++ src/builder.rs | 105 +++++++++++++++++++++++++++++------------- src/liquidity.rs | 67 ++++++++++++++++++++++++++- src/uniffi_types.rs | 2 +- 4 files changed, 152 insertions(+), 34 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 5c5238e0a7..805010356a 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -25,6 +25,18 @@ dictionary EsploraSyncConfig { u64 fee_rate_cache_update_interval_secs; }; +dictionary LSPS2ServiceConfig { + string? require_token; + boolean advertise_service; + u32 channel_opening_fee_ppm; + u32 channel_over_provisioning_ppm; + u64 min_channel_opening_fee_msat; + u32 min_channel_lifetime; + u32 max_client_to_self_delay; + u64 min_payment_size_msat; + u64 max_payment_size_msat; +}; + enum LogLevel { "Gossip", "Trace", diff --git a/src/builder.rs b/src/builder.rs index b631af1f99..0cbce79de6 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,7 +18,9 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; -use crate::liquidity::{LSPS1ClientConfig, LSPS2ClientConfig, LiquiditySourceBuilder}; +use crate::liquidity::{ + LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, +}; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::store::PaymentStore; @@ -77,6 +79,7 @@ use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvide const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; +const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { @@ -103,6 +106,8 @@ struct LiquiditySourceConfig { lsps1_client: Option, // Act as an LSPS2 client connecting to the given service. lsps2_client: Option, + // Act as an LSPS2 service. + lsps2_service: Option, } #[derive(Clone)] @@ -342,6 +347,21 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2( + &mut self, service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let liquidity_source_config = + self.liquidity_source_config.get_or_insert(LiquiditySourceConfig::default()); + liquidity_source_config.lsps2_service = Some(service_config); + self + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&mut self, storage_dir_path: String) -> &mut Self { self.config.storage_dir_path = storage_dir_path; @@ -699,6 +719,16 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_liquidity_source_lsps2(node_id, address, token); } + /// Configures the [`Node`] instance to provide an [LSPS2] service, issuing just-in-time + /// channels to clients. + /// + /// **Caution**: LSP service support is in **alpha** and is considered an experimental feature. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + pub fn set_liquidity_provider_lsps2(&self, service_config: LSPS2ServiceConfig) { + self.inner.write().unwrap().set_liquidity_provider_lsps2(service_config); + } + /// Sets the used storage directory path. pub fn set_storage_dir_path(&self, storage_dir_path: String) { self.inner.write().unwrap().set_storage_dir_path(storage_dir_path); @@ -1179,39 +1209,52 @@ fn build_with_store_internal( }, }; - let liquidity_source = liquidity_source_config.as_ref().map(|lsc| { - let mut liquidity_source_builder = LiquiditySourceBuilder::new( - Arc::clone(&channel_manager), - Arc::clone(&keys_manager), - Arc::clone(&chain_source), - Arc::clone(&config), - Arc::clone(&logger), - ); - - lsc.lsps1_client.as_ref().map(|config| { - liquidity_source_builder.lsps1_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + let (liquidity_source, custom_message_handler) = + if let Some(lsc) = liquidity_source_config.as_ref() { + let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&channel_manager), + Arc::clone(&keys_manager), + Arc::clone(&chain_source), + Arc::clone(&config), + Arc::clone(&logger), + ); - lsc.lsps2_client.as_ref().map(|config| { - liquidity_source_builder.lsps2_client( - config.node_id, - config.address.clone(), - config.token.clone(), - ) - }); + lsc.lsps1_client.as_ref().map(|config| { + liquidity_source_builder.lsps1_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - Arc::new(liquidity_source_builder.build()) - }); + lsc.lsps2_client.as_ref().map(|config| { + liquidity_source_builder.lsps2_client( + config.node_id, + config.address.clone(), + config.token.clone(), + ) + }); - let custom_message_handler = if let Some(liquidity_source) = liquidity_source.as_ref() { - Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))) - } else { - Arc::new(NodeCustomMessageHandler::new_ignoring()) - }; + let promise_secret = { + let lsps_xpriv = derive_xprv( + Arc::clone(&config), + &seed_bytes, + LSPS_HARDENED_CHILD_INDEX, + Arc::clone(&logger), + )?; + lsps_xpriv.private_key.secret_bytes() + }; + lsc.lsps2_service.as_ref().map(|config| { + liquidity_source_builder.lsps2_service(promise_secret, config.clone()) + }); + + let liquidity_source = Arc::new(liquidity_source_builder.build()); + let custom_message_handler = + Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); + (Some(liquidity_source), custom_message_handler) + } else { + (None, Arc::new(NodeCustomMessageHandler::new_ignoring())) + }; let msg_handler = match gossip_source.as_gossip_sync() { GossipSync::P2P(p2p_gossip_sync) => MessageHandler { diff --git a/src/liquidity.rs b/src/liquidity.rs index 43a76770b2..a2ce4912e0 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -25,8 +25,9 @@ use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, Order use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::LSPS2ClientEvent; use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; -use lightning_liquidity::LiquidityClientConfig; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -75,12 +76,56 @@ pub(crate) struct LSPS2ClientConfig { pub token: Option, } +struct LSPS2Service { + service_config: LSPS2ServiceConfig, + ldk_service_config: LdkLSPS2ServiceConfig, +} + +/// Represents the configuration of the LSPS2 service. +/// +/// See [bLIP-52 / LSPS2] for more information. +/// +/// [bLIP-52 / LSPS2]: https://github.com/lightning/blips/blob/master/blip-0052.md +#[derive(Debug, Clone)] +pub struct LSPS2ServiceConfig { + /// A token we may require to be sent by the clients. + /// + /// If set, only requests matching this token will be accepted. + pub require_token: Option, + /// Indicates whether the LSPS service will be announced via the gossip network. + pub advertise_service: bool, + /// The fee we withhold for the channel open from the initial payment. + /// + /// This fee is proportional to the client-requested amount, in parts-per-million. + pub channel_opening_fee_ppm: u32, + /// The proportional overprovisioning for the channel. + /// + /// This determines, in parts-per-million, how much value we'll provision on top of the amount + /// we need to forward the payment to the client. + /// + /// For example, setting this to `100_000` will result in a channel being opened that is 10% + /// larger than then the to-be-forwarded amount (i.e., client-requested amount minus the + /// channel opening fee fee). + pub channel_over_provisioning_ppm: u32, + /// The minimum fee required for opening a channel. + pub min_channel_opening_fee_msat: u64, + /// The minimum number of blocks after confirmation we promise to keep the channel open. + pub min_channel_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that we will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that we will accept when opening a channel. + pub max_payment_size_msat: u64, +} + pub(crate) struct LiquiditySourceBuilder where L::Target: LdkLogger, { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -98,9 +143,11 @@ where ) -> Self { let lsps1_client = None; let lsps2_client = None; + let lsps2_service = None; Self { lsps1_client, lsps2_client, + lsps2_service, channel_manager, keys_manager, chain_source, @@ -146,7 +193,21 @@ where self } + pub(crate) fn lsps2_service( + &mut self, promise_secret: [u8; 32], service_config: LSPS2ServiceConfig, + ) -> &mut Self { + let ldk_service_config = LdkLSPS2ServiceConfig { promise_secret }; + self.lsps2_service = Some(LSPS2Service { service_config, ldk_service_config }); + self + } + pub(crate) fn build(self) -> LiquiditySource { + let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { + let lsps2_service_config = Some(s.ldk_service_config.clone()); + let advertise_service = s.service_config.advertise_service; + LiquidityServiceConfig { lsps2_service_config, advertise_service } + }); + let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); let liquidity_client_config = @@ -157,13 +218,14 @@ where Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), None, - None, + liquidity_service_config, liquidity_client_config, )); LiquiditySource { lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, + lsps2_service: self.lsps2_service, channel_manager: self.channel_manager, keys_manager: self.keys_manager, liquidity_manager, @@ -179,6 +241,7 @@ where { lsps1_client: Option, lsps2_client: Option, + lsps2_service: Option, channel_manager: Arc, keys_manager: Arc, liquidity_manager: Arc, diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index c7a8960f75..58c577f8ee 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -14,7 +14,7 @@ pub use crate::config::{ default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; -pub use crate::liquidity::{LSPS1OrderStatus, OnchainPaymentInfo, PaymentInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, From 76a42069e8d6eab08930d0aaa5df457a69b993d0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:44:02 +0100 Subject: [PATCH 010/177] Add LDK event handling .. and forward it to our `LiquditySource`. --- src/builder.rs | 6 ++++ src/event.rs | 40 +++++++++++++++++++++++++-- src/lib.rs | 1 + src/liquidity.rs | 72 +++++++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 116 insertions(+), 3 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 0cbce79de6..d5100d8986 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1089,6 +1089,12 @@ fn build_with_store_internal( 100; } + if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { + // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the + // information to the service handler. + user_config.accept_intercept_htlcs = true; + } + let message_router = Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); diff --git a/src/event.rs b/src/event.rs index 13ec88fc3d..8c63c2a63f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -15,6 +15,8 @@ use crate::{ use crate::config::{may_announce_channel, Config}; use crate::connection::ConnectionManager; use crate::fee_estimator::ConfirmationTarget; +use crate::liquidity::LiquiditySource; +use crate::logger::Logger; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, @@ -446,6 +448,7 @@ where connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, @@ -462,6 +465,7 @@ where bump_tx_event_handler: Arc, channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, + liquidity_source: Option>>>, payment_store: Arc>, peer_store: Arc>, runtime: Arc>>>, logger: L, config: Arc, ) -> Self { @@ -473,6 +477,7 @@ where connection_manager, output_sweeper, network_graph, + liquidity_source, payment_store, peer_store, logger, @@ -1013,7 +1018,11 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { .. } => {}, + LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_handling_failed(failed_next_destination); + } + }, LdkEvent::PendingHTLCsForwardable { time_forwardable } => { let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; @@ -1248,6 +1257,10 @@ where fee_earned, ); } + + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_payment_forwarded(next_channel_id); + } }, LdkEvent::ChannelPending { channel_id, @@ -1321,6 +1334,14 @@ where counterparty_node_id, ); + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_channel_ready( + user_channel_id, + &channel_id, + &counterparty_node_id, + ); + } + let event = Event::ChannelReady { channel_id, user_channel_id: UserChannelId(user_channel_id), @@ -1359,7 +1380,22 @@ where }; }, LdkEvent::DiscardFunding { .. } => {}, - LdkEvent::HTLCIntercepted { .. } => {}, + LdkEvent::HTLCIntercepted { + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + .. + } => { + if let Some(liquidity_source) = self.liquidity_source.as_ref() { + liquidity_source.handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ); + } + }, LdkEvent::InvoiceReceived { .. } => { debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, diff --git a/src/lib.rs b/src/lib.rs index b2899da9ee..8d616353a8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -525,6 +525,7 @@ impl Node { Arc::clone(&self.connection_manager), Arc::clone(&self.output_sweeper), Arc::clone(&self.network_graph), + self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.runtime), diff --git a/src/liquidity.rs b/src/liquidity.rs index a2ce4912e0..f0c047054a 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -13,10 +13,14 @@ use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{Config, Error}; -use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use lightning::events::HTLCDestination; +use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; +use lightning::ln::types::ChannelId; use lightning::routing::router::{RouteHint, RouteHintHop}; + use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; + use lightning_liquidity::events::Event; use lightning_liquidity::lsps0::ser::RequestId; use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; @@ -29,6 +33,8 @@ use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceCo use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; +use lightning_types::payment::PaymentHash; + use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{PublicKey, Secp256k1}; @@ -944,6 +950,70 @@ where Error::InvoiceCreationFailed }) } + + pub(crate) fn handle_channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.channel_ready( + user_channel_id, + channel_id, + counterparty_node_id, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle ChannelReady event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCIntercepted event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failed_next_destination) { + log_error!( + self.logger, + "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", + e + ); + } + } + } + + pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + if let Some(next_channel_id) = next_channel_id { + if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { + if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id) { + log_error!( + self.logger, + "LSPS2 service failed to handle PaymentForwarded: {:?}", + e + ); + } + } + } + } } #[derive(Debug, Clone)] From ec1cff585c16a6589886faee693e9e5183a77c91 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 13:53:43 +0100 Subject: [PATCH 011/177] Move `PaymentForwarded` event emission down .. to align with other event handling variants: First log, then act, then emit event if everything went okay. --- src/event.rs | 39 +++++++++++++++++++-------------------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/event.rs b/src/event.rs index 8c63c2a63f..f0f9765698 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1188,23 +1188,6 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { - let event = Event::PaymentForwarded { - prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), - next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), - prev_user_channel_id: prev_user_channel_id.map(UserChannelId), - next_user_channel_id: next_user_channel_id.map(UserChannelId), - prev_node_id, - next_node_id, - total_fee_earned_msat, - skimmed_fee_msat, - claim_from_onchain_tx, - outbound_amount_forwarded_msat, - }; - self.event_queue.add_event(event).map_err(|e| { - log_error!(self.logger, "Failed to push to event queue: {}", e); - ReplayEvent() - })?; - let read_only_network_graph = self.network_graph.read_only(); let nodes = read_only_network_graph.nodes(); let channels = self.channel_manager.list_channels(); @@ -1237,14 +1220,13 @@ where format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); let fee_earned = total_fee_earned_msat.unwrap_or(0); - let outbound_amount_forwarded_msat = outbound_amount_forwarded_msat.unwrap_or(0); if claim_from_onchain_tx { log_info!( self.logger, "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } else { @@ -1253,7 +1235,7 @@ where "Forwarded payment{}{} of {}msat, earning {}msat in fees.", from_prev_str, to_next_str, - outbound_amount_forwarded_msat, + outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); } @@ -1261,6 +1243,23 @@ where if let Some(liquidity_source) = self.liquidity_source.as_ref() { liquidity_source.handle_payment_forwarded(next_channel_id); } + + let event = Event::PaymentForwarded { + prev_channel_id: prev_channel_id.expect("prev_channel_id expected for events generated by LDK versions greater than 0.0.107."), + next_channel_id: next_channel_id.expect("next_channel_id expected for events generated by LDK versions greater than 0.0.107."), + prev_user_channel_id: prev_user_channel_id.map(UserChannelId), + next_user_channel_id: next_user_channel_id.map(UserChannelId), + prev_node_id, + next_node_id, + total_fee_earned_msat, + skimmed_fee_msat, + claim_from_onchain_tx, + outbound_amount_forwarded_msat, + }; + self.event_queue.add_event(event).map_err(|e| { + log_error!(self.logger, "Failed to push to event queue: {}", e); + ReplayEvent() + })?; }, LdkEvent::ChannelPending { channel_id, From f78f0864bb738cc2bd55d274171a6012ca4b8d52 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 14:27:59 +0100 Subject: [PATCH 012/177] Add `LSPS2ServiceEvent` handling .. so far we just silently fail if something goes wrong, eventually we'll need to implement retrying channel opens to honor buy requests that didn't succeed on the first attempt. --- src/builder.rs | 1 + src/liquidity.rs | 271 ++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 268 insertions(+), 4 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index d5100d8986..97ff1ea211 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1218,6 +1218,7 @@ fn build_with_store_internal( let (liquidity_source, custom_message_handler) = if let Some(lsc) = liquidity_source_config.as_ref() { let mut liquidity_source_builder = LiquiditySourceBuilder::new( + Arc::clone(&wallet), Arc::clone(&channel_manager), Arc::clone(&keys_manager), Arc::clone(&chain_source), diff --git a/src/liquidity.rs b/src/liquidity.rs index f0c047054a..a7751026ba 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -11,7 +11,7 @@ use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; -use crate::{Config, Error}; +use crate::{total_anchor_channels_reserve_sats, Config, Error}; use lightning::events::HTLCDestination; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; @@ -27,8 +27,8 @@ use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfi use lightning_liquidity::lsps1::event::LSPS1ClientEvent; use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; -use lightning_liquidity::lsps2::event::LSPS2ClientEvent; -use lightning_liquidity::lsps2::msgs::OpeningFeeParams; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -40,6 +40,10 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; +use chrono::{DateTime, Utc}; + +use rand::Rng; + use std::collections::HashMap; use std::ops::Deref; use std::sync::{Arc, Mutex, RwLock}; @@ -47,6 +51,10 @@ use std::time::Duration; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; +const LSPS2_GETINFO_REQUEST_EXPIRY: Duration = Duration::from_secs(60 * 60 * 24); +const LSPS2_CLIENT_TRUSTS_LSP_MODE: bool = true; +const LSPS2_CHANNEL_CLTV_EXPIRY_DELTA: u32 = 72; + struct LSPS1Client { lsp_node_id: PublicKey, lsp_address: SocketAddress, @@ -132,6 +140,7 @@ where lsps1_client: Option, lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, @@ -144,7 +153,7 @@ where L::Target: LdkLogger, { pub(crate) fn new( - channel_manager: Arc, keys_manager: Arc, + wallet: Arc, channel_manager: Arc, keys_manager: Arc, chain_source: Arc, config: Arc, logger: L, ) -> Self { let lsps1_client = None; @@ -154,6 +163,7 @@ where lsps1_client, lsps2_client, lsps2_service, + wallet, channel_manager, keys_manager, chain_source, @@ -232,7 +242,9 @@ where lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, lsps2_service: self.lsps2_service, + wallet: self.wallet, channel_manager: self.channel_manager, + peer_manager: RwLock::new(None), keys_manager: self.keys_manager, liquidity_manager, config: self.config, @@ -248,7 +260,9 @@ where lsps1_client: Option, lsps2_client: Option, lsps2_service: Option, + wallet: Arc, channel_manager: Arc, + peer_manager: RwLock>>, keys_manager: Arc, liquidity_manager: Arc, config: Arc, @@ -260,6 +274,7 @@ where L::Target: LdkLogger, { pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { + *self.peer_manager.write().unwrap() = Some(Arc::clone(&peer_manager)); let process_msgs_callback = move || peer_manager.process_events(); self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); } @@ -447,6 +462,254 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, + Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + if let Some(required) = service_config.require_token { + if token != Some(required) { + log_error!( + self.logger, + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + request_id, + counterparty_node_id + ); + lsps2_service_handler.invalid_token_provided(&counterparty_node_id, request_id.clone()).unwrap_or_else(|e| { + debug_assert!(false, "Failed to reject LSPS2 request. This should never happen."); + log_error!( + self.logger, + "Failed to reject LSPS2 request {:?} from counterparty {} due to: {:?}. This should never happen.", + request_id, + counterparty_node_id, + e + ); + }); + return; + } + } + + let mut valid_until: DateTime = Utc::now(); + valid_until += LSPS2_GETINFO_REQUEST_EXPIRY; + + let opening_fee_params = RawOpeningFeeParams { + min_fee_msat: service_config.min_channel_opening_fee_msat, + proportional: service_config.channel_opening_fee_ppm, + valid_until, + min_lifetime: service_config.min_channel_lifetime, + max_client_to_self_delay: service_config.max_client_to_self_delay, + min_payment_size_msat: service_config.min_payment_size_msat, + max_payment_size_msat: service_config.max_payment_size_msat, + }; + + let opening_fee_params_menu = vec![opening_fee_params]; + + if let Err(e) = lsps2_service_handler.opening_fee_params_generated( + &counterparty_node_id, + request_id, + opening_fee_params_menu, + ) { + log_error!( + self.logger, + "Failed to handle generated opening fee params: {:?}", + e + ); + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: _, + payment_size_msat, + }) => { + if let Some(lsps2_service_handler) = + self.liquidity_manager.lsps2_service_handler().as_ref() + { + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let user_channel_id: u128 = rand::thread_rng().gen::(); + let intercept_scid = self.channel_manager.get_intercept_scid(); + + if let Some(payment_size_msat) = payment_size_msat { + // We already check this in `lightning-liquidity`, but better safe than + // sorry. + // + // TODO: We might want to eventually send back an error here, but we + // currently can't and have to trust `lightning-liquidity` is doing the + // right thing. + // + // TODO: Eventually we also might want to make sure that we have sufficient + // liquidity for the channel opening here. + if payment_size_msat > service_config.max_payment_size_msat + || payment_size_msat < service_config.min_payment_size_msat + { + log_error!( + self.logger, + "Rejecting to handle LSPS2 buy request {:?} from counterparty {} as the client requested an invalid payment size.", + request_id, + counterparty_node_id + ); + return; + } + } + + match lsps2_service_handler.invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + LSPS2_CLIENT_TRUSTS_LSP_MODE, + user_channel_id, + ) { + Ok(()) => {}, + Err(e) => { + log_error!( + self.logger, + "Failed to provide invoice parameters: {:?}", + e + ); + return; + }, + } + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + } + }, + Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key, + amt_to_forward_msat, + opening_fee_msat: _, + user_channel_id, + intercept_scid: _, + }) => { + if self.liquidity_manager.lsps2_service_handler().is_none() { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let service_config = if let Some(service_config) = + self.lsps2_service.as_ref().map(|s| s.service_config.clone()) + { + service_config + } else { + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as LSPS2 liquidity service was not configured.",); + return; + }; + + let init_features = if let Some(peer_manager) = + self.peer_manager.read().unwrap().as_ref() + { + // Fail if we're not connected to the prospective channel partner. + if let Some(peer) = peer_manager.peer_by_node_id(&their_network_key) { + peer.init_features + } else { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {} due to peer not being not connected.", + their_network_key, + ); + return; + } + } else { + debug_assert!(false, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + log_error!(self.logger, "Failed to handle LSPS2ServiceEvent as peer manager isn't available. This should never happen.",); + return; + }; + + // Fail if we have insufficient onchain funds available. + let over_provisioning_msat = (amt_to_forward_msat + * service_config.channel_over_provisioning_ppm as u64) + / 1_000_000; + let channel_amount_sats = (amt_to_forward_msat + over_provisioning_msat) / 1000; + let cur_anchor_reserve_sats = + total_anchor_channels_reserve_sats(&self.channel_manager, &self.config); + let spendable_amount_sats = + self.wallet.get_spendable_amount_sats(cur_anchor_reserve_sats).unwrap_or(0); + let required_funds_sats = channel_amount_sats + + self.config.anchor_channels_config.as_ref().map_or(0, |c| { + if init_features.requires_anchors_zero_fee_htlc_tx() + && !c.trusted_peers_no_reserve.contains(&their_network_key) + { + c.per_channel_reserve_sats + } else { + 0 + } + }); + if spendable_amount_sats < required_funds_sats { + log_error!(self.logger, + "Unable to create channel due to insufficient funds. Available: {}sats, Required: {}sats", + spendable_amount_sats, channel_amount_sats + ); + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + return; + } + + let mut config = *self.channel_manager.get_current_default_configuration(); + + // Set the HTLC-value-in-flight to 100% of the channel value to ensure we can + // forward the payment. + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + + // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. + // + // TODO: revisit this decision eventually. + config.channel_config.forwarding_fee_base_msat = 0; + config.channel_config.forwarding_fee_proportional_millionths = 0; + + match self.channel_manager.create_channel( + their_network_key, + channel_amount_sats, + 0, + user_channel_id, + None, + Some(config), + ) { + Ok(_) => {}, + Err(e) => { + // TODO: We just silently fail here. Eventually we will need to remember + // the pending requests and regularly retry opening the channel until we + // succeed. + log_error!( + self.logger, + "Failed to open LSPS2 channel to {}: {:?}", + their_network_key, + e + ); + return; + }, + } + }, Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, From 1091952a1b32705c043db53c71af69fb64dfd320 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 14 Feb 2025 15:39:52 +0100 Subject: [PATCH 013/177] Add LSPS2 client<>service integration test --- tests/integration_tests_rust.rs | 114 ++++++++++++++++++++++++++++++-- 1 file changed, 110 insertions(+), 4 deletions(-) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 89fad1083b..56f5a0fbab 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -8,13 +8,14 @@ mod common; use common::{ - do_channel_full_cycle, expect_channel_ready_event, expect_event, expect_payment_received_event, - expect_payment_successful_event, generate_blocks_and_wait, open_channel, - premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, + expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, + setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; +use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, SendingParameters, @@ -1038,3 +1039,108 @@ fn unified_qr_send_receive() { assert_eq!(node_b.list_balances().total_onchain_balance_sats, 800_000); assert_eq!(node_b.list_balances().total_lightning_balance_sats, 200_000); } + +#[test] +fn lsps2_client_service_integration() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); + + let mut sync_config = EsploraSyncConfig::default(); + sync_config.onchain_wallet_sync_interval_secs = 100000; + sync_config.lightning_wallet_sync_interval_secs = 100000; + + // Setup three nodes: service, client, and payer + let channel_opening_fee_ppm = 10_000; + let channel_over_provisioning_ppm = 100_000; + let lsps2_service_config = LSPS2ServiceConfig { + require_token: None, + advertise_service: false, + channel_opening_fee_ppm, + channel_over_provisioning_ppm, + max_payment_size_msat: 1_000_000_000, + min_payment_size_msat: 0, + min_channel_lifetime: 100, + min_channel_opening_fee_msat: 0, + max_client_to_self_delay: 1024, + }; + + let service_config = random_config(true); + setup_builder!(service_builder, service_config); + service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + service_builder.set_liquidity_provider_lsps2(lsps2_service_config); + let service_node = service_builder.build().unwrap(); + service_node.start().unwrap(); + + let service_node_id = service_node.node_id(); + let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); + + let client_config = random_config(true); + setup_builder!(client_builder, client_config); + client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); + let client_node = client_builder.build().unwrap(); + client_node.start().unwrap(); + + let payer_config = random_config(true); + setup_builder!(payer_builder, payer_config); + payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); + let payer_node = payer_builder.build().unwrap(); + payer_node.start().unwrap(); + + let service_addr = service_node.onchain_payment().new_address().unwrap(); + let client_addr = client_node.onchain_payment().new_address().unwrap(); + let payer_addr = payer_node.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 10_000_000; + + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![service_addr, client_addr, payer_addr], + Amount::from_sat(premine_amount_sat), + ); + service_node.sync_wallets().unwrap(); + client_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + + // Open a channel payer -> service that will allow paying the JIT invoice + println!("Opening channel payer_node -> service_node!"); + open_channel(&payer_node, &service_node, 5_000_000, false, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + service_node.sync_wallets().unwrap(); + payer_node.sync_wallets().unwrap(); + expect_channel_ready_event!(payer_node, service_node.node_id()); + expect_channel_ready_event!(service_node, payer_node.node_id()); + + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let jit_amount_msat = 100_000_000; + + println!("Generating JIT invoice!"); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel(jit_amount_msat, &invoice_description.into(), 1024, None) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, expected_received_amount_msat); + + let expected_channel_overprovisioning_msat = + (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; + let expected_channel_size_sat = + (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; + let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; + assert_eq!(channel_value_sats, expected_channel_size_sat); +} From 56d5dc9568ac6993d5fe977cf4f930850295cb52 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Sun, 9 Feb 2025 00:24:53 +0100 Subject: [PATCH 014/177] test: introduce TestConfig and improve logging tests General improvements: - Add TestConfig wrapper around Config to allow per-test log field overrides while preserving the general test setup. - Test facade logger. - Add logging module and validate log entries. - Improve UniFFI logging adding UniFFI flag for relevant log objects. - Remove facade level, because the concrete `Log` impl controls max log level. --- bindings/ldk_node.udl | 2 +- src/builder.rs | 25 ++---- src/logger.rs | 16 ++-- tests/common/logging.rs | 146 ++++++++++++++++++++++++++++++++ tests/common/mod.rs | 51 +++++++---- tests/integration_tests_cln.rs | 2 +- tests/integration_tests_rust.rs | 36 ++++++-- tests/integration_tests_vss.rs | 4 +- 8 files changed, 225 insertions(+), 57 deletions(-) create mode 100644 tests/common/logging.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 805010356a..d2425d6e5e 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -74,7 +74,7 @@ interface Builder { void set_liquidity_source_lsps2(PublicKey node_id, SocketAddress address, string? token); void set_storage_dir_path(string storage_dir_path); void set_filesystem_logger(string? log_file_path, LogLevel? max_log_level); - void set_log_facade_logger(LogLevel? max_log_level); + void set_log_facade_logger(); void set_custom_logger(LogWriter log_writer); void set_network(Network network); [Throws=BuildError] diff --git a/src/builder.rs b/src/builder.rs index 97ff1ea211..07606eb57a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -113,7 +113,7 @@ struct LiquiditySourceConfig { #[derive(Clone)] enum LogWriterConfig { File { log_file_path: Option, max_log_level: Option }, - Log { max_log_level: Option }, + Log, Custom(Arc), } @@ -125,9 +125,7 @@ impl std::fmt::Debug for LogWriterConfig { .field("max_log_level", max_log_level) .field("log_file_path", log_file_path) .finish(), - LogWriterConfig::Log { max_log_level } => { - f.debug_tuple("Log").field(max_log_level).finish() - }, + LogWriterConfig::Log => write!(f, "LogWriterConfig::Log"), LogWriterConfig::Custom(_) => { f.debug_tuple("Custom").field(&"").finish() }, @@ -385,11 +383,8 @@ impl NodeBuilder { } /// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade. - /// - /// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to - /// [`DEFAULT_LOG_LEVEL`]. - pub fn set_log_facade_logger(&mut self, max_log_level: Option) -> &mut Self { - self.log_writer_config = Some(LogWriterConfig::Log { max_log_level }); + pub fn set_log_facade_logger(&mut self) -> &mut Self { + self.log_writer_config = Some(LogWriterConfig::Log); self } @@ -750,11 +745,8 @@ impl ArcedNodeBuilder { } /// Configures the [`Node`] instance to write logs to the [`log`](https://crates.io/crates/log) facade. - /// - /// If set, the `max_log_level` sets the maximum log level. Otherwise, the latter defaults to - /// [`DEFAULT_LOG_LEVEL`]. - pub fn set_log_facade_logger(&self, log_level: Option) { - self.inner.write().unwrap().set_log_facade_logger(log_level); + pub fn set_log_facade_logger(&self) { + self.inner.write().unwrap().set_log_facade_logger(); } /// Configures the [`Node`] instance to write logs to the provided custom [`LogWriter`]. @@ -1421,10 +1413,7 @@ fn setup_logger( Logger::new_fs_writer(log_file_path, max_log_level) .map_err(|_| BuildError::LoggerSetupFailed)? }, - Some(LogWriterConfig::Log { max_log_level }) => { - let max_log_level = max_log_level.unwrap_or_else(|| DEFAULT_LOG_LEVEL); - Logger::new_log_facade(max_log_level) - }, + Some(LogWriterConfig::Log) => Logger::new_log_facade(), Some(LogWriterConfig::Custom(custom_log_writer)) => { Logger::new_custom_writer(Arc::clone(&custom_log_writer)) diff --git a/src/logger.rs b/src/logger.rs index 7a329fae64..073aa92bcc 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -108,7 +108,7 @@ pub(crate) enum Writer { /// Writes logs to the file system. FileWriter { file_path: String, max_log_level: LogLevel }, /// Forwards logs to the `log` facade. - LogFacadeWriter { max_log_level: LogLevel }, + LogFacadeWriter, /// Forwards logs to a custom writer. CustomWriter(Arc), } @@ -138,10 +138,7 @@ impl LogWriter for Writer { .write_all(log.as_bytes()) .expect("Failed to write to log file") }, - Writer::LogFacadeWriter { max_log_level } => { - if record.level < *max_log_level { - return; - } + Writer::LogFacadeWriter => { macro_rules! log_with_level { ($log_level:expr, $target: expr, $($args:tt)*) => { match $log_level { @@ -186,8 +183,8 @@ impl Logger { Ok(Self { writer: Writer::FileWriter { file_path, max_log_level } }) } - pub fn new_log_facade(max_log_level: LogLevel) -> Self { - Self { writer: Writer::LogFacadeWriter { max_log_level } } + pub fn new_log_facade() -> Self { + Self { writer: Writer::LogFacadeWriter } } pub fn new_custom_writer(log_writer: Arc) -> Self { @@ -204,10 +201,7 @@ impl LdkLogger for Logger { } self.writer.log(record.into()); }, - Writer::LogFacadeWriter { max_log_level } => { - if record.level < *max_log_level { - return; - } + Writer::LogFacadeWriter => { self.writer.log(record.into()); }, Writer::CustomWriter(_arc) => { diff --git a/tests/common/logging.rs b/tests/common/logging.rs new file mode 100644 index 0000000000..5d89474da3 --- /dev/null +++ b/tests/common/logging.rs @@ -0,0 +1,146 @@ +use chrono::Utc; +#[cfg(not(feature = "uniffi"))] +use ldk_node::logger::LogRecord; +use ldk_node::logger::{LogLevel, LogWriter}; +#[cfg(not(feature = "uniffi"))] +use log::Record as LogFacadeRecord; +use log::{Level as LogFacadeLevel, LevelFilter as LogFacadeLevelFilter, Log as LogFacadeLog}; +use std::sync::{Arc, Mutex}; + +#[derive(Clone)] +pub(crate) enum TestLogWriter { + FileWriter, + LogFacade, + Custom(Arc), +} + +impl Default for TestLogWriter { + fn default() -> Self { + TestLogWriter::FileWriter + } +} + +pub(crate) struct MockLogFacadeLogger { + logs: Arc>>, +} + +impl MockLogFacadeLogger { + pub fn new() -> Self { + Self { logs: Arc::new(Mutex::new(Vec::new())) } + } + + pub fn retrieve_logs(&self) -> Vec { + self.logs.lock().unwrap().to_vec() + } +} + +impl LogFacadeLog for MockLogFacadeLogger { + fn enabled(&self, _metadata: &log::Metadata) -> bool { + true + } + + fn log(&self, record: &log::Record) { + let message = format!( + "{} {:<5} [{}:{}] {}", + Utc::now().format("%Y-%m-%d %H:%M:%S"), + record.level().to_string(), + record.module_path().unwrap(), + record.line().unwrap(), + record.args() + ); + println!("{message}"); + self.logs.lock().unwrap().push(message); + } + + fn flush(&self) {} +} + +#[cfg(not(feature = "uniffi"))] +impl LogWriter for MockLogFacadeLogger { + fn log<'a>(&self, record: LogRecord) { + let record = MockLogRecord(record).into(); + LogFacadeLog::log(self, &record); + } +} + +#[cfg(not(feature = "uniffi"))] +struct MockLogRecord<'a>(LogRecord<'a>); +struct MockLogLevel(LogLevel); + +impl From for LogFacadeLevel { + fn from(level: MockLogLevel) -> Self { + match level.0 { + LogLevel::Gossip | LogLevel::Trace => LogFacadeLevel::Trace, + LogLevel::Debug => LogFacadeLevel::Debug, + LogLevel::Info => LogFacadeLevel::Info, + LogLevel::Warn => LogFacadeLevel::Warn, + LogLevel::Error => LogFacadeLevel::Error, + } + } +} + +#[cfg(not(feature = "uniffi"))] +impl<'a> From> for LogFacadeRecord<'a> { + fn from(log_record: MockLogRecord<'a>) -> Self { + let log_record = log_record.0; + let level = MockLogLevel(log_record.level).into(); + + let mut record_builder = LogFacadeRecord::builder(); + let record = record_builder + .level(level) + .module_path(Some(&log_record.module_path)) + .line(Some(log_record.line)) + .args(log_record.args); + + record.build() + } +} + +pub(crate) fn init_log_logger(level: LogFacadeLevelFilter) -> Arc { + let logger = Arc::new(MockLogFacadeLogger::new()); + log::set_boxed_logger(Box::new(logger.clone())).unwrap(); + log::set_max_level(level); + + logger +} + +pub(crate) fn validate_log_entry(entry: &String) { + let parts = entry.splitn(4, ' ').collect::>(); + assert_eq!(parts.len(), 4); + let (day, time, level, path_and_msg) = (parts[0], parts[1], parts[2], parts[3]); + + let day_parts = day.split('-').collect::>(); + assert_eq!(day_parts.len(), 3); + let (year, month, day) = (day_parts[0], day_parts[1], day_parts[2]); + assert!(year.len() == 4 && month.len() == 2 && day.len() == 2); + assert!( + year.chars().all(|c| c.is_digit(10)) + && month.chars().all(|c| c.is_digit(10)) + && day.chars().all(|c| c.is_digit(10)) + ); + + let time_parts = time.split(':').collect::>(); + assert_eq!(time_parts.len(), 3); + let (hour, minute, second) = (time_parts[0], time_parts[1], time_parts[2]); + assert!(hour.len() == 2 && minute.len() == 2 && second.len() == 2); + assert!( + hour.chars().all(|c| c.is_digit(10)) + && minute.chars().all(|c| c.is_digit(10)) + && second.chars().all(|c| c.is_digit(10)) + ); + + assert!(["GOSSIP", "TRACE", "DEBUG", "INFO", "WARN", "ERROR"].contains(&level),); + + let path = path_and_msg.split_whitespace().next().unwrap(); + assert!(path.contains('[') && path.contains(']')); + let module_path = &path[1..path.len() - 1]; + let path_parts = module_path.rsplitn(2, ':').collect::>(); + assert_eq!(path_parts.len(), 2); + let (line_number, module_name) = (path_parts[0], path_parts[1]); + assert!(module_name.chars().all(|c| c.is_alphanumeric() || c == '_' || c == ':')); + assert!(line_number.chars().all(|c| c.is_digit(10))); + + let msg_start_index = path_and_msg.find(']').unwrap() + 1; + let msg = &path_and_msg[msg_start_index..]; + assert!(!msg.is_empty()); +} diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2ad7368239..9687bc3dfc 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -8,9 +8,12 @@ #![cfg(any(test, cln_test, vss_test))] #![allow(dead_code)] +pub(crate) mod logging; + +use logging::TestLogWriter; + use ldk_node::config::{Config, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; -use ldk_node::logger::LogLevel; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, @@ -221,29 +224,29 @@ pub(crate) fn random_node_alias() -> Option { Some(NodeAlias(bytes)) } -pub(crate) fn random_config(anchor_channels: bool) -> Config { - let mut config = Config::default(); +pub(crate) fn random_config(anchor_channels: bool) -> TestConfig { + let mut node_config = Config::default(); if !anchor_channels { - config.anchor_channels_config = None; + node_config.anchor_channels_config = None; } - config.network = Network::Regtest; - println!("Setting network: {}", config.network); + node_config.network = Network::Regtest; + println!("Setting network: {}", node_config.network); let rand_dir = random_storage_path(); println!("Setting random LDK storage dir: {}", rand_dir.display()); - config.storage_dir_path = rand_dir.to_str().unwrap().to_owned(); + node_config.storage_dir_path = rand_dir.to_str().unwrap().to_owned(); let rand_listening_addresses = random_listening_addresses(); println!("Setting random LDK listening addresses: {:?}", rand_listening_addresses); - config.listening_addresses = Some(rand_listening_addresses); + node_config.listening_addresses = Some(rand_listening_addresses); let alias = random_node_alias(); println!("Setting random LDK node alias: {:?}", alias); - config.node_alias = alias; + node_config.node_alias = alias; - config + TestConfig { node_config, ..Default::default() } } #[cfg(feature = "uniffi")] @@ -257,6 +260,12 @@ pub(crate) enum TestChainSource<'a> { BitcoindRpc(&'a BitcoinD), } +#[derive(Clone, Default)] +pub(crate) struct TestConfig { + pub node_config: Config, + pub log_writer: TestLogWriter, +} + macro_rules! setup_builder { ($builder: ident, $config: expr) => { #[cfg(feature = "uniffi")] @@ -279,10 +288,11 @@ pub(crate) fn setup_two_nodes( println!("\n== Node B =="); let mut config_b = random_config(anchor_channels); if allow_0conf { - config_b.trusted_peers_0conf.push(node_a.node_id()); + config_b.node_config.trusted_peers_0conf.push(node_a.node_id()); } if anchor_channels && anchors_trusted_no_reserve { config_b + .node_config .anchor_channels_config .as_mut() .unwrap() @@ -294,9 +304,9 @@ pub(crate) fn setup_two_nodes( } pub(crate) fn setup_node( - chain_source: &TestChainSource, config: Config, seed_bytes: Option>, + chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, ) -> TestNode { - setup_builder!(builder, config); + setup_builder!(builder, config.node_config); match chain_source { TestChainSource::Esplora(electrsd) => { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); @@ -315,14 +325,23 @@ pub(crate) fn setup_node( }, } - let log_file_path = format!("{}/{}", config.storage_dir_path, "ldk_node.log"); - builder.set_filesystem_logger(Some(log_file_path), Some(LogLevel::Gossip)); + match &config.log_writer { + TestLogWriter::FileWriter => { + builder.set_filesystem_logger(None, None); + }, + TestLogWriter::LogFacade => { + builder.set_log_facade_logger(); + }, + TestLogWriter::Custom(custom_log_writer) => { + builder.set_custom_logger(Arc::clone(custom_log_writer)); + }, + } if let Some(seed) = seed_bytes { builder.set_entropy_seed_bytes(seed).unwrap(); } - let test_sync_store = Arc::new(TestSyncStore::new(config.storage_dir_path.into())); + let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); assert!(node.status().is_running); diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 02d091215c..875922ce20 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -45,7 +45,7 @@ fn test_cln() { // Setup LDK Node let config = common::random_config(true); - let mut builder = Builder::from_config(config); + let mut builder = Builder::from_config(config.node_config); builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); let node = builder.build().unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 56f5a0fbab..8dc0ca50ac 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -10,6 +10,7 @@ mod common; use common::{ do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; @@ -30,6 +31,7 @@ use bitcoincore_rpc::RpcApi; use bitcoin::hashes::Hash; use bitcoin::Amount; use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use log::LevelFilter; use std::sync::Arc; @@ -128,7 +130,7 @@ fn multi_hop_sending() { let mut sync_config = EsploraSyncConfig::default(); sync_config.onchain_wallet_sync_interval_secs = 100000; sync_config.lightning_wallet_sync_interval_secs = 100000; - setup_builder!(builder, config); + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let node = builder.build().unwrap(); node.start().unwrap(); @@ -223,12 +225,12 @@ fn start_stop_reinit() { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let test_sync_store: Arc = - Arc::new(TestSyncStore::new(config.storage_dir_path.clone().into())); + Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.clone().into())); let mut sync_config = EsploraSyncConfig::default(); sync_config.onchain_wallet_sync_interval_secs = 100000; sync_config.lightning_wallet_sync_interval_secs = 100000; - setup_builder!(builder, config); + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); @@ -252,7 +254,7 @@ fn start_stop_reinit() { node.sync_wallets().unwrap(); assert_eq!(node.list_balances().spendable_onchain_balance_sats, expected_amount.to_sat()); - let log_file = format!("{}/ldk_node.log", config.clone().storage_dir_path); + let log_file = format!("{}/ldk_node.log", config.node_config.clone().storage_dir_path); assert!(std::path::Path::new(&log_file).exists()); node.stop().unwrap(); @@ -265,7 +267,7 @@ fn start_stop_reinit() { assert_eq!(node.stop(), Err(NodeError::NotRunning)); drop(node); - setup_builder!(builder, config); + setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let reinitialized_node = builder.build_with_store(Arc::clone(&test_sync_store)).unwrap(); @@ -1066,7 +1068,7 @@ fn lsps2_client_service_integration() { }; let service_config = random_config(true); - setup_builder!(service_builder, service_config); + setup_builder!(service_builder, service_config.node_config); service_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); service_builder.set_liquidity_provider_lsps2(lsps2_service_config); let service_node = service_builder.build().unwrap(); @@ -1076,14 +1078,14 @@ fn lsps2_client_service_integration() { let service_addr = service_node.listening_addresses().unwrap().first().unwrap().clone(); let client_config = random_config(true); - setup_builder!(client_builder, client_config); + setup_builder!(client_builder, client_config.node_config); client_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); client_builder.set_liquidity_source_lsps2(service_node_id, service_addr, None); let client_node = client_builder.build().unwrap(); client_node.start().unwrap(); let payer_config = random_config(true); - setup_builder!(payer_builder, payer_config); + setup_builder!(payer_builder, payer_config.node_config); payer_builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let payer_node = payer_builder.build().unwrap(); payer_node.start().unwrap(); @@ -1144,3 +1146,21 @@ fn lsps2_client_service_integration() { let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; assert_eq!(channel_value_sats, expected_channel_size_sat); } + +#[test] +fn facade_logging() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let logger = init_log_logger(LevelFilter::Trace); + let mut config = random_config(false); + config.log_writer = TestLogWriter::LogFacade; + + println!("== Facade logging starts =="); + let _node = setup_node(&chain_source, config, None); + + assert!(!logger.retrieve_logs().is_empty()); + for (_, entry) in logger.retrieve_logs().iter().enumerate() { + validate_log_entry(entry); + } +} diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 525c1f1f17..9d6ec158c9 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -18,7 +18,7 @@ fn channel_full_cycle_with_vss_store() { println!("== Node A =="); let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); let config_a = common::random_config(true); - let mut builder_a = Builder::from_config(config_a); + let mut builder_a = Builder::from_config(config_a.node_config); 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 @@ -32,7 +32,7 @@ fn channel_full_cycle_with_vss_store() { println!("\n== Node B =="); let config_b = common::random_config(true); - let mut builder_b = Builder::from_config(config_b); + 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( From 7cf9ef9a23690827e73d5957ddfec627a348fc3b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 18 Mar 2025 10:18:32 +0100 Subject: [PATCH 015/177] Have `Builder::set_entropy_seed_bytes` take `[u8; 64]` for non-`uniffi` Historically, we aligned `uniffi` and non-`uniffi` APIs as much as possible (mostly because there are no rendered docs available for the latter). As the APIs have diverged a bit by now anyways, we here have `Builder::set_entropy_seed_bytes` take a `[u8; 64]`, which makes for a better API in Rust. --- src/builder.rs | 29 +++++++++++++++++------------ src/config.rs | 4 ++-- tests/common/mod.rs | 11 ++++++++++- 3 files changed, 29 insertions(+), 15 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 07606eb57a..7ab2ff889d 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -245,15 +245,11 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its wallet entropy from the given 64 seed bytes. - pub fn set_entropy_seed_bytes(&mut self, seed_bytes: Vec) -> Result<&mut Self, BuildError> { - if seed_bytes.len() != WALLET_KEYS_SEED_LEN { - return Err(BuildError::InvalidSeedBytes); - } - let mut bytes = [0u8; WALLET_KEYS_SEED_LEN]; - bytes.copy_from_slice(&seed_bytes); - self.entropy_source_config = Some(EntropySourceConfig::SeedBytes(bytes)); - Ok(self) + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. + pub fn set_entropy_seed_bytes(&mut self, seed_bytes: [u8; WALLET_KEYS_SEED_LEN]) -> &mut Self { + self.entropy_source_config = Some(EntropySourceConfig::SeedBytes(seed_bytes)); + self } /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. @@ -637,11 +633,20 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_entropy_seed_path(seed_path); } - /// Configures the [`Node`] instance to source its wallet entropy from the given 64 seed bytes. + /// Configures the [`Node`] instance to source its wallet entropy from the given + /// [`WALLET_KEYS_SEED_LEN`] seed bytes. /// - /// **Note:** Panics if the length of the given `seed_bytes` differs from 64. + /// **Note:** Will return an error if the length of the given `seed_bytes` differs from + /// [`WALLET_KEYS_SEED_LEN`]. pub fn set_entropy_seed_bytes(&self, seed_bytes: Vec) -> Result<(), BuildError> { - self.inner.write().unwrap().set_entropy_seed_bytes(seed_bytes).map(|_| ()) + if seed_bytes.len() != WALLET_KEYS_SEED_LEN { + return Err(BuildError::InvalidSeedBytes); + } + let mut bytes = [0u8; WALLET_KEYS_SEED_LEN]; + bytes.copy_from_slice(&seed_bytes); + + self.inner.write().unwrap().set_entropy_seed_bytes(bytes); + Ok(()) } /// Configures the [`Node`] instance to source its wallet entropy from a [BIP 39] mnemonic. diff --git a/src/config.rs b/src/config.rs index 146ac3bd20..d5094d31cb 100644 --- a/src/config.rs +++ b/src/config.rs @@ -78,8 +78,8 @@ pub(crate) const TX_BROADCAST_TIMEOUT_SECS: u64 = 5; // The timeout after which we abort a RGS sync operation. pub(crate) const RGS_SYNC_TIMEOUT_SECS: u64 = 5; -// The length in bytes of our wallets' keys seed. -pub(crate) const WALLET_KEYS_SEED_LEN: usize = 64; +/// The length in bytes of our wallets' keys seed. +pub const WALLET_KEYS_SEED_LEN: usize = 64; #[derive(Debug, Clone)] /// Represents the configuration of an [`Node`] instance. diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 9687bc3dfc..1ab219f14b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -338,7 +338,16 @@ pub(crate) fn setup_node( } if let Some(seed) = seed_bytes { - builder.set_entropy_seed_bytes(seed).unwrap(); + #[cfg(feature = "uniffi")] + { + builder.set_entropy_seed_bytes(seed).unwrap(); + } + #[cfg(not(feature = "uniffi"))] + { + let mut bytes = [0u8; 64]; + bytes.copy_from_slice(&seed); + builder.set_entropy_seed_bytes(bytes); + } } let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); From 171a3580dcdc5d2e2b8916ce5435e7f7694befc8 Mon Sep 17 00:00:00 2001 From: elnosh Date: Wed, 19 Mar 2025 16:46:12 -0500 Subject: [PATCH 016/177] Handle persistence failure in event_handled from Node --- bindings/ldk_node.udl | 1 + src/lib.rs | 8 ++++---- tests/common/mod.rs | 18 +++++++++--------- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d2425d6e5e..caeb419754 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -104,6 +104,7 @@ interface Node { Event wait_next_event(); [Async] Event next_event_async(); + [Throws=NodeError] void event_handled(); PublicKey node_id(); sequence? listening_addresses(); diff --git a/src/lib.rs b/src/lib.rs index 8d616353a8..a31b3701ad 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -781,15 +781,15 @@ impl Node { /// Confirm the last retrieved event handled. /// /// **Note:** This **MUST** be called after each event has been handled. - pub fn event_handled(&self) { - self.event_queue.event_handled().unwrap_or_else(|e| { + pub fn event_handled(&self) -> Result<(), Error> { + self.event_queue.event_handled().map_err(|e| { log_error!( self.logger, "Couldn't mark event handled due to persistence failure: {}", e ); - panic!("Couldn't mark event handled due to persistence failure"); - }); + e + }) } /// Returns our own node id diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1ab219f14b..0949cadd3c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -53,7 +53,7 @@ macro_rules! expect_event { match $node.wait_next_event() { ref e @ Event::$event_type { .. } => { println!("{} got event {:?}", $node.node_id(), e); - $node.event_handled(); + $node.event_handled().unwrap(); }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!($node), e); @@ -70,7 +70,7 @@ macro_rules! expect_channel_pending_event { ref e @ Event::ChannelPending { funding_txo, counterparty_node_id, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(counterparty_node_id, $counterparty_node_id); - $node.event_handled(); + $node.event_handled().unwrap(); funding_txo }, ref e => { @@ -88,7 +88,7 @@ macro_rules! expect_channel_ready_event { ref e @ Event::ChannelReady { user_channel_id, counterparty_node_id, .. } => { println!("{} got event {:?}", $node.node_id(), e); assert_eq!(counterparty_node_id, Some($counterparty_node_id)); - $node.event_handled(); + $node.event_handled().unwrap(); user_channel_id }, ref e => { @@ -110,7 +110,7 @@ macro_rules! expect_payment_received_event { if !matches!(payment.kind, PaymentKind::Onchain { .. }) { assert_eq!(payment.fee_paid_msat, None); } - $node.event_handled(); + $node.event_handled().unwrap(); payment_id }, ref e => { @@ -135,7 +135,7 @@ macro_rules! expect_payment_claimable_event { assert_eq!(payment_hash, $payment_hash); assert_eq!(payment_id, $payment_id); assert_eq!(claimable_amount_msat, $claimable_amount_msat); - $node.event_handled(); + $node.event_handled().unwrap(); claimable_amount_msat }, ref e => { @@ -158,7 +158,7 @@ macro_rules! expect_payment_successful_event { let payment = $node.payment(&$payment_id.unwrap()).unwrap(); assert_eq!(payment.fee_paid_msat, fee_paid_msat); assert_eq!(payment_id, $payment_id); - $node.event_handled(); + $node.event_handled().unwrap(); }, ref e => { panic!("{} got unexpected event!: {:?}", std::stringify!(node_b), e); @@ -707,7 +707,7 @@ pub(crate) fn do_channel_full_cycle( let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); amount_msat }, ref e => { @@ -745,7 +745,7 @@ pub(crate) fn do_channel_full_cycle( let received_amount = match node_b.wait_next_event() { ref e @ Event::PaymentReceived { amount_msat, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); amount_msat }, ref e => { @@ -868,7 +868,7 @@ pub(crate) fn do_channel_full_cycle( let (received_keysend_amount, received_custom_records) = match next_event { ref e @ Event::PaymentReceived { amount_msat, ref custom_records, .. } => { println!("{} got event {:?}", std::stringify!(node_b), e); - node_b.event_handled(); + node_b.event_handled().unwrap(); (amount_msat, custom_records) }, ref e => { From ca49cb7d82bb013dc2123a2c5709deacb75d8696 Mon Sep 17 00:00:00 2001 From: elnosh Date: Mon, 17 Mar 2025 07:19:06 -0500 Subject: [PATCH 017/177] Add `NetworkMismatch` error type Return specific error if the node is restarted with a different network from the one it was first created with. This will be caught when trying to load the `BDK` wallet. --- bindings/ldk_node.udl | 1 + src/builder.rs | 27 ++++++++++++++++++++++++--- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index caeb419754..55529684d8 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -322,6 +322,7 @@ enum BuildError { "KVStoreSetupFailed", "WalletSetupFailed", "LoggerSetupFailed", + "NetworkMismatch", }; [Trait] diff --git a/src/builder.rs b/src/builder.rs index 7ab2ff889d..b1f222770b 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -168,6 +168,8 @@ pub enum BuildError { WalletSetupFailed, /// We failed to setup the logger. LoggerSetupFailed, + /// The given network does not match the node's previously configured network. + NetworkMismatch, } impl fmt::Display for BuildError { @@ -189,6 +191,9 @@ impl fmt::Display for BuildError { Self::WalletSetupFailed => write!(f, "Failed to setup onchain wallet."), Self::LoggerSetupFailed => write!(f, "Failed to setup the logger."), Self::InvalidNodeAlias => write!(f, "Given node alias is invalid."), + Self::NetworkMismatch => { + write!(f, "Given network does not match the node's previously configured network.") + }, } } } @@ -904,9 +909,25 @@ fn build_with_store_internal( .extract_keys() .check_network(config.network) .load_wallet(&mut wallet_persister) - .map_err(|e| { - log_error!(logger, "Failed to set up wallet: {}", e); - BuildError::WalletSetupFailed + .map_err(|e| match e { + bdk_wallet::LoadWithPersistError::InvalidChangeSet( + bdk_wallet::LoadError::Mismatch(bdk_wallet::LoadMismatch::Network { + loaded, + expected, + }), + ) => { + log_error!( + logger, + "Failed to setup wallet: Networks do not match. Expected {} but got {}", + expected, + loaded + ); + BuildError::NetworkMismatch + }, + _ => { + log_error!(logger, "Failed to set up wallet: {}", e); + BuildError::WalletSetupFailed + }, })?; let bdk_wallet = match wallet_opt { Some(wallet) => wallet, From 9a38ab096bd280113fb3185088169709506f1a42 Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Wed, 26 Mar 2025 15:28:14 -0300 Subject: [PATCH 018/177] feat: add Docker Compose for LND, Bitcoind, and Electrs Sets up a local environment for LND integration testing in CI Closes 505 --- docker-compose-lnd.yml | 87 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100755 docker-compose-lnd.yml diff --git a/docker-compose-lnd.yml b/docker-compose-lnd.yml new file mode 100755 index 0000000000..a11f4cac18 --- /dev/null +++ b/docker-compose-lnd.yml @@ -0,0 +1,87 @@ +services: + bitcoin: + image: blockstream/bitcoind:24.1 + platform: linux/amd64 + command: + [ + "bitcoind", + "-printtoconsole", + "-regtest=1", + "-rpcallowip=0.0.0.0/0", + "-rpcbind=0.0.0.0", + "-rpcuser=user", + "-rpcpassword=pass", + "-fallbackfee=0.00001", + "-zmqpubrawblock=tcp://0.0.0.0:28332", + "-zmqpubrawtx=tcp://0.0.0.0:28333" + ] + ports: + - "18443:18443" # Regtest RPC port + - "18444:18444" # Regtest P2P port + - "28332:28332" # ZMQ block port + - "28333:28333" # ZMQ tx port + networks: + - bitcoin-electrs + healthcheck: + test: ["CMD", "bitcoin-cli", "-regtest", "-rpcuser=user", "-rpcpassword=pass", "getblockchaininfo"] + interval: 5s + timeout: 10s + retries: 5 + + electrs: + image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + platform: linux/amd64 + depends_on: + bitcoin: + condition: service_healthy + command: + [ + "/app/electrs_bitcoin/bin/electrs", + "-vvvv", + "--timestamp", + "--jsonrpc-import", + "--cookie=user:pass", + "--network=regtest", + "--daemon-rpc-addr=bitcoin:18443", + "--http-addr=0.0.0.0:3002", + "--electrum-rpc-addr=0.0.0.0:50001" + ] + ports: + - "3002:3002" + - "50001:50001" + networks: + - bitcoin-electrs + + lnd: + image: lightninglabs/lnd:v0.18.5-beta + container_name: ldk-node-lnd + depends_on: + - bitcoin + volumes: + - ${LND_DATA_DIR}:/root/.lnd + ports: + - "8081:8081" + - "9735:9735" + command: + - "--noseedbackup" + - "--trickledelay=5000" + - "--alias=ldk-node-lnd-test" + - "--externalip=lnd:9735" + - "--bitcoin.active" + - "--bitcoin.regtest" + - "--bitcoin.node=bitcoind" + - "--bitcoind.rpchost=bitcoin:18443" + - "--bitcoind.rpcuser=user" + - "--bitcoind.rpcpass=pass" + - "--bitcoind.zmqpubrawblock=tcp://bitcoin:28332" + - "--bitcoind.zmqpubrawtx=tcp://bitcoin:28333" + - "--accept-keysend" + - "--rpclisten=0.0.0.0:8081" + - "--tlsextradomain=lnd" + - "--tlsextraip=0.0.0.0" + networks: + - bitcoin-electrs + +networks: + bitcoin-electrs: + driver: bridge \ No newline at end of file From 19f4e2ccba270f29351ee3a78db739121a98ce62 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Mar 2025 10:04:48 +0100 Subject: [PATCH 019/177] Add `counterparty_skimmed_fee_msat` field to `PaymentKind::Bolt11Jit` .. we add a field describing exactly how much the LSP withheld. --- bindings/ldk_node.udl | 2 +- src/event.rs | 20 ++++++++++++++ src/payment/bolt11.rs | 1 + src/payment/store.rs | 47 +++++++++++++++++++++++++++++++-- tests/integration_tests_rust.rs | 10 ++++++- 5 files changed, 76 insertions(+), 4 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d2425d6e5e..cecd564ccd 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -385,7 +385,7 @@ interface ClosureReason { interface PaymentKind { Onchain(Txid txid, ConfirmationStatus status); Bolt11(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret); - Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, LSPFeeLimits lsp_fee_limits); + Bolt11Jit(PaymentHash hash, PaymentPreimage? preimage, PaymentSecret? secret, u64? counterparty_skimmed_fee_msat, LSPFeeLimits lsp_fee_limits); Bolt12Offer(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, OfferId offer_id, UntrustedString? payer_note, u64? quantity); Bolt12Refund(PaymentHash? hash, PaymentPreimage? preimage, PaymentSecret? secret, UntrustedString? payer_note, u64? quantity); Spontaneous(PaymentHash hash, PaymentPreimage? preimage); diff --git a/src/event.rs b/src/event.rs index f0f9765698..c0028d2202 100644 --- a/src/event.rs +++ b/src/event.rs @@ -659,6 +659,26 @@ where }; } + // If the LSP skimmed anything, update our stored payment. + if counterparty_skimmed_fee_msat > 0 { + match info.kind { + PaymentKind::Bolt11Jit { .. } => { + let update = PaymentDetailsUpdate { + counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)), + ..PaymentDetailsUpdate::new(payment_id) + }; + match self.payment_store.update(&update) { + Ok(_) => (), + Err(e) => { + log_error!(self.logger, "Failed to access payment store: {}", e); + return Err(ReplayEvent()); + }, + }; + } + _ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for JIT payments."), + } + } + // If this is known by the store but ChannelManager doesn't know the preimage, // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 49a5ecb6be..38226911f4 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -664,6 +664,7 @@ impl Bolt11Payment { hash: payment_hash, preimage, secret: Some(payment_secret.clone()), + counterparty_skimmed_fee_msat: None, lsp_fee_limits, }; let payment = PaymentDetails::new( diff --git a/src/payment/store.rs b/src/payment/store.rs index 8a9222912f..25bdf69ce3 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -165,6 +165,18 @@ impl PaymentDetails { update_if_necessary!(self.fee_paid_msat, fee_paid_msat_opt); } + if let Some(skimmed_fee_msat) = update.counterparty_skimmed_fee_msat { + match self.kind { + PaymentKind::Bolt11Jit { ref mut counterparty_skimmed_fee_msat, .. } => { + update_if_necessary!(*counterparty_skimmed_fee_msat, skimmed_fee_msat); + }, + _ => debug_assert!( + false, + "We should only ever override counterparty_skimmed_fee_msat for JIT payments" + ), + } + } + if let Some(status) = update.status { update_if_necessary!(self.status, status); } @@ -257,7 +269,14 @@ impl Readable for PaymentDetails { if secret.is_some() { if let Some(lsp_fee_limits) = lsp_fee_limits { - PaymentKind::Bolt11Jit { hash, preimage, secret, lsp_fee_limits } + let counterparty_skimmed_fee_msat = None; + PaymentKind::Bolt11Jit { + hash, + preimage, + secret, + counterparty_skimmed_fee_msat, + lsp_fee_limits, + } } else { PaymentKind::Bolt11 { hash, preimage, secret } } @@ -346,6 +365,12 @@ pub enum PaymentKind { preimage: Option, /// The secret used by the payment. secret: Option, + /// The value, in thousands of a satoshi, that was deducted from this payment as an extra + /// fee taken by our channel counterparty. + /// + /// Will only be `Some` once we received the payment. Will always be `None` for LDK Node + /// v0.4 and prior. + counterparty_skimmed_fee_msat: Option, /// Limits applying to how much fee we allow an LSP to deduct from the payment amount. /// /// Allowing them to deduct this fee from the first inbound payment will pay for the LSP's @@ -423,6 +448,7 @@ impl_writeable_tlv_based_enum!(PaymentKind, }, (4, Bolt11Jit) => { (0, hash, required), + (1, counterparty_skimmed_fee_msat, option), (2, preimage, option), (4, secret, option), (6, lsp_fee_limits, required), @@ -501,6 +527,7 @@ pub(crate) struct PaymentDetailsUpdate { pub secret: Option>, pub amount_msat: Option>, pub fee_paid_msat: Option>, + pub counterparty_skimmed_fee_msat: Option>, pub direction: Option, pub status: Option, pub confirmation_status: Option, @@ -515,6 +542,7 @@ impl PaymentDetailsUpdate { secret: None, amount_msat: None, fee_paid_msat: None, + counterparty_skimmed_fee_msat: None, direction: None, status: None, confirmation_status: None, @@ -538,6 +566,13 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { _ => None, }; + let counterparty_skimmed_fee_msat = match value.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + Some(counterparty_skimmed_fee_msat) + }, + _ => None, + }; + Self { id: value.id, hash: Some(hash), @@ -545,6 +580,7 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { secret: Some(secret), amount_msat: Some(value.amount_msat), fee_paid_msat: Some(value.fee_paid_msat), + counterparty_skimmed_fee_msat, direction: Some(value.direction), status: Some(value.status), confirmation_status, @@ -841,10 +877,17 @@ mod tests { ); match bolt11_jit_decoded.kind { - PaymentKind::Bolt11Jit { hash: h, preimage: p, secret: s, lsp_fee_limits: l } => { + PaymentKind::Bolt11Jit { + hash: h, + preimage: p, + secret: s, + counterparty_skimmed_fee_msat: c, + lsp_fee_limits: l, + } => { assert_eq!(hash, h); assert_eq!(preimage, p); assert_eq!(secret, s); + assert_eq!(None, c); assert_eq!(lsp_fee_limits, Some(l)); }, _ => { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 8dc0ca50ac..bd74bf105b 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1137,7 +1137,15 @@ fn lsps2_client_service_integration() { let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; let expected_received_amount_msat = jit_amount_msat - service_fee_msat; expect_payment_successful_event!(payer_node, Some(payment_id), None); - expect_payment_received_event!(client_node, expected_received_amount_msat); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } let expected_channel_overprovisioning_msat = (expected_received_amount_msat * channel_over_provisioning_ppm as u64) / 1_000_000; From 13c6d0ed8c3c845cc1255b46dba87bfb280ee365 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Mar 2025 10:12:16 +0100 Subject: [PATCH 020/177] Clarify when `PaymentDetails::amount_msat` might be `None` --- src/payment/store.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/payment/store.rs b/src/payment/store.rs index 25bdf69ce3..367f5a5bd2 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -41,6 +41,8 @@ pub struct PaymentDetails { /// The kind of the payment. pub kind: PaymentKind, /// The amount transferred. + /// + /// Will be `None` for variable-amount payments until we receive them. pub amount_msat: Option, /// The fees that were paid for this payment. /// From bbe8b1e5aedf94c6330fe2d0e233edfa3f696a9d Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Tue, 25 Mar 2025 16:39:05 +0100 Subject: [PATCH 021/177] Allow disabling background wallet synchronization Implements BackgroundSyncConfig to control background wallet synchronization behavior. - Allows users to explicitly disable background wallet synchronization - Sets intervals to minimum threshold if specified value is below it Context: Some users have setups where they need to rely exclusively on manual syncing. Previously, they had to set intervals to u64::MAX as a workaround to effectively disable background syncing. This implementation provides a cleaner, explicit approach for controlling sync behavior when manual syncing via sync_wallets() is preferred. Breaking change: Existing implementations may need updates to accommodate the new configuration structure. Closes #310 --- bindings/ldk_node.udl | 6 +- src/chain/mod.rs | 107 ++++++++++++++++++-------------- src/config.rs | 35 +++++++++-- src/lib.rs | 13 ++-- src/uniffi_types.rs | 3 +- tests/common/mod.rs | 4 +- tests/integration_tests_rust.rs | 12 +--- 7 files changed, 105 insertions(+), 75 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index caeb419754..4b72cb56ba 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -19,12 +19,16 @@ dictionary AnchorChannelsConfig { u64 per_channel_reserve_sats; }; -dictionary EsploraSyncConfig { +dictionary BackgroundSyncConfig { u64 onchain_wallet_sync_interval_secs; u64 lightning_wallet_sync_interval_secs; u64 fee_rate_cache_update_interval_secs; }; +dictionary EsploraSyncConfig { + BackgroundSyncConfig? background_sync_config; +}; + dictionary LSPS2ServiceConfig { string? require_token; boolean advertise_service; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index d96ab06efc..a2e01884a9 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -207,57 +207,68 @@ impl ChainSource { ) { match self { Self::Esplora { sync_config, logger, .. } => { - // Setup syncing intervals - let onchain_wallet_sync_interval_secs = sync_config - .onchain_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut onchain_wallet_sync_interval = - tokio::time::interval(Duration::from_secs(onchain_wallet_sync_interval_secs)); - onchain_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let fee_rate_cache_update_interval_secs = sync_config - .fee_rate_cache_update_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut fee_rate_update_interval = - tokio::time::interval(Duration::from_secs(fee_rate_cache_update_interval_secs)); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let lightning_wallet_sync_interval_secs = sync_config - .lightning_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut lightning_wallet_sync_interval = - tokio::time::interval(Duration::from_secs(lightning_wallet_sync_interval_secs)); - lightning_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + // Setup syncing intervals if enabled + if let Some(background_sync_config) = sync_config.background_sync_config { + let onchain_wallet_sync_interval_secs = background_sync_config + .onchain_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut onchain_wallet_sync_interval = tokio::time::interval( + Duration::from_secs(onchain_wallet_sync_interval_secs), + ); + onchain_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let fee_rate_cache_update_interval_secs = background_sync_config + .fee_rate_cache_update_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut fee_rate_update_interval = tokio::time::interval(Duration::from_secs( + fee_rate_cache_update_interval_secs, + )); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let lightning_wallet_sync_interval_secs = background_sync_config + .lightning_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut lightning_wallet_sync_interval = tokio::time::interval( + Duration::from_secs(lightning_wallet_sync_interval_secs), + ); + lightning_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - // Start the syncing loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - logger, - "Stopping background syncing on-chain wallet.", - ); - return; - } - _ = onchain_wallet_sync_interval.tick() => { - let _ = self.sync_onchain_wallet().await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - _ = lightning_wallet_sync_interval.tick() => { - let _ = self.sync_lightning_wallet( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&output_sweeper), - ).await; + // Start the syncing loop. + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + logger, + "Stopping background syncing on-chain wallet.", + ); + return; + } + _ = onchain_wallet_sync_interval.tick() => { + let _ = self.sync_onchain_wallet().await; + } + _ = fee_rate_update_interval.tick() => { + let _ = self.update_fee_rate_estimates().await; + } + _ = lightning_wallet_sync_interval.tick() => { + let _ = self.sync_lightning_wallet( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper), + ).await; + } } } + } else { + // Background syncing is disabled + log_info!( + logger, "Background syncing disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", + ); + return; } }, Self::BitcoindRpc { diff --git a/src/config.rs b/src/config.rs index d5094d31cb..f04ff62437 100644 --- a/src/config.rs +++ b/src/config.rs @@ -282,7 +282,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config } -/// Options related to syncing the Lightning and on-chain wallets via an Esplora backend. +/// Options related to background syncing the Lightning and on-chain wallets. /// /// ### Defaults /// @@ -292,22 +292,24 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { /// | `lightning_wallet_sync_interval_secs` | 30 | /// | `fee_rate_cache_update_interval_secs` | 600 | #[derive(Debug, Copy, Clone, PartialEq, Eq)] -pub struct EsploraSyncConfig { +pub struct BackgroundSyncConfig { /// The time in-between background sync attempts of the onchain wallet, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub onchain_wallet_sync_interval_secs: u64, + /// The time in-between background sync attempts of the LDK wallet, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub lightning_wallet_sync_interval_secs: u64, + /// The time in-between background update attempts to our fee rate cache, in seconds. /// - /// **Note:** A minimum of 10 seconds is always enforced. + /// **Note:** A minimum of 10 seconds is enforced when background syncing is enabled. pub fee_rate_cache_update_interval_secs: u64, } -impl Default for EsploraSyncConfig { +impl Default for BackgroundSyncConfig { fn default() -> Self { Self { onchain_wallet_sync_interval_secs: DEFAULT_BDK_WALLET_SYNC_INTERVAL_SECS, @@ -317,6 +319,27 @@ impl Default for EsploraSyncConfig { } } +/// Configuration for syncing with an Esplora backend. +/// +/// Background syncing is enabled by default, using the default values specified in +/// [`BackgroundSyncConfig`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct EsploraSyncConfig { + /// Background sync configuration. + /// + /// If set to `None`, background syncing will be disabled. Users will need to manually + /// sync via `Node::sync_wallets` for the wallets and fee rate updates. + /// + /// [`Node::sync_wallets`]: crate::Node::sync_wallets + pub background_sync_config: Option, +} + +impl Default for EsploraSyncConfig { + fn default() -> Self { + Self { background_sync_config: Some(BackgroundSyncConfig::default()) } + } +} + /// Options which apply on a per-channel basis and may change at runtime or based on negotiation /// with our counterparty. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index a31b3701ad..4eb1afe032 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1219,14 +1219,13 @@ impl Node { /// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate /// cache. /// - /// **Note:** The wallets are regularly synced in the background, which is configurable via the - /// respective config object, e.g., via - /// [`EsploraSyncConfig::onchain_wallet_sync_interval_secs`] and - /// [`EsploraSyncConfig::lightning_wallet_sync_interval_secs`]. Therefore, using this blocking - /// sync method is almost always redundant and should be avoided where possible. + /// **Note:** The wallets are regularly synced in the background if background syncing is enabled + /// via [`EsploraSyncConfig::background_sync_config`]. Therefore, using this blocking sync method + /// is almost always redundant when background syncing is enabled and should be avoided where possible. + /// However, if background syncing is disabled (i.e., `background_sync_config` is set to `None`), + /// this method must be called manually to keep wallets in sync with the chain state. /// - /// [`EsploraSyncConfig::onchain_wallet_sync_interval_secs`]: crate::config::EsploraSyncConfig::onchain_wallet_sync_interval_secs - /// [`EsploraSyncConfig::lightning_wallet_sync_interval_secs`]: crate::config::EsploraSyncConfig::lightning_wallet_sync_interval_secs + /// [`EsploraSyncConfig::background_sync_config`]: crate::config::EsploraSyncConfig::background_sync_config pub fn sync_wallets(&self) -> Result<(), Error> { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 58c577f8ee..410ce85312 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -11,7 +11,8 @@ // Make sure to add any re-exported items that need to be used in uniffi below. pub use crate::config::{ - default_config, AnchorChannelsConfig, EsploraSyncConfig, MaxDustHTLCExposure, + default_config, AnchorChannelsConfig, BackgroundSyncConfig, EsploraSyncConfig, + MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0949cadd3c..aac18225b6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -310,9 +310,7 @@ pub(crate) fn setup_node( match chain_source { TestChainSource::Esplora(electrsd) => { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; + let sync_config = EsploraSyncConfig { background_sync_config: None }; builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); }, TestChainSource::BitcoindRpc(bitcoind) => { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 8dc0ca50ac..a4d2c42338 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -127,9 +127,7 @@ fn multi_hop_sending() { let mut nodes = Vec::new(); for _ in 0..5 { let config = random_config(true); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; + let sync_config = EsploraSyncConfig { background_sync_config: None }; setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); let node = builder.build().unwrap(); @@ -227,9 +225,7 @@ fn start_stop_reinit() { let test_sync_store: Arc = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.clone().into())); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; + let sync_config = EsploraSyncConfig { background_sync_config: None }; setup_builder!(builder, config.node_config); builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); @@ -1048,9 +1044,7 @@ fn lsps2_client_service_integration() { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let mut sync_config = EsploraSyncConfig::default(); - sync_config.onchain_wallet_sync_interval_secs = 100000; - sync_config.lightning_wallet_sync_interval_secs = 100000; + let sync_config = EsploraSyncConfig { background_sync_config: None }; // Setup three nodes: service, client, and payer let channel_opening_fee_ppm = 10_000; From fb9243a2872e74c553c8660aa173c23ce5bcf688 Mon Sep 17 00:00:00 2001 From: Ayla Greystone Date: Fri, 14 Mar 2025 16:22:52 -0400 Subject: [PATCH 022/177] Add configuration for node announcement addresses In certain node configurations a user might want or need to announce different addresses to the network than the ones they bind their tcp listeners to. --- bindings/ldk_node.udl | 5 +++++ src/builder.rs | 32 ++++++++++++++++++++++++++++++++ src/config.rs | 13 +++++++++++++ src/lib.rs | 14 ++++++++++++-- 4 files changed, 62 insertions(+), 2 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c6ffa168d1..1e45263473 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -7,6 +7,7 @@ dictionary Config { string storage_dir_path; Network network; sequence? listening_addresses; + sequence? announcement_addresses; NodeAlias? node_alias; sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; @@ -84,6 +85,8 @@ interface Builder { [Throws=BuildError] void set_listening_addresses(sequence listening_addresses); [Throws=BuildError] + void set_announcement_addresses(sequence announcement_addresses); + [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] Node build(); @@ -112,6 +115,7 @@ interface Node { void event_handled(); PublicKey node_id(); sequence? listening_addresses(); + sequence? announcement_addresses(); NodeAlias? node_alias(); Bolt11Payment bolt11_payment(); Bolt12Payment bolt12_payment(); @@ -319,6 +323,7 @@ enum BuildError { "InvalidSystemTime", "InvalidChannelMonitor", "InvalidListeningAddresses", + "InvalidAnnouncementAddresses", "InvalidNodeAlias", "ReadFailed", "WriteFailed", diff --git a/src/builder.rs b/src/builder.rs index b1f222770b..94cf614712 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -148,6 +148,8 @@ pub enum BuildError { InvalidChannelMonitor, /// The given listening addresses are invalid, e.g. too many were passed. InvalidListeningAddresses, + /// The given announcement addresses are invalid, e.g. too many were passed. + InvalidAnnouncementAddresses, /// The provided alias is invalid. InvalidNodeAlias, /// We failed to read data from the [`KVStore`]. @@ -184,6 +186,9 @@ impl fmt::Display for BuildError { write!(f, "Failed to watch a deserialized ChannelMonitor") }, Self::InvalidListeningAddresses => write!(f, "Given listening addresses are invalid."), + Self::InvalidAnnouncementAddresses => { + write!(f, "Given announcement addresses are invalid.") + }, Self::ReadFailed => write!(f, "Failed to read from store."), Self::WriteFailed => write!(f, "Failed to write to store."), Self::StoragePathAccessFailed => write!(f, "Failed to access the given storage path."), @@ -413,6 +418,22 @@ impl NodeBuilder { Ok(self) } + /// Sets the IP address and TCP port which [`Node`] will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Self::set_listening_addresses + pub fn set_announcement_addresses( + &mut self, announcement_addresses: Vec, + ) -> Result<&mut Self, BuildError> { + if announcement_addresses.len() > 100 { + return Err(BuildError::InvalidAnnouncementAddresses); + } + + self.config.announcement_addresses = Some(announcement_addresses); + Ok(self) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// @@ -776,6 +797,17 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_listening_addresses(listening_addresses).map(|_| ()) } + /// Sets the IP address and TCP port which [`Node`] will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Self::set_listening_addresses + pub fn set_announcement_addresses( + &self, announcement_addresses: Vec, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_announcement_addresses(announcement_addresses).map(|_| ()) + } + /// Sets the node alias that will be used when broadcasting announcements to the gossip /// network. /// diff --git a/src/config.rs b/src/config.rs index f04ff62437..674a55984a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -117,6 +117,12 @@ pub struct Config { /// **Note**: We will only allow opening and accepting public channels if the `node_alias` and the /// `listening_addresses` are set. pub listening_addresses: Option>, + /// The addresses which the node will announce to the gossip network that it accepts connections on. + /// + /// **Note**: If unset, the [`listening_addresses`] will be used as the list of addresses to announce. + /// + /// [`listening_addresses`]: Config::listening_addresses + pub announcement_addresses: Option>, /// The node alias that will be used when broadcasting announcements to the gossip network. /// /// The provided alias must be a valid UTF-8 string and no longer than 32 bytes in total. @@ -168,6 +174,7 @@ impl Default for Config { storage_dir_path: DEFAULT_STORAGE_DIR_PATH.to_string(), network: DEFAULT_NETWORK, listening_addresses: None, + announcement_addresses: None, trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), @@ -480,6 +487,12 @@ mod tests { node_config.node_alias = Some(alias_frm_str("LDK_Node")); assert!(!may_announce_channel(&node_config)); + // Set announcement addresses with listening addresses unset + let announcement_address = SocketAddress::from_str("123.45.67.89:9735") + .expect("Socket address conversion failed."); + node_config.announcement_addresses = Some(vec![announcement_address]); + assert!(!may_announce_channel(&node_config)); + // Set node alias with an empty list of listening addresses node_config.listening_addresses = Some(vec![]); assert!(!may_announce_channel(&node_config)); diff --git a/src/lib.rs b/src/lib.rs index 4eb1afe032..e18432a693 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -457,8 +457,10 @@ impl Node { continue; } - let addresses = if let Some(addresses) = bcast_config.listening_addresses.clone() { - addresses + let addresses = if let Some(announcement_addresses) = bcast_config.announcement_addresses.clone() { + announcement_addresses + } else if let Some(listening_addresses) = bcast_config.listening_addresses.clone() { + listening_addresses } else { debug_assert!(false, "We checked whether the node may announce, so listening addresses should always be set"); continue; @@ -802,6 +804,14 @@ impl Node { self.config.listening_addresses.clone() } + /// Returns the addresses that the node will announce to the network. + pub fn announcement_addresses(&self) -> Option> { + self.config + .announcement_addresses + .clone() + .or_else(|| self.config.listening_addresses.clone()) + } + /// Returns our node alias. pub fn node_alias(&self) -> Option { self.config.node_alias From e2607440700ec2372cb64f1974bd87b919b5bab4 Mon Sep 17 00:00:00 2001 From: Ayla Greystone Date: Thu, 27 Mar 2025 12:47:53 -0400 Subject: [PATCH 023/177] Refactor may_announce_channel to return AnnounceError This error type can provide more context to callers about why the node configuration does not support channel announcements. --- src/builder.rs | 24 ++++++++++++++++---- src/config.rs | 60 ++++++++++++++++++++++++++++++++++++++++++-------- src/event.rs | 30 ++++++++++++------------- src/lib.rs | 24 ++++++++++---------- 4 files changed, 97 insertions(+), 41 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 94cf614712..7881f38e21 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,8 +7,8 @@ use crate::chain::{ChainSource, DEFAULT_ESPLORA_SERVER_URL}; use crate::config::{ - default_user_config, Config, EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, - WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AnnounceError, Config, EsploraSyncConfig, + DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; @@ -32,8 +32,7 @@ use crate::types::{ }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; -use crate::Node; -use crate::{io, NodeMetrics}; +use crate::{io, Node, NodeMetrics}; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::io::Cursor; @@ -912,6 +911,23 @@ fn build_with_store_internal( liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], logger: Arc, kv_store: Arc, ) -> Result { + if let Err(err) = may_announce_channel(&config) { + if config.announcement_addresses.is_some() { + log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err); + let build_error = if matches!(err, AnnounceError::MissingNodeAlias) { + BuildError::InvalidNodeAlias + } else { + BuildError::InvalidListeningAddresses + }; + return Err(build_error); + } + + if config.node_alias.is_some() { + log_error!(logger, "Node alias was set but some required configuration options for node announcement are missing: {}", err); + return Err(BuildError::InvalidListeningAddresses); + } + } + // Initialize the status fields. let is_listening = Arc::new(AtomicBool::new(false)); let node_metrics = match read_node_metrics(Arc::clone(&kv_store), Arc::clone(&logger)) { diff --git a/src/config.rs b/src/config.rs index 674a55984a..46b5284883 100644 --- a/src/config.rs +++ b/src/config.rs @@ -19,6 +19,7 @@ use lightning::util::config::UserConfig; use bitcoin::secp256k1::PublicKey; use bitcoin::Network; +use std::fmt; use std::time::Duration; // Config defaults @@ -263,9 +264,37 @@ pub fn default_config() -> Config { Config::default() } -pub(crate) fn may_announce_channel(config: &Config) -> bool { - config.node_alias.is_some() - && config.listening_addresses.as_ref().map_or(false, |addrs| !addrs.is_empty()) +#[derive(Debug, PartialEq)] +pub(crate) enum AnnounceError { + MissingNodeAlias, + MissingListeningAddresses, + MissingAliasAndAddresses, +} + +impl fmt::Display for AnnounceError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnnounceError::MissingNodeAlias => write!(f, "Node alias is not configured"), + AnnounceError::MissingListeningAddresses => { + write!(f, "Listening addresses are not configured") + }, + AnnounceError::MissingAliasAndAddresses => { + write!(f, "Node alias and listening addresses are not configured") + }, + } + } +} + +pub(crate) fn may_announce_channel(config: &Config) -> Result<(), AnnounceError> { + let has_listening_addresses = + config.listening_addresses.as_ref().map_or(false, |addrs| !addrs.is_empty()); + + match (config.node_alias.is_some(), has_listening_addresses) { + (true, true) => Ok(()), + (true, false) => Err(AnnounceError::MissingListeningAddresses), + (false, true) => Err(AnnounceError::MissingNodeAlias), + (false, false) => Err(AnnounceError::MissingAliasAndAddresses), + } } pub(crate) fn default_user_config(config: &Config) -> UserConfig { @@ -280,7 +309,7 @@ pub(crate) fn default_user_config(config: &Config) -> UserConfig { user_config.channel_handshake_config.negotiate_anchors_zero_fee_htlc_tx = config.anchor_channels_config.is_some(); - if !may_announce_channel(config) { + if may_announce_channel(config).is_err() { user_config.accept_forwards_to_priv_channels = false; user_config.channel_handshake_config.announce_for_forwarding = false; user_config.channel_handshake_limits.force_announced_channel_preference = true; @@ -468,6 +497,7 @@ mod tests { use std::str::FromStr; use super::may_announce_channel; + use super::AnnounceError; use super::Config; use super::NodeAlias; use super::SocketAddress; @@ -476,7 +506,10 @@ mod tests { fn node_announce_channel() { // Default configuration with node alias and listening addresses unset let mut node_config = Config::default(); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingAliasAndAddresses) + ); // Set node alias with listening addresses unset let alias_frm_str = |alias: &str| { @@ -485,17 +518,26 @@ mod tests { NodeAlias(bytes) }; node_config.node_alias = Some(alias_frm_str("LDK_Node")); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); // Set announcement addresses with listening addresses unset let announcement_address = SocketAddress::from_str("123.45.67.89:9735") .expect("Socket address conversion failed."); node_config.announcement_addresses = Some(vec![announcement_address]); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); // Set node alias with an empty list of listening addresses node_config.listening_addresses = Some(vec![]); - assert!(!may_announce_channel(&node_config)); + assert_eq!( + may_announce_channel(&node_config), + Err(AnnounceError::MissingListeningAddresses) + ); // Set node alias with a non-empty list of listening addresses let socket_address = @@ -503,6 +545,6 @@ mod tests { if let Some(ref mut addresses) = node_config.listening_addresses { addresses.push(socket_address); } - assert!(may_announce_channel(&node_config)); + assert!(may_announce_channel(&node_config).is_ok()); } } diff --git a/src/event.rs b/src/event.rs index c0028d2202..0de59b8585 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1077,23 +1077,21 @@ where is_announced, params: _, } => { - if is_announced && !may_announce_channel(&*self.config) { - log_error!( - self.logger, - "Rejecting inbound announced channel from peer {} as not all required details are set. Please ensure node alias and listening addresses have been configured.", - counterparty_node_id, - ); + if is_announced { + if let Err(err) = may_announce_channel(&*self.config) { + log_error!(self.logger, "Rejecting inbound announced channel from peer {} due to missing configuration: {}", counterparty_node_id, err); - self.channel_manager - .force_close_without_broadcasting_txn( - &temporary_channel_id, - &counterparty_node_id, - "Channel request rejected".to_string(), - ) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to reject channel: {:?}", e) - }); - return Ok(()); + self.channel_manager + .force_close_without_broadcasting_txn( + &temporary_channel_id, + &counterparty_node_id, + "Channel request rejected".to_string(), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to reject channel: {:?}", e) + }); + return Ok(()); + } } let anchor_channel = channel_type.requires_anchors_zero_fee_htlc_tx(); diff --git a/src/lib.rs b/src/lib.rs index e18432a693..a80db1e8c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -414,7 +414,7 @@ impl Node { let bcast_node_metrics = Arc::clone(&self.node_metrics); let mut stop_bcast = self.stop_sender.subscribe(); let node_alias = self.config.node_alias.clone(); - if may_announce_channel(&self.config) { + if may_announce_channel(&self.config).is_ok() { runtime.spawn(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. #[cfg(not(test))] @@ -1211,19 +1211,19 @@ impl Node { &self, node_id: PublicKey, address: SocketAddress, channel_amount_sats: u64, push_to_counterparty_msat: Option, channel_config: Option, ) -> Result { - if may_announce_channel(&self.config) { - self.open_channel_inner( - node_id, - address, - channel_amount_sats, - push_to_counterparty_msat, - channel_config, - true, - ) - } else { - log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node. Please make sure to configure listening addreesses and node alias"); + if let Err(err) = may_announce_channel(&self.config) { + log_error!(self.logger, "Failed to open announced channel as the node hasn't been sufficiently configured to act as a forwarding node: {}", err); return Err(Error::ChannelCreationFailed); } + + self.open_channel_inner( + node_id, + address, + channel_amount_sats, + push_to_counterparty_msat, + channel_config, + true, + ) } /// Manually sync the LDK and BDK wallets with the current chain state and update the fee rate From 5432ed8847ff56fc6b0db1b6fb2b72b2644b21fd Mon Sep 17 00:00:00 2001 From: Ayla Greystone Date: Fri, 28 Mar 2025 09:48:14 -0400 Subject: [PATCH 024/177] Add test for announcement address propagation A new integration test that configures nodes using the new announcement addresses configuration. Ensures that both nodes receive the correct alias and address for nodes using and not using this new feature. --- tests/integration_tests_rust.rs | 97 ++++++++++++++++++++++++++++++++- 1 file changed, 95 insertions(+), 2 deletions(-) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index dd5820650a..a41bca25cf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -11,8 +11,9 @@ use common::{ do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, logging::{init_log_logger, validate_log_entry, TestLogWriter}, - open_channel, premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, - setup_builder, setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + open_channel, premine_and_distribute_funds, random_config, random_listening_addresses, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, + TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; @@ -24,6 +25,7 @@ use ldk_node::payment::{ use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; +use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::util::persist::KVStore; use bitcoincore_rpc::RpcApi; @@ -885,6 +887,97 @@ fn simple_bolt12_send_receive() { assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } +#[test] +fn test_node_announcement_propagation() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + // Node A will use both listening and announcement addresses + let mut config_a = random_config(true); + let node_a_alias_string = "ldk-node-a".to_string(); + let mut node_a_alias_bytes = [0u8; 32]; + node_a_alias_bytes[..node_a_alias_string.as_bytes().len()] + .copy_from_slice(node_a_alias_string.as_bytes()); + let node_a_node_alias = Some(NodeAlias(node_a_alias_bytes)); + let node_a_announcement_addresses = random_listening_addresses(); + config_a.node_config.node_alias = node_a_node_alias.clone(); + config_a.node_config.listening_addresses = Some(random_listening_addresses()); + config_a.node_config.announcement_addresses = Some(node_a_announcement_addresses.clone()); + + // Node B will only use listening addresses + let mut config_b = random_config(true); + let node_b_alias_string = "ldk-node-b".to_string(); + let mut node_b_alias_bytes = [0u8; 32]; + node_b_alias_bytes[..node_b_alias_string.as_bytes().len()] + .copy_from_slice(node_b_alias_string.as_bytes()); + let node_b_node_alias = Some(NodeAlias(node_b_alias_bytes)); + let node_b_listening_addresses = random_listening_addresses(); + config_b.node_config.node_alias = node_b_node_alias.clone(); + config_b.node_config.listening_addresses = Some(node_b_listening_addresses.clone()); + config_b.node_config.announcement_addresses = None; + + let node_a = setup_node(&chain_source, config_a, None); + let node_b = setup_node(&chain_source, config_b, None); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 5_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_amount_sat), + ); + + node_a.sync_wallets().unwrap(); + + // Open an announced channel from node_a to node_b + open_channel(&node_a, &node_b, 4_000_000, true, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Wait until node_b broadcasts a node announcement + while node_b.status().latest_node_announcement_broadcast_timestamp.is_none() { + std::thread::sleep(std::time::Duration::from_millis(10)); + } + + // Sleep to make sure the node announcement propagates + std::thread::sleep(std::time::Duration::from_secs(1)); + + // Get node info from the other node's perspective + let node_a_info = node_b.network_graph().node(&NodeId::from_pubkey(&node_a.node_id())).unwrap(); + let node_a_announcement_info = node_a_info.announcement_info.as_ref().unwrap(); + + let node_b_info = node_a.network_graph().node(&NodeId::from_pubkey(&node_b.node_id())).unwrap(); + let node_b_announcement_info = node_b_info.announcement_info.as_ref().unwrap(); + + // Assert that the aliases and addresses match the expected values + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.alias(), &node_a_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.alias, node_a_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_a_announcement_info.addresses(), &node_a_announcement_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_a_announcement_info.addresses, node_a_announcement_addresses); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.alias(), &node_b_node_alias.unwrap()); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.alias, node_b_alias_string); + + #[cfg(not(feature = "uniffi"))] + assert_eq!(node_b_announcement_info.addresses(), &node_b_listening_addresses); + #[cfg(feature = "uniffi")] + assert_eq!(node_b_announcement_info.addresses, node_b_listening_addresses); +} + #[test] fn generate_bip21_uri() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From da5a839a22cdec7265e6c57c079dc998ea581a37 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 21 Mar 2025 17:03:10 +0100 Subject: [PATCH 025/177] Allow forwarding to unannounced channels if LSP service is enabled Previously, we'd always disallow forwarding to unannounced channels. However, if we're acting as an LSP, we *should* allow forwarding to unannounced channels. --- src/builder.rs | 8 ++++++++ src/liquidity.rs | 13 ++++++++----- tests/integration_tests_rust.rs | 16 ++++++++++++++++ 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 7ab2ff889d..c8fb848fcd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1090,6 +1090,14 @@ fn build_with_store_internal( // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the // information to the service handler. user_config.accept_intercept_htlcs = true; + + // If we act as an LSPS2 service, we allow forwarding to unnannounced channels. + user_config.accept_forwards_to_priv_channels = true; + + // If we act as an LSPS2 service, set the HTLC-value-in-flight to 100% of the channel value + // to ensure we can forward the initial payment. + user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = + 100; } let message_router = diff --git a/src/liquidity.rs b/src/liquidity.rs index a7751026ba..a47c23c812 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -675,11 +675,14 @@ where let mut config = *self.channel_manager.get_current_default_configuration(); - // Set the HTLC-value-in-flight to 100% of the channel value to ensure we can - // forward the payment. - config - .channel_handshake_config - .max_inbound_htlc_value_in_flight_percent_of_channel = 100; + // We set these LSP-specific values during Node building, here we're making sure it's actually set. + debug_assert_eq!( + config + .channel_handshake_config + .max_inbound_htlc_value_in_flight_percent_of_channel, + 100 + ); + debug_assert!(config.accept_forwards_to_priv_channels); // We set the forwarding fee to 0 for now as we're getting paid by the channel fee. // diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 8dc0ca50ac..3c406a3a99 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1145,6 +1145,22 @@ fn lsps2_client_service_integration() { (expected_received_amount_msat + expected_channel_overprovisioning_msat) / 1000; let channel_value_sats = client_node.list_channels().first().unwrap().channel_value_sats; assert_eq!(channel_value_sats, expected_channel_size_sat); + + println!("Generating regular invoice!"); + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + let amount_msat = 5_000_000; + let invoice = client_node + .bolt11_payment() + .receive(amount_msat, &invoice_description.into(), 1024) + .unwrap(); + + // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node + // are working as expected. + println!("Paying regular invoice!"); + let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_payment_received_event!(client_node, amount_msat); } #[test] From 8d81a4c99d8074a4d21895f51eb98ea5353d0d1b Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Wed, 26 Mar 2025 01:36:04 -0300 Subject: [PATCH 026/177] feat: implement LND integration tests with lnd_grpc_rust Adds tests to: - Open a payment channel with LND. - Request and pay an invoice, verifying receipt. - Generate an invoice for LND to pay, verifying receipt. Uses lnd_grpc_rust for gRPC communication. Closes #505 --- Cargo.toml | 5 + tests/common/mod.rs | 2 +- tests/integration_tests_lnd.rs | 227 +++++++++++++++++++++++++++++++++ 3 files changed, 233 insertions(+), 1 deletion(-) mode change 100644 => 100755 Cargo.toml create mode 100755 tests/integration_tests_lnd.rs diff --git a/Cargo.toml b/Cargo.toml old mode 100644 new mode 100755 index d89ad28e2f..5481c76ddd --- a/Cargo.toml +++ b/Cargo.toml @@ -106,6 +106,10 @@ electrsd = { version = "0.29.0", features = ["legacy"] } [target.'cfg(cln_test)'.dev-dependencies] clightningrpc = { version = "0.3.0-beta.8", default-features = false } +[target.'cfg(lnd_test)'.dev-dependencies] +lnd_grpc_rust = { version = "2.10.0", default-features = false } +tokio = { version = "1.37", features = ["fs"] } + [build-dependencies] uniffi = { version = "0.27.3", features = ["build"], optional = true } @@ -123,4 +127,5 @@ check-cfg = [ "cfg(ldk_bench)", "cfg(tokio_unstable)", "cfg(cln_test)", + "cfg(lnd_test)", ] diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0949cadd3c..f8bc62b809 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -#![cfg(any(test, cln_test, vss_test))] +#![cfg(any(test, cln_test, lnd_test, vss_test))] #![allow(dead_code)] pub(crate) mod logging; diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs new file mode 100755 index 0000000000..feb981d8e6 --- /dev/null +++ b/tests/integration_tests_lnd.rs @@ -0,0 +1,227 @@ +#![cfg(lnd_test)] + +mod common; + +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Amount; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::{Builder, Event}; + +use lnd_grpc_rust::lnrpc::{ + invoice::InvoiceState::Settled as LndInvoiceStateSettled, GetInfoRequest as LndGetInfoRequest, + GetInfoResponse as LndGetInfoResponse, Invoice as LndInvoice, + ListInvoiceRequest as LndListInvoiceRequest, QueryRoutesRequest as LndQueryRoutesRequest, + Route as LndRoute, SendRequest as LndSendRequest, +}; +use lnd_grpc_rust::{connect, LndClient}; + +use bitcoincore_rpc::Auth; +use bitcoincore_rpc::Client as BitcoindClient; + +use electrum_client::Client as ElectrumClient; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; + +use bitcoin::hex::DisplayHex; + +use std::default::Default; +use std::str::FromStr; +use tokio::fs; + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn test_lnd() { + // Setup bitcoind / electrs clients + let bitcoind_client = BitcoindClient::new( + "127.0.0.1:18443", + Auth::UserPass("user".to_string(), "pass".to_string()), + ) + .unwrap(); + let electrs_client = ElectrumClient::new("tcp://127.0.0.1:50001").unwrap(); + + // Give electrs a kick. + common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 1); + + // Setup LDK Node + let config = common::random_config(true); + let mut builder = Builder::from_config(config.node_config); + builder.set_chain_source_esplora("http://127.0.0.1:3002".to_string(), None); + + let node = builder.build().unwrap(); + node.start().unwrap(); + + // Premine some funds and distribute + let address = node.onchain_payment().new_address().unwrap(); + let premine_amount = Amount::from_sat(5_000_000); + common::premine_and_distribute_funds( + &bitcoind_client, + &electrs_client, + vec![address], + premine_amount, + ); + + // Setup LND + let endpoint = "127.0.0.1:8081"; + let cert_path = std::env::var("LND_CERT_PATH").expect("LND_CERT_PATH not set"); + let macaroon_path = std::env::var("LND_MACAROON_PATH").expect("LND_MACAROON_PATH not set"); + let mut lnd = TestLndClient::new(cert_path, macaroon_path, endpoint.to_string()).await; + + let lnd_node_info = lnd.get_node_info().await; + let lnd_node_id = PublicKey::from_str(&lnd_node_info.identity_pubkey).unwrap(); + let lnd_address: SocketAddress = "127.0.0.1:9735".parse().unwrap(); + + node.sync_wallets().unwrap(); + + // Open the channel + let funding_amount_sat = 1_000_000; + + node.open_channel(lnd_node_id, lnd_address, funding_amount_sat, Some(500_000_000), None) + .unwrap(); + + let funding_txo = common::expect_channel_pending_event!(node, lnd_node_id); + common::wait_for_tx(&electrs_client, funding_txo.txid); + common::generate_blocks_and_wait(&bitcoind_client, &electrs_client, 6); + node.sync_wallets().unwrap(); + let user_channel_id = common::expect_channel_ready_event!(node, lnd_node_id); + + // Send a payment to LND + let lnd_invoice = lnd.create_invoice(100_000_000).await; + let parsed_invoice = lightning_invoice::Bolt11Invoice::from_str(&lnd_invoice).unwrap(); + + node.bolt11_payment().send(&parsed_invoice, None).unwrap(); + common::expect_event!(node, PaymentSuccessful); + let lnd_listed_invoices = lnd.list_invoices().await; + assert_eq!(lnd_listed_invoices.len(), 1); + assert_eq!(lnd_listed_invoices.first().unwrap().state, LndInvoiceStateSettled as i32); + + // Check route LND -> LDK + let amount_msat = 9_000_000; + let max_retries = 7; + for attempt in 1..=max_retries { + match lnd.query_routes(&node.node_id().to_string(), amount_msat).await { + Ok(routes) => { + if !routes.is_empty() { + break; + } + }, + Err(err) => { + if attempt == max_retries { + panic!("Failed to find route from LND to LDK: {}", err); + } + }, + }; + // wait for the payment process + tokio::time::sleep(std::time::Duration::from_millis(200)).await; + } + + // Send a payment to LDK + let invoice_description = + Bolt11InvoiceDescription::Direct(Description::new("lndTest".to_string()).unwrap()); + let ldk_invoice = + node.bolt11_payment().receive(amount_msat, &invoice_description, 3600).unwrap(); + lnd.pay_invoice(&ldk_invoice.to_string()).await; + common::expect_event!(node, PaymentReceived); + + node.close_channel(&user_channel_id, lnd_node_id).unwrap(); + common::expect_event!(node, ChannelClosed); + node.stop().unwrap(); +} + +struct TestLndClient { + client: LndClient, +} + +impl TestLndClient { + async fn new(cert_path: String, macaroon_path: String, socket: String) -> Self { + // Read the contents of the file into a vector of bytes + let cert_bytes = fs::read(cert_path).await.expect("Failed to read tls cert file"); + let mac_bytes = fs::read(macaroon_path).await.expect("Failed to read macaroon file"); + + // Convert the bytes to a hex string + let cert = cert_bytes.as_hex().to_string(); + let macaroon = mac_bytes.as_hex().to_string(); + + let client = connect(cert, macaroon, socket).await.expect("Failed to connect to Lnd"); + + TestLndClient { client } + } + + async fn get_node_info(&mut self) -> LndGetInfoResponse { + let response = self + .client + .lightning() + .get_info(LndGetInfoRequest {}) + .await + .expect("Failed to fetch node info from LND") + .into_inner(); + + response + } + + async fn create_invoice(&mut self, amount_msat: u64) -> String { + let invoice = LndInvoice { value_msat: amount_msat as i64, ..Default::default() }; + + self.client + .lightning() + .add_invoice(invoice) + .await + .expect("Failed to create invoice on LND") + .into_inner() + .payment_request + } + + async fn list_invoices(&mut self) -> Vec { + self.client + .lightning() + .list_invoices(LndListInvoiceRequest { ..Default::default() }) + .await + .expect("Failed to list invoices from LND") + .into_inner() + .invoices + } + + async fn query_routes( + &mut self, pubkey: &str, amount_msat: u64, + ) -> Result, String> { + let request = LndQueryRoutesRequest { + pub_key: pubkey.to_string(), + amt_msat: amount_msat as i64, + ..Default::default() + }; + + let response = self + .client + .lightning() + .query_routes(request) + .await + .map_err(|err| format!("Failed to query routes from LND: {:?}", err))? + .into_inner(); + + if response.routes.is_empty() { + return Err(format!("No routes found for pubkey: {}", pubkey)); + } + + Ok(response.routes) + } + + async fn pay_invoice(&mut self, invoice_str: &str) { + let send_req = + LndSendRequest { payment_request: invoice_str.to_string(), ..Default::default() }; + let response = self + .client + .lightning() + .send_payment_sync(send_req) + .await + .expect("Failed to pay invoice on LND") + .into_inner(); + + if !response.payment_error.is_empty() || response.payment_preimage.is_empty() { + panic!( + "LND payment failed: {}", + if response.payment_error.is_empty() { + "No preimage returned" + } else { + &response.payment_error + } + ); + } + } +} From 7d592bb5d58b56795cae27a7eba4d08b977ba968 Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Wed, 26 Mar 2025 00:56:16 -0300 Subject: [PATCH 027/177] ci: add GitHub Actions workflow for LND integration tests Configures CI to run LND tests using the Docker Compose environment. Closes #505 --- .github/workflows/lnd-integration.yml | 33 +++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 .github/workflows/lnd-integration.yml diff --git a/.github/workflows/lnd-integration.yml b/.github/workflows/lnd-integration.yml new file mode 100644 index 0000000000..10a29c3551 --- /dev/null +++ b/.github/workflows/lnd-integration.yml @@ -0,0 +1,33 @@ +name: CI Checks - LND Integration Tests + +on: [push, pull_request] + +jobs: + check-lnd: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Create temporary directory for LND data + id: create-temp-dir + run: echo "LND_DATA_DIR=$(mktemp -d)" >> $GITHUB_ENV + + - name: Start bitcoind, electrs, and LND + run: docker compose -f docker-compose-lnd.yml up -d + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} + + - name: Set permissions for LND data directory + # In PR 4622 (https://github.com/lightningnetwork/lnd/pull/4622), + # LND sets file permissions to 0700, preventing test code from accessing them. + # This step ensures the test suite has the necessary permissions. + run: sudo chmod -R 755 $LND_DATA_DIR + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} + + - name: Run LND integration tests + run: LND_CERT_PATH=$LND_DATA_DIR/tls.cert LND_MACAROON_PATH=$LND_DATA_DIR/data/chain/bitcoin/regtest/admin.macaroon + RUSTFLAGS="--cfg lnd_test" cargo test --test integration_tests_lnd -- --exact --show-output + env: + LND_DATA_DIR: ${{ env.LND_DATA_DIR }} \ No newline at end of file From ce34d7e2ef9a40d4f35242859a3a2e0b4dd5adbf Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Fri, 28 Mar 2025 21:02:46 +0100 Subject: [PATCH 028/177] Fix: add retry logic to Esplora requests to handle connection issues - Add timeout, max retries, and error handling to wait_for_block and wait_for_tx functions in Python tests - Catch and log connection errors instead of failing immediately - Use shorter sleep interval (0.5s) to match Kotlin implementation - Add error messages during retries This addresses the "Connection reset by peer" errors in CI by making the Python tests more resilient to temporary network issues when connecting to the Esplora API. --- bindings/python/src/ldk_node/test_ldk_node.py | 44 +++++++++++++------ 1 file changed, 30 insertions(+), 14 deletions(-) diff --git a/bindings/python/src/ldk_node/test_ldk_node.py b/bindings/python/src/ldk_node/test_ldk_node.py index 4c5cdd828d..f71e89df8e 100644 --- a/bindings/python/src/ldk_node/test_ldk_node.py +++ b/bindings/python/src/ldk_node/test_ldk_node.py @@ -50,27 +50,43 @@ def mine_and_wait(esplora_endpoint, blocks): def wait_for_block(esplora_endpoint, block_hash): url = esplora_endpoint + "/block/" + block_hash + "/status" - esplora_picked_up_block = False - while not esplora_picked_up_block: - res = requests.get(url) + attempts = 0 + max_attempts = 30 + + while attempts < max_attempts: try: + res = requests.get(url, timeout=10) json = res.json() - esplora_picked_up_block = json['in_best_chain'] - except: - pass - time.sleep(1) + if json.get('in_best_chain'): + return + + except Exception as e: + print(f"Error: {e}") + + attempts += 1 + time.sleep(0.5) + + raise Exception(f"Failed to confirm block {block_hash} after {max_attempts} attempts") def wait_for_tx(esplora_endpoint, txid): url = esplora_endpoint + "/tx/" + txid - esplora_picked_up_tx = False - while not esplora_picked_up_tx: - res = requests.get(url) + attempts = 0 + max_attempts = 30 + + while attempts < max_attempts: try: + res = requests.get(url, timeout=10) json = res.json() - esplora_picked_up_tx = json['txid'] == txid - except: - pass - time.sleep(1) + if json.get('txid') == txid: + return + + except Exception as e: + print(f"Error: {e}") + + attempts += 1 + time.sleep(0.5) + + raise Exception(f"Failed to confirm transaction {txid} after {max_attempts} attempts") def send_to_address(address, amount_sats): amount_btc = amount_sats/100000000.0 From ce0de1cb3eb65dee720821d93eaf50678b71a47b Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 31 Mar 2025 00:00:00 +0000 Subject: [PATCH 029/177] Fix removing payment from in-memory store --- src/payment/store.rs | 38 +++++++++++++++++++++----------------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/src/payment/store.rs b/src/payment/store.rs index 367f5a5bd2..63432c53af 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -641,25 +641,29 @@ where } pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { - let store_key = hex_utils::to_string(&id.0); - self.kv_store - .remove( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - false, - ) - .map_err(|e| { - log_error!( - self.logger, - "Removing payment data for key {}/{}/{} failed due to: {}", + let removed = self.payments.lock().unwrap().remove(id).is_some(); + if removed { + let store_key = hex_utils::to_string(&id.0); + self.kv_store + .remove( PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - }) + &store_key, + false, + ) + .map_err(|e| { + log_error!( + self.logger, + "Removing payment data for key {}/{}/{} failed due to: {}", + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + store_key, + e + ); + Error::PersistenceFailed + })?; + } + Ok(()) } pub(crate) fn get(&self, id: &PaymentId) -> Option { From c32b646f37e30e31593a2ed807261be335c8e076 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 31 Mar 2025 00:00:00 +0000 Subject: [PATCH 030/177] Use `HashMap::values()` method to access only values --- src/payment/store.rs | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/src/payment/store.rs b/src/payment/store.rs index 63432c53af..636f2f0c86 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -686,14 +686,7 @@ where pub(crate) fn list_filter bool>( &self, f: F, ) -> Vec { - self.payments - .lock() - .unwrap() - .iter() - .map(|(_, p)| p) - .filter(f) - .cloned() - .collect::>() + self.payments.lock().unwrap().values().filter(f).cloned().collect::>() } fn persist_info(&self, id: &PaymentId, payment: &PaymentDetails) -> Result<(), Error> { From d79d1ba7032953793fc5e1fa1cd91dcd4f08d59e Mon Sep 17 00:00:00 2001 From: moisesPompilio Date: Thu, 3 Apr 2025 11:09:30 -0300 Subject: [PATCH 031/177] fix(ci): enforce compatible CMake version for LND tests Ensure CMake is between 3.5 and 4.0 to prevent failures in LND integration tests caused by prost-build v0.10.4. This safeguards against version changes in ubuntu-latest. Closes #515 --- .github/workflows/lnd-integration.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/.github/workflows/lnd-integration.yml b/.github/workflows/lnd-integration.yml index 10a29c3551..219e929b1b 100644 --- a/.github/workflows/lnd-integration.yml +++ b/.github/workflows/lnd-integration.yml @@ -9,6 +9,25 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 + - name: Check and install CMake if needed + # lnd_grpc_rust (via prost-build v0.10.4) requires CMake >= 3.5 but is incompatible with CMake >= 4.0. + # This step checks if CMake is missing, below 3.5, or 4.0 or higher, and installs CMake 3.31.6 if needed, + # ensuring compatibility with prost-build in ubuntu-latest. + run: | + if ! command -v cmake &> /dev/null || + [ "$(cmake --version | head -n1 | cut -d' ' -f3)" \< "3.5" ] || + [ "$(cmake --version | head -n1 | cut -d' ' -f3)" \> "4.0" ]; then + sudo apt-get update + sudo apt-get remove -y cmake + wget https://github.com/Kitware/CMake/releases/download/v3.31.6/cmake-3.31.6-Linux-x86_64.sh + echo "518c76bd18cc4ca5faab891db69b1289dc1bf134f394f0983a19576711b95210 cmake-3.31.6-Linux-x86_64.sh" | sha256sum -c - || { + echo "Error: The checksum of the downloaded file does not match the expected value!" + exit 1 + } + chmod +x cmake-3.31.6-Linux-x86_64.sh + sudo ./cmake-3.31.6-Linux-x86_64.sh --prefix=/usr/local --skip-license + fi + - name: Create temporary directory for LND data id: create-temp-dir run: echo "LND_DATA_DIR=$(mktemp -d)" >> $GITHUB_ENV From 4a40ab5e8c01b2fd1adfddf23552456567c1f00c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 3 Apr 2025 17:46:46 +0200 Subject: [PATCH 032/177] Accommodate minor change in `test_utils` API introduce by LDK 0.1.2 LDK 0.1.2 introduced a new `SyncPersist` trait that we accommodate here to fix our CI. --- src/io/test_utils.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index aed03b6fc7..df806779e8 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -80,7 +80,7 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s // Integration-test the given KVStore implementation. Test relaying a few payments and check that // the persisted data is updated the appropriate number of times. -pub(crate) fn do_test_store(store_0: &K, store_1: &K) { +pub(crate) fn do_test_store(store_0: &K, store_1: &K) { let chanmon_cfgs = create_chanmon_cfgs(2); let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let chain_mon_0 = test_utils::TestChainMonitor::new( From 3bf6e0a46d52ab99f5cfd73fb01a1b51badb2535 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 19 Mar 2025 13:11:27 +0100 Subject: [PATCH 033/177] Prefactor: Update bitcoind used in CI to v27.2 .. we need to bump the version, while making sure `electrsd` and CLN CI are using the same version. As the `blockstream/bitcoind` version hasn't reached v28 yet, we opt for v27.2 everywhere for now. --- docker-compose-cln.yml | 4 +--- scripts/download_bitcoind_electrs.sh | 12 ++++++++---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index 5fb1f2dcd0..7978bea358 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -1,8 +1,6 @@ -version: '3' - services: bitcoin: - image: blockstream/bitcoind:24.1 + image: blockstream/bitcoind:27.2 platform: linux/amd64 command: [ diff --git a/scripts/download_bitcoind_electrs.sh b/scripts/download_bitcoind_electrs.sh index fcdb31891e..47a95332ea 100755 --- a/scripts/download_bitcoind_electrs.sh +++ b/scripts/download_bitcoind_electrs.sh @@ -1,3 +1,6 @@ +#!/bin/bash +set -eox pipefail + # Our Esplora-based tests require `electrs` and `bitcoind` # binaries. Here, we download the binaries, validate them, and export their # location via `ELECTRS_EXE`/`BITCOIND_EXE` which will be used by the @@ -7,19 +10,20 @@ HOST_PLATFORM="$(rustc --version --verbose | grep "host:" | awk '{ print $2 }')" ELECTRS_DL_ENDPOINT="https://github.com/RCasatta/electrsd/releases/download/electrs_releases" ELECTRS_VERSION="esplora_a33e97e1a1fc63fa9c20a116bb92579bbf43b254" BITCOIND_DL_ENDPOINT="https://bitcoincore.org/bin/" -BITCOIND_VERSION="25.1" +BITCOIND_VERSION="27.2" if [[ "$HOST_PLATFORM" == *linux* ]]; then ELECTRS_DL_FILE_NAME=electrs_linux_"$ELECTRS_VERSION".zip ELECTRS_DL_HASH="865e26a96e8df77df01d96f2f569dcf9622fc87a8d99a9b8fe30861a4db9ddf1" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-linux-gnu.tar.gz - BITCOIND_DL_HASH="a978c407b497a727f0444156e397b50491ce862d1f906fef9b521415b3611c8b" + BITCOIND_DL_HASH="acc223af46c178064c132b235392476f66d486453ddbd6bca6f1f8411547da78" elif [[ "$HOST_PLATFORM" == *darwin* ]]; then ELECTRS_DL_FILE_NAME=electrs_macos_"$ELECTRS_VERSION".zip ELECTRS_DL_HASH="2d5ff149e8a2482d3658e9b386830dfc40c8fbd7c175ca7cbac58240a9505bcd" BITCOIND_DL_FILE_NAME=bitcoin-"$BITCOIND_VERSION"-x86_64-apple-darwin.tar.gz - BITCOIND_DL_HASH="1acfde0ec3128381b83e3e5f54d1c7907871d324549129592144dd12a821eff1" + BITCOIND_DL_HASH="6ebc56ca1397615d5a6df2b5cf6727b768e3dcac320c2d5c2f321dcaabc7efa2" else - echo "\n\nUnsupported platform: $HOST_PLATFORM Exiting.." + printf "\n\n" + echo "Unsupported platform: $HOST_PLATFORM Exiting.." exit 1 fi From c5a014f856d09e0d12cb2e80802fd22878bb92b6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 27 Mar 2025 09:12:58 +0100 Subject: [PATCH 034/177] Have the mock log facade not always print to `stdout` In #463 we introduced a mock logger that implements the `log` facade in tests. Previously, it defaulted to always print `TRACE`-level logs to `stdout`. This however can be very spammy (especially since it includes TRACE-level logs of `electrum_client` now). Here, we simply disable printing to `stdout` --- tests/common/logging.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/common/logging.rs b/tests/common/logging.rs index 5d89474da3..6bceac29a9 100644 --- a/tests/common/logging.rs +++ b/tests/common/logging.rs @@ -48,7 +48,6 @@ impl LogFacadeLog for MockLogFacadeLogger { record.line().unwrap(), record.args() ); - println!("{message}"); self.logs.lock().unwrap().push(message); } From df47d33472b2e759a4226e1bad3406759a31c68d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 3 Feb 2025 13:48:56 +0100 Subject: [PATCH 035/177] Prefactor: Upgrade to `electrum-client` v0.22 We upgrade our tests to use `electrum-client` v0.22 and `electrsd` v0.31 to ensure compatibility with `bdk_electrum` were about to start using. --- Cargo.toml | 8 ++++---- tests/common/mod.rs | 30 ++++++++++++------------------ tests/integration_tests_cln.rs | 8 ++++---- tests/integration_tests_lnd.rs | 8 ++++---- tests/integration_tests_rust.rs | 32 ++++++++++---------------------- 5 files changed, 34 insertions(+), 52 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5481c76ddd..43d6bc8725 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -92,16 +92,16 @@ winapi = { version = "0.3", features = ["winbase"] } lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } -electrum-client = { version = "0.21.0", default-features = true } -bitcoincore-rpc = { version = "0.19.0", default-features = false } +electrum-client = { version = "0.22.0", default-features = true } proptest = "1.0.0" regex = "1.5.6" [target.'cfg(not(no_download))'.dev-dependencies] -electrsd = { version = "0.29.0", features = ["legacy", "esplora_a33e97e1", "bitcoind_25_0"] } +electrsd = { version = "0.33.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } [target.'cfg(no_download)'.dev-dependencies] -electrsd = { version = "0.29.0", features = ["legacy"] } +electrsd = { version = "0.33.0", default-features = false, features = ["legacy"] } +corepc-node = { version = "0.7.0", default-features = false, features = ["27_2"] } [target.'cfg(cln_test)'.dev-dependencies] clightningrpc = { version = "0.3.0-beta.8", default-features = false } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 2a3e99c0fd..ac8d1ce908 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -33,11 +33,9 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::{Address, Amount, Network, OutPoint, Txid}; -use bitcoincore_rpc::bitcoincore_rpc_json::AddressType; -use bitcoincore_rpc::Client as BitcoindClient; -use bitcoincore_rpc::RpcApi; - -use electrsd::{bitcoind, bitcoind::BitcoinD, ElectrsD}; +use electrsd::corepc_node::Client as BitcoindClient; +use electrsd::corepc_node::Node as BitcoinD; +use electrsd::{corepc_node, ElectrsD}; use electrum_client::ElectrumApi; use rand::distributions::Alphanumeric; @@ -171,10 +169,10 @@ pub(crate) use expect_payment_successful_event; pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { let bitcoind_exe = - env::var("BITCOIND_EXE").ok().or_else(|| bitcoind::downloaded_exe_path().ok()).expect( + env::var("BITCOIND_EXE").ok().or_else(|| corepc_node::downloaded_exe_path().ok()).expect( "you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature", ); - let mut bitcoind_conf = bitcoind::Conf::default(); + let mut bitcoind_conf = corepc_node::Conf::default(); bitcoind_conf.network = "regtest"; let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); @@ -359,17 +357,14 @@ pub(crate) fn setup_node( pub(crate) fn generate_blocks_and_wait( bitcoind: &BitcoindClient, electrs: &E, num: usize, ) { - let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None); + let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); print!("Generating {} blocks...", num); - let cur_height = bitcoind.get_block_count().expect("failed to get current block height"); - let address = bitcoind - .get_new_address(Some("test"), Some(AddressType::Legacy)) - .expect("failed to get new address") - .require_network(bitcoin::Network::Regtest) - .expect("failed to get new address"); + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let cur_height = blockchain_info.blocks; + let address = bitcoind.new_address().expect("failed to get new address"); // TODO: expect this Result once the WouldBlock issue is resolved upstream. - let _block_hashes_res = bitcoind.generate_to_address(num as u64, &address); + let _block_hashes_res = bitcoind.generate_to_address(num, &address); wait_for_block(electrs, cur_height as usize + num); print!(" Done!"); println!("\n"); @@ -450,13 +445,12 @@ where pub(crate) fn premine_and_distribute_funds( bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, ) { - let _ = bitcoind.create_wallet("ldk_node_test", None, None, None, None); + let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); generate_blocks_and_wait(bitcoind, electrs, 101); for addr in addrs { - let txid = - bitcoind.send_to_address(&addr, amount, None, None, None, None, None, None).unwrap(); + let txid = bitcoind.send_to_address(&addr, amount).unwrap().0.parse().unwrap(); wait_for_tx(electrs, txid); } diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index 875922ce20..b6300576c0 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -18,8 +18,8 @@ use lightning_invoice::{Bolt11InvoiceDescription, Description}; use clightningrpc::lightningrpc::LightningRPC; use clightningrpc::responses::NetworkAddress; -use bitcoincore_rpc::Auth; -use bitcoincore_rpc::Client as BitcoindClient; +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node::Client as BitcoindClient; use electrum_client::Client as ElectrumClient; use lightning_invoice::Bolt11Invoice; @@ -33,8 +33,8 @@ use std::str::FromStr; #[test] fn test_cln() { // Setup bitcoind / electrs clients - let bitcoind_client = BitcoindClient::new( - "127.0.0.1:18443", + let bitcoind_client = BitcoindClient::new_with_auth( + "http://127.0.0.1:18443", Auth::UserPass("user".to_string(), "pass".to_string()), ) .unwrap(); diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs index feb981d8e6..0232e8f2ea 100755 --- a/tests/integration_tests_lnd.rs +++ b/tests/integration_tests_lnd.rs @@ -15,8 +15,8 @@ use lnd_grpc_rust::lnrpc::{ }; use lnd_grpc_rust::{connect, LndClient}; -use bitcoincore_rpc::Auth; -use bitcoincore_rpc::Client as BitcoindClient; +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node::Client as BitcoindClient; use electrum_client::Client as ElectrumClient; use lightning_invoice::{Bolt11InvoiceDescription, Description}; @@ -30,8 +30,8 @@ use tokio::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] async fn test_lnd() { // Setup bitcoind / electrs clients - let bitcoind_client = BitcoindClient::new( - "127.0.0.1:18443", + let bitcoind_client = BitcoindClient::new_with_auth( + "http://127.0.0.1:18443", Auth::UserPass("user".to_string(), "pass".to_string()), ) .unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index da9eea48f7..9bbd566899 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -28,11 +28,11 @@ use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::util::persist::KVStore; -use bitcoincore_rpc::RpcApi; +use lightning_invoice::{Bolt11InvoiceDescription, Description}; use bitcoin::hashes::Hash; use bitcoin::Amount; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; + use log::LevelFilter; use std::sync::Arc; @@ -500,16 +500,10 @@ fn onchain_wallet_recovery() { let txid = bitcoind .client - .send_to_address( - &addr_2, - Amount::from_sat(premine_amount_sat), - None, - None, - None, - None, - None, - None, - ) + .send_to_address(&addr_2, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() .unwrap(); wait_for_tx(&electrsd.client, txid); @@ -542,16 +536,10 @@ fn onchain_wallet_recovery() { let txid = bitcoind .client - .send_to_address( - &addr_6, - Amount::from_sat(premine_amount_sat), - None, - None, - None, - None, - None, - None, - ) + .send_to_address(&addr_6, Amount::from_sat(premine_amount_sat)) + .unwrap() + .0 + .parse() .unwrap(); wait_for_tx(&electrsd.client, txid); From 2187046e90e404516163adcde83b1fd4c7705ce4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 18 Mar 2025 12:23:19 +0100 Subject: [PATCH 036/177] Install `bindgen-cli` in Kotlin CI By default `rustls`, our TLS implementation of choice, uses `aws-lc-rs` which requires `bindgen` on some platforms. To allow building with `aws-lc-rs` on Android, we here install the `bindgen-cli` tool before running the bindings generation script in CI. --- .github/workflows/kotlin.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index a1711ba498..5cb1b8c279 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -39,6 +39,9 @@ jobs: - name: Generate Kotlin JVM run: ./scripts/uniffi_bindgen_generate_kotlin.sh + - name: Install `bindgen-cli` + run: cargo install --force bindgen-cli + - name: Generate Kotlin Android run: ./scripts/uniffi_bindgen_generate_kotlin_android.sh From 274a21ff5d22187076c61cc0d8175adb66c53439 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 4 Feb 2025 14:45:34 +0100 Subject: [PATCH 037/177] Add `bdk_electrum`/`transaction-sync` `electrum` support in `Cargo.toml` --- Cargo.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 43d6bc8725..5166a3ec5e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ lightning-persister = { version = "0.1.0" } lightning-background-processor = { version = "0.1.0", features = ["futures"] } lightning-rapid-gossip-sync = { version = "0.1.0" } lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } -lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time"] } +lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } lightning-liquidity = { version = "0.1.0", features = ["std"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } @@ -63,6 +63,7 @@ lightning-liquidity = { version = "0.1.0", features = ["std"] } bdk_chain = { version = "0.21.1", default-features = false, features = ["std"] } bdk_esplora = { version = "0.20.1", default-features = false, features = ["async-https-rustls", "tokio"]} +bdk_electrum = { version = "0.20.1", default-features = false, features = ["use-rustls"]} bdk_wallet = { version = "1.0.0", default-features = false, features = ["std", "keys-bip39"]} reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } @@ -76,6 +77,7 @@ rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } esplora-client = { version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } +electrum-client = { version = "0.22.0", default-features = true } libc = "0.2" uniffi = { version = "0.27.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } @@ -92,7 +94,6 @@ winapi = { version = "0.3", features = ["winbase"] } lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } -electrum-client = { version = "0.22.0", default-features = true } proptest = "1.0.0" regex = "1.5.6" From e793b6f23128384916d0b0212bd3cf66bf6e568c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 7 Mar 2025 13:45:23 +0100 Subject: [PATCH 038/177] Allow to configure `ChainSource::Electrum` via builder We here setup the basic API and structure for the `ChainSource::Electrum`. Currently, we won't have a `Runtime` available when initializing `ChainSource::Electrum` in `Builder::build`. We therefore isolate any runtime-specific behavior into an `ElectrumRuntimeClient`. This might change in the future, but for now we do need this workaround. --- bindings/ldk_node.udl | 5 ++ src/builder.rs | 41 +++++++++++++- src/chain/electrum.rs | 60 +++++++++++++++++++++ src/chain/mod.rs | 121 +++++++++++++++++++++++++++++++++++++++++- src/config.rs | 23 +++++++- src/lib.rs | 16 ++++++ src/uniffi_types.rs | 4 +- 7 files changed, 264 insertions(+), 6 deletions(-) create mode 100644 src/chain/electrum.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1e45263473..1a56a7d4b6 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -30,6 +30,10 @@ dictionary EsploraSyncConfig { BackgroundSyncConfig? background_sync_config; }; +dictionary ElectrumSyncConfig { + BackgroundSyncConfig? background_sync_config; +}; + dictionary LSPS2ServiceConfig { string? require_token; boolean advertise_service; @@ -72,6 +76,7 @@ interface Builder { void set_entropy_seed_bytes(sequence seed_bytes); void set_entropy_bip39_mnemonic(Mnemonic mnemonic, string? passphrase); void set_chain_source_esplora(string server_url, EsploraSyncConfig? config); + void set_chain_source_electrum(string server_url, ElectrumSyncConfig? config); void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); diff --git a/src/builder.rs b/src/builder.rs index 3e8fd2cbac..224cc9fa75 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,8 +7,8 @@ use crate::chain::{ChainSource, DEFAULT_ESPLORA_SERVER_URL}; use crate::config::{ - default_user_config, may_announce_channel, AnnounceError, Config, EsploraSyncConfig, - DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AnnounceError, Config, ElectrumSyncConfig, + EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; @@ -83,6 +83,7 @@ const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { Esplora { server_url: String, sync_config: Option }, + Electrum { server_url: String, sync_config: Option }, BitcoindRpc { rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String }, } @@ -284,6 +285,18 @@ impl NodeBuilder { self } + /// Configures the [`Node`] instance to source its chain data from the given Electrum server. + /// + /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more + /// information. + pub fn set_chain_source_electrum( + &mut self, server_url: String, sync_config: Option, + ) -> &mut Self { + self.chain_data_source_config = + Some(ChainDataSourceConfig::Electrum { server_url, sync_config }); + self + } + /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC /// endpoint. pub fn set_chain_source_bitcoind_rpc( @@ -691,6 +704,16 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_chain_source_esplora(server_url, sync_config); } + /// Configures the [`Node`] instance to source its chain data from the given Electrum server. + /// + /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more + /// information. + pub fn set_chain_source_electrum( + &self, server_url: String, sync_config: Option, + ) { + self.inner.write().unwrap().set_chain_source_electrum(server_url, sync_config); + } + /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC /// endpoint. pub fn set_chain_source_bitcoind_rpc( @@ -1024,6 +1047,20 @@ fn build_with_store_internal( Arc::clone(&node_metrics), )) }, + Some(ChainDataSourceConfig::Electrum { server_url, sync_config }) => { + let sync_config = sync_config.unwrap_or(ElectrumSyncConfig::default()); + Arc::new(ChainSource::new_electrum( + server_url.clone(), + sync_config, + Arc::clone(&wallet), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + )) + }, Some(ChainDataSourceConfig::BitcoindRpc { rpc_host, rpc_port, rpc_user, rpc_password }) => { Arc::new(ChainSource::new_bitcoind_rpc( rpc_host.clone(), diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs new file mode 100644 index 0000000000..4e62a75a10 --- /dev/null +++ b/src/chain/electrum.rs @@ -0,0 +1,60 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use crate::error::Error; +use crate::logger::{log_error, LdkLogger, Logger}; + +use lightning_transaction_sync::ElectrumSyncClient; + +use bdk_electrum::BdkElectrumClient; + +use electrum_client::Client as ElectrumClient; +use electrum_client::ConfigBuilder as ElectrumConfigBuilder; + +use std::sync::Arc; + +const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; +const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 20; + +pub(crate) struct ElectrumRuntimeClient { + electrum_client: Arc, + bdk_electrum_client: Arc>, + tx_sync: Arc>>, + runtime: Arc, + logger: Arc, +} + +impl ElectrumRuntimeClient { + pub(crate) fn new( + server_url: String, runtime: Arc, logger: Arc, + ) -> Result { + let electrum_config = ElectrumConfigBuilder::new() + .retry(ELECTRUM_CLIENT_NUM_RETRIES) + .timeout(Some(ELECTRUM_CLIENT_TIMEOUT_SECS)) + .build(); + + let electrum_client = Arc::new( + ElectrumClient::from_config(&server_url, electrum_config.clone()).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server: {}", e); + Error::ConnectionFailed + })?, + ); + let electrum_client_2 = + ElectrumClient::from_config(&server_url, electrum_config).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server: {}", e); + Error::ConnectionFailed + })?; + let bdk_electrum_client = Arc::new(BdkElectrumClient::new(electrum_client_2)); + let tx_sync = Arc::new( + ElectrumSyncClient::new(server_url.clone(), Arc::clone(&logger)).map_err(|e| { + log_error!(logger, "Failed to connect to electrum server: {}", e); + Error::ConnectionFailed + })?, + ); + Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger }) + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index a2e01884a9..ad213405a6 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -6,12 +6,14 @@ // accordance with one or both of these licenses. mod bitcoind_rpc; +mod electrum; use crate::chain::bitcoind_rpc::{ BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; +use crate::chain::electrum::ElectrumRuntimeClient; use crate::config::{ - Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, + Config, ElectrumSyncConfig, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, @@ -107,6 +109,45 @@ impl WalletSyncStatus { } } +pub(crate) enum ElectrumRuntimeStatus { + Started(Arc), + Stopped, +} + +impl ElectrumRuntimeStatus { + pub(crate) fn new() -> Self { + Self::Stopped + } + + pub(crate) fn start( + &mut self, server_url: String, runtime: Arc, logger: Arc, + ) -> Result<(), Error> { + match self { + Self::Stopped => { + let client = + Arc::new(ElectrumRuntimeClient::new(server_url.clone(), runtime, logger)?); + + *self = Self::Started(client); + }, + Self::Started(_) => { + debug_assert!(false, "We shouldn't call start if we're already started") + }, + } + Ok(()) + } + + pub(crate) fn stop(&mut self) { + *self = Self::new() + } + + pub(crate) fn client(&self) -> Option<&Arc> { + match self { + Self::Started(client) => Some(&client), + Self::Stopped { .. } => None, + } + } +} + pub(crate) enum ChainSource { Esplora { sync_config: EsploraSyncConfig, @@ -122,6 +163,20 @@ pub(crate) enum ChainSource { logger: Arc, node_metrics: Arc>, }, + Electrum { + server_url: String, + sync_config: ElectrumSyncConfig, + electrum_runtime_status: RwLock, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, + }, BitcoindRpc { bitcoind_rpc_client: Arc, header_cache: tokio::sync::Mutex, @@ -167,6 +222,31 @@ impl ChainSource { } } + pub(crate) fn new_electrum( + server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, + fee_estimator: Arc, tx_broadcaster: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self::Electrum { + server_url, + sync_config, + electrum_runtime_status, + onchain_wallet, + onchain_wallet_sync_status, + lightning_wallet_sync_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger, + node_metrics, + } + } + pub(crate) fn new_bitcoind_rpc( host: String, port: u16, rpc_user: String, rpc_password: String, onchain_wallet: Arc, fee_estimator: Arc, @@ -193,6 +273,33 @@ impl ChainSource { } } + pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { + match self { + Self::Electrum { server_url, electrum_runtime_status, logger, .. } => { + electrum_runtime_status.write().unwrap().start( + server_url.clone(), + runtime, + Arc::clone(&logger), + )?; + }, + _ => { + // Nothing to do for other chain sources. + }, + } + Ok(()) + } + + pub(crate) fn stop(&self) { + match self { + Self::Electrum { electrum_runtime_status, .. } => { + electrum_runtime_status.write().unwrap().stop(); + }, + _ => { + // Nothing to do for other chain sources. + }, + } + } + pub(crate) fn as_utxo_source(&self) -> Option> { match self { Self::BitcoindRpc { bitcoind_rpc_client, .. } => Some(bitcoind_rpc_client.rpc_client()), @@ -271,6 +378,7 @@ impl ChainSource { return; } }, + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { bitcoind_rpc_client, header_cache, @@ -538,6 +646,7 @@ impl ChainSource { res }, + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via // `ChainPoller`. So nothing to do here. @@ -637,6 +746,7 @@ impl ChainSource { res }, + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via // `ChainPoller`. So nothing to do here. @@ -655,6 +765,11 @@ impl ChainSource { // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, + Self::Electrum { .. } => { + // In Electrum mode we sync lightning and onchain wallets via + // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. + unreachable!("Listeners will be synced via transction-based syncing") + }, Self::BitcoindRpc { bitcoind_rpc_client, header_cache, @@ -875,6 +990,7 @@ impl ChainSource { Ok(()) }, + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { bitcoind_rpc_client, fee_estimator, @@ -1085,6 +1201,7 @@ impl ChainSource { } } }, + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { bitcoind_rpc_client, tx_broadcaster, logger, .. } => { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the @@ -1147,12 +1264,14 @@ impl Filter for ChainSource { fn register_tx(&self, txid: &bitcoin::Txid, script_pubkey: &bitcoin::Script) { match self { Self::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { match self { Self::Esplora { tx_sync, .. } => tx_sync.register_output(output), + Self::Electrum { .. } => todo!(), Self::BitcoindRpc { .. } => (), } } diff --git a/src/config.rs b/src/config.rs index 46b5284883..4a39c1b561 100644 --- a/src/config.rs +++ b/src/config.rs @@ -364,7 +364,7 @@ pub struct EsploraSyncConfig { /// Background sync configuration. /// /// If set to `None`, background syncing will be disabled. Users will need to manually - /// sync via `Node::sync_wallets` for the wallets and fee rate updates. + /// sync via [`Node::sync_wallets`] for the wallets and fee rate updates. /// /// [`Node::sync_wallets`]: crate::Node::sync_wallets pub background_sync_config: Option, @@ -376,6 +376,27 @@ impl Default for EsploraSyncConfig { } } +/// Configuration for syncing with an Electrum backend. +/// +/// Background syncing is enabled by default, using the default values specified in +/// [`BackgroundSyncConfig`]. +#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub struct ElectrumSyncConfig { + /// Background sync configuration. + /// + /// If set to `None`, background syncing will be disabled. Users will need to manually + /// sync via [`Node::sync_wallets`] for the wallets and fee rate updates. + /// + /// [`Node::sync_wallets`]: crate::Node::sync_wallets + pub background_sync_config: Option, +} + +impl Default for ElectrumSyncConfig { + fn default() -> Self { + Self { background_sync_config: Some(BackgroundSyncConfig::default()) } + } +} + /// Options which apply on a per-channel basis and may change at runtime or based on negotiation /// with our counterparty. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index a80db1e8c0..93393585d8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -236,6 +236,12 @@ impl Node { self.config.network ); + // Start up any runtime-dependant chain sources (e.g. Electrum) + self.chain_source.start(Arc::clone(&runtime)).map_err(|e| { + log_error!(self.logger, "Failed to start chain syncing: {}", e); + e + })?; + // Block to ensure we update our fee rate cache once on startup let chain_source = Arc::clone(&self.chain_source); let runtime_ref = &runtime; @@ -640,6 +646,9 @@ impl Node { log_info!(self.logger, "Shutting down LDK Node with node ID {}...", self.node_id()); + // Stop any runtime-dependant chain sources. + self.chain_source.stop(); + // Stop the runtime. match self.stop_sender.send(()) { Ok(_) => (), @@ -1257,6 +1266,13 @@ impl Node { .await?; chain_source.sync_onchain_wallet().await?; }, + ChainSource::Electrum { .. } => { + chain_source.update_fee_rate_estimates().await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) + .await?; + chain_source.sync_onchain_wallet().await?; + }, ChainSource::BitcoindRpc { .. } => { chain_source.update_fee_rate_estimates().await?; chain_source diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index 410ce85312..acdee3a944 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -11,8 +11,8 @@ // Make sure to add any re-exported items that need to be used in uniffi below. pub use crate::config::{ - default_config, AnchorChannelsConfig, BackgroundSyncConfig, EsploraSyncConfig, - MaxDustHTLCExposure, + default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, + EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; From 70014d697e3dbfa7cb7be4adac9c81ba177d6bb6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 7 Mar 2025 13:46:19 +0100 Subject: [PATCH 039/177] Implement (cached) `Filter` for `ElectrumRuntimeClient` Currently, we won't have a `Runtime` available when initializing `ChainSource::Electrum`. We therefore isolate any runtime-specific behavior into the `ElectrumRuntimeStatus`. Here, we implement `Filter` for `ElectrumRuntimeClient`, but we need to cache the registrations as they might happen prior to `ElectrumRuntimeClient` becoming available. --- src/chain/electrum.rs | 12 +++++++++ src/chain/mod.rs | 58 +++++++++++++++++++++++++++++++++++-------- 2 files changed, 59 insertions(+), 11 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 4e62a75a10..3e1f1465e3 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -8,6 +8,7 @@ use crate::error::Error; use crate::logger::{log_error, LdkLogger, Logger}; +use lightning::chain::{Filter, WatchedOutput}; use lightning_transaction_sync::ElectrumSyncClient; use bdk_electrum::BdkElectrumClient; @@ -15,6 +16,8 @@ use bdk_electrum::BdkElectrumClient; use electrum_client::Client as ElectrumClient; use electrum_client::ConfigBuilder as ElectrumConfigBuilder; +use bitcoin::{Script, Txid}; + use std::sync::Arc; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; @@ -58,3 +61,12 @@ impl ElectrumRuntimeClient { Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger }) } } + +impl Filter for ElectrumRuntimeClient { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.tx_sync.register_tx(txid, script_pubkey) + } + fn register_output(&self, output: WatchedOutput) { + self.tx_sync.register_output(output) + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ad213405a6..5de9a8bc18 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -28,7 +28,7 @@ use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, use crate::{Error, NodeMetrics}; use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::{Confirm, Filter, Listen}; +use lightning::chain::{Confirm, Filter, Listen, WatchedOutput}; use lightning::util::ser::Writeable; use lightning_transaction_sync::EsploraSyncClient; @@ -42,7 +42,7 @@ use bdk_esplora::EsploraAsyncExt; use esplora_client::AsyncClient as EsploraAsyncClient; -use bitcoin::{FeeRate, Network}; +use bitcoin::{FeeRate, Network, Script, ScriptBuf, Txid}; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; @@ -111,22 +111,36 @@ impl WalletSyncStatus { pub(crate) enum ElectrumRuntimeStatus { Started(Arc), - Stopped, + Stopped { + pending_registered_txs: Vec<(Txid, ScriptBuf)>, + pending_registered_outputs: Vec, + }, } impl ElectrumRuntimeStatus { pub(crate) fn new() -> Self { - Self::Stopped + let pending_registered_txs = Vec::new(); + let pending_registered_outputs = Vec::new(); + Self::Stopped { pending_registered_txs, pending_registered_outputs } } pub(crate) fn start( &mut self, server_url: String, runtime: Arc, logger: Arc, ) -> Result<(), Error> { match self { - Self::Stopped => { + Self::Stopped { pending_registered_txs, pending_registered_outputs } => { let client = Arc::new(ElectrumRuntimeClient::new(server_url.clone(), runtime, logger)?); + // Apply any pending `Filter` entries + for (txid, script_pubkey) in pending_registered_txs.drain(..) { + client.register_tx(&txid, &script_pubkey); + } + + for output in pending_registered_outputs.drain(..) { + client.register_output(output) + } + *self = Self::Started(client); }, Self::Started(_) => { @@ -140,12 +154,30 @@ impl ElectrumRuntimeStatus { *self = Self::new() } - pub(crate) fn client(&self) -> Option<&Arc> { + pub(crate) fn client(&self) -> Option<&ElectrumRuntimeClient> { match self { - Self::Started(client) => Some(&client), + Self::Started(client) => Some(&*client), Self::Stopped { .. } => None, } } + + fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { + match self { + Self::Started(client) => client.register_tx(txid, script_pubkey), + Self::Stopped { pending_registered_txs, .. } => { + pending_registered_txs.push((*txid, script_pubkey.to_owned())) + }, + } + } + + fn register_output(&mut self, output: lightning::chain::WatchedOutput) { + match self { + Self::Started(client) => client.register_output(output), + Self::Stopped { pending_registered_outputs, .. } => { + pending_registered_outputs.push(output) + }, + } + } } pub(crate) enum ChainSource { @@ -278,7 +310,7 @@ impl ChainSource { Self::Electrum { server_url, electrum_runtime_status, logger, .. } => { electrum_runtime_status.write().unwrap().start( server_url.clone(), - runtime, + Arc::clone(&runtime), Arc::clone(&logger), )?; }, @@ -1261,17 +1293,21 @@ impl ChainSource { } impl Filter for ChainSource { - fn register_tx(&self, txid: &bitcoin::Txid, script_pubkey: &bitcoin::Script) { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { match self { Self::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), - Self::Electrum { .. } => todo!(), + Self::Electrum { electrum_runtime_status, .. } => { + electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) + }, Self::BitcoindRpc { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { match self { Self::Esplora { tx_sync, .. } => tx_sync.register_output(output), - Self::Electrum { .. } => todo!(), + Self::Electrum { electrum_runtime_status, .. } => { + electrum_runtime_status.write().unwrap().register_output(output) + }, Self::BitcoindRpc { .. } => (), } } From 6ead9be0f97753e1448abcbaef02bcf1737d9038 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 7 Mar 2025 14:44:16 +0100 Subject: [PATCH 040/177] Implement `continuously_sync_wallets` for `ChainSource::Electrum` --- src/chain/mod.rs | 155 ++++++++++++++++++++++++++++------------------- 1 file changed, 94 insertions(+), 61 deletions(-) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 5de9a8bc18..647a6ab1d0 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -13,10 +13,10 @@ use crate::chain::bitcoind_rpc::{ }; use crate::chain::electrum::ElectrumRuntimeClient; use crate::config::{ - Config, ElectrumSyncConfig, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, - BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, - RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, - WALLET_SYNC_INTERVAL_MINIMUM_SECS, + BackgroundSyncConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, + BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, + LDK_WALLET_SYNC_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, + TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use crate::fee_estimator::{ apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, @@ -346,71 +346,45 @@ impl ChainSource { ) { match self { Self::Esplora { sync_config, logger, .. } => { - // Setup syncing intervals if enabled - if let Some(background_sync_config) = sync_config.background_sync_config { - let onchain_wallet_sync_interval_secs = background_sync_config - .onchain_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut onchain_wallet_sync_interval = tokio::time::interval( - Duration::from_secs(onchain_wallet_sync_interval_secs), - ); - onchain_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let fee_rate_cache_update_interval_secs = background_sync_config - .fee_rate_cache_update_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut fee_rate_update_interval = tokio::time::interval(Duration::from_secs( - fee_rate_cache_update_interval_secs, - )); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let lightning_wallet_sync_interval_secs = background_sync_config - .lightning_wallet_sync_interval_secs - .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); - let mut lightning_wallet_sync_interval = tokio::time::interval( - Duration::from_secs(lightning_wallet_sync_interval_secs), + if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { + self.start_tx_based_sync_loop( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, + background_sync_config, + Arc::clone(&logger), + ) + .await + } else { + // Background syncing is disabled + log_info!( + logger, + "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", ); - lightning_wallet_sync_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - // Start the syncing loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - logger, - "Stopping background syncing on-chain wallet.", - ); - return; - } - _ = onchain_wallet_sync_interval.tick() => { - let _ = self.sync_onchain_wallet().await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - _ = lightning_wallet_sync_interval.tick() => { - let _ = self.sync_lightning_wallet( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&output_sweeper), - ).await; - } - } - } + return; + } + }, + Self::Electrum { sync_config, logger, .. } => { + if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { + self.start_tx_based_sync_loop( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, + background_sync_config, + Arc::clone(&logger), + ) + .await } else { // Background syncing is disabled log_info!( - logger, "Background syncing disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", + logger, + "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", ); return; } }, - Self::Electrum { .. } => todo!(), Self::BitcoindRpc { bitcoind_rpc_client, header_cache, @@ -561,6 +535,65 @@ impl ChainSource { } } + async fn start_tx_based_sync_loop( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, background_sync_config: &BackgroundSyncConfig, + logger: Arc, + ) { + // Setup syncing intervals + let onchain_wallet_sync_interval_secs = background_sync_config + .onchain_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut onchain_wallet_sync_interval = + tokio::time::interval(Duration::from_secs(onchain_wallet_sync_interval_secs)); + onchain_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let fee_rate_cache_update_interval_secs = background_sync_config + .fee_rate_cache_update_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut fee_rate_update_interval = + tokio::time::interval(Duration::from_secs(fee_rate_cache_update_interval_secs)); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let lightning_wallet_sync_interval_secs = background_sync_config + .lightning_wallet_sync_interval_secs + .max(WALLET_SYNC_INTERVAL_MINIMUM_SECS); + let mut lightning_wallet_sync_interval = + tokio::time::interval(Duration::from_secs(lightning_wallet_sync_interval_secs)); + lightning_wallet_sync_interval + .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + // Start the syncing loop. + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + logger, + "Stopping background syncing on-chain wallet.", + ); + return; + } + _ = onchain_wallet_sync_interval.tick() => { + let _ = self.sync_onchain_wallet().await; + } + _ = fee_rate_update_interval.tick() => { + let _ = self.update_fee_rate_estimates().await; + } + _ = lightning_wallet_sync_interval.tick() => { + let _ = self.sync_lightning_wallet( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper), + ).await; + } + } + } + } + // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { From 98150cdeec01bbd8359bc446a37545998fbdbe75 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 7 Mar 2025 15:37:25 +0100 Subject: [PATCH 041/177] Implement `process_broadcast_queue` for `ChainSource::Electrum` --- src/chain/electrum.rs | 50 +++++++++++++++++++++++++++++++++++++++++-- src/chain/mod.rs | 25 +++++++++++++++++++--- 2 files changed, 70 insertions(+), 5 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 3e1f1465e3..ea98fd91c5 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -5,20 +5,24 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use crate::config::TX_BROADCAST_TIMEOUT_SECS; use crate::error::Error; -use crate::logger::{log_error, LdkLogger, Logger}; +use crate::logger::{log_bytes, log_error, log_trace, LdkLogger, Logger}; use lightning::chain::{Filter, WatchedOutput}; +use lightning::util::ser::Writeable; use lightning_transaction_sync::ElectrumSyncClient; use bdk_electrum::BdkElectrumClient; use electrum_client::Client as ElectrumClient; use electrum_client::ConfigBuilder as ElectrumConfigBuilder; +use electrum_client::ElectrumApi; -use bitcoin::{Script, Txid}; +use bitcoin::{Script, Transaction, Txid}; use std::sync::Arc; +use std::time::Duration; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 20; @@ -60,6 +64,48 @@ impl ElectrumRuntimeClient { ); Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger }) } + + pub(crate) async fn broadcast(&self, tx: Transaction) { + let electrum_client = Arc::clone(&self.electrum_client); + + let txid = tx.compute_txid(); + let tx_bytes = tx.encode(); + + let spawn_fut = + self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx)); + + let timeout_fut = + tokio::time::timeout(Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), spawn_fut); + + match timeout_fut.await { + Ok(res) => match res { + Ok(_) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => { + log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx_bytes) + ); + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx_bytes) + ); + }, + } + } } impl Filter for ElectrumRuntimeClient { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 647a6ab1d0..f6a30c4d01 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -154,9 +154,9 @@ impl ElectrumRuntimeStatus { *self = Self::new() } - pub(crate) fn client(&self) -> Option<&ElectrumRuntimeClient> { + pub(crate) fn client(&self) -> Option> { match self { - Self::Started(client) => Some(&*client), + Self::Started(client) => Some(Arc::clone(&client)), Self::Stopped { .. } => None, } } @@ -1266,7 +1266,26 @@ impl ChainSource { } } }, - Self::Electrum { .. } => todo!(), + Self::Electrum { electrum_runtime_status, tx_broadcaster, .. } => { + let electrum_client: Arc = if let Some(client) = + electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before broadcasting" + ); + return; + }; + + let mut receiver = tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in next_package { + electrum_client.broadcast(tx).await; + } + } + }, Self::BitcoindRpc { bitcoind_rpc_client, tx_broadcaster, logger, .. } => { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the From af64dd9ff383552b81b10e19366c609c9a4c25f3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 17 Mar 2025 11:05:48 +0100 Subject: [PATCH 042/177] Implement `update_fee_rate_estimates` for `ChainSource::Electrum` --- src/chain/electrum.rs | 100 +++++++++++++++++++++++++++++++++++++++--- src/chain/mod.rs | 60 ++++++++++++++++++++++--- 2 files changed, 150 insertions(+), 10 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index ea98fd91c5..838ff78f77 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -5,8 +5,12 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::config::TX_BROADCAST_TIMEOUT_SECS; +use crate::config::{Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS}; use crate::error::Error; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + ConfirmationTarget, +}; use crate::logger::{log_bytes, log_error, log_trace, LdkLogger, Logger}; use lightning::chain::{Filter, WatchedOutput}; @@ -17,10 +21,11 @@ use bdk_electrum::BdkElectrumClient; use electrum_client::Client as ElectrumClient; use electrum_client::ConfigBuilder as ElectrumConfigBuilder; -use electrum_client::ElectrumApi; +use electrum_client::{Batch, ElectrumApi}; -use bitcoin::{Script, Transaction, Txid}; +use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use std::collections::HashMap; use std::sync::Arc; use std::time::Duration; @@ -32,12 +37,14 @@ pub(crate) struct ElectrumRuntimeClient { bdk_electrum_client: Arc>, tx_sync: Arc>>, runtime: Arc, + config: Arc, logger: Arc, } impl ElectrumRuntimeClient { pub(crate) fn new( - server_url: String, runtime: Arc, logger: Arc, + server_url: String, runtime: Arc, config: Arc, + logger: Arc, ) -> Result { let electrum_config = ElectrumConfigBuilder::new() .retry(ELECTRUM_CLIENT_NUM_RETRIES) @@ -62,7 +69,7 @@ impl ElectrumRuntimeClient { Error::ConnectionFailed })?, ); - Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, logger }) + Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger }) } pub(crate) async fn broadcast(&self, tx: Transaction) { @@ -106,6 +113,89 @@ impl ElectrumRuntimeClient { }, } } + + pub(crate) async fn get_fee_rate_cache_update( + &self, + ) -> Result, Error> { + let electrum_client = Arc::clone(&self.electrum_client); + + let mut batch = Batch::default(); + let confirmation_targets = get_all_conf_targets(); + for target in confirmation_targets { + let num_blocks = get_num_block_defaults_for_target(target); + batch.estimate_fee(num_blocks); + } + + let spawn_fut = self.runtime.spawn_blocking(move || electrum_client.batch_call(&batch)); + + let timeout_fut = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + spawn_fut, + ); + + let raw_estimates_btc_kvb = timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if raw_estimates_btc_kvb.len() != confirmation_targets.len() + && self.config.network == Network::Bitcoin + { + // Ensure we fail if we didn't receive all estimates. + debug_assert!(false, + "Electrum server didn't return all expected results. This is disallowed on Mainnet." + ); + log_error!(self.logger, + "Failed to retrieve fee rate estimates: Electrum server didn't return all expected results. This is disallowed on Mainnet." + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + for (target, raw_fee_rate_btc_per_kvb) in + confirmation_targets.into_iter().zip(raw_estimates_btc_kvb.into_iter()) + { + // Parse the retrieved serde_json::Value and fall back to 1 sat/vb (10^3 / 10^8 = 10^-5 + // = 0.00001 btc/kvb) if we fail or it yields less than that. This is mostly necessary + // to continue on `signet`/`regtest` where we might not get estimates (or bogus + // values). + let fee_rate_btc_per_kvb = raw_fee_rate_btc_per_kvb + .as_f64() + .map_or(0.00001, |converted| converted.max(0.00001)); + + // Electrum, just like Bitcoin Core, gives us a feerate in BTC/KvB. + // Thus, we multiply by 25_000_000 (10^8 / 4) to get satoshis/kwu. + let fee_rate = { + let fee_rate_sat_per_kwu = (fee_rate_btc_per_kvb * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + Ok(new_fee_rate_cache) + } } impl Filter for ElectrumRuntimeClient { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index f6a30c4d01..75f0adaebb 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -125,12 +125,17 @@ impl ElectrumRuntimeStatus { } pub(crate) fn start( - &mut self, server_url: String, runtime: Arc, logger: Arc, + &mut self, server_url: String, runtime: Arc, config: Arc, + logger: Arc, ) -> Result<(), Error> { match self { Self::Stopped { pending_registered_txs, pending_registered_outputs } => { - let client = - Arc::new(ElectrumRuntimeClient::new(server_url.clone(), runtime, logger)?); + let client = Arc::new(ElectrumRuntimeClient::new( + server_url.clone(), + runtime, + config, + logger, + )?); // Apply any pending `Filter` entries for (txid, script_pubkey) in pending_registered_txs.drain(..) { @@ -307,10 +312,11 @@ impl ChainSource { pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { match self { - Self::Electrum { server_url, electrum_runtime_status, logger, .. } => { + Self::Electrum { server_url, electrum_runtime_status, config, logger, .. } => { electrum_runtime_status.write().unwrap().start( server_url.clone(), Arc::clone(&runtime), + Arc::clone(&config), Arc::clone(&logger), )?; }, @@ -1055,7 +1061,51 @@ impl ChainSource { Ok(()) }, - Self::Electrum { .. } => todo!(), + Self::Electrum { + electrum_runtime_status, + fee_estimator, + kv_store, + logger, + node_metrics, + .. + } => { + let electrum_client: Arc = if let Some(client) = + electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before updating fees" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; + + let now = Instant::now(); + + let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; + fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + log_info!( + logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&kv_store), + Arc::clone(&logger), + )?; + } + + Ok(()) + }, Self::BitcoindRpc { bitcoind_rpc_client, fee_estimator, From 6ac8b914f05a3ec72cf999d32ed23df24e9aacf7 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 17 Mar 2025 11:36:58 +0100 Subject: [PATCH 043/177] Implement `sync_lightning_wallet` for `ChainSource::Electrum` --- src/chain/electrum.rs | 45 ++++++++++++++++++++++++--- src/chain/mod.rs | 72 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 112 insertions(+), 5 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 838ff78f77..5907357d16 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -5,15 +5,18 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::config::{Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS}; +use crate::config::{ + Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, + TX_BROADCAST_TIMEOUT_SECS, +}; use crate::error::Error; use crate::fee_estimator::{ apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, ConfirmationTarget, }; -use crate::logger::{log_bytes, log_error, log_trace, LdkLogger, Logger}; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; -use lightning::chain::{Filter, WatchedOutput}; +use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; use lightning_transaction_sync::ElectrumSyncClient; @@ -27,7 +30,7 @@ use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; use std::collections::HashMap; use std::sync::Arc; -use std::time::Duration; +use std::time::{Duration, Instant}; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 20; @@ -72,6 +75,40 @@ impl ElectrumRuntimeClient { Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger }) } + pub(crate) async fn sync_confirmables( + &self, confirmables: Vec>, + ) -> Result<(), Error> { + let now = Instant::now(); + + let tx_sync = Arc::clone(&self.tx_sync); + let spawn_fut = self.runtime.spawn_blocking(move || tx_sync.sync(confirmables)); + let timeout_fut = + tokio::time::timeout(Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + let res = timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet timed out: {}", e); + Error::TxSyncTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Error::TxSyncFailed + })? + .map_err(|e| { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Error::TxSyncFailed + })?; + + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + + Ok(res) + } + pub(crate) async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 75f0adaebb..57f4d9f65e 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -817,7 +817,77 @@ impl ChainSource { res }, - Self::Electrum { .. } => todo!(), + Self::Electrum { + electrum_runtime_status, + lightning_wallet_sync_status, + kv_store, + logger, + node_metrics, + .. + } => { + let electrum_client: Arc = if let Some(client) = + electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the lightning wallet" + ); + return Err(Error::TxSyncFailed); + }; + + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + sync_cman as Arc, + sync_cmon as Arc, + sync_sweeper as Arc, + ]; + + let receiver_res = { + let mut status_lock = lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(logger, "Failed to receive wallet sync result: {:?}", e); + Error::TxSyncFailed + })?; + } + + let res = electrum_client.sync_confirmables(confirmables).await; + + if let Ok(_) = res { + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&kv_store), + Arc::clone(&logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&kv_store), + Arc::clone(&logger), + Arc::clone(&node_metrics), + )?; + } + + lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + }, Self::BitcoindRpc { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via // `ChainPoller`. So nothing to do here. From 2bd559bd83519e26bedd55c0df07f47fd5ae7e92 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 17 Mar 2025 13:58:53 +0100 Subject: [PATCH 044/177] Implement `sync_onchain_wallet` for `ChainSource::Electrum` --- src/chain/electrum.rs | 74 +++++++++++++++++++++++++++++++++- src/chain/mod.rs | 94 ++++++++++++++++++++++++++++++++++++++++++- src/wallet/mod.rs | 4 ++ 3 files changed, 169 insertions(+), 3 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 5907357d16..6e62d9c081 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -6,8 +6,8 @@ // accordance with one or both of these licenses. use crate::config::{ - Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, - TX_BROADCAST_TIMEOUT_SECS, + Config, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, + LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, }; use crate::error::Error; use crate::fee_estimator::{ @@ -20,6 +20,12 @@ use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; use lightning_transaction_sync::ElectrumSyncClient; +use bdk_chain::bdk_core::spk_client::FullScanRequest as BdkFullScanRequest; +use bdk_chain::bdk_core::spk_client::FullScanResponse as BdkFullScanResponse; +use bdk_chain::bdk_core::spk_client::SyncRequest as BdkSyncRequest; +use bdk_chain::bdk_core::spk_client::SyncResponse as BdkSyncResponse; +use bdk_wallet::KeychainKind as BdkKeyChainKind; + use bdk_electrum::BdkElectrumClient; use electrum_client::Client as ElectrumClient; @@ -32,6 +38,7 @@ use std::collections::HashMap; use std::sync::Arc; use std::time::{Duration, Instant}; +const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 20; @@ -109,6 +116,69 @@ impl ElectrumRuntimeClient { Ok(res) } + pub(crate) async fn get_full_scan_wallet_update( + &self, request: BdkFullScanRequest, + cached_txs: impl IntoIterator>>, + ) -> Result, Error> { + let bdk_electrum_client = Arc::clone(&self.bdk_electrum_client); + bdk_electrum_client.populate_tx_cache(cached_txs); + + let spawn_fut = self.runtime.spawn_blocking(move || { + bdk_electrum_client.full_scan( + request, + BDK_CLIENT_STOP_GAP, + BDK_ELECTRUM_CLIENT_BATCH_SIZE, + true, + ) + }); + let wallet_sync_timeout_fut = + tokio::time::timeout(Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + wallet_sync_timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet timed out: {}", e); + Error::WalletOperationTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + })? + .map_err(|e| { + log_error!(self.logger, "Sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + }) + } + + pub(crate) async fn get_incremental_sync_wallet_update( + &self, request: BdkSyncRequest<(BdkKeyChainKind, u32)>, + cached_txs: impl IntoIterator>>, + ) -> Result { + let bdk_electrum_client = Arc::clone(&self.bdk_electrum_client); + bdk_electrum_client.populate_tx_cache(cached_txs); + + let spawn_fut = self.runtime.spawn_blocking(move || { + bdk_electrum_client.sync(request, BDK_ELECTRUM_CLIENT_BATCH_SIZE, true) + }); + let wallet_sync_timeout_fut = + tokio::time::timeout(Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), spawn_fut); + + wallet_sync_timeout_fut + .await + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet timed out: {}", e); + Error::WalletOperationTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + })? + .map_err(|e| { + log_error!(self.logger, "Incremental sync of on-chain wallet failed: {}", e); + Error::WalletOperationFailed + }) + } + pub(crate) async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 57f4d9f65e..62627797e1 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -39,6 +39,7 @@ use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::SpvClient; use bdk_esplora::EsploraAsyncExt; +use bdk_wallet::Update as BdkUpdate; use esplora_client::AsyncClient as EsploraAsyncClient; @@ -717,7 +718,98 @@ impl ChainSource { res }, - Self::Electrum { .. } => todo!(), + Self::Electrum { + electrum_runtime_status, + onchain_wallet, + onchain_wallet_sync_status, + kv_store, + logger, + node_metrics, + .. + } => { + let electrum_client: Arc = if let Some(client) = + electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the onchain wallet" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; + let receiver_res = { + let mut status_lock = onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + let apply_wallet_update = + |update_res: Result, now: Instant| match update_res { + Ok(update) => match onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()); + { + let mut locked_node_metrics = node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&kv_store), + Arc::clone(&logger), + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => Err(e), + }; + + let cached_txs = onchain_wallet.get_cached_txs(); + + let res = if incremental_sync { + let incremental_sync_request = onchain_wallet.get_incremental_sync_request(); + let incremental_sync_fut = electrum_client + .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); + + let now = Instant::now(); + let update_res = incremental_sync_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + } else { + let full_scan_request = onchain_wallet.get_full_scan_request(); + let full_scan_fut = + electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); + let now = Instant::now(); + let update_res = full_scan_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + }; + + onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + }, Self::BitcoindRpc { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via // `ChainPoller`. So nothing to do here. diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 58a28308d4..53de4b8d54 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -98,6 +98,10 @@ where self.inner.lock().unwrap().start_sync_with_revealed_spks().build() } + pub(crate) fn get_cached_txs(&self) -> Vec> { + self.inner.lock().unwrap().tx_graph().full_txs().map(|tx_node| tx_node.tx).collect() + } + pub(crate) fn current_best_block(&self) -> BestBlock { let checkpoint = self.inner.lock().unwrap().latest_checkpoint(); BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height() } From 95e75d497003b634a9da063362219b897d47d701 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 17 Mar 2025 14:12:07 +0100 Subject: [PATCH 045/177] Add `full_cycle` test using the new `Electrum` chain source .. as we do with `BitcoindRpc`, we now test `Electrum` support by running the `full_cycle` test in CI. --- tests/common/mod.rs | 8 +++++++- tests/integration_tests_rust.rs | 8 ++++++++ 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ac8d1ce908..3258df791a 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod logging; use logging::TestLogWriter; -use ldk_node::config::{Config, EsploraSyncConfig}; +use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ @@ -255,6 +255,7 @@ type TestNode = Node; #[derive(Clone)] pub(crate) enum TestChainSource<'a> { Esplora(&'a ElectrsD), + Electrum(&'a ElectrsD), BitcoindRpc(&'a BitcoinD), } @@ -311,6 +312,11 @@ pub(crate) fn setup_node( let sync_config = EsploraSyncConfig { background_sync_config: None }; builder.set_chain_source_esplora(esplora_url.clone(), Some(sync_config)); }, + TestChainSource::Electrum(electrsd) => { + let electrum_url = format!("tcp://{}", electrsd.electrum_url); + let sync_config = ElectrumSyncConfig { background_sync_config: None }; + builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); + }, TestChainSource::BitcoindRpc(bitcoind) => { let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); let rpc_port = bitcoind.params.rpc_socket.port(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9bbd566899..f2dfa4b5ec 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -45,6 +45,14 @@ fn channel_full_cycle() { do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } +#[test] +fn channel_full_cycle_electrum() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Electrum(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); +} + #[test] fn channel_full_cycle_bitcoind() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From 5cdd2e3a6bd469824db8a206183a8e8772923bc1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 17 Mar 2025 15:13:00 +0100 Subject: [PATCH 046/177] Update README to signal Electrum support --- README.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index f8a3664d4e..ed35d86408 100644 --- a/README.md +++ b/README.md @@ -55,7 +55,7 @@ fn main() { LDK Node currently comes with a decidedly opinionated set of design choices: - On-chain data is handled by the integrated [BDK][bdk] wallet. -- Chain data may currently be sourced from the Bitcoin Core RPC interface or an [Esplora][esplora] server, while support for Electrum will follow soon. +- Chain data may currently be sourced from the Bitcoin Core RPC interface, or from an [Electrum][electrum] or [Esplora][esplora] server. - Wallet and channel state may be persisted to an [SQLite][sqlite] database, to file system, or to a custom back-end to be implemented by the user. - Gossip data may be sourced via Lightning's peer-to-peer network or the [Rapid Gossip Sync](https://docs.rs/lightning-rapid-gossip-sync/*/lightning_rapid_gossip_sync/) protocol. - Entropy for the Lightning and on-chain wallets may be sourced from raw bytes or a [BIP39](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) mnemonic. In addition, LDK Node offers the means to generate and persist the entropy bytes to disk. @@ -72,6 +72,7 @@ The Minimum Supported Rust Version (MSRV) is currently 1.75.0. [rust_crate]: https://crates.io/ [ldk]: https://lightningdevkit.org/ [bdk]: https://bitcoindevkit.org/ +[electrum]: https://github.com/spesmilo/electrum-protocol [esplora]: https://github.com/Blockstream/esplora [sqlite]: https://sqlite.org/ [rust]: https://www.rust-lang.org/ From 66a8d5ebaf12a90302a3c7b361c74d4221513423 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 17 Apr 2025 12:15:14 +0200 Subject: [PATCH 047/177] Switch docker files to use `mempool/electrs` .. as Blockstream seems to have yanked the `electrs` image we were using. --- docker-compose-cln.yml | 3 +-- docker-compose-lnd.yml | 5 ++--- docker-compose.yml | 3 +-- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/docker-compose-cln.yml b/docker-compose-cln.yml index 7978bea358..e1fb117e57 100644 --- a/docker-compose-cln.yml +++ b/docker-compose-cln.yml @@ -25,14 +25,13 @@ services: retries: 5 electrs: - image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + image: mempool/electrs:v3.2.0 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ - "/app/electrs_bitcoin/bin/electrs", "-vvvv", "--timestamp", "--jsonrpc-import", diff --git a/docker-compose-lnd.yml b/docker-compose-lnd.yml index a11f4cac18..54be50ade4 100755 --- a/docker-compose-lnd.yml +++ b/docker-compose-lnd.yml @@ -29,14 +29,13 @@ services: retries: 5 electrs: - image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + image: mempool/electrs:v3.2.0 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ - "/app/electrs_bitcoin/bin/electrs", "-vvvv", "--timestamp", "--jsonrpc-import", @@ -84,4 +83,4 @@ services: networks: bitcoin-electrs: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml index 3c3233bff6..bde1ed90a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -27,14 +27,13 @@ services: retries: 5 electrs: - image: blockstream/esplora:electrs-cd9f90c115751eb9d2bca9a4da89d10d048ae931 + image: mempool/electrs:v3.2.0 platform: linux/amd64 depends_on: bitcoin: condition: service_healthy command: [ - "/app/electrs_bitcoin/bin/electrs", "-vvvv", "--timestamp", "--jsonrpc-import", From 4affc746bb7fd48febd99ef3e236e2fd8a9ac828 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 17 Apr 2025 12:16:29 +0200 Subject: [PATCH 048/177] Bump `bitcoind` versions used in docker files to 27.2 .. to align the RPC API with what we're using in Rust and CLN already. --- docker-compose-lnd.yml | 2 +- docker-compose.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docker-compose-lnd.yml b/docker-compose-lnd.yml index 54be50ade4..8b44aba2d2 100755 --- a/docker-compose-lnd.yml +++ b/docker-compose-lnd.yml @@ -1,6 +1,6 @@ services: bitcoin: - image: blockstream/bitcoind:24.1 + image: blockstream/bitcoind:27.2 platform: linux/amd64 command: [ diff --git a/docker-compose.yml b/docker-compose.yml index bde1ed90a0..425dc129a0 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -2,7 +2,7 @@ version: '3' services: bitcoin: - image: blockstream/bitcoind:24.1 + image: blockstream/bitcoind:27.2 platform: linux/amd64 command: [ From 4c2e5f3f448134757e647013ee6ffe27d7ed5f2b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 27 Mar 2025 13:09:27 +0100 Subject: [PATCH 049/177] Drop `debug_assert` that would have us panic for replayed `PaymentClaimed`s In cd8958f we changed the internal API behavior of `PaymentStore::update`. While previously it would return `Ok(true)` the to-be-updated entry was found in the store, it now returns `Ok(true)` if not only the entry was found but it was actually updated and re-persisted. This was an improvement as it allows us to avoid unnecessary re-persists if nothing changed. However, there were 1-2 places that implicitly relied on that behavior for logging purposes which we didn't correctly update to the new behavior. Unfortunately, one instance in handling `PaymentClaimed` events actually even `debug_assert`ed on the return value, which lead to some unnecessary panics in `debug` in case `PaymentClaimed` got replayed. Here we rectify this by dropping the `debug_assert`. --- src/event.rs | 13 ++++++++----- src/payment/bolt11.rs | 28 ++++++++++++++++++++-------- src/payment/store.rs | 35 ++++++++++++++++++++++++++++++----- 3 files changed, 58 insertions(+), 18 deletions(-) diff --git a/src/event.rs b/src/event.rs index 0de59b8585..00d8441e55 100644 --- a/src/event.rs +++ b/src/event.rs @@ -20,7 +20,7 @@ use crate::logger::Logger; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, - PaymentStore, + PaymentStore, PaymentStoreUpdateResult, }; use crate::io::{ @@ -906,14 +906,17 @@ where }; match self.payment_store.update(&update) { - Ok(true) => (), - Ok(false) => { + Ok(PaymentStoreUpdateResult::Updated) + | Ok(PaymentStoreUpdateResult::Unchanged) => ( + // No need to do anything if the idempotent update was applied, which might + // be the result of a replayed event. + ), + Ok(PaymentStoreUpdateResult::NotFound) => { log_error!( self.logger, - "Payment with ID {} couldn't be found in store", + "Claimed payment with ID {} couldn't be found in store", payment_id, ); - debug_assert!(false); }, Err(e) => { log_error!( diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 38226911f4..ee0f24f059 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -16,7 +16,7 @@ use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, - PaymentStatus, PaymentStore, + PaymentStatus, PaymentStore, PaymentStoreUpdateResult, }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; @@ -408,13 +408,25 @@ impl Bolt11Payment { ..PaymentDetailsUpdate::new(payment_id) }; - if !self.payment_store.update(&update)? { - log_error!( - self.logger, - "Failed to manually fail unknown payment with hash: {}", - payment_hash - ); - return Err(Error::InvalidPaymentHash); + match self.payment_store.update(&update) { + Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => (), + Ok(PaymentStoreUpdateResult::NotFound) => { + log_error!( + self.logger, + "Failed to manually fail unknown payment with hash {}", + payment_hash, + ); + return Err(Error::InvalidPaymentHash); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to manually fail payment with hash {}: {}", + payment_hash, + e + ); + return Err(e); + }, } self.channel_manager.fail_htlc_backwards(&payment_hash); diff --git a/src/payment/store.rs b/src/payment/store.rs index 636f2f0c86..2a074031c4 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -590,6 +590,13 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { } } +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(crate) enum PaymentStoreUpdateResult { + Updated, + Unchanged, + NotFound, +} + pub(crate) struct PaymentStore where L::Target: LdkLogger, @@ -670,17 +677,22 @@ where self.payments.lock().unwrap().get(id).cloned() } - pub(crate) fn update(&self, update: &PaymentDetailsUpdate) -> Result { - let mut updated = false; + pub(crate) fn update( + &self, update: &PaymentDetailsUpdate, + ) -> Result { let mut locked_payments = self.payments.lock().unwrap(); if let Some(payment) = locked_payments.get_mut(&update.id) { - updated = payment.update(update); + let updated = payment.update(update); if updated { self.persist_info(&update.id, payment)?; + Ok(PaymentStoreUpdateResult::Updated) + } else { + Ok(PaymentStoreUpdateResult::Unchanged) } + } else { + Ok(PaymentStoreUpdateResult::NotFound) } - Ok(updated) } pub(crate) fn list_filter bool>( @@ -789,9 +801,22 @@ mod tests { assert_eq!(Ok(true), payment_store.insert(payment)); assert!(payment_store.get(&id).is_some()); + // Check update returns `Updated` let mut update = PaymentDetailsUpdate::new(id); update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(true), payment_store.update(&update)); + assert_eq!(Ok(PaymentStoreUpdateResult::Updated), payment_store.update(&update)); + + // Check no-op update yields `Unchanged` + let mut update = PaymentDetailsUpdate::new(id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(PaymentStoreUpdateResult::Unchanged), payment_store.update(&update)); + + // Check bogus update yields `NotFound` + let bogus_id = PaymentId([84u8; 32]); + let mut update = PaymentDetailsUpdate::new(bogus_id); + update.status = Some(PaymentStatus::Succeeded); + assert_eq!(Ok(PaymentStoreUpdateResult::NotFound), payment_store.update(&update)); + assert!(payment_store.get(&id).is_some()); assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); From 58ff65f78561fd3e33b24ce31fc0449bac080a59 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 18 Apr 2025 11:56:36 +0200 Subject: [PATCH 050/177] Release 0.5.0-rc.1 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 5166a3ec5e..4363509ac0 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.5.0+git" +version = "0.5.0-rc.1" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" From 71aac7e38d06c3fbd87a0646280fbb933fbefbeb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 28 Apr 2025 14:08:40 +0200 Subject: [PATCH 051/177] Update `CHANGELOG.md` for v0.5.0 --- CHANGELOG.md | 57 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 384fa0f0fc..931b98f287 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,60 @@ +# 0.5.0 - Apr. 29, 2025 +Besides numerous API improvements and bugfixes this fifth minor release notably adds support for sourcing chain and fee rate data from an Electrum backend, requesting channels via the [bLIP-51 / LSPS1](https://github.com/lightning/blips/blob/master/blip-0051.md) protocol, as well as experimental support for operating as a [bLIP-52 / LSPS2](https://github.com/lightning/blips/blob/master/blip-0052.md) service. + +## Feature and API updates +- The `PaymentSuccessful` event now exposes a `payment_preimage` field (#392). +- The node now emits `PaymentForwarded` events for forwarded payments (#404). +- The ability to send custom TLVs as part of spontaneous payments has been added (#411). +- The ability to override the used fee rates for on-chain sending has been added (#434). +- The ability to set a description hash when creating a BOLT11 invoice has been added (#438). +- The ability to export pathfinding scores has been added (#458). +- The ability to request inbound channels from an LSP via the bLIP-51 / LSPS1 protocol has been added (#418). +- The `ChannelDetails` returned by `Node::list_channels` now exposes fields for the channel's SCIDs (#444). +- Lightning peer-to-peer gossip data is now being verified when syncing from a Bitcoin Core RPC backend (#428). +- The logging sub-system was reworked to allow logging to backends using the Rust [`log`](https://crates.io/crates/log) facade, as well as via a custom logger trait (#407, #450, #454). +- On-chain transactions are now added to the internal payment store and exposed via `Node::list_payments` (#432). +- Inbound announced channels are now rejected if not all requirements for operating as a forwarding node (set listening addresses and node alias) have been met (#467). +- Initial support for operating as an bLIP-52 / LSPS2 service has been added (#420). + - **Note**: bLIP-52 / LSPS2 support is considered 'alpha'/'experimental' and should *not* yet be used in production. +- The `Builder::set_entropy_seed_bytes` method now takes an array rather than a `Vec` (#493). +- The builder will now return a `NetworkMismatch` error in case of network switching (#485). +- The `Bolt11Jit` payment variant now exposes a field telling how much fee the LSP withheld (#497). +- The ability to disable syncing Lightning and on-chain wallets in the background has been added. If it is disabled, the user is responsible for running `Node::sync_wallets` manually (#508). +- The ability to configure the node's announcement addresses independently from the listening addresses has been added (#484). +- The ability to choose whether to honor the Anchor reserves when calling `send_all_to_address` has been added (#345). +- The ability to sync the node via an Electrum backend has been added (#486). + +## Bug Fixes and Improvements +- When syncing from Bitcoin Core RPC, syncing mempool entries has been made more efficient (#410, #465). +- We now ensure the our configured fallback rates are used when the configured chain source would return huge bogus values during fee estimation (#430). +- We now re-enabled trying to bump Anchor channel transactions for trusted counterparties in the `ContentiousClaimable` case to reduce the risk of losing funds in certain edge cases (#461). +- An issue that would potentially have us panic on retrying the chain listening initialization when syncing from Bitcoin Core RPC has been fixed (#471). +- The `Node::remove_payment` now also removes the respective entry from the in-memory state, not only from the persisted payment store (#514). + +## Compatibility Notes +- The filesystem logger was simplified and its default path changed to `ldk_node.log` in the configured storage directory (#394). +- The BDK dependency has been bumped to `bdk_wallet` v1.0 (#426). +- The LDK dependency has been bumped to `lightning` v0.1 (#426). +- The `rusqlite` dependency has been bumped to v0.31 (#403). +- The minimum supported Rust version (MSRV) has been bumped to v1.75 (#429). + +In total, this release features 53 files changed, 6147 insertions, 1193 deletions, in 191 commits from 14 authors in alphabetical order: + +- alexanderwiederin +- Andrei +- Artur Gontijo +- Ayla Greystone +- Elias Rohrer +- elnosh +- Enigbe Ochekliye +- Evan Feenstra +- G8XSU +- Joost Jager +- maan2003 +- moisesPompilio +- Rob N +- Vincenzo Palazzo + # 0.4.3 - Jan. 23, 2025 This patch release fixes the broken Rust build resulting from `cargo` treating the recent v0.1.0 release of `lightning-liquidity` as API-compatible with the previous v0.1.0-alpha.6 release (even though it's not). From aa10e545e4840210eb2490017171727e2cfa2412 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Apr 2025 09:58:09 +0200 Subject: [PATCH 052/177] Bump Rust version number adding `+git` --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 4363509ac0..9535ddfc83 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.5.0-rc.1" +version = "0.6.0+git" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" From 91ec1b6690b1486392e3b92e419b48483598bf9d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Apr 2025 09:58:30 +0200 Subject: [PATCH 053/177] Bump Kotlin/Swift/Python version numbers --- Package.swift | 2 +- bindings/kotlin/ldk-node-android/gradle.properties | 2 +- bindings/kotlin/ldk-node-jvm/gradle.properties | 2 +- bindings/python/pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 0590524929..248339c939 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription -let tag = "v0.4.2" +let tag = "v0.5.0" let checksum = "95ea5307eb3a99203e39cfa21d962bfe3e879e62429e8c7cdf5292cae5dc35cc" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index 44a51cfafb..1976b71232 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.4.2 +libraryVersion=0.5.0 diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index 338b60d965..62d6602357 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.4.2 +libraryVersion=0.5.0 diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 781542ec3f..8af86d2bbe 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.4.2" +version = "0.5.0" authors = [ { name="Elias Rohrer", email="dev@tnull.de" }, ] From 1b6875d9248e829fddadd8e24b396039197a7b78 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 28 Apr 2025 15:04:51 +0200 Subject: [PATCH 054/177] Use `stable` Rust toolchain for Swift builds --- scripts/uniffi_bindgen_generate_swift.sh | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/scripts/uniffi_bindgen_generate_swift.sh b/scripts/uniffi_bindgen_generate_swift.sh index ba2d84d2ab..ce1151a3bd 100755 --- a/scripts/uniffi_bindgen_generate_swift.sh +++ b/scripts/uniffi_bindgen_generate_swift.sh @@ -1,4 +1,6 @@ #!/bin/bash +set -eox pipefail + BINDINGS_DIR="./bindings/swift" UNIFFI_BINDGEN_BIN="cargo run --manifest-path bindings/uniffi-bindgen/Cargo.toml" @@ -8,11 +10,11 @@ $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language swift -o "$BINDING mkdir -p $BINDINGS_DIR # Install rust target toolchains -rustup install 1.73.0 -rustup component add rust-src --toolchain 1.73.0 -rustup target add aarch64-apple-ios x86_64-apple-ios --toolchain 1.73.0 -rustup target add aarch64-apple-ios-sim --toolchain 1.73.0 -rustup target add aarch64-apple-darwin x86_64-apple-darwin --toolchain 1.73.0 +rustup upgrade stable +rustup component add rust-src --toolchain stable +rustup target add aarch64-apple-ios x86_64-apple-ios --toolchain stable +rustup target add aarch64-apple-ios-sim --toolchain stable +rustup target add aarch64-apple-darwin x86_64-apple-darwin --toolchain stable # Build rust target libs cargo build --profile release-smaller --features uniffi || exit 1 @@ -20,7 +22,7 @@ cargo build --profile release-smaller --features uniffi --target x86_64-apple-da cargo build --profile release-smaller --features uniffi --target aarch64-apple-darwin || exit 1 cargo build --profile release-smaller --features uniffi --target x86_64-apple-ios || exit 1 cargo build --profile release-smaller --features uniffi --target aarch64-apple-ios || exit 1 -cargo +1.73.0 build --release --features uniffi --target aarch64-apple-ios-sim || exit 1 +cargo +stable build --release --features uniffi --target aarch64-apple-ios-sim || exit 1 # Combine ios-sim and apple-darwin (macos) libs for x86_64 and aarch64 (m1) mkdir -p target/lipo-ios-sim/release-smaller || exit 1 From a7f4b7724c5dfb3880c2308dc90c25d3ff387d7f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 5 May 2025 09:29:43 +0200 Subject: [PATCH 055/177] Make minor adjustments to commented-out LDK deps .. as `lightning-transaction-sync` now requires the `electrum` feature. --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4363509ac0..fc0854dfe4 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -47,7 +47,7 @@ lightning-liquidity = { version = "0.1.0", features = ["std"] } #lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["futures"] } #lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rpc-client", "tokio"] } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "time"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum", "time"] } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } @@ -58,7 +58,7 @@ lightning-liquidity = { version = "0.1.0", features = ["std"] } #lightning-background-processor = { path = "../rust-lightning/lightning-background-processor", features = ["futures"] } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } #lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rpc-client", "tokio"] } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "time"] } +#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } bdk_chain = { version = "0.21.1", default-features = false, features = ["std"] } From 923da5c38c78f6e706121a7334f3f7a58630b423 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 5 May 2025 09:47:27 +0200 Subject: [PATCH 056/177] Update Swift files for v0.5.0 --- Package.swift | 2 +- bindings/swift/Sources/LDKNode/LDKNode.swift | 2709 +++++++++++++++--- 2 files changed, 2291 insertions(+), 420 deletions(-) diff --git a/Package.swift b/Package.swift index 248339c939..61da2d5e73 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let tag = "v0.5.0" -let checksum = "95ea5307eb3a99203e39cfa21d962bfe3e879e62429e8c7cdf5292cae5dc35cc" +let checksum = "fd9eb84a478402af8f790519a463b6e1bf6ab3987f5951cd8375afb9d39e7a4b" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 835816b9f5..de5df5e002 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -493,6 +493,21 @@ fileprivate struct FfiConverterString: FfiConverter { } } +fileprivate struct FfiConverterData: FfiConverterRustBuffer { + typealias SwiftType = Data + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Data { + let len: Int32 = try readInt(&buf) + return Data(try readBytes(&buf, count: Int(len))) + } + + public static func write(_ value: Data, into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + writeBytes(&buf, value) + } +} + @@ -502,17 +517,17 @@ public protocol Bolt11PaymentProtocol : AnyObject { func failForHash(paymentHash: PaymentHash) throws - func receive(amountMsat: UInt64, description: String, expirySecs: UInt32) throws -> Bolt11Invoice + func receive(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32) throws -> Bolt11Invoice - func receiveForHash(amountMsat: UInt64, description: String, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice + func receiveForHash(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice - func receiveVariableAmount(description: String, expirySecs: UInt32) throws -> Bolt11Invoice + func receiveVariableAmount(description: Bolt11InvoiceDescription, expirySecs: UInt32) throws -> Bolt11Invoice - func receiveVariableAmountForHash(description: String, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice + func receiveVariableAmountForHash(description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash) throws -> Bolt11Invoice - func receiveVariableAmountViaJitChannel(description: String, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?) throws -> Bolt11Invoice + func receiveVariableAmountViaJitChannel(description: Bolt11InvoiceDescription, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?) throws -> Bolt11Invoice - func receiveViaJitChannel(amountMsat: UInt64, description: String, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?) throws -> Bolt11Invoice + func receiveViaJitChannel(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?) throws -> Bolt11Invoice func send(invoice: Bolt11Invoice, sendingParameters: SendingParameters?) throws -> PaymentId @@ -581,61 +596,61 @@ open func failForHash(paymentHash: PaymentHash)throws {try rustCallWithError(Ff } } -open func receive(amountMsat: UInt64, description: String, expirySecs: UInt32)throws -> Bolt11Invoice { +open func receive(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs),$0 ) }) } -open func receiveForHash(amountMsat: UInt64, description: String, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { +open func receiveForHash(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_for_hash(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterTypePaymentHash.lower(paymentHash),$0 ) }) } -open func receiveVariableAmount(description: String, expirySecs: UInt32)throws -> Bolt11Invoice { +open func receiveVariableAmount(description: Bolt11InvoiceDescription, expirySecs: UInt32)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs),$0 ) }) } -open func receiveVariableAmountForHash(description: String, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { +open func receiveVariableAmountForHash(description: Bolt11InvoiceDescription, expirySecs: UInt32, paymentHash: PaymentHash)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount_for_hash(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterTypePaymentHash.lower(paymentHash),$0 ) }) } -open func receiveVariableAmountViaJitChannel(description: String, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?)throws -> Bolt11Invoice { +open func receiveVariableAmountViaJitChannel(description: Bolt11InvoiceDescription, expirySecs: UInt32, maxProportionalLspFeeLimitPpmMsat: UInt64?)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_variable_amount_via_jit_channel(self.uniffiClonePointer(), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterOptionUInt64.lower(maxProportionalLspFeeLimitPpmMsat),$0 ) }) } -open func receiveViaJitChannel(amountMsat: UInt64, description: String, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?)throws -> Bolt11Invoice { +open func receiveViaJitChannel(amountMsat: UInt64, description: Bolt11InvoiceDescription, expirySecs: UInt32, maxLspFeeLimitMsat: UInt64?)throws -> Bolt11Invoice { return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_bolt11payment_receive_via_jit_channel(self.uniffiClonePointer(), FfiConverterUInt64.lower(amountMsat), - FfiConverterString.lower(description), + FfiConverterTypeBolt11InvoiceDescription.lower(description), FfiConverterUInt32.lower(expirySecs), FfiConverterOptionUInt64.lower(maxLspFeeLimitMsat),$0 ) @@ -901,24 +916,36 @@ public protocol BuilderProtocol : AnyObject { func buildWithVssStoreAndHeaderProvider(vssUrl: String, storeId: String, headerProvider: VssHeaderProvider) throws -> Node + func setAnnouncementAddresses(announcementAddresses: [SocketAddress]) throws + func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: String, rpcPassword: String) + func setChainSourceElectrum(serverUrl: String, config: ElectrumSyncConfig?) + func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) + func setCustomLogger(logWriter: LogWriter) + func setEntropyBip39Mnemonic(mnemonic: Mnemonic, passphrase: String?) func setEntropySeedBytes(seedBytes: [UInt8]) throws func setEntropySeedPath(seedPath: String) + func setFilesystemLogger(logFilePath: String?, maxLogLevel: LogLevel?) + func setGossipSourceP2p() func setGossipSourceRgs(rgsServerUrl: String) - func setLiquiditySourceLsps2(address: SocketAddress, nodeId: PublicKey, token: String?) + func setLiquiditySourceLsps1(nodeId: PublicKey, address: SocketAddress, token: String?) + + func setLiquiditySourceLsps2(nodeId: PublicKey, address: SocketAddress, token: String?) func setListeningAddresses(listeningAddresses: [SocketAddress]) throws + func setLogFacadeLogger() + func setNetwork(network: Network) func setNodeAlias(nodeAlias: String) throws @@ -1028,6 +1055,13 @@ open func buildWithVssStoreAndHeaderProvider(vssUrl: String, storeId: String, he }) } +open func setAnnouncementAddresses(announcementAddresses: [SocketAddress])throws {try rustCallWithError(FfiConverterTypeBuildError.lift) { + uniffi_ldk_node_fn_method_builder_set_announcement_addresses(self.uniffiClonePointer(), + FfiConverterSequenceTypeSocketAddress.lower(announcementAddresses),$0 + ) +} +} + open func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: String, rpcPassword: String) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_chain_source_bitcoind_rpc(self.uniffiClonePointer(), FfiConverterString.lower(rpcHost), @@ -1038,6 +1072,14 @@ open func setChainSourceBitcoindRpc(rpcHost: String, rpcPort: UInt16, rpcUser: S } } +open func setChainSourceElectrum(serverUrl: String, config: ElectrumSyncConfig?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_chain_source_electrum(self.uniffiClonePointer(), + FfiConverterString.lower(serverUrl), + FfiConverterOptionTypeElectrumSyncConfig.lower(config),$0 + ) +} +} + open func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_chain_source_esplora(self.uniffiClonePointer(), FfiConverterString.lower(serverUrl), @@ -1046,6 +1088,13 @@ open func setChainSourceEsplora(serverUrl: String, config: EsploraSyncConfig?) { } } +open func setCustomLogger(logWriter: LogWriter) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_custom_logger(self.uniffiClonePointer(), + FfiConverterTypeLogWriter.lower(logWriter),$0 + ) +} +} + open func setEntropyBip39Mnemonic(mnemonic: Mnemonic, passphrase: String?) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_entropy_bip39_mnemonic(self.uniffiClonePointer(), FfiConverterTypeMnemonic.lower(mnemonic), @@ -1068,6 +1117,14 @@ open func setEntropySeedPath(seedPath: String) {try! rustCall() { } } +open func setFilesystemLogger(logFilePath: String?, maxLogLevel: LogLevel?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_filesystem_logger(self.uniffiClonePointer(), + FfiConverterOptionString.lower(logFilePath), + FfiConverterOptionTypeLogLevel.lower(maxLogLevel),$0 + ) +} +} + open func setGossipSourceP2p() {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_gossip_source_p2p(self.uniffiClonePointer(),$0 ) @@ -1081,10 +1138,19 @@ open func setGossipSourceRgs(rgsServerUrl: String) {try! rustCall() { } } -open func setLiquiditySourceLsps2(address: SocketAddress, nodeId: PublicKey, token: String?) {try! rustCall() { - uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps2(self.uniffiClonePointer(), +open func setLiquiditySourceLsps1(nodeId: PublicKey, address: SocketAddress, token: String?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps1(self.uniffiClonePointer(), + FfiConverterTypePublicKey.lower(nodeId), FfiConverterTypeSocketAddress.lower(address), + FfiConverterOptionString.lower(token),$0 + ) +} +} + +open func setLiquiditySourceLsps2(nodeId: PublicKey, address: SocketAddress, token: String?) {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_liquidity_source_lsps2(self.uniffiClonePointer(), FfiConverterTypePublicKey.lower(nodeId), + FfiConverterTypeSocketAddress.lower(address), FfiConverterOptionString.lower(token),$0 ) } @@ -1097,6 +1163,12 @@ open func setListeningAddresses(listeningAddresses: [SocketAddress])throws {try } } +open func setLogFacadeLogger() {try! rustCall() { + uniffi_ldk_node_fn_method_builder_set_log_facade_logger(self.uniffiClonePointer(),$0 + ) +} +} + open func setNetwork(network: Network) {try! rustCall() { uniffi_ldk_node_fn_method_builder_set_network(self.uniffiClonePointer(), FfiConverterTypeNetwork.lower(network),$0 @@ -1166,20 +1238,18 @@ public func FfiConverterTypeBuilder_lower(_ value: Builder) -> UnsafeMutableRawP -public protocol NetworkGraphProtocol : AnyObject { +public protocol FeeRateProtocol : AnyObject { - func channel(shortChannelId: UInt64) -> ChannelInfo? - - func listChannels() -> [UInt64] + func toSatPerKwu() -> UInt64 - func listNodes() -> [NodeId] + func toSatPerVbCeil() -> UInt64 - func node(nodeId: NodeId) -> NodeInfo? + func toSatPerVbFloor() -> UInt64 } -open class NetworkGraph: - NetworkGraphProtocol { +open class FeeRate: + FeeRateProtocol { fileprivate let pointer: UnsafeMutableRawPointer! /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. @@ -1204,7 +1274,7 @@ open class NetworkGraph: } public func uniffiClonePointer() -> UnsafeMutableRawPointer { - return try! rustCall { uniffi_ldk_node_fn_clone_networkgraph(self.pointer, $0) } + return try! rustCall { uniffi_ldk_node_fn_clone_feerate(self.pointer, $0) } } // No primary constructor declared for this class. @@ -1213,38 +1283,45 @@ open class NetworkGraph: return } - try! rustCall { uniffi_ldk_node_fn_free_networkgraph(pointer, $0) } + try! rustCall { uniffi_ldk_node_fn_free_feerate(pointer, $0) } } - +public static func fromSatPerKwu(satKwu: UInt64) -> FeeRate { + return try! FfiConverterTypeFeeRate.lift(try! rustCall() { + uniffi_ldk_node_fn_constructor_feerate_from_sat_per_kwu( + FfiConverterUInt64.lower(satKwu),$0 + ) +}) +} -open func channel(shortChannelId: UInt64) -> ChannelInfo? { - return try! FfiConverterOptionTypeChannelInfo.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_channel(self.uniffiClonePointer(), - FfiConverterUInt64.lower(shortChannelId),$0 +public static func fromSatPerVbUnchecked(satVb: UInt64) -> FeeRate { + return try! FfiConverterTypeFeeRate.lift(try! rustCall() { + uniffi_ldk_node_fn_constructor_feerate_from_sat_per_vb_unchecked( + FfiConverterUInt64.lower(satVb),$0 ) }) } -open func listChannels() -> [UInt64] { - return try! FfiConverterSequenceUInt64.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_list_channels(self.uniffiClonePointer(),$0 + + +open func toSatPerKwu() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_kwu(self.uniffiClonePointer(),$0 ) }) } -open func listNodes() -> [NodeId] { - return try! FfiConverterSequenceTypeNodeId.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_list_nodes(self.uniffiClonePointer(),$0 +open func toSatPerVbCeil() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_vb_ceil(self.uniffiClonePointer(),$0 ) }) } -open func node(nodeId: NodeId) -> NodeInfo? { - return try! FfiConverterOptionTypeNodeInfo.lift(try! rustCall() { - uniffi_ldk_node_fn_method_networkgraph_node(self.uniffiClonePointer(), - FfiConverterTypeNodeId.lower(nodeId),$0 +open func toSatPerVbFloor() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_feerate_to_sat_per_vb_floor(self.uniffiClonePointer(),$0 ) }) } @@ -1252,20 +1329,20 @@ open func node(nodeId: NodeId) -> NodeInfo? { } -public struct FfiConverterTypeNetworkGraph: FfiConverter { +public struct FfiConverterTypeFeeRate: FfiConverter { typealias FfiType = UnsafeMutableRawPointer - typealias SwiftType = NetworkGraph + typealias SwiftType = FeeRate - public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> NetworkGraph { - return NetworkGraph(unsafeFromRawPointer: pointer) + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> FeeRate { + return FeeRate(unsafeFromRawPointer: pointer) } - public static func lower(_ value: NetworkGraph) -> UnsafeMutableRawPointer { + public static func lower(_ value: FeeRate) -> UnsafeMutableRawPointer { return value.uniffiClonePointer() } - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> NetworkGraph { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> FeeRate { let v: UInt64 = try readInt(&buf) // The Rust code won't compile if a pointer won't fit in a UInt64. // We have to go via `UInt` because that's the thing that's the size of a pointer. @@ -1276,7 +1353,7 @@ public struct FfiConverterTypeNetworkGraph: FfiConverter { return try lift(ptr!) } - public static func write(_ value: NetworkGraph, into buf: inout [UInt8]) { + public static func write(_ value: FeeRate, into buf: inout [UInt8]) { // This fiddling is because `Int` is the thing that's the same size as a pointer. // The Rust code won't compile if a pointer won't fit in a `UInt64`. writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) @@ -1286,89 +1363,27 @@ public struct FfiConverterTypeNetworkGraph: FfiConverter { -public func FfiConverterTypeNetworkGraph_lift(_ pointer: UnsafeMutableRawPointer) throws -> NetworkGraph { - return try FfiConverterTypeNetworkGraph.lift(pointer) +public func FfiConverterTypeFeeRate_lift(_ pointer: UnsafeMutableRawPointer) throws -> FeeRate { + return try FfiConverterTypeFeeRate.lift(pointer) } -public func FfiConverterTypeNetworkGraph_lower(_ value: NetworkGraph) -> UnsafeMutableRawPointer { - return FfiConverterTypeNetworkGraph.lower(value) +public func FfiConverterTypeFeeRate_lower(_ value: FeeRate) -> UnsafeMutableRawPointer { + return FfiConverterTypeFeeRate.lower(value) } -public protocol NodeProtocol : AnyObject { - - func bolt11Payment() -> Bolt11Payment - - func bolt12Payment() -> Bolt12Payment - - func closeChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey) throws - - func config() -> Config - - func connect(nodeId: PublicKey, address: SocketAddress, persist: Bool) throws - - func disconnect(nodeId: PublicKey) throws - - func eventHandled() - - func forceCloseChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, reason: String?) throws - - func listBalances() -> BalanceDetails - - func listChannels() -> [ChannelDetails] - - func listPayments() -> [PaymentDetails] - - func listPeers() -> [PeerDetails] - - func listeningAddresses() -> [SocketAddress]? - - func networkGraph() -> NetworkGraph - - func nextEvent() -> Event? - - func nextEventAsync() async -> Event - - func nodeAlias() -> NodeAlias? - - func nodeId() -> PublicKey +public protocol Lsps1LiquidityProtocol : AnyObject { - func onchainPayment() -> OnchainPayment - - func openAnnouncedChannel(nodeId: PublicKey, address: SocketAddress, channelAmountSats: UInt64, pushToCounterpartyMsat: UInt64?, channelConfig: ChannelConfig?) throws -> UserChannelId - - func openChannel(nodeId: PublicKey, address: SocketAddress, channelAmountSats: UInt64, pushToCounterpartyMsat: UInt64?, channelConfig: ChannelConfig?) throws -> UserChannelId - - func payment(paymentId: PaymentId) -> PaymentDetails? - - func removePayment(paymentId: PaymentId) throws - - func signMessage(msg: [UInt8]) -> String - - func spontaneousPayment() -> SpontaneousPayment - - func start() throws - - func status() -> NodeStatus - - func stop() throws - - func syncWallets() throws - - func unifiedQrPayment() -> UnifiedQrPayment - - func updateChannelConfig(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, channelConfig: ChannelConfig) throws - - func verifySignature(msg: [UInt8], sig: String, pkey: PublicKey) -> Bool + func checkOrderStatus(orderId: OrderId) throws -> Lsps1OrderStatus - func waitNextEvent() -> Event + func requestChannel(lspBalanceSat: UInt64, clientBalanceSat: UInt64, channelExpiryBlocks: UInt32, announceChannel: Bool) throws -> Lsps1OrderStatus } -open class Node: - NodeProtocol { +open class Lsps1Liquidity: + Lsps1LiquidityProtocol { fileprivate let pointer: UnsafeMutableRawPointer! /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. @@ -1393,7 +1408,7 @@ open class Node: } public func uniffiClonePointer() -> UnsafeMutableRawPointer { - return try! rustCall { uniffi_ldk_node_fn_clone_node(self.pointer, $0) } + return try! rustCall { uniffi_ldk_node_fn_clone_lsps1liquidity(self.pointer, $0) } } // No primary constructor declared for this class. @@ -1402,63 +1417,548 @@ open class Node: return } - try! rustCall { uniffi_ldk_node_fn_free_node(pointer, $0) } + try! rustCall { uniffi_ldk_node_fn_free_lsps1liquidity(pointer, $0) } } -open func bolt11Payment() -> Bolt11Payment { - return try! FfiConverterTypeBolt11Payment.lift(try! rustCall() { - uniffi_ldk_node_fn_method_node_bolt11_payment(self.uniffiClonePointer(),$0 +open func checkOrderStatus(orderId: OrderId)throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_lsps1liquidity_check_order_status(self.uniffiClonePointer(), + FfiConverterTypeOrderId.lower(orderId),$0 ) }) } -open func bolt12Payment() -> Bolt12Payment { - return try! FfiConverterTypeBolt12Payment.lift(try! rustCall() { - uniffi_ldk_node_fn_method_node_bolt12_payment(self.uniffiClonePointer(),$0 +open func requestChannel(lspBalanceSat: UInt64, clientBalanceSat: UInt64, channelExpiryBlocks: UInt32, announceChannel: Bool)throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_lsps1liquidity_request_channel(self.uniffiClonePointer(), + FfiConverterUInt64.lower(lspBalanceSat), + FfiConverterUInt64.lower(clientBalanceSat), + FfiConverterUInt32.lower(channelExpiryBlocks), + FfiConverterBool.lower(announceChannel),$0 ) }) } -open func closeChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { - uniffi_ldk_node_fn_method_node_close_channel(self.uniffiClonePointer(), - FfiConverterTypeUserChannelId.lower(userChannelId), - FfiConverterTypePublicKey.lower(counterpartyNodeId),$0 - ) + +} + +public struct FfiConverterTypeLSPS1Liquidity: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Lsps1Liquidity + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Lsps1Liquidity { + return Lsps1Liquidity(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Lsps1Liquidity) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps1Liquidity { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Lsps1Liquidity, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeLSPS1Liquidity_lift(_ pointer: UnsafeMutableRawPointer) throws -> Lsps1Liquidity { + return try FfiConverterTypeLSPS1Liquidity.lift(pointer) } + +public func FfiConverterTypeLSPS1Liquidity_lower(_ value: Lsps1Liquidity) -> UnsafeMutableRawPointer { + return FfiConverterTypeLSPS1Liquidity.lower(value) } + + + + +public protocol LogWriter : AnyObject { + + func log(record: LogRecord) -open func config() -> Config { - return try! FfiConverterTypeConfig.lift(try! rustCall() { - uniffi_ldk_node_fn_method_node_config(self.uniffiClonePointer(),$0 - ) -}) } + +open class LogWriterImpl: + LogWriter { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_logwriter(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_logwriter(pointer, $0) } + } + -open func connect(nodeId: PublicKey, address: SocketAddress, persist: Bool)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { - uniffi_ldk_node_fn_method_node_connect(self.uniffiClonePointer(), - FfiConverterTypePublicKey.lower(nodeId), - FfiConverterTypeSocketAddress.lower(address), - FfiConverterBool.lower(persist),$0 + + +open func log(record: LogRecord) {try! rustCall() { + uniffi_ldk_node_fn_method_logwriter_log(self.uniffiClonePointer(), + FfiConverterTypeLogRecord.lower(record),$0 ) } } -open func disconnect(nodeId: PublicKey)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { - uniffi_ldk_node_fn_method_node_disconnect(self.uniffiClonePointer(), + +} +// Magic number for the Rust proxy to call using the same mechanism as every other method, +// to free the callback once it's dropped by Rust. +private let IDX_CALLBACK_FREE: Int32 = 0 +// Callback return codes +private let UNIFFI_CALLBACK_SUCCESS: Int32 = 0 +private let UNIFFI_CALLBACK_ERROR: Int32 = 1 +private let UNIFFI_CALLBACK_UNEXPECTED_ERROR: Int32 = 2 + +// Put the implementation in a struct so we don't pollute the top-level namespace +fileprivate struct UniffiCallbackInterfaceLogWriter { + + // Create the VTable using a series of closures. + // Swift automatically converts these into C callback functions. + static var vtable: UniffiVTableCallbackInterfaceLogWriter = UniffiVTableCallbackInterfaceLogWriter( + log: { ( + uniffiHandle: UInt64, + record: RustBuffer, + uniffiOutReturn: UnsafeMutableRawPointer, + uniffiCallStatus: UnsafeMutablePointer + ) in + let makeCall = { + () throws -> () in + guard let uniffiObj = try? FfiConverterTypeLogWriter.handleMap.get(handle: uniffiHandle) else { + throw UniffiInternalError.unexpectedStaleHandle + } + return uniffiObj.log( + record: try FfiConverterTypeLogRecord.lift(record) + ) + } + + + let writeReturn = { () } + uniffiTraitInterfaceCall( + callStatus: uniffiCallStatus, + makeCall: makeCall, + writeReturn: writeReturn + ) + }, + uniffiFree: { (uniffiHandle: UInt64) -> () in + let result = try? FfiConverterTypeLogWriter.handleMap.remove(handle: uniffiHandle) + if result == nil { + print("Uniffi callback interface LogWriter: handle missing in uniffiFree") + } + } + ) +} + +private func uniffiCallbackInitLogWriter() { + uniffi_ldk_node_fn_init_callback_vtable_logwriter(&UniffiCallbackInterfaceLogWriter.vtable) +} + +public struct FfiConverterTypeLogWriter: FfiConverter { + fileprivate static var handleMap = UniffiHandleMap() + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = LogWriter + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> LogWriter { + return LogWriterImpl(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: LogWriter) -> UnsafeMutableRawPointer { + guard let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: handleMap.insert(obj: value))) else { + fatalError("Cast to UnsafeMutableRawPointer failed") + } + return ptr + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LogWriter { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: LogWriter, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeLogWriter_lift(_ pointer: UnsafeMutableRawPointer) throws -> LogWriter { + return try FfiConverterTypeLogWriter.lift(pointer) +} + +public func FfiConverterTypeLogWriter_lower(_ value: LogWriter) -> UnsafeMutableRawPointer { + return FfiConverterTypeLogWriter.lower(value) +} + + + + +public protocol NetworkGraphProtocol : AnyObject { + + func channel(shortChannelId: UInt64) -> ChannelInfo? + + func listChannels() -> [UInt64] + + func listNodes() -> [NodeId] + + func node(nodeId: NodeId) -> NodeInfo? + +} + +open class NetworkGraph: + NetworkGraphProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_networkgraph(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_networkgraph(pointer, $0) } + } + + + + +open func channel(shortChannelId: UInt64) -> ChannelInfo? { + return try! FfiConverterOptionTypeChannelInfo.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_channel(self.uniffiClonePointer(), + FfiConverterUInt64.lower(shortChannelId),$0 + ) +}) +} + +open func listChannels() -> [UInt64] { + return try! FfiConverterSequenceUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_list_channels(self.uniffiClonePointer(),$0 + ) +}) +} + +open func listNodes() -> [NodeId] { + return try! FfiConverterSequenceTypeNodeId.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_list_nodes(self.uniffiClonePointer(),$0 + ) +}) +} + +open func node(nodeId: NodeId) -> NodeInfo? { + return try! FfiConverterOptionTypeNodeInfo.lift(try! rustCall() { + uniffi_ldk_node_fn_method_networkgraph_node(self.uniffiClonePointer(), + FfiConverterTypeNodeId.lower(nodeId),$0 + ) +}) +} + + +} + +public struct FfiConverterTypeNetworkGraph: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = NetworkGraph + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> NetworkGraph { + return NetworkGraph(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: NetworkGraph) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> NetworkGraph { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: NetworkGraph, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeNetworkGraph_lift(_ pointer: UnsafeMutableRawPointer) throws -> NetworkGraph { + return try FfiConverterTypeNetworkGraph.lift(pointer) +} + +public func FfiConverterTypeNetworkGraph_lower(_ value: NetworkGraph) -> UnsafeMutableRawPointer { + return FfiConverterTypeNetworkGraph.lower(value) +} + + + + +public protocol NodeProtocol : AnyObject { + + func announcementAddresses() -> [SocketAddress]? + + func bolt11Payment() -> Bolt11Payment + + func bolt12Payment() -> Bolt12Payment + + func closeChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey) throws + + func config() -> Config + + func connect(nodeId: PublicKey, address: SocketAddress, persist: Bool) throws + + func disconnect(nodeId: PublicKey) throws + + func eventHandled() throws + + func exportPathfindingScores() throws -> Data + + func forceCloseChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, reason: String?) throws + + func listBalances() -> BalanceDetails + + func listChannels() -> [ChannelDetails] + + func listPayments() -> [PaymentDetails] + + func listPeers() -> [PeerDetails] + + func listeningAddresses() -> [SocketAddress]? + + func lsps1Liquidity() -> Lsps1Liquidity + + func networkGraph() -> NetworkGraph + + func nextEvent() -> Event? + + func nextEventAsync() async -> Event + + func nodeAlias() -> NodeAlias? + + func nodeId() -> PublicKey + + func onchainPayment() -> OnchainPayment + + func openAnnouncedChannel(nodeId: PublicKey, address: SocketAddress, channelAmountSats: UInt64, pushToCounterpartyMsat: UInt64?, channelConfig: ChannelConfig?) throws -> UserChannelId + + func openChannel(nodeId: PublicKey, address: SocketAddress, channelAmountSats: UInt64, pushToCounterpartyMsat: UInt64?, channelConfig: ChannelConfig?) throws -> UserChannelId + + func payment(paymentId: PaymentId) -> PaymentDetails? + + func removePayment(paymentId: PaymentId) throws + + func signMessage(msg: [UInt8]) -> String + + func spontaneousPayment() -> SpontaneousPayment + + func start() throws + + func status() -> NodeStatus + + func stop() throws + + func syncWallets() throws + + func unifiedQrPayment() -> UnifiedQrPayment + + func updateChannelConfig(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, channelConfig: ChannelConfig) throws + + func verifySignature(msg: [UInt8], sig: String, pkey: PublicKey) -> Bool + + func waitNextEvent() -> Event + +} + +open class Node: + NodeProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_node(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_node(pointer, $0) } + } + + + + +open func announcementAddresses() -> [SocketAddress]? { + return try! FfiConverterOptionSequenceTypeSocketAddress.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_announcement_addresses(self.uniffiClonePointer(),$0 + ) +}) +} + +open func bolt11Payment() -> Bolt11Payment { + return try! FfiConverterTypeBolt11Payment.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_bolt11_payment(self.uniffiClonePointer(),$0 + ) +}) +} + +open func bolt12Payment() -> Bolt12Payment { + return try! FfiConverterTypeBolt12Payment.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_bolt12_payment(self.uniffiClonePointer(),$0 + ) +}) +} + +open func closeChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_close_channel(self.uniffiClonePointer(), + FfiConverterTypeUserChannelId.lower(userChannelId), + FfiConverterTypePublicKey.lower(counterpartyNodeId),$0 + ) +} +} + +open func config() -> Config { + return try! FfiConverterTypeConfig.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_config(self.uniffiClonePointer(),$0 + ) +}) +} + +open func connect(nodeId: PublicKey, address: SocketAddress, persist: Bool)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_connect(self.uniffiClonePointer(), + FfiConverterTypePublicKey.lower(nodeId), + FfiConverterTypeSocketAddress.lower(address), + FfiConverterBool.lower(persist),$0 + ) +} +} + +open func disconnect(nodeId: PublicKey)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_disconnect(self.uniffiClonePointer(), FfiConverterTypePublicKey.lower(nodeId),$0 ) } } -open func eventHandled() {try! rustCall() { +open func eventHandled()throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_event_handled(self.uniffiClonePointer(),$0 ) } } +open func exportPathfindingScores()throws -> Data { + return try FfiConverterData.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_node_export_pathfinding_scores(self.uniffiClonePointer(),$0 + ) +}) +} + open func forceCloseChannel(userChannelId: UserChannelId, counterpartyNodeId: PublicKey, reason: String?)throws {try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_node_force_close_channel(self.uniffiClonePointer(), FfiConverterTypeUserChannelId.lower(userChannelId), @@ -1503,6 +2003,13 @@ open func listeningAddresses() -> [SocketAddress]? { }) } +open func lsps1Liquidity() -> Lsps1Liquidity { + return try! FfiConverterTypeLSPS1Liquidity.lift(try! rustCall() { + uniffi_ldk_node_fn_method_node_lsps1_liquidity(self.uniffiClonePointer(),$0 + ) +}) +} + open func networkGraph() -> NetworkGraph { return try! FfiConverterTypeNetworkGraph.lift(try! rustCall() { uniffi_ldk_node_fn_method_node_network_graph(self.uniffiClonePointer(),$0 @@ -1720,9 +2227,9 @@ public protocol OnchainPaymentProtocol : AnyObject { func newAddress() throws -> Address - func sendAllToAddress(address: Address) throws -> Txid + func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?) throws -> Txid - func sendToAddress(address: Address, amountSats: UInt64) throws -> Txid + func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?) throws -> Txid } @@ -1774,19 +2281,22 @@ open func newAddress()throws -> Address { }) } -open func sendAllToAddress(address: Address)throws -> Txid { +open func sendAllToAddress(address: Address, retainReserve: Bool, feeRate: FeeRate?)throws -> Txid { return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_send_all_to_address(self.uniffiClonePointer(), - FfiConverterTypeAddress.lower(address),$0 + FfiConverterTypeAddress.lower(address), + FfiConverterBool.lower(retainReserve), + FfiConverterOptionTypeFeeRate.lower(feeRate),$0 ) }) } -open func sendToAddress(address: Address, amountSats: UInt64)throws -> Txid { +open func sendToAddress(address: Address, amountSats: UInt64, feeRate: FeeRate?)throws -> Txid { return try FfiConverterTypeTxid.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { uniffi_ldk_node_fn_method_onchainpayment_send_to_address(self.uniffiClonePointer(), FfiConverterTypeAddress.lower(address), - FfiConverterUInt64.lower(amountSats),$0 + FfiConverterUInt64.lower(amountSats), + FfiConverterOptionTypeFeeRate.lower(feeRate),$0 ) }) } @@ -1845,6 +2355,8 @@ public protocol SpontaneousPaymentProtocol : AnyObject { func sendProbes(amountMsat: UInt64, nodeId: PublicKey) throws + func sendWithCustomTlvs(amountMsat: UInt64, nodeId: PublicKey, sendingParameters: SendingParameters?, customTlvs: [CustomTlvRecord]) throws -> PaymentId + } open class SpontaneousPayment: @@ -1906,6 +2418,17 @@ open func sendProbes(amountMsat: UInt64, nodeId: PublicKey)throws {try rustCall } } +open func sendWithCustomTlvs(amountMsat: UInt64, nodeId: PublicKey, sendingParameters: SendingParameters?, customTlvs: [CustomTlvRecord])throws -> PaymentId { + return try FfiConverterTypePaymentId.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_method_spontaneouspayment_send_with_custom_tlvs(self.uniffiClonePointer(), + FfiConverterUInt64.lower(amountMsat), + FfiConverterTypePublicKey.lower(nodeId), + FfiConverterOptionTypeSendingParameters.lower(sendingParameters), + FfiConverterSequenceTypeCustomTlvRecord.lower(customTlvs),$0 + ) +}) +} + } @@ -2236,6 +2759,71 @@ public func FfiConverterTypeAnchorChannelsConfig_lower(_ value: AnchorChannelsCo } +public struct BackgroundSyncConfig { + public var onchainWalletSyncIntervalSecs: UInt64 + public var lightningWalletSyncIntervalSecs: UInt64 + public var feeRateCacheUpdateIntervalSecs: UInt64 + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(onchainWalletSyncIntervalSecs: UInt64, lightningWalletSyncIntervalSecs: UInt64, feeRateCacheUpdateIntervalSecs: UInt64) { + self.onchainWalletSyncIntervalSecs = onchainWalletSyncIntervalSecs + self.lightningWalletSyncIntervalSecs = lightningWalletSyncIntervalSecs + self.feeRateCacheUpdateIntervalSecs = feeRateCacheUpdateIntervalSecs + } +} + + + +extension BackgroundSyncConfig: Equatable, Hashable { + public static func ==(lhs: BackgroundSyncConfig, rhs: BackgroundSyncConfig) -> Bool { + if lhs.onchainWalletSyncIntervalSecs != rhs.onchainWalletSyncIntervalSecs { + return false + } + if lhs.lightningWalletSyncIntervalSecs != rhs.lightningWalletSyncIntervalSecs { + return false + } + if lhs.feeRateCacheUpdateIntervalSecs != rhs.feeRateCacheUpdateIntervalSecs { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(onchainWalletSyncIntervalSecs) + hasher.combine(lightningWalletSyncIntervalSecs) + hasher.combine(feeRateCacheUpdateIntervalSecs) + } +} + + +public struct FfiConverterTypeBackgroundSyncConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BackgroundSyncConfig { + return + try BackgroundSyncConfig( + onchainWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), + lightningWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), + feeRateCacheUpdateIntervalSecs: FfiConverterUInt64.read(from: &buf) + ) + } + + public static func write(_ value: BackgroundSyncConfig, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.onchainWalletSyncIntervalSecs, into: &buf) + FfiConverterUInt64.write(value.lightningWalletSyncIntervalSecs, into: &buf) + FfiConverterUInt64.write(value.feeRateCacheUpdateIntervalSecs, into: &buf) + } +} + + +public func FfiConverterTypeBackgroundSyncConfig_lift(_ buf: RustBuffer) throws -> BackgroundSyncConfig { + return try FfiConverterTypeBackgroundSyncConfig.lift(buf) +} + +public func FfiConverterTypeBackgroundSyncConfig_lower(_ value: BackgroundSyncConfig) -> RustBuffer { + return FfiConverterTypeBackgroundSyncConfig.lower(value) +} + + public struct BalanceDetails { public var totalOnchainBalanceSats: UInt64 public var spendableOnchainBalanceSats: UInt64 @@ -2360,25 +2948,106 @@ extension BestBlock: Equatable, Hashable { public struct FfiConverterTypeBestBlock: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BestBlock { return - try BestBlock( - blockHash: FfiConverterTypeBlockHash.read(from: &buf), - height: FfiConverterUInt32.read(from: &buf) + try BestBlock( + blockHash: FfiConverterTypeBlockHash.read(from: &buf), + height: FfiConverterUInt32.read(from: &buf) + ) + } + + public static func write(_ value: BestBlock, into buf: inout [UInt8]) { + FfiConverterTypeBlockHash.write(value.blockHash, into: &buf) + FfiConverterUInt32.write(value.height, into: &buf) + } +} + + +public func FfiConverterTypeBestBlock_lift(_ buf: RustBuffer) throws -> BestBlock { + return try FfiConverterTypeBestBlock.lift(buf) +} + +public func FfiConverterTypeBestBlock_lower(_ value: BestBlock) -> RustBuffer { + return FfiConverterTypeBestBlock.lower(value) +} + + +public struct Bolt11PaymentInfo { + public var state: PaymentState + public var expiresAt: DateTime + public var feeTotalSat: UInt64 + public var orderTotalSat: UInt64 + public var invoice: Bolt11Invoice + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(state: PaymentState, expiresAt: DateTime, feeTotalSat: UInt64, orderTotalSat: UInt64, invoice: Bolt11Invoice) { + self.state = state + self.expiresAt = expiresAt + self.feeTotalSat = feeTotalSat + self.orderTotalSat = orderTotalSat + self.invoice = invoice + } +} + + + +extension Bolt11PaymentInfo: Equatable, Hashable { + public static func ==(lhs: Bolt11PaymentInfo, rhs: Bolt11PaymentInfo) -> Bool { + if lhs.state != rhs.state { + return false + } + if lhs.expiresAt != rhs.expiresAt { + return false + } + if lhs.feeTotalSat != rhs.feeTotalSat { + return false + } + if lhs.orderTotalSat != rhs.orderTotalSat { + return false + } + if lhs.invoice != rhs.invoice { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(state) + hasher.combine(expiresAt) + hasher.combine(feeTotalSat) + hasher.combine(orderTotalSat) + hasher.combine(invoice) + } +} + + +public struct FfiConverterTypeBolt11PaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11PaymentInfo { + return + try Bolt11PaymentInfo( + state: FfiConverterTypePaymentState.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf), + feeTotalSat: FfiConverterUInt64.read(from: &buf), + orderTotalSat: FfiConverterUInt64.read(from: &buf), + invoice: FfiConverterTypeBolt11Invoice.read(from: &buf) ) } - public static func write(_ value: BestBlock, into buf: inout [UInt8]) { - FfiConverterTypeBlockHash.write(value.blockHash, into: &buf) - FfiConverterUInt32.write(value.height, into: &buf) + public static func write(_ value: Bolt11PaymentInfo, into buf: inout [UInt8]) { + FfiConverterTypePaymentState.write(value.state, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + FfiConverterUInt64.write(value.feeTotalSat, into: &buf) + FfiConverterUInt64.write(value.orderTotalSat, into: &buf) + FfiConverterTypeBolt11Invoice.write(value.invoice, into: &buf) } } -public func FfiConverterTypeBestBlock_lift(_ buf: RustBuffer) throws -> BestBlock { - return try FfiConverterTypeBestBlock.lift(buf) +public func FfiConverterTypeBolt11PaymentInfo_lift(_ buf: RustBuffer) throws -> Bolt11PaymentInfo { + return try FfiConverterTypeBolt11PaymentInfo.lift(buf) } -public func FfiConverterTypeBestBlock_lower(_ value: BestBlock) -> RustBuffer { - return FfiConverterTypeBestBlock.lower(value) +public func FfiConverterTypeBolt11PaymentInfo_lower(_ value: Bolt11PaymentInfo) -> RustBuffer { + return FfiConverterTypeBolt11PaymentInfo.lower(value) } @@ -2475,6 +3144,9 @@ public struct ChannelDetails { public var channelId: ChannelId public var counterpartyNodeId: PublicKey public var fundingTxo: OutPoint? + public var shortChannelId: UInt64? + public var outboundScidAlias: UInt64? + public var inboundScidAlias: UInt64? public var channelValueSats: UInt64 public var unspendablePunishmentReserve: UInt64? public var userChannelId: UserChannelId @@ -2503,10 +3175,13 @@ public struct ChannelDetails { // Default memberwise initializers are never public by default, so we // declare one manually. - public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig) { + public init(channelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint?, shortChannelId: UInt64?, outboundScidAlias: UInt64?, inboundScidAlias: UInt64?, channelValueSats: UInt64, unspendablePunishmentReserve: UInt64?, userChannelId: UserChannelId, feerateSatPer1000Weight: UInt32, outboundCapacityMsat: UInt64, inboundCapacityMsat: UInt64, confirmationsRequired: UInt32?, confirmations: UInt32?, isOutbound: Bool, isChannelReady: Bool, isUsable: Bool, isAnnounced: Bool, cltvExpiryDelta: UInt16?, counterpartyUnspendablePunishmentReserve: UInt64, counterpartyOutboundHtlcMinimumMsat: UInt64?, counterpartyOutboundHtlcMaximumMsat: UInt64?, counterpartyForwardingInfoFeeBaseMsat: UInt32?, counterpartyForwardingInfoFeeProportionalMillionths: UInt32?, counterpartyForwardingInfoCltvExpiryDelta: UInt16?, nextOutboundHtlcLimitMsat: UInt64, nextOutboundHtlcMinimumMsat: UInt64, forceCloseSpendDelay: UInt16?, inboundHtlcMinimumMsat: UInt64, inboundHtlcMaximumMsat: UInt64?, config: ChannelConfig) { self.channelId = channelId self.counterpartyNodeId = counterpartyNodeId self.fundingTxo = fundingTxo + self.shortChannelId = shortChannelId + self.outboundScidAlias = outboundScidAlias + self.inboundScidAlias = inboundScidAlias self.channelValueSats = channelValueSats self.unspendablePunishmentReserve = unspendablePunishmentReserve self.userChannelId = userChannelId @@ -2548,6 +3223,15 @@ extension ChannelDetails: Equatable, Hashable { if lhs.fundingTxo != rhs.fundingTxo { return false } + if lhs.shortChannelId != rhs.shortChannelId { + return false + } + if lhs.outboundScidAlias != rhs.outboundScidAlias { + return false + } + if lhs.inboundScidAlias != rhs.inboundScidAlias { + return false + } if lhs.channelValueSats != rhs.channelValueSats { return false } @@ -2630,6 +3314,9 @@ extension ChannelDetails: Equatable, Hashable { hasher.combine(channelId) hasher.combine(counterpartyNodeId) hasher.combine(fundingTxo) + hasher.combine(shortChannelId) + hasher.combine(outboundScidAlias) + hasher.combine(inboundScidAlias) hasher.combine(channelValueSats) hasher.combine(unspendablePunishmentReserve) hasher.combine(userChannelId) @@ -2666,6 +3353,9 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { channelId: FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: FfiConverterTypePublicKey.read(from: &buf), fundingTxo: FfiConverterOptionTypeOutPoint.read(from: &buf), + shortChannelId: FfiConverterOptionUInt64.read(from: &buf), + outboundScidAlias: FfiConverterOptionUInt64.read(from: &buf), + inboundScidAlias: FfiConverterOptionUInt64.read(from: &buf), channelValueSats: FfiConverterUInt64.read(from: &buf), unspendablePunishmentReserve: FfiConverterOptionUInt64.read(from: &buf), userChannelId: FfiConverterTypeUserChannelId.read(from: &buf), @@ -2698,6 +3388,9 @@ public struct FfiConverterTypeChannelDetails: FfiConverterRustBuffer { FfiConverterTypeChannelId.write(value.channelId, into: &buf) FfiConverterTypePublicKey.write(value.counterpartyNodeId, into: &buf) FfiConverterOptionTypeOutPoint.write(value.fundingTxo, into: &buf) + FfiConverterOptionUInt64.write(value.shortChannelId, into: &buf) + FfiConverterOptionUInt64.write(value.outboundScidAlias, into: &buf) + FfiConverterOptionUInt64.write(value.inboundScidAlias, into: &buf) FfiConverterUInt64.write(value.channelValueSats, into: &buf) FfiConverterOptionUInt64.write(value.unspendablePunishmentReserve, into: &buf) FfiConverterTypeUserChannelId.write(value.userChannelId, into: &buf) @@ -2817,6 +3510,71 @@ public func FfiConverterTypeChannelInfo_lower(_ value: ChannelInfo) -> RustBuffe } +public struct ChannelOrderInfo { + public var fundedAt: DateTime + public var fundingOutpoint: OutPoint + public var expiresAt: DateTime + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(fundedAt: DateTime, fundingOutpoint: OutPoint, expiresAt: DateTime) { + self.fundedAt = fundedAt + self.fundingOutpoint = fundingOutpoint + self.expiresAt = expiresAt + } +} + + + +extension ChannelOrderInfo: Equatable, Hashable { + public static func ==(lhs: ChannelOrderInfo, rhs: ChannelOrderInfo) -> Bool { + if lhs.fundedAt != rhs.fundedAt { + return false + } + if lhs.fundingOutpoint != rhs.fundingOutpoint { + return false + } + if lhs.expiresAt != rhs.expiresAt { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(fundedAt) + hasher.combine(fundingOutpoint) + hasher.combine(expiresAt) + } +} + + +public struct FfiConverterTypeChannelOrderInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ChannelOrderInfo { + return + try ChannelOrderInfo( + fundedAt: FfiConverterTypeDateTime.read(from: &buf), + fundingOutpoint: FfiConverterTypeOutPoint.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf) + ) + } + + public static func write(_ value: ChannelOrderInfo, into buf: inout [UInt8]) { + FfiConverterTypeDateTime.write(value.fundedAt, into: &buf) + FfiConverterTypeOutPoint.write(value.fundingOutpoint, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + } +} + + +public func FfiConverterTypeChannelOrderInfo_lift(_ buf: RustBuffer) throws -> ChannelOrderInfo { + return try FfiConverterTypeChannelOrderInfo.lift(buf) +} + +public func FfiConverterTypeChannelOrderInfo_lower(_ value: ChannelOrderInfo) -> RustBuffer { + return FfiConverterTypeChannelOrderInfo.lower(value) +} + + public struct ChannelUpdateInfo { public var lastUpdate: UInt32 public var enabled: Bool @@ -2908,27 +3666,25 @@ public func FfiConverterTypeChannelUpdateInfo_lower(_ value: ChannelUpdateInfo) public struct Config { public var storageDirPath: String - public var logDirPath: String? public var network: Network public var listeningAddresses: [SocketAddress]? + public var announcementAddresses: [SocketAddress]? public var nodeAlias: NodeAlias? public var trustedPeers0conf: [PublicKey] public var probingLiquidityLimitMultiplier: UInt64 - public var logLevel: LogLevel public var anchorChannelsConfig: AnchorChannelsConfig? public var sendingParameters: SendingParameters? // Default memberwise initializers are never public by default, so we // declare one manually. - public init(storageDirPath: String, logDirPath: String?, network: Network, listeningAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, logLevel: LogLevel, anchorChannelsConfig: AnchorChannelsConfig?, sendingParameters: SendingParameters?) { + public init(storageDirPath: String, network: Network, listeningAddresses: [SocketAddress]?, announcementAddresses: [SocketAddress]?, nodeAlias: NodeAlias?, trustedPeers0conf: [PublicKey], probingLiquidityLimitMultiplier: UInt64, anchorChannelsConfig: AnchorChannelsConfig?, sendingParameters: SendingParameters?) { self.storageDirPath = storageDirPath - self.logDirPath = logDirPath self.network = network self.listeningAddresses = listeningAddresses + self.announcementAddresses = announcementAddresses self.nodeAlias = nodeAlias self.trustedPeers0conf = trustedPeers0conf self.probingLiquidityLimitMultiplier = probingLiquidityLimitMultiplier - self.logLevel = logLevel self.anchorChannelsConfig = anchorChannelsConfig self.sendingParameters = sendingParameters } @@ -2941,15 +3697,15 @@ extension Config: Equatable, Hashable { if lhs.storageDirPath != rhs.storageDirPath { return false } - if lhs.logDirPath != rhs.logDirPath { - return false - } if lhs.network != rhs.network { return false } if lhs.listeningAddresses != rhs.listeningAddresses { return false } + if lhs.announcementAddresses != rhs.announcementAddresses { + return false + } if lhs.nodeAlias != rhs.nodeAlias { return false } @@ -2959,9 +3715,6 @@ extension Config: Equatable, Hashable { if lhs.probingLiquidityLimitMultiplier != rhs.probingLiquidityLimitMultiplier { return false } - if lhs.logLevel != rhs.logLevel { - return false - } if lhs.anchorChannelsConfig != rhs.anchorChannelsConfig { return false } @@ -2973,179 +3726,499 @@ extension Config: Equatable, Hashable { public func hash(into hasher: inout Hasher) { hasher.combine(storageDirPath) - hasher.combine(logDirPath) hasher.combine(network) hasher.combine(listeningAddresses) + hasher.combine(announcementAddresses) hasher.combine(nodeAlias) hasher.combine(trustedPeers0conf) hasher.combine(probingLiquidityLimitMultiplier) - hasher.combine(logLevel) hasher.combine(anchorChannelsConfig) hasher.combine(sendingParameters) } } -public struct FfiConverterTypeConfig: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Config { +public struct FfiConverterTypeConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Config { + return + try Config( + storageDirPath: FfiConverterString.read(from: &buf), + network: FfiConverterTypeNetwork.read(from: &buf), + listeningAddresses: FfiConverterOptionSequenceTypeSocketAddress.read(from: &buf), + announcementAddresses: FfiConverterOptionSequenceTypeSocketAddress.read(from: &buf), + nodeAlias: FfiConverterOptionTypeNodeAlias.read(from: &buf), + trustedPeers0conf: FfiConverterSequenceTypePublicKey.read(from: &buf), + probingLiquidityLimitMultiplier: FfiConverterUInt64.read(from: &buf), + anchorChannelsConfig: FfiConverterOptionTypeAnchorChannelsConfig.read(from: &buf), + sendingParameters: FfiConverterOptionTypeSendingParameters.read(from: &buf) + ) + } + + public static func write(_ value: Config, into buf: inout [UInt8]) { + FfiConverterString.write(value.storageDirPath, into: &buf) + FfiConverterTypeNetwork.write(value.network, into: &buf) + FfiConverterOptionSequenceTypeSocketAddress.write(value.listeningAddresses, into: &buf) + FfiConverterOptionSequenceTypeSocketAddress.write(value.announcementAddresses, into: &buf) + FfiConverterOptionTypeNodeAlias.write(value.nodeAlias, into: &buf) + FfiConverterSequenceTypePublicKey.write(value.trustedPeers0conf, into: &buf) + FfiConverterUInt64.write(value.probingLiquidityLimitMultiplier, into: &buf) + FfiConverterOptionTypeAnchorChannelsConfig.write(value.anchorChannelsConfig, into: &buf) + FfiConverterOptionTypeSendingParameters.write(value.sendingParameters, into: &buf) + } +} + + +public func FfiConverterTypeConfig_lift(_ buf: RustBuffer) throws -> Config { + return try FfiConverterTypeConfig.lift(buf) +} + +public func FfiConverterTypeConfig_lower(_ value: Config) -> RustBuffer { + return FfiConverterTypeConfig.lower(value) +} + + +public struct CustomTlvRecord { + public var typeNum: UInt64 + public var value: [UInt8] + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(typeNum: UInt64, value: [UInt8]) { + self.typeNum = typeNum + self.value = value + } +} + + + +extension CustomTlvRecord: Equatable, Hashable { + public static func ==(lhs: CustomTlvRecord, rhs: CustomTlvRecord) -> Bool { + if lhs.typeNum != rhs.typeNum { + return false + } + if lhs.value != rhs.value { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(typeNum) + hasher.combine(value) + } +} + + +public struct FfiConverterTypeCustomTlvRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> CustomTlvRecord { + return + try CustomTlvRecord( + typeNum: FfiConverterUInt64.read(from: &buf), + value: FfiConverterSequenceUInt8.read(from: &buf) + ) + } + + public static func write(_ value: CustomTlvRecord, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.typeNum, into: &buf) + FfiConverterSequenceUInt8.write(value.value, into: &buf) + } +} + + +public func FfiConverterTypeCustomTlvRecord_lift(_ buf: RustBuffer) throws -> CustomTlvRecord { + return try FfiConverterTypeCustomTlvRecord.lift(buf) +} + +public func FfiConverterTypeCustomTlvRecord_lower(_ value: CustomTlvRecord) -> RustBuffer { + return FfiConverterTypeCustomTlvRecord.lower(value) +} + + +public struct ElectrumSyncConfig { + public var backgroundSyncConfig: BackgroundSyncConfig? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(backgroundSyncConfig: BackgroundSyncConfig?) { + self.backgroundSyncConfig = backgroundSyncConfig + } +} + + + +extension ElectrumSyncConfig: Equatable, Hashable { + public static func ==(lhs: ElectrumSyncConfig, rhs: ElectrumSyncConfig) -> Bool { + if lhs.backgroundSyncConfig != rhs.backgroundSyncConfig { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(backgroundSyncConfig) + } +} + + +public struct FfiConverterTypeElectrumSyncConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ElectrumSyncConfig { + return + try ElectrumSyncConfig( + backgroundSyncConfig: FfiConverterOptionTypeBackgroundSyncConfig.read(from: &buf) + ) + } + + public static func write(_ value: ElectrumSyncConfig, into buf: inout [UInt8]) { + FfiConverterOptionTypeBackgroundSyncConfig.write(value.backgroundSyncConfig, into: &buf) + } +} + + +public func FfiConverterTypeElectrumSyncConfig_lift(_ buf: RustBuffer) throws -> ElectrumSyncConfig { + return try FfiConverterTypeElectrumSyncConfig.lift(buf) +} + +public func FfiConverterTypeElectrumSyncConfig_lower(_ value: ElectrumSyncConfig) -> RustBuffer { + return FfiConverterTypeElectrumSyncConfig.lower(value) +} + + +public struct EsploraSyncConfig { + public var backgroundSyncConfig: BackgroundSyncConfig? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(backgroundSyncConfig: BackgroundSyncConfig?) { + self.backgroundSyncConfig = backgroundSyncConfig + } +} + + + +extension EsploraSyncConfig: Equatable, Hashable { + public static func ==(lhs: EsploraSyncConfig, rhs: EsploraSyncConfig) -> Bool { + if lhs.backgroundSyncConfig != rhs.backgroundSyncConfig { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(backgroundSyncConfig) + } +} + + +public struct FfiConverterTypeEsploraSyncConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EsploraSyncConfig { + return + try EsploraSyncConfig( + backgroundSyncConfig: FfiConverterOptionTypeBackgroundSyncConfig.read(from: &buf) + ) + } + + public static func write(_ value: EsploraSyncConfig, into buf: inout [UInt8]) { + FfiConverterOptionTypeBackgroundSyncConfig.write(value.backgroundSyncConfig, into: &buf) + } +} + + +public func FfiConverterTypeEsploraSyncConfig_lift(_ buf: RustBuffer) throws -> EsploraSyncConfig { + return try FfiConverterTypeEsploraSyncConfig.lift(buf) +} + +public func FfiConverterTypeEsploraSyncConfig_lower(_ value: EsploraSyncConfig) -> RustBuffer { + return FfiConverterTypeEsploraSyncConfig.lower(value) +} + + +public struct LspFeeLimits { + public var maxTotalOpeningFeeMsat: UInt64? + public var maxProportionalOpeningFeePpmMsat: UInt64? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(maxTotalOpeningFeeMsat: UInt64?, maxProportionalOpeningFeePpmMsat: UInt64?) { + self.maxTotalOpeningFeeMsat = maxTotalOpeningFeeMsat + self.maxProportionalOpeningFeePpmMsat = maxProportionalOpeningFeePpmMsat + } +} + + + +extension LspFeeLimits: Equatable, Hashable { + public static func ==(lhs: LspFeeLimits, rhs: LspFeeLimits) -> Bool { + if lhs.maxTotalOpeningFeeMsat != rhs.maxTotalOpeningFeeMsat { + return false + } + if lhs.maxProportionalOpeningFeePpmMsat != rhs.maxProportionalOpeningFeePpmMsat { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(maxTotalOpeningFeeMsat) + hasher.combine(maxProportionalOpeningFeePpmMsat) + } +} + + +public struct FfiConverterTypeLSPFeeLimits: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LspFeeLimits { + return + try LspFeeLimits( + maxTotalOpeningFeeMsat: FfiConverterOptionUInt64.read(from: &buf), + maxProportionalOpeningFeePpmMsat: FfiConverterOptionUInt64.read(from: &buf) + ) + } + + public static func write(_ value: LspFeeLimits, into buf: inout [UInt8]) { + FfiConverterOptionUInt64.write(value.maxTotalOpeningFeeMsat, into: &buf) + FfiConverterOptionUInt64.write(value.maxProportionalOpeningFeePpmMsat, into: &buf) + } +} + + +public func FfiConverterTypeLSPFeeLimits_lift(_ buf: RustBuffer) throws -> LspFeeLimits { + return try FfiConverterTypeLSPFeeLimits.lift(buf) +} + +public func FfiConverterTypeLSPFeeLimits_lower(_ value: LspFeeLimits) -> RustBuffer { + return FfiConverterTypeLSPFeeLimits.lower(value) +} + + +public struct Lsps1OrderStatus { + public var orderId: OrderId + public var orderParams: OrderParameters + public var paymentOptions: PaymentInfo + public var channelState: ChannelOrderInfo? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(orderId: OrderId, orderParams: OrderParameters, paymentOptions: PaymentInfo, channelState: ChannelOrderInfo?) { + self.orderId = orderId + self.orderParams = orderParams + self.paymentOptions = paymentOptions + self.channelState = channelState + } +} + + + +public struct FfiConverterTypeLSPS1OrderStatus: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps1OrderStatus { return - try Config( - storageDirPath: FfiConverterString.read(from: &buf), - logDirPath: FfiConverterOptionString.read(from: &buf), - network: FfiConverterTypeNetwork.read(from: &buf), - listeningAddresses: FfiConverterOptionSequenceTypeSocketAddress.read(from: &buf), - nodeAlias: FfiConverterOptionTypeNodeAlias.read(from: &buf), - trustedPeers0conf: FfiConverterSequenceTypePublicKey.read(from: &buf), - probingLiquidityLimitMultiplier: FfiConverterUInt64.read(from: &buf), - logLevel: FfiConverterTypeLogLevel.read(from: &buf), - anchorChannelsConfig: FfiConverterOptionTypeAnchorChannelsConfig.read(from: &buf), - sendingParameters: FfiConverterOptionTypeSendingParameters.read(from: &buf) + try Lsps1OrderStatus( + orderId: FfiConverterTypeOrderId.read(from: &buf), + orderParams: FfiConverterTypeOrderParameters.read(from: &buf), + paymentOptions: FfiConverterTypePaymentInfo.read(from: &buf), + channelState: FfiConverterOptionTypeChannelOrderInfo.read(from: &buf) ) } - public static func write(_ value: Config, into buf: inout [UInt8]) { - FfiConverterString.write(value.storageDirPath, into: &buf) - FfiConverterOptionString.write(value.logDirPath, into: &buf) - FfiConverterTypeNetwork.write(value.network, into: &buf) - FfiConverterOptionSequenceTypeSocketAddress.write(value.listeningAddresses, into: &buf) - FfiConverterOptionTypeNodeAlias.write(value.nodeAlias, into: &buf) - FfiConverterSequenceTypePublicKey.write(value.trustedPeers0conf, into: &buf) - FfiConverterUInt64.write(value.probingLiquidityLimitMultiplier, into: &buf) - FfiConverterTypeLogLevel.write(value.logLevel, into: &buf) - FfiConverterOptionTypeAnchorChannelsConfig.write(value.anchorChannelsConfig, into: &buf) - FfiConverterOptionTypeSendingParameters.write(value.sendingParameters, into: &buf) + public static func write(_ value: Lsps1OrderStatus, into buf: inout [UInt8]) { + FfiConverterTypeOrderId.write(value.orderId, into: &buf) + FfiConverterTypeOrderParameters.write(value.orderParams, into: &buf) + FfiConverterTypePaymentInfo.write(value.paymentOptions, into: &buf) + FfiConverterOptionTypeChannelOrderInfo.write(value.channelState, into: &buf) } } -public func FfiConverterTypeConfig_lift(_ buf: RustBuffer) throws -> Config { - return try FfiConverterTypeConfig.lift(buf) +public func FfiConverterTypeLSPS1OrderStatus_lift(_ buf: RustBuffer) throws -> Lsps1OrderStatus { + return try FfiConverterTypeLSPS1OrderStatus.lift(buf) } -public func FfiConverterTypeConfig_lower(_ value: Config) -> RustBuffer { - return FfiConverterTypeConfig.lower(value) +public func FfiConverterTypeLSPS1OrderStatus_lower(_ value: Lsps1OrderStatus) -> RustBuffer { + return FfiConverterTypeLSPS1OrderStatus.lower(value) } -public struct EsploraSyncConfig { - public var onchainWalletSyncIntervalSecs: UInt64 - public var lightningWalletSyncIntervalSecs: UInt64 - public var feeRateCacheUpdateIntervalSecs: UInt64 +public struct Lsps2ServiceConfig { + public var requireToken: String? + public var advertiseService: Bool + public var channelOpeningFeePpm: UInt32 + public var channelOverProvisioningPpm: UInt32 + public var minChannelOpeningFeeMsat: UInt64 + public var minChannelLifetime: UInt32 + public var maxClientToSelfDelay: UInt32 + public var minPaymentSizeMsat: UInt64 + public var maxPaymentSizeMsat: UInt64 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(onchainWalletSyncIntervalSecs: UInt64, lightningWalletSyncIntervalSecs: UInt64, feeRateCacheUpdateIntervalSecs: UInt64) { - self.onchainWalletSyncIntervalSecs = onchainWalletSyncIntervalSecs - self.lightningWalletSyncIntervalSecs = lightningWalletSyncIntervalSecs - self.feeRateCacheUpdateIntervalSecs = feeRateCacheUpdateIntervalSecs + public init(requireToken: String?, advertiseService: Bool, channelOpeningFeePpm: UInt32, channelOverProvisioningPpm: UInt32, minChannelOpeningFeeMsat: UInt64, minChannelLifetime: UInt32, maxClientToSelfDelay: UInt32, minPaymentSizeMsat: UInt64, maxPaymentSizeMsat: UInt64) { + self.requireToken = requireToken + self.advertiseService = advertiseService + self.channelOpeningFeePpm = channelOpeningFeePpm + self.channelOverProvisioningPpm = channelOverProvisioningPpm + self.minChannelOpeningFeeMsat = minChannelOpeningFeeMsat + self.minChannelLifetime = minChannelLifetime + self.maxClientToSelfDelay = maxClientToSelfDelay + self.minPaymentSizeMsat = minPaymentSizeMsat + self.maxPaymentSizeMsat = maxPaymentSizeMsat } } -extension EsploraSyncConfig: Equatable, Hashable { - public static func ==(lhs: EsploraSyncConfig, rhs: EsploraSyncConfig) -> Bool { - if lhs.onchainWalletSyncIntervalSecs != rhs.onchainWalletSyncIntervalSecs { +extension Lsps2ServiceConfig: Equatable, Hashable { + public static func ==(lhs: Lsps2ServiceConfig, rhs: Lsps2ServiceConfig) -> Bool { + if lhs.requireToken != rhs.requireToken { return false } - if lhs.lightningWalletSyncIntervalSecs != rhs.lightningWalletSyncIntervalSecs { + if lhs.advertiseService != rhs.advertiseService { return false } - if lhs.feeRateCacheUpdateIntervalSecs != rhs.feeRateCacheUpdateIntervalSecs { + if lhs.channelOpeningFeePpm != rhs.channelOpeningFeePpm { + return false + } + if lhs.channelOverProvisioningPpm != rhs.channelOverProvisioningPpm { + return false + } + if lhs.minChannelOpeningFeeMsat != rhs.minChannelOpeningFeeMsat { + return false + } + if lhs.minChannelLifetime != rhs.minChannelLifetime { + return false + } + if lhs.maxClientToSelfDelay != rhs.maxClientToSelfDelay { + return false + } + if lhs.minPaymentSizeMsat != rhs.minPaymentSizeMsat { + return false + } + if lhs.maxPaymentSizeMsat != rhs.maxPaymentSizeMsat { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(onchainWalletSyncIntervalSecs) - hasher.combine(lightningWalletSyncIntervalSecs) - hasher.combine(feeRateCacheUpdateIntervalSecs) + hasher.combine(requireToken) + hasher.combine(advertiseService) + hasher.combine(channelOpeningFeePpm) + hasher.combine(channelOverProvisioningPpm) + hasher.combine(minChannelOpeningFeeMsat) + hasher.combine(minChannelLifetime) + hasher.combine(maxClientToSelfDelay) + hasher.combine(minPaymentSizeMsat) + hasher.combine(maxPaymentSizeMsat) } } -public struct FfiConverterTypeEsploraSyncConfig: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> EsploraSyncConfig { +public struct FfiConverterTypeLSPS2ServiceConfig: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Lsps2ServiceConfig { return - try EsploraSyncConfig( - onchainWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), - lightningWalletSyncIntervalSecs: FfiConverterUInt64.read(from: &buf), - feeRateCacheUpdateIntervalSecs: FfiConverterUInt64.read(from: &buf) + try Lsps2ServiceConfig( + requireToken: FfiConverterOptionString.read(from: &buf), + advertiseService: FfiConverterBool.read(from: &buf), + channelOpeningFeePpm: FfiConverterUInt32.read(from: &buf), + channelOverProvisioningPpm: FfiConverterUInt32.read(from: &buf), + minChannelOpeningFeeMsat: FfiConverterUInt64.read(from: &buf), + minChannelLifetime: FfiConverterUInt32.read(from: &buf), + maxClientToSelfDelay: FfiConverterUInt32.read(from: &buf), + minPaymentSizeMsat: FfiConverterUInt64.read(from: &buf), + maxPaymentSizeMsat: FfiConverterUInt64.read(from: &buf) ) } - public static func write(_ value: EsploraSyncConfig, into buf: inout [UInt8]) { - FfiConverterUInt64.write(value.onchainWalletSyncIntervalSecs, into: &buf) - FfiConverterUInt64.write(value.lightningWalletSyncIntervalSecs, into: &buf) - FfiConverterUInt64.write(value.feeRateCacheUpdateIntervalSecs, into: &buf) + public static func write(_ value: Lsps2ServiceConfig, into buf: inout [UInt8]) { + FfiConverterOptionString.write(value.requireToken, into: &buf) + FfiConverterBool.write(value.advertiseService, into: &buf) + FfiConverterUInt32.write(value.channelOpeningFeePpm, into: &buf) + FfiConverterUInt32.write(value.channelOverProvisioningPpm, into: &buf) + FfiConverterUInt64.write(value.minChannelOpeningFeeMsat, into: &buf) + FfiConverterUInt32.write(value.minChannelLifetime, into: &buf) + FfiConverterUInt32.write(value.maxClientToSelfDelay, into: &buf) + FfiConverterUInt64.write(value.minPaymentSizeMsat, into: &buf) + FfiConverterUInt64.write(value.maxPaymentSizeMsat, into: &buf) } } -public func FfiConverterTypeEsploraSyncConfig_lift(_ buf: RustBuffer) throws -> EsploraSyncConfig { - return try FfiConverterTypeEsploraSyncConfig.lift(buf) +public func FfiConverterTypeLSPS2ServiceConfig_lift(_ buf: RustBuffer) throws -> Lsps2ServiceConfig { + return try FfiConverterTypeLSPS2ServiceConfig.lift(buf) } -public func FfiConverterTypeEsploraSyncConfig_lower(_ value: EsploraSyncConfig) -> RustBuffer { - return FfiConverterTypeEsploraSyncConfig.lower(value) +public func FfiConverterTypeLSPS2ServiceConfig_lower(_ value: Lsps2ServiceConfig) -> RustBuffer { + return FfiConverterTypeLSPS2ServiceConfig.lower(value) } -public struct LspFeeLimits { - public var maxTotalOpeningFeeMsat: UInt64? - public var maxProportionalOpeningFeePpmMsat: UInt64? +public struct LogRecord { + public var level: LogLevel + public var args: String + public var modulePath: String + public var line: UInt32 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(maxTotalOpeningFeeMsat: UInt64?, maxProportionalOpeningFeePpmMsat: UInt64?) { - self.maxTotalOpeningFeeMsat = maxTotalOpeningFeeMsat - self.maxProportionalOpeningFeePpmMsat = maxProportionalOpeningFeePpmMsat + public init(level: LogLevel, args: String, modulePath: String, line: UInt32) { + self.level = level + self.args = args + self.modulePath = modulePath + self.line = line } } -extension LspFeeLimits: Equatable, Hashable { - public static func ==(lhs: LspFeeLimits, rhs: LspFeeLimits) -> Bool { - if lhs.maxTotalOpeningFeeMsat != rhs.maxTotalOpeningFeeMsat { +extension LogRecord: Equatable, Hashable { + public static func ==(lhs: LogRecord, rhs: LogRecord) -> Bool { + if lhs.level != rhs.level { return false } - if lhs.maxProportionalOpeningFeePpmMsat != rhs.maxProportionalOpeningFeePpmMsat { + if lhs.args != rhs.args { + return false + } + if lhs.modulePath != rhs.modulePath { + return false + } + if lhs.line != rhs.line { return false } return true } public func hash(into hasher: inout Hasher) { - hasher.combine(maxTotalOpeningFeeMsat) - hasher.combine(maxProportionalOpeningFeePpmMsat) + hasher.combine(level) + hasher.combine(args) + hasher.combine(modulePath) + hasher.combine(line) } } -public struct FfiConverterTypeLSPFeeLimits: FfiConverterRustBuffer { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LspFeeLimits { +public struct FfiConverterTypeLogRecord: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> LogRecord { return - try LspFeeLimits( - maxTotalOpeningFeeMsat: FfiConverterOptionUInt64.read(from: &buf), - maxProportionalOpeningFeePpmMsat: FfiConverterOptionUInt64.read(from: &buf) + try LogRecord( + level: FfiConverterTypeLogLevel.read(from: &buf), + args: FfiConverterString.read(from: &buf), + modulePath: FfiConverterString.read(from: &buf), + line: FfiConverterUInt32.read(from: &buf) ) } - public static func write(_ value: LspFeeLimits, into buf: inout [UInt8]) { - FfiConverterOptionUInt64.write(value.maxTotalOpeningFeeMsat, into: &buf) - FfiConverterOptionUInt64.write(value.maxProportionalOpeningFeePpmMsat, into: &buf) + public static func write(_ value: LogRecord, into buf: inout [UInt8]) { + FfiConverterTypeLogLevel.write(value.level, into: &buf) + FfiConverterString.write(value.args, into: &buf) + FfiConverterString.write(value.modulePath, into: &buf) + FfiConverterUInt32.write(value.line, into: &buf) } } -public func FfiConverterTypeLSPFeeLimits_lift(_ buf: RustBuffer) throws -> LspFeeLimits { - return try FfiConverterTypeLSPFeeLimits.lift(buf) +public func FfiConverterTypeLogRecord_lift(_ buf: RustBuffer) throws -> LogRecord { + return try FfiConverterTypeLogRecord.lift(buf) } -public func FfiConverterTypeLSPFeeLimits_lower(_ value: LspFeeLimits) -> RustBuffer { - return FfiConverterTypeLSPFeeLimits.lower(value) +public func FfiConverterTypeLogRecord_lower(_ value: LogRecord) -> RustBuffer { + return FfiConverterTypeLogRecord.lower(value) } @@ -3384,6 +4457,166 @@ public func FfiConverterTypeNodeStatus_lower(_ value: NodeStatus) -> RustBuffer } +public struct OnchainPaymentInfo { + public var state: PaymentState + public var expiresAt: DateTime + public var feeTotalSat: UInt64 + public var orderTotalSat: UInt64 + public var address: Address + public var minOnchainPaymentConfirmations: UInt16? + public var minFeeFor0conf: FeeRate + public var refundOnchainAddress: Address? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(state: PaymentState, expiresAt: DateTime, feeTotalSat: UInt64, orderTotalSat: UInt64, address: Address, minOnchainPaymentConfirmations: UInt16?, minFeeFor0conf: FeeRate, refundOnchainAddress: Address?) { + self.state = state + self.expiresAt = expiresAt + self.feeTotalSat = feeTotalSat + self.orderTotalSat = orderTotalSat + self.address = address + self.minOnchainPaymentConfirmations = minOnchainPaymentConfirmations + self.minFeeFor0conf = minFeeFor0conf + self.refundOnchainAddress = refundOnchainAddress + } +} + + + +public struct FfiConverterTypeOnchainPaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OnchainPaymentInfo { + return + try OnchainPaymentInfo( + state: FfiConverterTypePaymentState.read(from: &buf), + expiresAt: FfiConverterTypeDateTime.read(from: &buf), + feeTotalSat: FfiConverterUInt64.read(from: &buf), + orderTotalSat: FfiConverterUInt64.read(from: &buf), + address: FfiConverterTypeAddress.read(from: &buf), + minOnchainPaymentConfirmations: FfiConverterOptionUInt16.read(from: &buf), + minFeeFor0conf: FfiConverterTypeFeeRate.read(from: &buf), + refundOnchainAddress: FfiConverterOptionTypeAddress.read(from: &buf) + ) + } + + public static func write(_ value: OnchainPaymentInfo, into buf: inout [UInt8]) { + FfiConverterTypePaymentState.write(value.state, into: &buf) + FfiConverterTypeDateTime.write(value.expiresAt, into: &buf) + FfiConverterUInt64.write(value.feeTotalSat, into: &buf) + FfiConverterUInt64.write(value.orderTotalSat, into: &buf) + FfiConverterTypeAddress.write(value.address, into: &buf) + FfiConverterOptionUInt16.write(value.minOnchainPaymentConfirmations, into: &buf) + FfiConverterTypeFeeRate.write(value.minFeeFor0conf, into: &buf) + FfiConverterOptionTypeAddress.write(value.refundOnchainAddress, into: &buf) + } +} + + +public func FfiConverterTypeOnchainPaymentInfo_lift(_ buf: RustBuffer) throws -> OnchainPaymentInfo { + return try FfiConverterTypeOnchainPaymentInfo.lift(buf) +} + +public func FfiConverterTypeOnchainPaymentInfo_lower(_ value: OnchainPaymentInfo) -> RustBuffer { + return FfiConverterTypeOnchainPaymentInfo.lower(value) +} + + +public struct OrderParameters { + public var lspBalanceSat: UInt64 + public var clientBalanceSat: UInt64 + public var requiredChannelConfirmations: UInt16 + public var fundingConfirmsWithinBlocks: UInt16 + public var channelExpiryBlocks: UInt32 + public var token: String? + public var announceChannel: Bool + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(lspBalanceSat: UInt64, clientBalanceSat: UInt64, requiredChannelConfirmations: UInt16, fundingConfirmsWithinBlocks: UInt16, channelExpiryBlocks: UInt32, token: String?, announceChannel: Bool) { + self.lspBalanceSat = lspBalanceSat + self.clientBalanceSat = clientBalanceSat + self.requiredChannelConfirmations = requiredChannelConfirmations + self.fundingConfirmsWithinBlocks = fundingConfirmsWithinBlocks + self.channelExpiryBlocks = channelExpiryBlocks + self.token = token + self.announceChannel = announceChannel + } +} + + + +extension OrderParameters: Equatable, Hashable { + public static func ==(lhs: OrderParameters, rhs: OrderParameters) -> Bool { + if lhs.lspBalanceSat != rhs.lspBalanceSat { + return false + } + if lhs.clientBalanceSat != rhs.clientBalanceSat { + return false + } + if lhs.requiredChannelConfirmations != rhs.requiredChannelConfirmations { + return false + } + if lhs.fundingConfirmsWithinBlocks != rhs.fundingConfirmsWithinBlocks { + return false + } + if lhs.channelExpiryBlocks != rhs.channelExpiryBlocks { + return false + } + if lhs.token != rhs.token { + return false + } + if lhs.announceChannel != rhs.announceChannel { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(lspBalanceSat) + hasher.combine(clientBalanceSat) + hasher.combine(requiredChannelConfirmations) + hasher.combine(fundingConfirmsWithinBlocks) + hasher.combine(channelExpiryBlocks) + hasher.combine(token) + hasher.combine(announceChannel) + } +} + + +public struct FfiConverterTypeOrderParameters: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OrderParameters { + return + try OrderParameters( + lspBalanceSat: FfiConverterUInt64.read(from: &buf), + clientBalanceSat: FfiConverterUInt64.read(from: &buf), + requiredChannelConfirmations: FfiConverterUInt16.read(from: &buf), + fundingConfirmsWithinBlocks: FfiConverterUInt16.read(from: &buf), + channelExpiryBlocks: FfiConverterUInt32.read(from: &buf), + token: FfiConverterOptionString.read(from: &buf), + announceChannel: FfiConverterBool.read(from: &buf) + ) + } + + public static func write(_ value: OrderParameters, into buf: inout [UInt8]) { + FfiConverterUInt64.write(value.lspBalanceSat, into: &buf) + FfiConverterUInt64.write(value.clientBalanceSat, into: &buf) + FfiConverterUInt16.write(value.requiredChannelConfirmations, into: &buf) + FfiConverterUInt16.write(value.fundingConfirmsWithinBlocks, into: &buf) + FfiConverterUInt32.write(value.channelExpiryBlocks, into: &buf) + FfiConverterOptionString.write(value.token, into: &buf) + FfiConverterBool.write(value.announceChannel, into: &buf) + } +} + + +public func FfiConverterTypeOrderParameters_lift(_ buf: RustBuffer) throws -> OrderParameters { + return try FfiConverterTypeOrderParameters.lift(buf) +} + +public func FfiConverterTypeOrderParameters_lower(_ value: OrderParameters) -> RustBuffer { + return FfiConverterTypeOrderParameters.lower(value) +} + + public struct OutPoint { public var txid: Txid public var vout: UInt32 @@ -3445,16 +4678,18 @@ public struct PaymentDetails { public var id: PaymentId public var kind: PaymentKind public var amountMsat: UInt64? + public var feePaidMsat: UInt64? public var direction: PaymentDirection public var status: PaymentStatus public var latestUpdateTimestamp: UInt64 // Default memberwise initializers are never public by default, so we // declare one manually. - public init(id: PaymentId, kind: PaymentKind, amountMsat: UInt64?, direction: PaymentDirection, status: PaymentStatus, latestUpdateTimestamp: UInt64) { + public init(id: PaymentId, kind: PaymentKind, amountMsat: UInt64?, feePaidMsat: UInt64?, direction: PaymentDirection, status: PaymentStatus, latestUpdateTimestamp: UInt64) { self.id = id self.kind = kind self.amountMsat = amountMsat + self.feePaidMsat = feePaidMsat self.direction = direction self.status = status self.latestUpdateTimestamp = latestUpdateTimestamp @@ -3474,6 +4709,9 @@ extension PaymentDetails: Equatable, Hashable { if lhs.amountMsat != rhs.amountMsat { return false } + if lhs.feePaidMsat != rhs.feePaidMsat { + return false + } if lhs.direction != rhs.direction { return false } @@ -3490,6 +4728,7 @@ extension PaymentDetails: Equatable, Hashable { hasher.combine(id) hasher.combine(kind) hasher.combine(amountMsat) + hasher.combine(feePaidMsat) hasher.combine(direction) hasher.combine(status) hasher.combine(latestUpdateTimestamp) @@ -3504,6 +4743,7 @@ public struct FfiConverterTypePaymentDetails: FfiConverterRustBuffer { id: FfiConverterTypePaymentId.read(from: &buf), kind: FfiConverterTypePaymentKind.read(from: &buf), amountMsat: FfiConverterOptionUInt64.read(from: &buf), + feePaidMsat: FfiConverterOptionUInt64.read(from: &buf), direction: FfiConverterTypePaymentDirection.read(from: &buf), status: FfiConverterTypePaymentStatus.read(from: &buf), latestUpdateTimestamp: FfiConverterUInt64.read(from: &buf) @@ -3514,6 +4754,7 @@ public struct FfiConverterTypePaymentDetails: FfiConverterRustBuffer { FfiConverterTypePaymentId.write(value.id, into: &buf) FfiConverterTypePaymentKind.write(value.kind, into: &buf) FfiConverterOptionUInt64.write(value.amountMsat, into: &buf) + FfiConverterOptionUInt64.write(value.feePaidMsat, into: &buf) FfiConverterTypePaymentDirection.write(value.direction, into: &buf) FfiConverterTypePaymentStatus.write(value.status, into: &buf) FfiConverterUInt64.write(value.latestUpdateTimestamp, into: &buf) @@ -3530,6 +4771,45 @@ public func FfiConverterTypePaymentDetails_lower(_ value: PaymentDetails) -> Rus } +public struct PaymentInfo { + public var bolt11: Bolt11PaymentInfo? + public var onchain: OnchainPaymentInfo? + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(bolt11: Bolt11PaymentInfo?, onchain: OnchainPaymentInfo?) { + self.bolt11 = bolt11 + self.onchain = onchain + } +} + + + +public struct FfiConverterTypePaymentInfo: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PaymentInfo { + return + try PaymentInfo( + bolt11: FfiConverterOptionTypeBolt11PaymentInfo.read(from: &buf), + onchain: FfiConverterOptionTypeOnchainPaymentInfo.read(from: &buf) + ) + } + + public static func write(_ value: PaymentInfo, into buf: inout [UInt8]) { + FfiConverterOptionTypeBolt11PaymentInfo.write(value.bolt11, into: &buf) + FfiConverterOptionTypeOnchainPaymentInfo.write(value.onchain, into: &buf) + } +} + + +public func FfiConverterTypePaymentInfo_lift(_ buf: RustBuffer) throws -> PaymentInfo { + return try FfiConverterTypePaymentInfo.lift(buf) +} + +public func FfiConverterTypePaymentInfo_lower(_ value: PaymentInfo) -> RustBuffer { + return FfiConverterTypePaymentInfo.lower(value) +} + + public struct PeerDetails { public var nodeId: PublicKey public var address: SocketAddress @@ -3744,60 +5024,121 @@ public enum BalanceSource { } -public struct FfiConverterTypeBalanceSource: FfiConverterRustBuffer { - typealias SwiftType = BalanceSource +public struct FfiConverterTypeBalanceSource: FfiConverterRustBuffer { + typealias SwiftType = BalanceSource + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BalanceSource { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .holderForceClosed + + case 2: return .counterpartyForceClosed + + case 3: return .coopClose + + case 4: return .htlc + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: BalanceSource, into buf: inout [UInt8]) { + switch value { + + + case .holderForceClosed: + writeInt(&buf, Int32(1)) + + + case .counterpartyForceClosed: + writeInt(&buf, Int32(2)) + + + case .coopClose: + writeInt(&buf, Int32(3)) + + + case .htlc: + writeInt(&buf, Int32(4)) + + } + } +} + + +public func FfiConverterTypeBalanceSource_lift(_ buf: RustBuffer) throws -> BalanceSource { + return try FfiConverterTypeBalanceSource.lift(buf) +} + +public func FfiConverterTypeBalanceSource_lower(_ value: BalanceSource) -> RustBuffer { + return FfiConverterTypeBalanceSource.lower(value) +} + + + +extension BalanceSource: Equatable, Hashable {} + + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum Bolt11InvoiceDescription { + + case hash(hash: String + ) + case direct(description: String + ) +} + + +public struct FfiConverterTypeBolt11InvoiceDescription: FfiConverterRustBuffer { + typealias SwiftType = Bolt11InvoiceDescription - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> BalanceSource { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11InvoiceDescription { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .holderForceClosed - - case 2: return .counterpartyForceClosed - - case 3: return .coopClose + case 1: return .hash(hash: try FfiConverterString.read(from: &buf) + ) - case 4: return .htlc + case 2: return .direct(description: try FfiConverterString.read(from: &buf) + ) default: throw UniffiInternalError.unexpectedEnumCase } } - public static func write(_ value: BalanceSource, into buf: inout [UInt8]) { + public static func write(_ value: Bolt11InvoiceDescription, into buf: inout [UInt8]) { switch value { - case .holderForceClosed: + case let .hash(hash): writeInt(&buf, Int32(1)) + FfiConverterString.write(hash, into: &buf) + - - case .counterpartyForceClosed: + case let .direct(description): writeInt(&buf, Int32(2)) - - - case .coopClose: - writeInt(&buf, Int32(3)) - - - case .htlc: - writeInt(&buf, Int32(4)) - + FfiConverterString.write(description, into: &buf) + } } } -public func FfiConverterTypeBalanceSource_lift(_ buf: RustBuffer) throws -> BalanceSource { - return try FfiConverterTypeBalanceSource.lift(buf) +public func FfiConverterTypeBolt11InvoiceDescription_lift(_ buf: RustBuffer) throws -> Bolt11InvoiceDescription { + return try FfiConverterTypeBolt11InvoiceDescription.lift(buf) } -public func FfiConverterTypeBalanceSource_lower(_ value: BalanceSource) -> RustBuffer { - return FfiConverterTypeBalanceSource.lower(value) +public func FfiConverterTypeBolt11InvoiceDescription_lower(_ value: Bolt11InvoiceDescription) -> RustBuffer { + return FfiConverterTypeBolt11InvoiceDescription.lower(value) } -extension BalanceSource: Equatable, Hashable {} +extension Bolt11InvoiceDescription: Equatable, Hashable {} @@ -3816,6 +5157,8 @@ public enum BuildError { case InvalidListeningAddresses(message: String) + case InvalidAnnouncementAddresses(message: String) + case InvalidNodeAlias(message: String) case ReadFailed(message: String) @@ -3830,6 +5173,8 @@ public enum BuildError { case LoggerSetupFailed(message: String) + case NetworkMismatch(message: String) + } @@ -3863,31 +5208,39 @@ public struct FfiConverterTypeBuildError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) - case 6: return .InvalidNodeAlias( + case 6: return .InvalidAnnouncementAddresses( + message: try FfiConverterString.read(from: &buf) + ) + + case 7: return .InvalidNodeAlias( + message: try FfiConverterString.read(from: &buf) + ) + + case 8: return .ReadFailed( message: try FfiConverterString.read(from: &buf) ) - case 7: return .ReadFailed( + case 9: return .WriteFailed( message: try FfiConverterString.read(from: &buf) ) - case 8: return .WriteFailed( + case 10: return .StoragePathAccessFailed( message: try FfiConverterString.read(from: &buf) ) - case 9: return .StoragePathAccessFailed( + case 11: return .KvStoreSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 10: return .KvStoreSetupFailed( + case 12: return .WalletSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 11: return .WalletSetupFailed( + case 13: return .LoggerSetupFailed( message: try FfiConverterString.read(from: &buf) ) - case 12: return .LoggerSetupFailed( + case 14: return .NetworkMismatch( message: try FfiConverterString.read(from: &buf) ) @@ -3912,20 +5265,24 @@ public struct FfiConverterTypeBuildError: FfiConverterRustBuffer { writeInt(&buf, Int32(4)) case .InvalidListeningAddresses(_ /* message is ignored*/): writeInt(&buf, Int32(5)) - case .InvalidNodeAlias(_ /* message is ignored*/): + case .InvalidAnnouncementAddresses(_ /* message is ignored*/): writeInt(&buf, Int32(6)) - case .ReadFailed(_ /* message is ignored*/): + case .InvalidNodeAlias(_ /* message is ignored*/): writeInt(&buf, Int32(7)) - case .WriteFailed(_ /* message is ignored*/): + case .ReadFailed(_ /* message is ignored*/): writeInt(&buf, Int32(8)) - case .StoragePathAccessFailed(_ /* message is ignored*/): + case .WriteFailed(_ /* message is ignored*/): writeInt(&buf, Int32(9)) - case .KvStoreSetupFailed(_ /* message is ignored*/): + case .StoragePathAccessFailed(_ /* message is ignored*/): writeInt(&buf, Int32(10)) - case .WalletSetupFailed(_ /* message is ignored*/): + case .KvStoreSetupFailed(_ /* message is ignored*/): writeInt(&buf, Int32(11)) - case .LoggerSetupFailed(_ /* message is ignored*/): + case .WalletSetupFailed(_ /* message is ignored*/): writeInt(&buf, Int32(12)) + case .LoggerSetupFailed(_ /* message is ignored*/): + writeInt(&buf, Int32(13)) + case .NetworkMismatch(_ /* message is ignored*/): + writeInt(&buf, Int32(14)) } @@ -4089,18 +5446,80 @@ extension ClosureReason: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum ConfirmationStatus { + + case confirmed(blockHash: BlockHash, height: UInt32, timestamp: UInt64 + ) + case unconfirmed +} + + +public struct FfiConverterTypeConfirmationStatus: FfiConverterRustBuffer { + typealias SwiftType = ConfirmationStatus + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> ConfirmationStatus { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .confirmed(blockHash: try FfiConverterTypeBlockHash.read(from: &buf), height: try FfiConverterUInt32.read(from: &buf), timestamp: try FfiConverterUInt64.read(from: &buf) + ) + + case 2: return .unconfirmed + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: ConfirmationStatus, into buf: inout [UInt8]) { + switch value { + + + case let .confirmed(blockHash,height,timestamp): + writeInt(&buf, Int32(1)) + FfiConverterTypeBlockHash.write(blockHash, into: &buf) + FfiConverterUInt32.write(height, into: &buf) + FfiConverterUInt64.write(timestamp, into: &buf) + + + case .unconfirmed: + writeInt(&buf, Int32(2)) + + } + } +} + + +public func FfiConverterTypeConfirmationStatus_lift(_ buf: RustBuffer) throws -> ConfirmationStatus { + return try FfiConverterTypeConfirmationStatus.lift(buf) +} + +public func FfiConverterTypeConfirmationStatus_lower(_ value: ConfirmationStatus) -> RustBuffer { + return FfiConverterTypeConfirmationStatus.lower(value) +} + + + +extension ConfirmationStatus: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. public enum Event { - case paymentSuccessful(paymentId: PaymentId?, paymentHash: PaymentHash, feePaidMsat: UInt64? + case paymentSuccessful(paymentId: PaymentId?, paymentHash: PaymentHash, paymentPreimage: PaymentPreimage?, feePaidMsat: UInt64? ) case paymentFailed(paymentId: PaymentId?, paymentHash: PaymentHash?, reason: PaymentFailureReason? ) - case paymentReceived(paymentId: PaymentId?, paymentHash: PaymentHash, amountMsat: UInt64 + case paymentReceived(paymentId: PaymentId?, paymentHash: PaymentHash, amountMsat: UInt64, customRecords: [CustomTlvRecord] ) - case paymentClaimable(paymentId: PaymentId, paymentHash: PaymentHash, claimableAmountMsat: UInt64, claimDeadline: UInt32? + case paymentClaimable(paymentId: PaymentId, paymentHash: PaymentHash, claimableAmountMsat: UInt64, claimDeadline: UInt32?, customRecords: [CustomTlvRecord] + ) + case paymentForwarded(prevChannelId: ChannelId, nextChannelId: ChannelId, prevUserChannelId: UserChannelId?, nextUserChannelId: UserChannelId?, prevNodeId: PublicKey?, nextNodeId: PublicKey?, totalFeeEarnedMsat: UInt64?, skimmedFeeMsat: UInt64?, claimFromOnchainTx: Bool, outboundAmountForwardedMsat: UInt64? ) case channelPending(channelId: ChannelId, userChannelId: UserChannelId, formerTemporaryChannelId: ChannelId, counterpartyNodeId: PublicKey, fundingTxo: OutPoint ) @@ -4118,25 +5537,28 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .paymentSuccessful(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), feePaidMsat: try FfiConverterOptionUInt64.read(from: &buf) + case 1: return .paymentSuccessful(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), paymentPreimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), feePaidMsat: try FfiConverterOptionUInt64.read(from: &buf) ) case 2: return .paymentFailed(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterOptionTypePaymentHash.read(from: &buf), reason: try FfiConverterOptionTypePaymentFailureReason.read(from: &buf) ) - case 3: return .paymentReceived(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), amountMsat: try FfiConverterUInt64.read(from: &buf) + case 3: return .paymentReceived(paymentId: try FfiConverterOptionTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), amountMsat: try FfiConverterUInt64.read(from: &buf), customRecords: try FfiConverterSequenceTypeCustomTlvRecord.read(from: &buf) + ) + + case 4: return .paymentClaimable(paymentId: try FfiConverterTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), claimableAmountMsat: try FfiConverterUInt64.read(from: &buf), claimDeadline: try FfiConverterOptionUInt32.read(from: &buf), customRecords: try FfiConverterSequenceTypeCustomTlvRecord.read(from: &buf) ) - case 4: return .paymentClaimable(paymentId: try FfiConverterTypePaymentId.read(from: &buf), paymentHash: try FfiConverterTypePaymentHash.read(from: &buf), claimableAmountMsat: try FfiConverterUInt64.read(from: &buf), claimDeadline: try FfiConverterOptionUInt32.read(from: &buf) + case 5: return .paymentForwarded(prevChannelId: try FfiConverterTypeChannelId.read(from: &buf), nextChannelId: try FfiConverterTypeChannelId.read(from: &buf), prevUserChannelId: try FfiConverterOptionTypeUserChannelId.read(from: &buf), nextUserChannelId: try FfiConverterOptionTypeUserChannelId.read(from: &buf), prevNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), nextNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), totalFeeEarnedMsat: try FfiConverterOptionUInt64.read(from: &buf), skimmedFeeMsat: try FfiConverterOptionUInt64.read(from: &buf), claimFromOnchainTx: try FfiConverterBool.read(from: &buf), outboundAmountForwardedMsat: try FfiConverterOptionUInt64.read(from: &buf) ) - case 5: return .channelPending(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), formerTemporaryChannelId: try FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterTypePublicKey.read(from: &buf), fundingTxo: try FfiConverterTypeOutPoint.read(from: &buf) + case 6: return .channelPending(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), formerTemporaryChannelId: try FfiConverterTypeChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterTypePublicKey.read(from: &buf), fundingTxo: try FfiConverterTypeOutPoint.read(from: &buf) ) - case 6: return .channelReady(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf) + case 7: return .channelReady(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf) ) - case 7: return .channelClosed(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), reason: try FfiConverterOptionTypeClosureReason.read(from: &buf) + case 8: return .channelClosed(channelId: try FfiConverterTypeChannelId.read(from: &buf), userChannelId: try FfiConverterTypeUserChannelId.read(from: &buf), counterpartyNodeId: try FfiConverterOptionTypePublicKey.read(from: &buf), reason: try FfiConverterOptionTypeClosureReason.read(from: &buf) ) default: throw UniffiInternalError.unexpectedEnumCase @@ -4147,10 +5569,11 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { switch value { - case let .paymentSuccessful(paymentId,paymentHash,feePaidMsat): + case let .paymentSuccessful(paymentId,paymentHash,paymentPreimage,feePaidMsat): writeInt(&buf, Int32(1)) FfiConverterOptionTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) + FfiConverterOptionTypePaymentPreimage.write(paymentPreimage, into: &buf) FfiConverterOptionUInt64.write(feePaidMsat, into: &buf) @@ -4161,23 +5584,39 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { FfiConverterOptionTypePaymentFailureReason.write(reason, into: &buf) - case let .paymentReceived(paymentId,paymentHash,amountMsat): + case let .paymentReceived(paymentId,paymentHash,amountMsat,customRecords): writeInt(&buf, Int32(3)) FfiConverterOptionTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) FfiConverterUInt64.write(amountMsat, into: &buf) + FfiConverterSequenceTypeCustomTlvRecord.write(customRecords, into: &buf) - case let .paymentClaimable(paymentId,paymentHash,claimableAmountMsat,claimDeadline): + case let .paymentClaimable(paymentId,paymentHash,claimableAmountMsat,claimDeadline,customRecords): writeInt(&buf, Int32(4)) FfiConverterTypePaymentId.write(paymentId, into: &buf) FfiConverterTypePaymentHash.write(paymentHash, into: &buf) FfiConverterUInt64.write(claimableAmountMsat, into: &buf) FfiConverterOptionUInt32.write(claimDeadline, into: &buf) + FfiConverterSequenceTypeCustomTlvRecord.write(customRecords, into: &buf) - case let .channelPending(channelId,userChannelId,formerTemporaryChannelId,counterpartyNodeId,fundingTxo): + case let .paymentForwarded(prevChannelId,nextChannelId,prevUserChannelId,nextUserChannelId,prevNodeId,nextNodeId,totalFeeEarnedMsat,skimmedFeeMsat,claimFromOnchainTx,outboundAmountForwardedMsat): writeInt(&buf, Int32(5)) + FfiConverterTypeChannelId.write(prevChannelId, into: &buf) + FfiConverterTypeChannelId.write(nextChannelId, into: &buf) + FfiConverterOptionTypeUserChannelId.write(prevUserChannelId, into: &buf) + FfiConverterOptionTypeUserChannelId.write(nextUserChannelId, into: &buf) + FfiConverterOptionTypePublicKey.write(prevNodeId, into: &buf) + FfiConverterOptionTypePublicKey.write(nextNodeId, into: &buf) + FfiConverterOptionUInt64.write(totalFeeEarnedMsat, into: &buf) + FfiConverterOptionUInt64.write(skimmedFeeMsat, into: &buf) + FfiConverterBool.write(claimFromOnchainTx, into: &buf) + FfiConverterOptionUInt64.write(outboundAmountForwardedMsat, into: &buf) + + + case let .channelPending(channelId,userChannelId,formerTemporaryChannelId,counterpartyNodeId,fundingTxo): + writeInt(&buf, Int32(6)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterTypeChannelId.write(formerTemporaryChannelId, into: &buf) @@ -4186,14 +5625,14 @@ public struct FfiConverterTypeEvent: FfiConverterRustBuffer { case let .channelReady(channelId,userChannelId,counterpartyNodeId): - writeInt(&buf, Int32(6)) + writeInt(&buf, Int32(7)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterOptionTypePublicKey.write(counterpartyNodeId, into: &buf) case let .channelClosed(channelId,userChannelId,counterpartyNodeId,reason): - writeInt(&buf, Int32(7)) + writeInt(&buf, Int32(8)) FfiConverterTypeChannelId.write(channelId, into: &buf) FfiConverterTypeUserChannelId.write(userChannelId, into: &buf) FfiConverterOptionTypePublicKey.write(counterpartyNodeId, into: &buf) @@ -4640,6 +6079,8 @@ public enum NodeError { case PaymentSendingFailed(message: String) + case InvalidCustomTlvs(message: String) + case ProbeSendingFailed(message: String) case ChannelCreationFailed(message: String) @@ -4710,6 +6151,10 @@ public enum NodeError { case InvalidNodeAlias(message: String) + case InvalidDateTime(message: String) + + case InvalidFeeRate(message: String) + case DuplicatePayment(message: String) case UnsupportedCurrency(message: String) @@ -4769,163 +6214,175 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { message: try FfiConverterString.read(from: &buf) ) - case 10: return .ProbeSendingFailed( + case 10: return .InvalidCustomTlvs( + message: try FfiConverterString.read(from: &buf) + ) + + case 11: return .ProbeSendingFailed( + message: try FfiConverterString.read(from: &buf) + ) + + case 12: return .ChannelCreationFailed( + message: try FfiConverterString.read(from: &buf) + ) + + case 13: return .ChannelClosingFailed( message: try FfiConverterString.read(from: &buf) ) - case 11: return .ChannelCreationFailed( + case 14: return .ChannelConfigUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 12: return .ChannelClosingFailed( + case 15: return .PersistenceFailed( message: try FfiConverterString.read(from: &buf) ) - case 13: return .ChannelConfigUpdateFailed( + case 16: return .FeerateEstimationUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 14: return .PersistenceFailed( + case 17: return .FeerateEstimationUpdateTimeout( message: try FfiConverterString.read(from: &buf) ) - case 15: return .FeerateEstimationUpdateFailed( + case 18: return .WalletOperationFailed( message: try FfiConverterString.read(from: &buf) ) - case 16: return .FeerateEstimationUpdateTimeout( + case 19: return .WalletOperationTimeout( message: try FfiConverterString.read(from: &buf) ) - case 17: return .WalletOperationFailed( + case 20: return .OnchainTxSigningFailed( message: try FfiConverterString.read(from: &buf) ) - case 18: return .WalletOperationTimeout( + case 21: return .TxSyncFailed( message: try FfiConverterString.read(from: &buf) ) - case 19: return .OnchainTxSigningFailed( + case 22: return .TxSyncTimeout( message: try FfiConverterString.read(from: &buf) ) - case 20: return .TxSyncFailed( + case 23: return .GossipUpdateFailed( message: try FfiConverterString.read(from: &buf) ) - case 21: return .TxSyncTimeout( + case 24: return .GossipUpdateTimeout( message: try FfiConverterString.read(from: &buf) ) - case 22: return .GossipUpdateFailed( + case 25: return .LiquidityRequestFailed( message: try FfiConverterString.read(from: &buf) ) - case 23: return .GossipUpdateTimeout( + case 26: return .UriParameterParsingFailed( message: try FfiConverterString.read(from: &buf) ) - case 24: return .LiquidityRequestFailed( + case 27: return .InvalidAddress( message: try FfiConverterString.read(from: &buf) ) - case 25: return .UriParameterParsingFailed( + case 28: return .InvalidSocketAddress( message: try FfiConverterString.read(from: &buf) ) - case 26: return .InvalidAddress( + case 29: return .InvalidPublicKey( message: try FfiConverterString.read(from: &buf) ) - case 27: return .InvalidSocketAddress( + case 30: return .InvalidSecretKey( message: try FfiConverterString.read(from: &buf) ) - case 28: return .InvalidPublicKey( + case 31: return .InvalidOfferId( message: try FfiConverterString.read(from: &buf) ) - case 29: return .InvalidSecretKey( + case 32: return .InvalidNodeId( message: try FfiConverterString.read(from: &buf) ) - case 30: return .InvalidOfferId( + case 33: return .InvalidPaymentId( message: try FfiConverterString.read(from: &buf) ) - case 31: return .InvalidNodeId( + case 34: return .InvalidPaymentHash( message: try FfiConverterString.read(from: &buf) ) - case 32: return .InvalidPaymentId( + case 35: return .InvalidPaymentPreimage( message: try FfiConverterString.read(from: &buf) ) - case 33: return .InvalidPaymentHash( + case 36: return .InvalidPaymentSecret( message: try FfiConverterString.read(from: &buf) ) - case 34: return .InvalidPaymentPreimage( + case 37: return .InvalidAmount( message: try FfiConverterString.read(from: &buf) ) - case 35: return .InvalidPaymentSecret( + case 38: return .InvalidInvoice( message: try FfiConverterString.read(from: &buf) ) - case 36: return .InvalidAmount( + case 39: return .InvalidOffer( message: try FfiConverterString.read(from: &buf) ) - case 37: return .InvalidInvoice( + case 40: return .InvalidRefund( message: try FfiConverterString.read(from: &buf) ) - case 38: return .InvalidOffer( + case 41: return .InvalidChannelId( message: try FfiConverterString.read(from: &buf) ) - case 39: return .InvalidRefund( + case 42: return .InvalidNetwork( message: try FfiConverterString.read(from: &buf) ) - case 40: return .InvalidChannelId( + case 43: return .InvalidUri( message: try FfiConverterString.read(from: &buf) ) - case 41: return .InvalidNetwork( + case 44: return .InvalidQuantity( message: try FfiConverterString.read(from: &buf) ) - case 42: return .InvalidUri( + case 45: return .InvalidNodeAlias( message: try FfiConverterString.read(from: &buf) ) - case 43: return .InvalidQuantity( + case 46: return .InvalidDateTime( message: try FfiConverterString.read(from: &buf) ) - case 44: return .InvalidNodeAlias( + case 47: return .InvalidFeeRate( message: try FfiConverterString.read(from: &buf) ) - case 45: return .DuplicatePayment( + case 48: return .DuplicatePayment( message: try FfiConverterString.read(from: &buf) ) - case 46: return .UnsupportedCurrency( + case 49: return .UnsupportedCurrency( message: try FfiConverterString.read(from: &buf) ) - case 47: return .InsufficientFunds( + case 50: return .InsufficientFunds( message: try FfiConverterString.read(from: &buf) ) - case 48: return .LiquiditySourceUnavailable( + case 51: return .LiquiditySourceUnavailable( message: try FfiConverterString.read(from: &buf) ) - case 49: return .LiquidityFeeTooHigh( + case 52: return .LiquidityFeeTooHigh( message: try FfiConverterString.read(from: &buf) ) @@ -4958,86 +6415,92 @@ public struct FfiConverterTypeNodeError: FfiConverterRustBuffer { writeInt(&buf, Int32(8)) case .PaymentSendingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(9)) - case .ProbeSendingFailed(_ /* message is ignored*/): + case .InvalidCustomTlvs(_ /* message is ignored*/): writeInt(&buf, Int32(10)) - case .ChannelCreationFailed(_ /* message is ignored*/): + case .ProbeSendingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(11)) - case .ChannelClosingFailed(_ /* message is ignored*/): + case .ChannelCreationFailed(_ /* message is ignored*/): writeInt(&buf, Int32(12)) - case .ChannelConfigUpdateFailed(_ /* message is ignored*/): + case .ChannelClosingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(13)) - case .PersistenceFailed(_ /* message is ignored*/): + case .ChannelConfigUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(14)) - case .FeerateEstimationUpdateFailed(_ /* message is ignored*/): + case .PersistenceFailed(_ /* message is ignored*/): writeInt(&buf, Int32(15)) - case .FeerateEstimationUpdateTimeout(_ /* message is ignored*/): + case .FeerateEstimationUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(16)) - case .WalletOperationFailed(_ /* message is ignored*/): + case .FeerateEstimationUpdateTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(17)) - case .WalletOperationTimeout(_ /* message is ignored*/): + case .WalletOperationFailed(_ /* message is ignored*/): writeInt(&buf, Int32(18)) - case .OnchainTxSigningFailed(_ /* message is ignored*/): + case .WalletOperationTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(19)) - case .TxSyncFailed(_ /* message is ignored*/): + case .OnchainTxSigningFailed(_ /* message is ignored*/): writeInt(&buf, Int32(20)) - case .TxSyncTimeout(_ /* message is ignored*/): + case .TxSyncFailed(_ /* message is ignored*/): writeInt(&buf, Int32(21)) - case .GossipUpdateFailed(_ /* message is ignored*/): + case .TxSyncTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(22)) - case .GossipUpdateTimeout(_ /* message is ignored*/): + case .GossipUpdateFailed(_ /* message is ignored*/): writeInt(&buf, Int32(23)) - case .LiquidityRequestFailed(_ /* message is ignored*/): + case .GossipUpdateTimeout(_ /* message is ignored*/): writeInt(&buf, Int32(24)) - case .UriParameterParsingFailed(_ /* message is ignored*/): + case .LiquidityRequestFailed(_ /* message is ignored*/): writeInt(&buf, Int32(25)) - case .InvalidAddress(_ /* message is ignored*/): + case .UriParameterParsingFailed(_ /* message is ignored*/): writeInt(&buf, Int32(26)) - case .InvalidSocketAddress(_ /* message is ignored*/): + case .InvalidAddress(_ /* message is ignored*/): writeInt(&buf, Int32(27)) - case .InvalidPublicKey(_ /* message is ignored*/): + case .InvalidSocketAddress(_ /* message is ignored*/): writeInt(&buf, Int32(28)) - case .InvalidSecretKey(_ /* message is ignored*/): + case .InvalidPublicKey(_ /* message is ignored*/): writeInt(&buf, Int32(29)) - case .InvalidOfferId(_ /* message is ignored*/): + case .InvalidSecretKey(_ /* message is ignored*/): writeInt(&buf, Int32(30)) - case .InvalidNodeId(_ /* message is ignored*/): + case .InvalidOfferId(_ /* message is ignored*/): writeInt(&buf, Int32(31)) - case .InvalidPaymentId(_ /* message is ignored*/): + case .InvalidNodeId(_ /* message is ignored*/): writeInt(&buf, Int32(32)) - case .InvalidPaymentHash(_ /* message is ignored*/): + case .InvalidPaymentId(_ /* message is ignored*/): writeInt(&buf, Int32(33)) - case .InvalidPaymentPreimage(_ /* message is ignored*/): + case .InvalidPaymentHash(_ /* message is ignored*/): writeInt(&buf, Int32(34)) - case .InvalidPaymentSecret(_ /* message is ignored*/): + case .InvalidPaymentPreimage(_ /* message is ignored*/): writeInt(&buf, Int32(35)) - case .InvalidAmount(_ /* message is ignored*/): + case .InvalidPaymentSecret(_ /* message is ignored*/): writeInt(&buf, Int32(36)) - case .InvalidInvoice(_ /* message is ignored*/): + case .InvalidAmount(_ /* message is ignored*/): writeInt(&buf, Int32(37)) - case .InvalidOffer(_ /* message is ignored*/): + case .InvalidInvoice(_ /* message is ignored*/): writeInt(&buf, Int32(38)) - case .InvalidRefund(_ /* message is ignored*/): + case .InvalidOffer(_ /* message is ignored*/): writeInt(&buf, Int32(39)) - case .InvalidChannelId(_ /* message is ignored*/): + case .InvalidRefund(_ /* message is ignored*/): writeInt(&buf, Int32(40)) - case .InvalidNetwork(_ /* message is ignored*/): + case .InvalidChannelId(_ /* message is ignored*/): writeInt(&buf, Int32(41)) - case .InvalidUri(_ /* message is ignored*/): + case .InvalidNetwork(_ /* message is ignored*/): writeInt(&buf, Int32(42)) - case .InvalidQuantity(_ /* message is ignored*/): + case .InvalidUri(_ /* message is ignored*/): writeInt(&buf, Int32(43)) - case .InvalidNodeAlias(_ /* message is ignored*/): + case .InvalidQuantity(_ /* message is ignored*/): writeInt(&buf, Int32(44)) - case .DuplicatePayment(_ /* message is ignored*/): + case .InvalidNodeAlias(_ /* message is ignored*/): writeInt(&buf, Int32(45)) - case .UnsupportedCurrency(_ /* message is ignored*/): + case .InvalidDateTime(_ /* message is ignored*/): writeInt(&buf, Int32(46)) - case .InsufficientFunds(_ /* message is ignored*/): + case .InvalidFeeRate(_ /* message is ignored*/): writeInt(&buf, Int32(47)) - case .LiquiditySourceUnavailable(_ /* message is ignored*/): + case .DuplicatePayment(_ /* message is ignored*/): writeInt(&buf, Int32(48)) - case .LiquidityFeeTooHigh(_ /* message is ignored*/): + case .UnsupportedCurrency(_ /* message is ignored*/): writeInt(&buf, Int32(49)) + case .InsufficientFunds(_ /* message is ignored*/): + writeInt(&buf, Int32(50)) + case .LiquiditySourceUnavailable(_ /* message is ignored*/): + writeInt(&buf, Int32(51)) + case .LiquidityFeeTooHigh(_ /* message is ignored*/): + writeInt(&buf, Int32(52)) } @@ -5118,6 +6581,7 @@ public enum PaymentFailureReason { case unknownRequiredFeatures case invoiceRequestExpired case invoiceRequestRejected + case blindedPathCreationFailed } @@ -5146,6 +6610,8 @@ public struct FfiConverterTypePaymentFailureReason: FfiConverterRustBuffer { case 9: return .invoiceRequestRejected + case 10: return .blindedPathCreationFailed + default: throw UniffiInternalError.unexpectedEnumCase } } @@ -5189,6 +6655,10 @@ public struct FfiConverterTypePaymentFailureReason: FfiConverterRustBuffer { case .invoiceRequestRejected: writeInt(&buf, Int32(9)) + + case .blindedPathCreationFailed: + writeInt(&buf, Int32(10)) + } } } @@ -5213,10 +6683,11 @@ extension PaymentFailureReason: Equatable, Hashable {} public enum PaymentKind { - case onchain + case onchain(txid: Txid, status: ConfirmationStatus + ) case bolt11(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret? ) - case bolt11Jit(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret?, lspFeeLimits: LspFeeLimits + case bolt11Jit(hash: PaymentHash, preimage: PaymentPreimage?, secret: PaymentSecret?, counterpartySkimmedFeeMsat: UInt64?, lspFeeLimits: LspFeeLimits ) case bolt12Offer(hash: PaymentHash?, preimage: PaymentPreimage?, secret: PaymentSecret?, offerId: OfferId, payerNote: UntrustedString?, quantity: UInt64? ) @@ -5234,12 +6705,13 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { let variant: Int32 = try readInt(&buf) switch variant { - case 1: return .onchain + case 1: return .onchain(txid: try FfiConverterTypeTxid.read(from: &buf), status: try FfiConverterTypeConfirmationStatus.read(from: &buf) + ) case 2: return .bolt11(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf) ) - case 3: return .bolt11Jit(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), lspFeeLimits: try FfiConverterTypeLSPFeeLimits.read(from: &buf) + case 3: return .bolt11Jit(hash: try FfiConverterTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), counterpartySkimmedFeeMsat: try FfiConverterOptionUInt64.read(from: &buf), lspFeeLimits: try FfiConverterTypeLSPFeeLimits.read(from: &buf) ) case 4: return .bolt12Offer(hash: try FfiConverterOptionTypePaymentHash.read(from: &buf), preimage: try FfiConverterOptionTypePaymentPreimage.read(from: &buf), secret: try FfiConverterOptionTypePaymentSecret.read(from: &buf), offerId: try FfiConverterTypeOfferId.read(from: &buf), payerNote: try FfiConverterOptionTypeUntrustedString.read(from: &buf), quantity: try FfiConverterOptionUInt64.read(from: &buf) @@ -5259,9 +6731,11 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { switch value { - case .onchain: + case let .onchain(txid,status): writeInt(&buf, Int32(1)) - + FfiConverterTypeTxid.write(txid, into: &buf) + FfiConverterTypeConfirmationStatus.write(status, into: &buf) + case let .bolt11(hash,preimage,secret): writeInt(&buf, Int32(2)) @@ -5270,11 +6744,12 @@ public struct FfiConverterTypePaymentKind: FfiConverterRustBuffer { FfiConverterOptionTypePaymentSecret.write(secret, into: &buf) - case let .bolt11Jit(hash,preimage,secret,lspFeeLimits): + case let .bolt11Jit(hash,preimage,secret,counterpartySkimmedFeeMsat,lspFeeLimits): writeInt(&buf, Int32(3)) FfiConverterTypePaymentHash.write(hash, into: &buf) FfiConverterOptionTypePaymentPreimage.write(preimage, into: &buf) FfiConverterOptionTypePaymentSecret.write(secret, into: &buf) + FfiConverterOptionUInt64.write(counterpartySkimmedFeeMsat, into: &buf) FfiConverterTypeLSPFeeLimits.write(lspFeeLimits, into: &buf) @@ -5311,13 +6786,75 @@ public func FfiConverterTypePaymentKind_lift(_ buf: RustBuffer) throws -> Paymen return try FfiConverterTypePaymentKind.lift(buf) } -public func FfiConverterTypePaymentKind_lower(_ value: PaymentKind) -> RustBuffer { - return FfiConverterTypePaymentKind.lower(value) +public func FfiConverterTypePaymentKind_lower(_ value: PaymentKind) -> RustBuffer { + return FfiConverterTypePaymentKind.lower(value) +} + + + +extension PaymentKind: Equatable, Hashable {} + + + +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum PaymentState { + + case expectPayment + case paid + case refunded +} + + +public struct FfiConverterTypePaymentState: FfiConverterRustBuffer { + typealias SwiftType = PaymentState + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> PaymentState { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .expectPayment + + case 2: return .paid + + case 3: return .refunded + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: PaymentState, into buf: inout [UInt8]) { + switch value { + + + case .expectPayment: + writeInt(&buf, Int32(1)) + + + case .paid: + writeInt(&buf, Int32(2)) + + + case .refunded: + writeInt(&buf, Int32(3)) + + } + } +} + + +public func FfiConverterTypePaymentState_lift(_ buf: RustBuffer) throws -> PaymentState { + return try FfiConverterTypePaymentState.lift(buf) +} + +public func FfiConverterTypePaymentState_lower(_ value: PaymentState) -> RustBuffer { + return FfiConverterTypePaymentState.lower(value) } -extension PaymentKind: Equatable, Hashable {} +extension PaymentState: Equatable, Hashable {} @@ -5731,6 +7268,27 @@ fileprivate struct FfiConverterOptionString: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeFeeRate: FfiConverterRustBuffer { + typealias SwiftType = FeeRate? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeFeeRate.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeFeeRate.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeAnchorChannelsConfig: FfiConverterRustBuffer { typealias SwiftType = AnchorChannelsConfig? @@ -5752,6 +7310,48 @@ fileprivate struct FfiConverterOptionTypeAnchorChannelsConfig: FfiConverterRustB } } +fileprivate struct FfiConverterOptionTypeBackgroundSyncConfig: FfiConverterRustBuffer { + typealias SwiftType = BackgroundSyncConfig? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeBackgroundSyncConfig.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeBackgroundSyncConfig.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + +fileprivate struct FfiConverterOptionTypeBolt11PaymentInfo: FfiConverterRustBuffer { + typealias SwiftType = Bolt11PaymentInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeBolt11PaymentInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeBolt11PaymentInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelConfig: FfiConverterRustBuffer { typealias SwiftType = ChannelConfig? @@ -5794,6 +7394,27 @@ fileprivate struct FfiConverterOptionTypeChannelInfo: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeChannelOrderInfo: FfiConverterRustBuffer { + typealias SwiftType = ChannelOrderInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeChannelOrderInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeChannelOrderInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelUpdateInfo: FfiConverterRustBuffer { typealias SwiftType = ChannelUpdateInfo? @@ -5815,6 +7436,27 @@ fileprivate struct FfiConverterOptionTypeChannelUpdateInfo: FfiConverterRustBuff } } +fileprivate struct FfiConverterOptionTypeElectrumSyncConfig: FfiConverterRustBuffer { + typealias SwiftType = ElectrumSyncConfig? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeElectrumSyncConfig.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeElectrumSyncConfig.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeEsploraSyncConfig: FfiConverterRustBuffer { typealias SwiftType = EsploraSyncConfig? @@ -5878,6 +7520,27 @@ fileprivate struct FfiConverterOptionTypeNodeInfo: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeOnchainPaymentInfo: FfiConverterRustBuffer { + typealias SwiftType = OnchainPaymentInfo? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeOnchainPaymentInfo.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeOnchainPaymentInfo.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeOutPoint: FfiConverterRustBuffer { typealias SwiftType = OutPoint? @@ -5983,6 +7646,27 @@ fileprivate struct FfiConverterOptionTypeEvent: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterOptionTypeLogLevel: FfiConverterRustBuffer { + typealias SwiftType = LogLevel? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeLogLevel.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeLogLevel.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeMaxTotalRoutingFeeLimit: FfiConverterRustBuffer { typealias SwiftType = MaxTotalRoutingFeeLimit? @@ -6046,6 +7730,27 @@ fileprivate struct FfiConverterOptionSequenceTypeSocketAddress: FfiConverterRust } } +fileprivate struct FfiConverterOptionTypeAddress: FfiConverterRustBuffer { + typealias SwiftType = Address? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeAddress.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeAddress.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterOptionTypeChannelId: FfiConverterRustBuffer { typealias SwiftType = ChannelId? @@ -6214,6 +7919,27 @@ fileprivate struct FfiConverterOptionTypeUntrustedString: FfiConverterRustBuffer } } +fileprivate struct FfiConverterOptionTypeUserChannelId: FfiConverterRustBuffer { + typealias SwiftType = UserChannelId? + + public static func write(_ value: SwiftType, into buf: inout [UInt8]) { + guard let value = value else { + writeInt(&buf, Int8(0)) + return + } + writeInt(&buf, Int8(1)) + FfiConverterTypeUserChannelId.write(value, into: &buf) + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> SwiftType { + switch try readInt(&buf) as Int8 { + case 0: return nil + case 1: return try FfiConverterTypeUserChannelId.read(from: &buf) + default: throw UniffiInternalError.unexpectedOptionalTag + } + } +} + fileprivate struct FfiConverterSequenceUInt8: FfiConverterRustBuffer { typealias SwiftType = [UInt8] @@ -6280,6 +8006,28 @@ fileprivate struct FfiConverterSequenceTypeChannelDetails: FfiConverterRustBuffe } } +fileprivate struct FfiConverterSequenceTypeCustomTlvRecord: FfiConverterRustBuffer { + typealias SwiftType = [CustomTlvRecord] + + public static func write(_ value: [CustomTlvRecord], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeCustomTlvRecord.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [CustomTlvRecord] { + let len: Int32 = try readInt(&buf) + var seq = [CustomTlvRecord]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeCustomTlvRecord.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypePaymentDetails: FfiConverterRustBuffer { typealias SwiftType = [PaymentDetails] @@ -6628,6 +8376,40 @@ public func FfiConverterTypeChannelId_lower(_ value: ChannelId) -> RustBuffer { +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias DateTime = String +public struct FfiConverterTypeDateTime: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> DateTime { + return try FfiConverterString.read(from: &buf) + } + + public static func write(_ value: DateTime, into buf: inout [UInt8]) { + return FfiConverterString.write(value, into: &buf) + } + + public static func lift(_ value: RustBuffer) throws -> DateTime { + return try FfiConverterString.lift(value) + } + + public static func lower(_ value: DateTime) -> RustBuffer { + return FfiConverterString.lower(value) + } +} + + +public func FfiConverterTypeDateTime_lift(_ value: RustBuffer) throws -> DateTime { + return try FfiConverterTypeDateTime.lift(value) +} + +public func FfiConverterTypeDateTime_lower(_ value: DateTime) -> RustBuffer { + return FfiConverterTypeDateTime.lower(value) +} + + + /** * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. @@ -6798,6 +8580,40 @@ public func FfiConverterTypeOfferId_lower(_ value: OfferId) -> RustBuffer { +/** + * Typealias from the type name used in the UDL file to the builtin type. This + * is needed because the UDL type name is used in function/method signatures. + */ +public typealias OrderId = String +public struct FfiConverterTypeOrderId: FfiConverter { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> OrderId { + return try FfiConverterString.read(from: &buf) + } + + public static func write(_ value: OrderId, into buf: inout [UInt8]) { + return FfiConverterString.write(value, into: &buf) + } + + public static func lift(_ value: RustBuffer) throws -> OrderId { + return try FfiConverterString.lift(value) + } + + public static func lower(_ value: OrderId) -> RustBuffer { + return FfiConverterString.lower(value) + } +} + + +public func FfiConverterTypeOrderId_lift(_ value: RustBuffer) throws -> OrderId { + return try FfiConverterTypeOrderId.lift(value) +} + +public func FfiConverterTypeOrderId_lower(_ value: OrderId) -> RustBuffer { + return FfiConverterTypeOrderId.lower(value) +} + + + /** * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. @@ -7222,22 +9038,22 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_bolt11payment_fail_for_hash() != 24516) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 28084) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 47624) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 3869) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 36395) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 51453) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 38916) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 21975) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 9075) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 58617) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 58805) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 50555) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 30211) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 39133) { @@ -7285,12 +9101,21 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_build_with_vss_store_and_header_provider() != 9090) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_announcement_addresses() != 39271) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_chain_source_bitcoind_rpc() != 2111) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_chain_source_electrum() != 55552) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_chain_source_esplora() != 1781) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_custom_logger() != 51232) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_entropy_bip39_mnemonic() != 827) { return InitializationResult.apiChecksumMismatch } @@ -7300,18 +9125,27 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_set_entropy_seed_path() != 64056) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_filesystem_logger() != 10249) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_gossip_source_p2p() != 9279) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_builder_set_gossip_source_rgs() != 64312) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps2() != 2667) { + if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps1() != 51527) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_builder_set_liquidity_source_lsps2() != 14430) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_builder_set_listening_addresses() != 14051) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_builder_set_log_facade_logger() != 58410) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_builder_set_network() != 27539) { return InitializationResult.apiChecksumMismatch } @@ -7321,6 +9155,24 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_builder_set_storage_dir_path() != 59019) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_kwu() != 58911) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_vb_ceil() != 58575) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_feerate_to_sat_per_vb_floor() != 59617) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_lsps1liquidity_check_order_status() != 64731) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_lsps1liquidity_request_channel() != 18153) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_logwriter_log() != 3299) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_networkgraph_channel() != 38070) { return InitializationResult.apiChecksumMismatch } @@ -7333,6 +9185,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_networkgraph_node() != 48925) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_node_announcement_addresses() != 61426) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_node_bolt11_payment() != 41402) { return InitializationResult.apiChecksumMismatch } @@ -7351,7 +9206,10 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_node_disconnect() != 43538) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_node_event_handled() != 47939) { + if (uniffi_ldk_node_checksum_method_node_event_handled() != 38712) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_node_export_pathfinding_scores() != 62331) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_node_force_close_channel() != 48831) { @@ -7372,6 +9230,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_node_listening_addresses() != 2665) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_node_lsps1_liquidity() != 38201) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_node_network_graph() != 2695) { return InitializationResult.apiChecksumMismatch } @@ -7435,10 +9296,10 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_onchainpayment_new_address() != 37251) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_onchainpayment_send_all_to_address() != 20046) { + if (uniffi_ldk_node_checksum_method_onchainpayment_send_all_to_address() != 37748) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 55731) { + if (uniffi_ldk_node_checksum_method_onchainpayment_send_to_address() != 55646) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_spontaneouspayment_send() != 48210) { @@ -7447,6 +9308,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_spontaneouspayment_send_probes() != 25937) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_spontaneouspayment_send_with_custom_tlvs() != 2376) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_unifiedqrpayment_receive() != 913) { return InitializationResult.apiChecksumMismatch } @@ -7462,7 +9326,14 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_constructor_builder_new() != 40499) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_constructor_feerate_from_sat_per_kwu() != 50548) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_constructor_feerate_from_sat_per_vb_unchecked() != 41808) { + return InitializationResult.apiChecksumMismatch + } + uniffiCallbackInitLogWriter() return InitializationResult.ok } From f8e758d399b611d0665f5904e2fa0f7c4390d664 Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Fri, 11 Apr 2025 16:42:42 +0200 Subject: [PATCH 057/177] feat: Add Bolt11Invoice wrapper for FFI bindings Implement Bolt11Invoice struct in uniffi_types to provide a wrapper around LDK's Bolt11Invoice for cross-language bindings. Modified payment handling in bolt11.rs to: - Support both native and FFI-compatible invoice types via type aliasing - Add maybe_wrap_invoice and maybe_convert_invoice helper functions - Implement conditional compilation for transparent FFI support - Update all payment functions to handle wrapped invoice types Integrated with unified_qr.rs to ensure consistent invoice handling across the QR code generation and payment workflows. Functionality tested with test coverage to ensure that data does not change when wrapping/unwrapping. --- bindings/ldk_node.udl | 41 +++- src/lib.rs | 3 + src/liquidity.rs | 7 +- src/payment/bolt11.rs | 56 ++++-- src/payment/unified_qr.rs | 3 +- src/uniffi_types.rs | 381 +++++++++++++++++++++++++++++++++++--- 6 files changed, 451 insertions(+), 40 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 1a56a7d4b6..c2f0166c84 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -698,6 +698,44 @@ dictionary NodeAnnouncementInfo { sequence addresses; }; +enum Currency { + "Bitcoin", + "BitcoinTestnet", + "Regtest", + "Simnet", + "Signet", +}; + +dictionary RouteHintHop { + PublicKey src_node_id; + u64 short_channel_id; + u16 cltv_expiry_delta; + u64? htlc_minimum_msat; + u64? htlc_maximum_msat; + RoutingFees fees; +}; + +interface Bolt11Invoice { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string invoice_str); + sequence signable_hash(); + PaymentHash payment_hash(); + PaymentSecret payment_secret(); + u64? amount_milli_satoshis(); + u64 expiry_time_seconds(); + u64 seconds_since_epoch(); + u64 seconds_until_expiry(); + boolean is_expired(); + boolean would_expire(u64 at_time_seconds); + Bolt11InvoiceDescription description(); + u64 min_final_cltv_expiry_delta(); + Network network(); + Currency currency(); + sequence
fallback_addresses(); + sequence> route_hints(); + PublicKey recover_payee_pub_key(); +}; + [Custom] typedef string Txid; @@ -716,9 +754,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Bolt11Invoice; - [Custom] typedef string Offer; diff --git a/src/lib.rs b/src/lib.rs index 93393585d8..7859a092ed 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -23,6 +23,8 @@ //! controlled via commands such as [`start`], [`stop`], [`open_channel`], [`send`], etc.: //! //! ```no_run +//! # #[cfg(not(feature = "uniffi"))] +//! # { //! use ldk_node::Builder; //! use ldk_node::lightning_invoice::Bolt11Invoice; //! use ldk_node::lightning::ln::msgs::SocketAddress; @@ -57,6 +59,7 @@ //! //! node.stop().unwrap(); //! } +//! # } //! ``` //! //! [`build`]: Builder::build diff --git a/src/liquidity.rs b/src/liquidity.rs index a47c23c812..47f3dcce45 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -1308,7 +1308,7 @@ type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct PaymentInfo { /// A Lightning payment using BOLT 11. - pub bolt11: Option, + pub bolt11: Option, /// An onchain payment. pub onchain: Option, } @@ -1316,7 +1316,10 @@ pub struct PaymentInfo { #[cfg(feature = "uniffi")] impl From for PaymentInfo { fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { - PaymentInfo { bolt11: value.bolt11, onchain: value.onchain.map(|o| o.into()) } + PaymentInfo { + bolt11: value.bolt11.map(|b| b.into()), + onchain: value.onchain.map(|o| o.into()), + } } } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index ee0f24f059..22e12681f5 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -30,7 +30,7 @@ use lightning::routing::router::{PaymentParameters, RouteParameters}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice; use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; use bitcoin::hashes::sha256::Hash as Sha256; @@ -38,6 +38,29 @@ use bitcoin::hashes::Hash; use std::sync::{Arc, RwLock}; +#[cfg(not(feature = "uniffi"))] +type Bolt11Invoice = LdkBolt11Invoice; +#[cfg(feature = "uniffi")] +type Bolt11Invoice = Arc; + +#[cfg(not(feature = "uniffi"))] +pub(crate) fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { + invoice +} +#[cfg(feature = "uniffi")] +pub(crate) fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { + Arc::new(invoice.into()) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { + invoice +} +#[cfg(feature = "uniffi")] +pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { + &invoice.inner +} + #[cfg(not(feature = "uniffi"))] type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] @@ -101,6 +124,7 @@ impl Bolt11Payment { pub fn send( &self, invoice: &Bolt11Invoice, sending_parameters: Option, ) -> Result { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -209,6 +233,7 @@ impl Bolt11Payment { &self, invoice: &Bolt11Invoice, amount_msat: u64, sending_parameters: Option, ) -> Result { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -441,7 +466,8 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, None) + let invoice = self.receive_inner(Some(amount_msat), description, expiry_secs, None)?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a payment of the amount @@ -463,7 +489,9 @@ impl Bolt11Payment { payment_hash: PaymentHash, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash)) + let invoice = + self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -474,7 +502,8 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, None) + let invoice = self.receive_inner(None, description, expiry_secs, None)?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a payment for the given payment hash @@ -495,13 +524,14 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { let description = maybe_convert_description!(description); - self.receive_inner(None, description, expiry_secs, Some(payment_hash)) + let invoice = self.receive_inner(None, description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap_invoice(invoice)) } pub(crate) fn receive_inner( &self, amount_msat: Option, invoice_description: &LdkBolt11InvoiceDescription, expiry_secs: u32, manual_claim_payment_hash: Option, - ) -> Result { + ) -> Result { let invoice = { let invoice_params = Bolt11InvoiceParameters { amount_msats: amount_msat, @@ -571,13 +601,14 @@ impl Bolt11Payment { max_total_lsp_fee_limit_msat: Option, ) -> Result { let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), description, expiry_secs, max_total_lsp_fee_limit_msat, None, - ) + )?; + Ok(maybe_wrap_invoice(invoice)) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -596,20 +627,21 @@ impl Bolt11Payment { max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { let description = maybe_convert_description!(description); - self.receive_via_jit_channel_inner( + let invoice = self.receive_via_jit_channel_inner( None, description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, - ) + )?; + Ok(maybe_wrap_invoice(invoice)) } fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, max_proportional_lsp_fee_limit_ppm_msat: Option, - ) -> Result { + ) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -709,6 +741,7 @@ impl Bolt11Payment { /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -741,6 +774,7 @@ impl Bolt11Payment { pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, ) -> Result<(), Error> { + let invoice = maybe_convert_invoice(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 92c405056c..ec37931a09 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -13,7 +13,7 @@ //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md use crate::error::Error; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::payment::{bolt11::maybe_wrap_invoice, Bolt11Payment, Bolt12Payment, OnchainPayment}; use crate::Config; use lightning::ln::channelmanager::PaymentId; @@ -149,6 +149,7 @@ impl UnifiedQrPayment { } if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { + let invoice = maybe_wrap_invoice(invoice); match self.bolt11_invoice.send(&invoice, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), diff --git a/src/uniffi_types.rs b/src/uniffi_types.rs index acdee3a944..77f9348cc7 100644 --- a/src/uniffi_types.rs +++ b/src/uniffi_types.rs @@ -33,12 +33,10 @@ pub use lightning::util::string::UntrustedString; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; -pub use lightning_invoice::{Bolt11Invoice, Description}; +pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; -pub use lightning_liquidity::lsps1::msgs::{ - Bolt11PaymentInfo, OrderId, OrderParameters, PaymentState, -}; +pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState}; pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; @@ -60,10 +58,12 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; use lightning::util::ser::Writeable; -use lightning_invoice::SignedRawBolt11Invoice; +use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; use std::convert::TryInto; use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; @@ -113,24 +113,6 @@ impl UniffiCustomTypeConverter for Address { } } -impl UniffiCustomTypeConverter for Bolt11Invoice { - type Builtin = String; - - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Ok(signed) = val.parse::() { - if let Ok(invoice) = Bolt11Invoice::from_signed(signed) { - return Ok(invoice); - } - } - - Err(Error::InvalidInvoice.into()) - } - - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() - } -} - impl UniffiCustomTypeConverter for Offer { type Builtin = String; @@ -405,6 +387,251 @@ impl From for Bolt11InvoiceDescript } } +impl<'a> From> for Bolt11InvoiceDescription { + fn from(value: Bolt11InvoiceDescriptionRef<'a>) -> Self { + match value { + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(description) => { + Bolt11InvoiceDescription::Direct { description: description.to_string() } + }, + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(hash) => { + Bolt11InvoiceDescription::Hash { hash: hex_utils::to_string(hash.0.as_ref()) } + }, + } + } +} + +/// Enum representing the crypto currencies (or networks) supported by this library +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Currency { + /// Bitcoin mainnet + Bitcoin, + + /// Bitcoin testnet + BitcoinTestnet, + + /// Bitcoin regtest + Regtest, + + /// Bitcoin simnet + Simnet, + + /// Bitcoin signet + Signet, +} + +impl From for Currency { + fn from(currency: lightning_invoice::Currency) -> Self { + match currency { + lightning_invoice::Currency::Bitcoin => Currency::Bitcoin, + lightning_invoice::Currency::BitcoinTestnet => Currency::BitcoinTestnet, + lightning_invoice::Currency::Regtest => Currency::Regtest, + lightning_invoice::Currency::Simnet => Currency::Simnet, + lightning_invoice::Currency::Signet => Currency::Signet, + } + } +} + +/// A channel descriptor for a hop along a payment path. +/// +/// While this generally comes from BOLT 11's `r` field, this struct includes more fields than are +/// available in BOLT 11. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct RouteHintHop { + /// The node_id of the non-target end of the route + pub src_node_id: PublicKey, + /// The short_channel_id of this channel + pub short_channel_id: u64, + /// The fees which must be paid to use this channel + pub fees: RoutingFees, + /// The difference in CLTV values between this node and the next node. + pub cltv_expiry_delta: u16, + /// The minimum value, in msat, which must be relayed to the next hop. + pub htlc_minimum_msat: Option, + /// The maximum value in msat available for routing with a single HTLC. + pub htlc_maximum_msat: Option, +} + +impl From for RouteHintHop { + fn from(hop: lightning::routing::router::RouteHintHop) -> Self { + Self { + src_node_id: hop.src_node_id, + short_channel_id: hop.short_channel_id, + cltv_expiry_delta: hop.cltv_expiry_delta, + htlc_minimum_msat: hop.htlc_minimum_msat, + htlc_maximum_msat: hop.htlc_maximum_msat, + fees: hop.fees, + } + } +} + +/// Represents a syntactically and semantically correct lightning BOLT11 invoice. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt11Invoice { + pub inner: LdkBolt11Invoice, +} + +impl Bolt11Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// Returns the underlying invoice [`LdkBolt11Invoice`] + pub fn into_inner(self) -> LdkBolt11Invoice { + self.inner + } + + /// The hash of the [`RawBolt11Invoice`] that was signed. + /// + /// [`RawBolt11Invoice`]: lightning_invoice::RawBolt11Invoice + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// Returns the hash to which we will receive the preimage on completion of the payment + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().to_byte_array()) + } + + /// Get the payment secret if one was included in the invoice + pub fn payment_secret(&self) -> PaymentSecret { + PaymentSecret(self.inner.payment_secret().0) + } + + /// Returns the amount if specified in the invoice as millisatoshis. + pub fn amount_milli_satoshis(&self) -> Option { + self.inner.amount_milli_satoshis() + } + + /// Returns the invoice's expiry time (in seconds), if present, otherwise [`DEFAULT_EXPIRY_TIME`]. + /// + /// [`DEFAULT_EXPIRY_TIME`]: lightning_invoice::DEFAULT_EXPIRY_TIME + pub fn expiry_time_seconds(&self) -> u64 { + self.inner.expiry_time().as_secs() + } + + /// Returns the `Bolt11Invoice`'s timestamp as seconds since the Unix epoch + pub fn seconds_since_epoch(&self) -> u64 { + self.inner.duration_since_epoch().as_secs() + } + + /// Returns the seconds remaining until the invoice expires. + pub fn seconds_until_expiry(&self) -> u64 { + self.inner.duration_until_expiry().as_secs() + } + + /// Returns whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// Returns whether the expiry time would pass at the given point in time. + /// `at_time_seconds` is the timestamp as seconds since the Unix epoch. + pub fn would_expire(&self, at_time_seconds: u64) -> bool { + self.inner.would_expire(Duration::from_secs(at_time_seconds)) + } + + /// Return the description or a hash of it for longer ones + pub fn description(&self) -> Bolt11InvoiceDescription { + self.inner.description().into() + } + + /// Returns the invoice's `min_final_cltv_expiry_delta` time, if present, otherwise + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]. + /// + /// [`DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA`]: lightning_invoice::DEFAULT_MIN_FINAL_CLTV_EXPIRY_DELTA + pub fn min_final_cltv_expiry_delta(&self) -> u64 { + self.inner.min_final_cltv_expiry_delta() + } + + /// Returns the network for which the invoice was issued + pub fn network(&self) -> Network { + self.inner.network() + } + + /// Returns the currency for which the invoice was issued + pub fn currency(&self) -> Currency { + self.inner.currency().into() + } + + /// Returns a list of all fallback addresses as [`Address`]es + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallback_addresses() + } + + /// Returns a list of all routes included in the invoice as the underlying hints + pub fn route_hints(&self) -> Vec> { + self.inner + .route_hints() + .iter() + .map(|route| route.0.iter().map(|hop| RouteHintHop::from(hop.clone())).collect()) + .collect() + } + + /// Recover the payee's public key (only to be used if none was included in the invoice) + pub fn recover_payee_pub_key(&self) -> PublicKey { + self.inner.recover_payee_pub_key() + } +} + +impl std::str::FromStr for Bolt11Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + match invoice_str.parse::() { + Ok(signed) => match LdkBolt11Invoice::from_signed(signed) { + Ok(invoice) => Ok(Bolt11Invoice { inner: invoice }), + Err(_) => Err(Error::InvalidInvoice), + }, + Err(_) => Err(Error::InvalidInvoice), + } + } +} + +impl From for Bolt11Invoice { + fn from(invoice: LdkBolt11Invoice) -> Self { + Bolt11Invoice { inner: invoice } + } +} + +impl From for LdkBolt11Invoice { + fn from(wrapper: Bolt11Invoice) -> Self { + wrapper.into_inner() + } +} + +impl std::fmt::Display for Bolt11Invoice { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) + } +} + +/// A Lightning payment using BOLT 11. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct Bolt11PaymentInfo { + /// Indicates the current state of the payment. + pub state: PaymentState, + /// The datetime when the payment option expires. + pub expires_at: chrono::DateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// A BOLT11 invoice the client can pay to have to channel opened. + pub invoice: Arc, +} + +impl From for Bolt11PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo) -> Self { + Self { + state: info.state, + expires_at: info.expires_at, + fee_total_sat: info.fee_total_sat, + order_total_sat: info.order_total_sat, + invoice: Arc::new(info.invoice.into()), + } + } +} + impl UniffiCustomTypeConverter for OrderId { type Builtin = String; @@ -432,6 +659,14 @@ impl UniffiCustomTypeConverter for DateTime { #[cfg(test)] mod tests { use super::*; + + fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { + let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; + let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap(); + let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone()); + (ldk_invoice, wrapped_invoice) + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); @@ -441,4 +676,104 @@ mod tests { let reconverted_description: Bolt11InvoiceDescription = converted_description.into(); assert_eq!(description, reconverted_description); } + + #[test] + fn test_bolt11_invoice_basic_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().to_string(), + wrapped_invoice.payment_hash().to_string() + ); + assert_eq!(ldk_invoice.amount_milli_satoshis(), wrapped_invoice.amount_milli_satoshis()); + + assert_eq!( + ldk_invoice.min_final_cltv_expiry_delta(), + wrapped_invoice.min_final_cltv_expiry_delta() + ); + assert_eq!( + ldk_invoice.payment_secret().0.to_vec(), + wrapped_invoice.payment_secret().0.to_vec() + ); + + assert_eq!(ldk_invoice.network(), wrapped_invoice.network()); + assert_eq!( + format!("{:?}", ldk_invoice.currency()), + format!("{:?}", wrapped_invoice.currency()) + ); + } + + #[test] + fn test_bolt11_invoice_time_related_fields() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds()); + assert_eq!( + ldk_invoice.duration_until_expiry().as_secs(), + wrapped_invoice.seconds_until_expiry() + ); + assert_eq!( + ldk_invoice.duration_since_epoch().as_secs(), + wrapped_invoice.seconds_since_epoch() + ); + + let future_time = Duration::from_secs(wrapped_invoice.seconds_since_epoch() + 10000); + assert!(!ldk_invoice.would_expire(future_time)); + assert!(!wrapped_invoice.would_expire(future_time.as_secs())); + } + + #[test] + fn test_bolt11_invoice_description() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let ldk_description = ldk_invoice.description(); + let wrapped_description = wrapped_invoice.description(); + + match (ldk_description, &wrapped_description) { + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Direct(ldk_description), + Bolt11InvoiceDescription::Direct { description }, + ) => { + assert_eq!(ldk_description.to_string(), *description) + }, + ( + lightning_invoice::Bolt11InvoiceDescriptionRef::Hash(ldk_hash), + Bolt11InvoiceDescription::Hash { hash }, + ) => { + assert_eq!(hex_utils::to_string(ldk_hash.0.as_ref()), *hash) + }, + _ => panic!("Description types don't match"), + } + } + + #[test] + fn test_bolt11_invoice_route_hints() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let wrapped_route_hints = wrapped_invoice.route_hints(); + let ldk_route_hints = ldk_invoice.route_hints(); + assert_eq!(ldk_route_hints.len(), wrapped_route_hints.len()); + + let ldk_hop = &ldk_route_hints[0].0[0]; + let wrapped_hop = &wrapped_route_hints[0][0]; + assert_eq!(ldk_hop.src_node_id, wrapped_hop.src_node_id); + assert_eq!(ldk_hop.short_channel_id, wrapped_hop.short_channel_id); + assert_eq!(ldk_hop.cltv_expiry_delta, wrapped_hop.cltv_expiry_delta); + assert_eq!(ldk_hop.htlc_minimum_msat, wrapped_hop.htlc_minimum_msat); + assert_eq!(ldk_hop.htlc_maximum_msat, wrapped_hop.htlc_maximum_msat); + assert_eq!(ldk_hop.fees.base_msat, wrapped_hop.fees.base_msat); + assert_eq!(ldk_hop.fees.proportional_millionths, wrapped_hop.fees.proportional_millionths); + } + + #[test] + fn test_bolt11_invoice_roundtrip() { + let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + + let invoice_str = wrapped_invoice.to_string(); + let parsed_invoice: LdkBolt11Invoice = invoice_str.parse().unwrap(); + assert_eq!( + ldk_invoice.payment_hash().to_byte_array().to_vec(), + parsed_invoice.payment_hash().to_byte_array().to_vec() + ); + } } From 94b32c978ca512b7d06906f93db2cfeee9f96192 Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Sat, 5 Apr 2025 00:41:09 +0100 Subject: [PATCH 058/177] Add onchain address validation for network-specific addresses Validate onchain addresses match their network type before processing transactions. This prevents sending funds to invalid addresses by: - Checking address network match expected network - Providing clear error messages for mismatches The validation is now integrated into Wallet::send_to_address flow --- src/builder.rs | 1 + src/wallet/mod.rs | 23 +++++++++++++++++++---- tests/integration_tests_rust.rs | 18 +++++++++++++++++- 3 files changed, 37 insertions(+), 5 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 224cc9fa75..d48b71c7c4 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1029,6 +1029,7 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&fee_estimator), Arc::clone(&payment_store), + Arc::clone(&config), Arc::clone(&logger), )); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 53de4b8d54..c4ef157311 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -7,6 +7,7 @@ use persist::KVStoreWalletPersister; +use crate::config::Config; use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; @@ -34,6 +35,7 @@ use lightning_invoice::RawBolt11Invoice; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update}; +use bitcoin::address::NetworkUnchecked; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; use bitcoin::hashes::Hash; @@ -43,11 +45,12 @@ use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; use bitcoin::{ - Amount, FeeRate, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, - WitnessVersion, + Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, + WitnessProgram, WitnessVersion, }; use std::ops::Deref; +use std::str::FromStr; use std::sync::{Arc, Mutex}; pub(crate) enum OnchainSendAmount { @@ -71,6 +74,7 @@ where broadcaster: B, fee_estimator: E, payment_store: Arc>>, + config: Arc, logger: L, } @@ -83,11 +87,11 @@ where pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, - payment_store: Arc>>, logger: L, + payment_store: Arc>>, config: Arc, logger: L, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); - Self { inner, persister, broadcaster, fee_estimator, payment_store, logger } + Self { inner, persister, broadcaster, fee_estimator, payment_store, config, logger } } pub(crate) fn get_full_scan_request(&self) -> FullScanRequest { @@ -327,10 +331,21 @@ where self.get_balances(total_anchor_channels_reserve_sats).map(|(_, s)| s) } + fn parse_and_validate_address( + &self, network: Network, address: &Address, + ) -> Result { + Address::::from_str(address.to_string().as_str()) + .map_err(|_| Error::InvalidAddress)? + .require_network(network) + .map_err(|_| Error::InvalidAddress) + } + pub(crate) fn send_to_address( &self, address: &bitcoin::Address, send_amount: OnchainSendAmount, fee_rate: Option, ) -> Result { + self.parse_and_validate_address(self.config.network, &address)?; + // Use the set fee_rate or default to fee estimation. let confirmation_target = ConfirmationTarget::OnchainPayment; let fee_rate = diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f2dfa4b5ec..ded88d35c3 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -30,11 +30,13 @@ use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::Hash; +use bitcoin::Address; use bitcoin::Amount; - use log::LevelFilter; +use std::str::FromStr; use std::sync::Arc; #[test] @@ -302,6 +304,10 @@ fn onchain_send_receive() { let addr_a = node_a.onchain_payment().new_address().unwrap(); let addr_b = node_b.onchain_payment().new_address().unwrap(); + // This is a Bitcoin Testnet address. Sending funds to this address from the Regtest network will fail + let static_address = "tb1q0d40e5rta4fty63z64gztf8c3v20cvet6v2jdh"; + let unchecked_address = Address::::from_str(static_address).unwrap(); + let addr_c = unchecked_address.assume_checked(); let premine_amount_sat = 1_100_000; premine_and_distribute_funds( @@ -366,6 +372,16 @@ fn onchain_send_receive() { node_a.onchain_payment().send_to_address(&addr_b, expected_node_a_balance + 1, None) ); + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_to_address(&addr_c, expected_node_a_balance + 1, None) + ); + + assert_eq!( + Err(NodeError::InvalidAddress), + node_a.onchain_payment().send_all_to_address(&addr_c, true, None) + ); + let amount_to_send_sats = 54321; let txid = node_b.onchain_payment().send_to_address(&addr_a, amount_to_send_sats, None).unwrap(); From 705d55f7ba20b6998bb39dc9b05ab8d8e785e71e Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 16 May 2025 15:12:13 +0200 Subject: [PATCH 059/177] Don't fail `send_all` retaining reserves for 0 channels Previouly, `OnchainPayment::send_all_to_address` would fail in the `retain_reserves` mode if the maintained reserves were below the dust limit. Most notably this would happen if we had no channels open at all. Here, we fix this by simply falling back to the draining case (not considering reserves) if the anchor reserves are below dust. We also add a unit test that would have caught this regression in the first place. --- src/wallet/mod.rs | 8 +++- tests/integration_tests_rust.rs | 83 +++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+), 2 deletions(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c4ef157311..c104a23465 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -355,6 +355,7 @@ where let mut locked_wallet = self.inner.lock().unwrap(); // Prepare the tx_builder. We properly check the reserve requirements (again) further down. + const DUST_LIMIT_SATS: u64 = 546; let tx_builder = match send_amount { OnchainSendAmount::ExactRetainingReserve { amount_sats, .. } => { let mut tx_builder = locked_wallet.build_tx(); @@ -362,7 +363,9 @@ where tx_builder.add_recipient(address.script_pubkey(), amount).fee_rate(fee_rate); tx_builder }, - OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } => { + OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats } + if cur_anchor_reserve_sats > DUST_LIMIT_SATS => + { let change_address_info = locked_wallet.peek_address(KeychainKind::Internal, 0); let balance = locked_wallet.balance(); let spendable_amount_sats = self @@ -419,7 +422,8 @@ where .fee_absolute(estimated_tx_fee); tx_builder }, - OnchainSendAmount::AllDrainingReserve => { + OnchainSendAmount::AllDrainingReserve + | OnchainSendAmount::AllRetainingReserve { cur_anchor_reserve_sats: _ } => { let mut tx_builder = locked_wallet.build_tx(); tx_builder.drain_wallet().drain_to(address.script_pubkey()).fee_rate(fee_rate); tx_builder diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index ded88d35c3..fde566202c 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -496,6 +496,89 @@ fn onchain_send_receive() { assert_eq!(node_b_payments.len(), 5); } +#[test] +fn onchain_send_all_retains_reserve() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + // Setup nodes + let addr_a = node_a.onchain_payment().new_address().unwrap(); + let addr_b = node_b.onchain_payment().new_address().unwrap(); + + let premine_amount_sat = 1_000_000; + let reserve_amount_sat = 25_000; + let onchain_fee_buffer_sat = 1000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![addr_a.clone(), addr_b.clone()], + Amount::from_sat(premine_amount_sat), + ); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, premine_amount_sat); + + // Send all over, with 0 reserve as we don't have any channels open. + let txid = node_a.onchain_payment().send_all_to_address(&addr_b, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat * 2 - onchain_fee_buffer_sat)..=(premine_amount_sat * 2)) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Refill to make sure we have enough reserve for the channel open. + let txid = bitcoind + .client + .send_to_address(&addr_a, Amount::from_sat(reserve_amount_sat)) + .unwrap() + .0 + .parse() + .unwrap(); + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, reserve_amount_sat); + + // Open a channel. + open_channel(&node_b, &node_a, premine_amount_sat, false, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + // Check node a sent all and node b received it + assert_eq!(node_a.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_b.list_balances().spendable_onchain_balance_sats)); + + // Send all over again, this time ensuring the reserve is accounted for + let txid = node_b.onchain_payment().send_all_to_address(&addr_a, true, None).unwrap(); + + wait_for_tx(&electrsd.client, txid); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + + // Check node b sent all and node a received it + assert_eq!(node_b.list_balances().total_onchain_balance_sats, reserve_amount_sat); + assert_eq!(node_b.list_balances().spendable_onchain_balance_sats, 0); + assert!(((premine_amount_sat - reserve_amount_sat - onchain_fee_buffer_sat) + ..=premine_amount_sat) + .contains(&node_a.list_balances().spendable_onchain_balance_sats)); +} + #[test] fn onchain_wallet_recovery() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From a8aab2bdf6df73a403838c3420f62715f612d14d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 16 May 2025 15:17:39 +0200 Subject: [PATCH 060/177] Log how much we got when rejecting Anchor channels due to reserves .. as this is useful information. --- src/event.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/event.rs b/src/event.rs index 00d8441e55..28923675e5 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1123,8 +1123,10 @@ where if spendable_amount_sats < required_amount_sats { log_error!( self.logger, - "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves.", + "Rejecting inbound Anchor channel from peer {} due to insufficient available on-chain reserves. Available: {}/{}sats", counterparty_node_id, + spendable_amount_sats, + required_amount_sats, ); self.channel_manager .force_close_without_broadcasting_txn( From 93622f7468e6deb89978afca210b64505efb761b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 20 May 2025 15:26:24 +0200 Subject: [PATCH 061/177] Add generic `DataStore` We increasingly feature different data stores that essentially do the same thing, mod different data types. Here we add a generalized `DataStore` that will be used to DRY up our logic. --- src/data_store.rs | 287 ++++++++++++++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + 2 files changed, 288 insertions(+) create mode 100644 src/data_store.rs diff --git a/src/data_store.rs b/src/data_store.rs new file mode 100644 index 0000000000..78e3e78701 --- /dev/null +++ b/src/data_store.rs @@ -0,0 +1,287 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use crate::logger::{log_error, LdkLogger}; +use crate::types::DynStore; +use crate::Error; + +use lightning::util::ser::{Readable, Writeable}; + +use std::collections::hash_map; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; + +pub(crate) trait StorableObject: Clone + Readable + Writeable { + type Id: StorableObjectId; + type Update: StorableObjectUpdate; + + fn id(&self) -> Self::Id; + fn update(&mut self, update: &Self::Update) -> bool; + fn to_update(&self) -> Self::Update; +} + +pub(crate) trait StorableObjectId: std::hash::Hash + PartialEq + Eq { + fn encode_to_hex_str(&self) -> String; +} + +pub(crate) trait StorableObjectUpdate { + fn id(&self) -> SO::Id; +} + +#[derive(PartialEq, Eq, Debug, Clone, Copy)] +pub(crate) enum DataStoreUpdateResult { + Updated, + Unchanged, + NotFound, +} + +pub(crate) struct DataStore +where + L::Target: LdkLogger, +{ + objects: Mutex>, + primary_namespace: String, + secondary_namespace: String, + kv_store: Arc, + logger: L, +} + +impl DataStore +where + L::Target: LdkLogger, +{ + pub(crate) fn new( + objects: Vec, primary_namespace: String, secondary_namespace: String, + kv_store: Arc, logger: L, + ) -> Self { + let objects = + Mutex::new(HashMap::from_iter(objects.into_iter().map(|obj| (obj.id(), obj)))); + Self { objects, primary_namespace, secondary_namespace, kv_store, logger } + } + + pub(crate) fn insert(&self, object: SO) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + self.persist(&object)?; + let updated = locked_objects.insert(object.id(), object).is_some(); + Ok(updated) + } + + pub(crate) fn insert_or_update(&self, object: SO) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + let updated; + match locked_objects.entry(object.id()) { + hash_map::Entry::Occupied(mut e) => { + let update = object.to_update(); + updated = e.get_mut().update(&update); + if updated { + self.persist(&e.get())?; + } + }, + hash_map::Entry::Vacant(e) => { + e.insert(object.clone()); + self.persist(&object)?; + updated = true; + }, + } + + Ok(updated) + } + + pub(crate) fn remove(&self, id: &SO::Id) -> Result<(), Error> { + let removed = self.objects.lock().unwrap().remove(id).is_some(); + if removed { + let store_key = id.encode_to_hex_str(); + self.kv_store + .remove(&self.primary_namespace, &self.secondary_namespace, &store_key, false) + .map_err(|e| { + log_error!( + self.logger, + "Removing object data for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; + } + Ok(()) + } + + pub(crate) fn get(&self, id: &SO::Id) -> Option { + self.objects.lock().unwrap().get(id).cloned() + } + + pub(crate) fn update(&self, update: &SO::Update) -> Result { + let mut locked_objects = self.objects.lock().unwrap(); + + if let Some(object) = locked_objects.get_mut(&update.id()) { + let updated = object.update(update); + if updated { + self.persist(&object)?; + Ok(DataStoreUpdateResult::Updated) + } else { + Ok(DataStoreUpdateResult::Unchanged) + } + } else { + Ok(DataStoreUpdateResult::NotFound) + } + } + + pub(crate) fn list_filter bool>(&self, f: F) -> Vec { + self.objects.lock().unwrap().values().filter(f).cloned().collect::>() + } + + fn persist(&self, object: &SO) -> Result<(), Error> { + let store_key = object.id().encode_to_hex_str(); + let data = object.encode(); + self.kv_store + .write(&self.primary_namespace, &self.secondary_namespace, &store_key, &data) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use lightning::impl_writeable_tlv_based; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use crate::hex_utils; + + use super::*; + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + struct TestObjectId { + id: [u8; 4], + } + + impl StorableObjectId for TestObjectId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.id) + } + } + impl_writeable_tlv_based!(TestObjectId, { (0, id, required) }); + + struct TestObjectUpdate { + id: TestObjectId, + data: [u8; 3], + } + impl StorableObjectUpdate for TestObjectUpdate { + fn id(&self) -> TestObjectId { + self.id + } + } + + #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] + struct TestObject { + id: TestObjectId, + data: [u8; 3], + } + + impl StorableObject for TestObject { + type Id = TestObjectId; + type Update = TestObjectUpdate; + + fn id(&self) -> Self::Id { + self.id + } + + fn update(&mut self, update: &Self::Update) -> bool { + if self.data != update.data { + self.data = update.data; + true + } else { + false + } + } + + fn to_update(&self) -> Self::Update { + Self::Update { id: self.id, data: self.data } + } + } + + impl_writeable_tlv_based!(TestObject, { + (0, id, required), + (2, data, required), + }); + + #[test] + fn data_is_persisted() { + let store: Arc = Arc::new(TestStore::new(false)); + let logger = Arc::new(TestLogger::new()); + let primary_namespace = "datastore_test_primary".to_string(); + let secondary_namespace = "datastore_test_secondary".to_string(); + let data_store: DataStore> = DataStore::new( + Vec::new(), + primary_namespace.clone(), + secondary_namespace.clone(), + Arc::clone(&store), + logger, + ); + + let id = TestObjectId { id: [42u8; 4] }; + assert!(data_store.get(&id).is_none()); + + let store_key = id.encode_to_hex_str(); + + // Check we start empty. + assert!(store.read(&primary_namespace, &secondary_namespace, &store_key).is_err()); + + // Check we successfully store an object and return `false` + let object = TestObject { id, data: [23u8; 3] }; + assert_eq!(Ok(false), data_store.insert(object.clone())); + assert_eq!(Some(object), data_store.get(&id)); + assert!(store.read(&primary_namespace, &secondary_namespace, &store_key).is_ok()); + + // Test re-insertion returns `true` + let mut override_object = object.clone(); + override_object.data = [24u8; 3]; + assert_eq!(Ok(true), data_store.insert(override_object)); + assert_eq!(Some(override_object), data_store.get(&id)); + + // Check update returns `Updated` + let update = TestObjectUpdate { id, data: [25u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::Updated), data_store.update(&update)); + assert_eq!(data_store.get(&id).unwrap().data, [25u8; 3]); + + // Check no-op update yields `Unchanged` + let update = TestObjectUpdate { id, data: [25u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::Unchanged), data_store.update(&update)); + + // Check bogus update yields `NotFound` + let bogus_id = TestObjectId { id: [84u8; 4] }; + let update = TestObjectUpdate { id: bogus_id, data: [12u8; 3] }; + assert_eq!(Ok(DataStoreUpdateResult::NotFound), data_store.update(&update)); + + // Check `insert_or_update` inserts unknown objects + let iou_id = TestObjectId { id: [55u8; 4] }; + let iou_object = TestObject { id: iou_id, data: [34u8; 3] }; + assert_eq!(Ok(true), data_store.insert_or_update(iou_object.clone())); + + // Check `insert_or_update` doesn't update the same object + assert_eq!(Ok(false), data_store.insert_or_update(iou_object.clone())); + + // Check `insert_or_update` updates if object changed + let mut new_iou_object = iou_object; + new_iou_object.data[0] += 1; + assert_eq!(Ok(true), data_store.insert_or_update(new_iou_object)); + } +} diff --git a/src/lib.rs b/src/lib.rs index 7859a092ed..7334eacd58 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -80,6 +80,7 @@ mod builder; mod chain; pub mod config; mod connection; +mod data_store; mod error; mod event; mod fee_estimator; From 75aa06963ba1c952c51a6de932cc7e5111768afa Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 20 May 2025 16:32:07 +0200 Subject: [PATCH 062/177] Refactor `PaymentStore` to use `DataStore` .. we utilize the just-introduced generalized `DataStore` for our `PaymentStore`. --- src/builder.rs | 18 +- src/event.rs | 13 +- src/lib.rs | 5 +- src/payment/bolt11.rs | 13 +- src/payment/bolt12.rs | 10 +- src/payment/spontaneous.rs | 10 +- src/payment/store.rs | 428 +++++++++++-------------------------- src/types.rs | 4 + src/wallet/mod.rs | 11 +- 9 files changed, 165 insertions(+), 347 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index d48b71c7c4..31a0fee456 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -18,21 +18,23 @@ use crate::gossip::GossipSource; use crate::io::sqlite_store::SqliteStore; use crate::io::utils::{read_node_metrics, write_node_metrics}; use crate::io::vss_store::VssStore; +use crate::io::{ + self, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, +}; use crate::liquidity::{ LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, }; use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::store::PaymentStore; use crate::peer_store::PeerStore; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, - OnionMessenger, PeerManager, + OnionMessenger, PaymentStore, PeerManager, }; use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; -use crate::{io, Node, NodeMetrics}; +use crate::{Node, NodeMetrics}; use lightning::chain::{chainmonitor, BestBlock, Watch}; use lightning::io::Cursor; @@ -1015,9 +1017,13 @@ fn build_with_store_internal( let fee_estimator = Arc::new(OnchainFeeEstimator::new()); let payment_store = match io::utils::read_payments(Arc::clone(&kv_store), Arc::clone(&logger)) { - Ok(payments) => { - Arc::new(PaymentStore::new(payments, Arc::clone(&kv_store), Arc::clone(&logger))) - }, + Ok(payments) => Arc::new(PaymentStore::new( + payments, + PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE.to_string(), + PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE.to_string(), + Arc::clone(&kv_store), + Arc::clone(&logger), + )), Err(_) => { return Err(BuildError::ReadFailed); }, diff --git a/src/event.rs b/src/event.rs index 00d8441e55..e95983710a 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{CustomTlvRecord, DynStore, Sweeper, Wallet}; +use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet}; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, @@ -14,13 +14,13 @@ use crate::{ use crate::config::{may_announce_channel, Config}; use crate::connection::ConnectionManager; +use crate::data_store::DataStoreUpdateResult; use crate::fee_estimator::ConfirmationTarget; use crate::liquidity::LiquiditySource; use crate::logger::Logger; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, - PaymentStore, PaymentStoreUpdateResult, }; use crate::io::{ @@ -449,7 +449,7 @@ where output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc>, + payment_store: Arc, peer_store: Arc>, runtime: Arc>>>, logger: L, @@ -466,7 +466,7 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc>, peer_store: Arc>, + payment_store: Arc, peer_store: Arc>, runtime: Arc>>>, logger: L, config: Arc, ) -> Self { Self { @@ -906,12 +906,11 @@ where }; match self.payment_store.update(&update) { - Ok(PaymentStoreUpdateResult::Updated) - | Ok(PaymentStoreUpdateResult::Unchanged) => ( + Ok(DataStoreUpdateResult::Updated) | Ok(DataStoreUpdateResult::Unchanged) => ( // No need to do anything if the idempotent update was applied, which might // be the result of a replayed event. ), - Ok(PaymentStoreUpdateResult::NotFound) => { + Ok(DataStoreUpdateResult::NotFound) => { log_error!( self.logger, "Claimed payment with ID {} couldn't be found in store", diff --git a/src/lib.rs b/src/lib.rs index 7334eacd58..c3bfe16d8f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,6 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; -use payment::store::PaymentStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, UnifiedQrPayment, @@ -144,7 +143,7 @@ use payment::{ use peer_store::{PeerInfo, PeerStore}; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - KeysManager, OnionMessenger, PeerManager, Router, Scorer, Sweeper, Wallet, + KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; @@ -200,7 +199,7 @@ pub struct Node { _router: Arc, scorer: Arc>, peer_store: Arc>>, - payment_store: Arc>>, + payment_store: Arc, is_listening: Arc, node_metrics: Arc>, } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 22e12681f5..0525718182 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -11,16 +11,17 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::connection::ConnectionManager; +use crate::data_store::DataStoreUpdateResult; use crate::error::Error; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, - PaymentStatus, PaymentStore, PaymentStoreUpdateResult, + PaymentStatus, }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; -use crate::types::ChannelManager; +use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::bolt11_payment; use lightning::ln::channelmanager::{ @@ -90,7 +91,7 @@ pub struct Bolt11Payment { channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, - payment_store: Arc>>, + payment_store: Arc, peer_store: Arc>>, config: Arc, logger: Arc, @@ -102,7 +103,7 @@ impl Bolt11Payment { channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, - payment_store: Arc>>, peer_store: Arc>>, + payment_store: Arc, peer_store: Arc>>, config: Arc, logger: Arc, ) -> Self { Self { @@ -434,8 +435,8 @@ impl Bolt11Payment { }; match self.payment_store.update(&update) { - Ok(PaymentStoreUpdateResult::Updated) | Ok(PaymentStoreUpdateResult::Unchanged) => (), - Ok(PaymentStoreUpdateResult::NotFound) => { + Ok(DataStoreUpdateResult::Updated) | Ok(DataStoreUpdateResult::Unchanged) => (), + Ok(DataStoreUpdateResult::NotFound) => { log_error!( self.logger, "Failed to manually fail unknown payment with hash {}", diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index dbeee0ab81..8006f4bb94 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -12,10 +12,8 @@ use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; use crate::error::Error; use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{ - PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, -}; -use crate::types::ChannelManager; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::invoice::Bolt12Invoice; @@ -39,14 +37,14 @@ use std::time::{Duration, SystemTime, UNIX_EPOCH}; pub struct Bolt12Payment { runtime: Arc>>>, channel_manager: Arc, - payment_store: Arc>>, + payment_store: Arc, logger: Arc, } impl Bolt12Payment { pub(crate) fn new( runtime: Arc>>>, - channel_manager: Arc, payment_store: Arc>>, + channel_manager: Arc, payment_store: Arc, logger: Arc, ) -> Self { Self { runtime, channel_manager, payment_store, logger } diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index f33ea15cca..1508b6cd84 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -10,11 +10,9 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{ - PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, PaymentStore, -}; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; use crate::payment::SendingParameters; -use crate::types::{ChannelManager, CustomTlvRecord, KeysManager}; +use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; use lightning::routing::router::{PaymentParameters, RouteParameters}; @@ -38,7 +36,7 @@ pub struct SpontaneousPayment { runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, - payment_store: Arc>>, + payment_store: Arc, config: Arc, logger: Arc, } @@ -47,7 +45,7 @@ impl SpontaneousPayment { pub(crate) fn new( runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, - payment_store: Arc>>, config: Arc, logger: Arc, + payment_store: Arc, config: Arc, logger: Arc, ) -> Self { Self { runtime, channel_manager, keys_manager, payment_store, config, logger } } diff --git a/src/payment/store.rs b/src/payment/store.rs index 2a074031c4..75b2b1b2aa 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -5,14 +5,6 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::hex_utils; -use crate::io::{ - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_error, LdkLogger}; -use crate::types::DynStore; -use crate::Error; - use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; @@ -27,12 +19,11 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use bitcoin::{BlockHash, Txid}; -use std::collections::hash_map; -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; +use crate::hex_utils; + /// Represents a payment. #[derive(Clone, Debug, PartialEq, Eq)] pub struct PaymentDetails { @@ -70,8 +61,118 @@ impl PaymentDetails { .as_secs(); Self { id, kind, amount_msat, fee_paid_msat, direction, status, latest_update_timestamp } } +} + +impl Writeable for PaymentDetails { + fn write( + &self, writer: &mut W, + ) -> Result<(), lightning::io::Error> { + write_tlv_fields!(writer, { + (0, self.id, required), // Used to be `hash` for v0.2.1 and prior + // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. + // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 + (2, None::>, required), + (3, self.kind, required), + // 4 used to be `secret` before it was moved to `kind` in v0.3.0 + (4, None::>, required), + (5, self.latest_update_timestamp, required), + (6, self.amount_msat, required), + (7, self.fee_paid_msat, option), + (8, self.direction, required), + (10, self.status, required) + }); + Ok(()) + } +} + +impl Readable for PaymentDetails { + fn read(reader: &mut R) -> Result { + let unix_time_secs = SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or(Duration::from_secs(0)) + .as_secs(); + _init_and_read_len_prefixed_tlv_fields!(reader, { + (0, id, required), // Used to be `hash` + (1, lsp_fee_limits, option), + (2, preimage, required), + (3, kind_opt, option), + (4, secret, required), + (5, latest_update_timestamp, (default_value, unix_time_secs)), + (6, amount_msat, required), + (7, fee_paid_msat, option), + (8, direction, required), + (10, status, required) + }); - pub(crate) fn update(&mut self, update: &PaymentDetailsUpdate) -> bool { + let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; + let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; + let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; + let latest_update_timestamp: u64 = + latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?; + let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; + let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; + let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; + + let kind = if let Some(kind) = kind_opt { + // If we serialized the payment kind, use it. + // This will always be the case for any version after v0.2.1. + kind + } else { + // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the + // provided fields. + + // We used to track everything by hash, but switched to track everything by id + // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above + // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in + // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the + // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always + // true. + let hash = PaymentHash(id.0); + + if secret.is_some() { + if let Some(lsp_fee_limits) = lsp_fee_limits { + let counterparty_skimmed_fee_msat = None; + PaymentKind::Bolt11Jit { + hash, + preimage, + secret, + counterparty_skimmed_fee_msat, + lsp_fee_limits, + } + } else { + PaymentKind::Bolt11 { hash, preimage, secret } + } + } else { + PaymentKind::Spontaneous { hash, preimage } + } + }; + + Ok(PaymentDetails { + id, + kind, + amount_msat, + fee_paid_msat, + direction, + status, + latest_update_timestamp, + }) + } +} + +impl StorableObjectId for PaymentId { + fn encode_to_hex_str(&self) -> String { + hex_utils::to_string(&self.0) + } +} +impl StorableObject for PaymentDetails { + type Id = PaymentId; + type Update = PaymentDetailsUpdate; + + fn id(&self) -> Self::Id { + self.id + } + + fn update(&mut self, update: &Self::Update) -> bool { debug_assert_eq!( self.id, update.id, "We should only ever override payment data for the same payment id" @@ -201,101 +302,9 @@ impl PaymentDetails { updated } -} -impl Writeable for PaymentDetails { - fn write( - &self, writer: &mut W, - ) -> Result<(), lightning::io::Error> { - write_tlv_fields!(writer, { - (0, self.id, required), // Used to be `hash` for v0.2.1 and prior - // 1 briefly used to be lsp_fee_limits, could probably be reused at some point in the future. - // 2 used to be `preimage` before it was moved to `kind` in v0.3.0 - (2, None::>, required), - (3, self.kind, required), - // 4 used to be `secret` before it was moved to `kind` in v0.3.0 - (4, None::>, required), - (5, self.latest_update_timestamp, required), - (6, self.amount_msat, required), - (7, self.fee_paid_msat, option), - (8, self.direction, required), - (10, self.status, required) - }); - Ok(()) - } -} - -impl Readable for PaymentDetails { - fn read(reader: &mut R) -> Result { - let unix_time_secs = SystemTime::now() - .duration_since(UNIX_EPOCH) - .unwrap_or(Duration::from_secs(0)) - .as_secs(); - _init_and_read_len_prefixed_tlv_fields!(reader, { - (0, id, required), // Used to be `hash` - (1, lsp_fee_limits, option), - (2, preimage, required), - (3, kind_opt, option), - (4, secret, required), - (5, latest_update_timestamp, (default_value, unix_time_secs)), - (6, amount_msat, required), - (7, fee_paid_msat, option), - (8, direction, required), - (10, status, required) - }); - - let id: PaymentId = id.0.ok_or(DecodeError::InvalidValue)?; - let preimage: Option = preimage.0.ok_or(DecodeError::InvalidValue)?; - let secret: Option = secret.0.ok_or(DecodeError::InvalidValue)?; - let latest_update_timestamp: u64 = - latest_update_timestamp.0.ok_or(DecodeError::InvalidValue)?; - let amount_msat: Option = amount_msat.0.ok_or(DecodeError::InvalidValue)?; - let direction: PaymentDirection = direction.0.ok_or(DecodeError::InvalidValue)?; - let status: PaymentStatus = status.0.ok_or(DecodeError::InvalidValue)?; - - let kind = if let Some(kind) = kind_opt { - // If we serialized the payment kind, use it. - // This will always be the case for any version after v0.2.1. - kind - } else { - // Otherwise we persisted with v0.2.1 or before, and puzzle together the kind from the - // provided fields. - - // We used to track everything by hash, but switched to track everything by id - // post-v0.2.1. As both are serialized identically, we just switched the `0`-type field above - // from `PaymentHash` to `PaymentId` and serialize a separate `PaymentHash` in - // `PaymentKind` when needed. Here, for backwards compat, we can just re-create the - // `PaymentHash` from the id, as 'back then' `payment_hash == payment_id` was always - // true. - let hash = PaymentHash(id.0); - - if secret.is_some() { - if let Some(lsp_fee_limits) = lsp_fee_limits { - let counterparty_skimmed_fee_msat = None; - PaymentKind::Bolt11Jit { - hash, - preimage, - secret, - counterparty_skimmed_fee_msat, - lsp_fee_limits, - } - } else { - PaymentKind::Bolt11 { hash, preimage, secret } - } - } else { - PaymentKind::Spontaneous { hash, preimage } - } - }; - - Ok(PaymentDetails { - id, - kind, - amount_msat, - fee_paid_msat, - direction, - status, - latest_update_timestamp, - }) + fn to_update(&self) -> Self::Update { + self.into() } } @@ -590,139 +599,9 @@ impl From<&PaymentDetails> for PaymentDetailsUpdate { } } -#[derive(PartialEq, Eq, Debug, Clone, Copy)] -pub(crate) enum PaymentStoreUpdateResult { - Updated, - Unchanged, - NotFound, -} - -pub(crate) struct PaymentStore -where - L::Target: LdkLogger, -{ - payments: Mutex>, - kv_store: Arc, - logger: L, -} - -impl PaymentStore -where - L::Target: LdkLogger, -{ - pub(crate) fn new(payments: Vec, kv_store: Arc, logger: L) -> Self { - let payments = Mutex::new(HashMap::from_iter( - payments.into_iter().map(|payment| (payment.id, payment)), - )); - Self { payments, kv_store, logger } - } - - pub(crate) fn insert(&self, payment: PaymentDetails) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - let updated = locked_payments.insert(payment.id, payment.clone()).is_some(); - self.persist_info(&payment.id, &payment)?; - Ok(updated) - } - - pub(crate) fn insert_or_update(&self, payment: &PaymentDetails) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - let updated; - match locked_payments.entry(payment.id) { - hash_map::Entry::Occupied(mut e) => { - let update = payment.into(); - updated = e.get_mut().update(&update); - if updated { - self.persist_info(&payment.id, e.get())?; - } - }, - hash_map::Entry::Vacant(e) => { - e.insert(payment.clone()); - self.persist_info(&payment.id, payment)?; - updated = true; - }, - } - - Ok(updated) - } - - pub(crate) fn remove(&self, id: &PaymentId) -> Result<(), Error> { - let removed = self.payments.lock().unwrap().remove(id).is_some(); - if removed { - let store_key = hex_utils::to_string(&id.0); - self.kv_store - .remove( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - false, - ) - .map_err(|e| { - log_error!( - self.logger, - "Removing payment data for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - })?; - } - Ok(()) - } - - pub(crate) fn get(&self, id: &PaymentId) -> Option { - self.payments.lock().unwrap().get(id).cloned() - } - - pub(crate) fn update( - &self, update: &PaymentDetailsUpdate, - ) -> Result { - let mut locked_payments = self.payments.lock().unwrap(); - - if let Some(payment) = locked_payments.get_mut(&update.id) { - let updated = payment.update(update); - if updated { - self.persist_info(&update.id, payment)?; - Ok(PaymentStoreUpdateResult::Updated) - } else { - Ok(PaymentStoreUpdateResult::Unchanged) - } - } else { - Ok(PaymentStoreUpdateResult::NotFound) - } - } - - pub(crate) fn list_filter bool>( - &self, f: F, - ) -> Vec { - self.payments.lock().unwrap().values().filter(f).cloned().collect::>() - } - - fn persist_info(&self, id: &PaymentId, payment: &PaymentDetails) -> Result<(), Error> { - let store_key = hex_utils::to_string(&id.0); - let data = payment.encode(); - self.kv_store - .write( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key, - &data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - store_key, - e - ); - Error::PersistenceFailed - })?; - Ok(()) +impl StorableObjectUpdate for PaymentDetailsUpdate { + fn id(&self) -> ::Id { + self.id } } @@ -730,11 +609,7 @@ where mod tests { use super::*; use bitcoin::io::Cursor; - use lightning::util::{ - ser::Readable, - test_utils::{TestLogger, TestStore}, - }; - use std::sync::Arc; + use lightning::util::ser::Readable; /// We refactored `PaymentDetails` to hold a payment id and moved some required fields into /// `PaymentKind`. Here, we keep the old layout available in order test de/ser compatibility. @@ -759,69 +634,6 @@ mod tests { (10, status, required) }); - #[test] - fn payment_info_is_persisted() { - let store: Arc = Arc::new(TestStore::new(false)); - let logger = Arc::new(TestLogger::new()); - let payment_store = PaymentStore::new(Vec::new(), Arc::clone(&store), logger); - - let hash = PaymentHash([42u8; 32]); - let id = PaymentId([42u8; 32]); - assert!(payment_store.get(&id).is_none()); - - let store_key = hex_utils::to_string(&hash.0); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_err()); - - let kind = PaymentKind::Bolt11 { hash, preimage: None, secret: None }; - let payment = PaymentDetails::new( - id, - kind, - None, - None, - PaymentDirection::Inbound, - PaymentStatus::Pending, - ); - - assert_eq!(Ok(false), payment_store.insert(payment.clone())); - assert!(payment_store.get(&id).is_some()); - assert!(store - .read( - PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &store_key - ) - .is_ok()); - - assert_eq!(Ok(true), payment_store.insert(payment)); - assert!(payment_store.get(&id).is_some()); - - // Check update returns `Updated` - let mut update = PaymentDetailsUpdate::new(id); - update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(PaymentStoreUpdateResult::Updated), payment_store.update(&update)); - - // Check no-op update yields `Unchanged` - let mut update = PaymentDetailsUpdate::new(id); - update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(PaymentStoreUpdateResult::Unchanged), payment_store.update(&update)); - - // Check bogus update yields `NotFound` - let bogus_id = PaymentId([84u8; 32]); - let mut update = PaymentDetailsUpdate::new(bogus_id); - update.status = Some(PaymentStatus::Succeeded); - assert_eq!(Ok(PaymentStoreUpdateResult::NotFound), payment_store.update(&update)); - - assert!(payment_store.get(&id).is_some()); - - assert_eq!(PaymentStatus::Succeeded, payment_store.get(&id).unwrap().status); - } - #[test] fn old_payment_details_deser_compat() { // We refactored `PaymentDetails` to hold a payment id and moved some required fields into diff --git a/src/types.rs b/src/types.rs index 1c9ab64b97..3103ead3f3 100644 --- a/src/types.rs +++ b/src/types.rs @@ -7,10 +7,12 @@ use crate::chain::ChainSource; use crate::config::ChannelConfig; +use crate::data_store::DataStore; use crate::fee_estimator::OnchainFeeEstimator; use crate::gossip::RuntimeSpawner; use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::PaymentDetails; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; @@ -143,6 +145,8 @@ pub(crate) type BumpTransactionEventHandler = Arc, >; +pub(crate) type PaymentStore = DataStore>; + /// A local, potentially user-provided, identifier of a channel. /// /// By default, this will be randomly generated for the user to ensure local uniqueness. diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c4ef157311..6e5a0ddea0 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -8,11 +8,12 @@ use persist::KVStoreWalletPersister; use crate::config::Config; -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; -use crate::payment::store::{ConfirmationStatus, PaymentStore}; +use crate::payment::store::ConfirmationStatus; use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; +use crate::types::PaymentStore; use crate::Error; use lightning::chain::chaininterface::BroadcasterInterface; @@ -73,7 +74,7 @@ where persister: Mutex, broadcaster: B, fee_estimator: E, - payment_store: Arc>>, + payment_store: Arc, config: Arc, logger: L, } @@ -87,7 +88,7 @@ where pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, - payment_store: Arc>>, config: Arc, logger: L, + payment_store: Arc, config: Arc, logger: L, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); @@ -218,7 +219,7 @@ where payment_status, ); - self.payment_store.insert_or_update(&payment)?; + self.payment_store.insert_or_update(payment)?; } Ok(()) From 79eebfebe6bfacecacc023ec81dbc8b298e52508 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 20 May 2025 10:51:31 +0200 Subject: [PATCH 063/177] Call `Wallet::cancel_tx` for temporary transactions We create temporary transactions for fee estimation purposes. Doing this might advance the descriptor state though. So here we call `cancel_tx` to tell the wallet we no longer intend to broadcast the temporary transactions. --- src/wallet/mod.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c104a23465..c4f37427e9 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -403,6 +403,10 @@ where ); e })?; + + // 'cancel' the transaction to free up any used change addresses + locked_wallet.cancel_tx(&tmp_tx); + let estimated_spendable_amount = Amount::from_sat( spendable_amount_sats.saturating_sub(estimated_tx_fee.to_sat()), ); From 825738ee96cdf04fa924dd7e2339e1110de5b84a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 26 May 2025 15:48:45 +0200 Subject: [PATCH 064/177] Fix `module_path`/`line` numbers in `log` facade logging Previously, we'd reuse the `log` facade macros which would always log the line number in `logger.rs`, not at the line number given at the record. Here, we rectify this by utilizing `log`'s `RecordBuilder` to craft a record and directly giving it to the configured logger. --- src/logger.rs | 43 ++++++++++++++++++++++++++++--------------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/logger.rs b/src/logger.rs index 073aa92bcc..d357f018d9 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -13,7 +13,8 @@ pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace} pub use lightning::util::logger::Level as LogLevel; use chrono::Utc; -use log::{debug, error, info, trace, warn}; +use log::Level as LogFacadeLevel; +use log::Record as LogFacadeRecord; #[cfg(not(feature = "uniffi"))] use core::fmt; @@ -139,20 +140,32 @@ impl LogWriter for Writer { .expect("Failed to write to log file") }, Writer::LogFacadeWriter => { - macro_rules! log_with_level { - ($log_level:expr, $target: expr, $($args:tt)*) => { - match $log_level { - LogLevel::Gossip | LogLevel::Trace => trace!(target: $target, $($args)*), - LogLevel::Debug => debug!(target: $target, $($args)*), - LogLevel::Info => info!(target: $target, $($args)*), - LogLevel::Warn => warn!(target: $target, $($args)*), - LogLevel::Error => error!(target: $target, $($args)*), - } - }; - } - - let target = format!("[{}:{}]", record.module_path, record.line); - log_with_level!(record.level, &target, " {}", record.args) + let mut builder = LogFacadeRecord::builder(); + + match record.level { + LogLevel::Gossip | LogLevel::Trace => builder.level(LogFacadeLevel::Trace), + LogLevel::Debug => builder.level(LogFacadeLevel::Debug), + LogLevel::Info => builder.level(LogFacadeLevel::Info), + LogLevel::Warn => builder.level(LogFacadeLevel::Warn), + LogLevel::Error => builder.level(LogFacadeLevel::Error), + }; + + #[cfg(not(feature = "uniffi"))] + log::logger().log( + &builder + .module_path(Some(record.module_path)) + .line(Some(record.line)) + .args(format_args!("{}", record.args)) + .build(), + ); + #[cfg(feature = "uniffi")] + log::logger().log( + &builder + .module_path(Some(&record.module_path)) + .line(Some(record.line)) + .args(format_args!("{}", record.args)) + .build(), + ); }, Writer::CustomWriter(custom_logger) => custom_logger.log(record), } From e396a1a63bf20183a36706db1ba43172c7c32aef Mon Sep 17 00:00:00 2001 From: Camillarhi Date: Tue, 27 May 2025 00:04:36 +0100 Subject: [PATCH 065/177] continue receive flow on offer/invoice creation failure --- src/payment/unified_qr.rs | 14 ++++++++----- tests/integration_tests_rust.rs | 37 +++++++++++++++++++++------------ 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index ec37931a09..5e6c1ef602 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -68,6 +68,10 @@ impl UnifiedQrPayment { /// can always pay using the provided on-chain address, while newer wallets will /// typically opt to use the provided BOLT11 invoice or BOLT12 offer. /// + /// The URI will always include an on-chain address. A BOLT11 invoice will be included + /// unless invoice generation fails, while a BOLT12 offer will only be included when + /// the node has suitable channels for routing. + /// /// # Parameters /// - `amount_sats`: The amount to be received, specified in satoshis. /// - `description`: A description or note associated with the payment. @@ -75,9 +79,9 @@ impl UnifiedQrPayment { /// - `expiry_sec`: The expiration time for the payment, specified in seconds. /// /// Returns a payable URI that can be used to request and receive a payment of the amount - /// given. In case of an error, the function returns `Error::WalletOperationFailed`for on-chain - /// address issues, `Error::InvoiceCreationFailed` for BOLT11 invoice issues, or - /// `Error::OfferCreationFailed` for BOLT12 offer issues. + /// given. Failure to generate the on-chain address will result in an error return + /// (`Error::WalletOperationFailed`), while failures in invoice or offer generation will + /// result in those components being omitted from the URI. /// /// The generated URI can then be given to a QR code library. /// @@ -95,7 +99,7 @@ impl UnifiedQrPayment { Ok(offer) => Some(offer), Err(e) => { log_error!(self.logger, "Failed to create offer: {}", e); - return Err(Error::OfferCreationFailed); + None }, }; @@ -111,7 +115,7 @@ impl UnifiedQrPayment { Ok(invoice) => Some(invoice), Err(e) => { log_error!(self.logger, "Failed to create invoice {}", e); - return Err(Error::InvoiceCreationFailed); + None }, }; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fde566202c..db48eca236 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1083,6 +1083,21 @@ fn generate_bip21_uri() { let address_a = node_a.onchain_payment().new_address().unwrap(); let premined_sats = 5_000_000; + let expected_amount_sats = 100_000; + let expiry_sec = 4_000; + + // Test 1: Verify URI generation (on-chain + BOLT11) works + // even before any channels are opened. This checks the graceful fallback behavior. + let initial_uqr_payment = node_b + .unified_qr_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); + println!("Initial URI (no channels): {}", initial_uqr_payment); + + assert!(initial_uqr_payment.contains("bitcoin:")); + assert!(initial_uqr_payment.contains("lightning=")); + assert!(!initial_uqr_payment.contains("lno=")); // BOLT12 requires channels + premine_and_distribute_funds( &bitcoind.client, &electrsd.client, @@ -1100,20 +1115,16 @@ fn generate_bip21_uri() { expect_channel_ready_event!(node_a, node_b.node_id()); expect_channel_ready_event!(node_b, node_a.node_id()); - let expected_amount_sats = 100_000; - let expiry_sec = 4_000; - - let uqr_payment = node_b.unified_qr_payment().receive(expected_amount_sats, "asdf", expiry_sec); + // Test 2: Verify URI generation (on-chain + BOLT11 + BOLT12) works after channels are established. + let uqr_payment = node_b + .unified_qr_payment() + .receive(expected_amount_sats, "asdf", expiry_sec) + .expect("Failed to generate URI"); - match uqr_payment.clone() { - Ok(ref uri) => { - println!("Generated URI: {}", uri); - assert!(uri.contains("bitcoin:")); - assert!(uri.contains("lightning=")); - assert!(uri.contains("lno=")); - }, - Err(e) => panic!("Failed to generate URI: {:?}", e), - } + println!("Generated URI: {}", uqr_payment); + assert!(uqr_payment.contains("bitcoin:")); + assert!(uqr_payment.contains("lightning=")); + assert!(uqr_payment.contains("lno=")); } #[test] From 00cf9d9f5f12a64df18cfa5078ceb907e8887b08 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 2 Jun 2025 15:51:43 +0200 Subject: [PATCH 066/177] Upgrade to BDK 2.0 --- Cargo.toml | 25 +++++++++++++++---------- src/chain/mod.rs | 11 +++++++++-- src/wallet/ser.rs | 15 ++++++++++++++- 3 files changed, 38 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5ce10d6adb..8c91e9b797 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -61,14 +61,14 @@ lightning-liquidity = { version = "0.1.0", features = ["std"] } #lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } -bdk_chain = { version = "0.21.1", default-features = false, features = ["std"] } -bdk_esplora = { version = "0.20.1", default-features = false, features = ["async-https-rustls", "tokio"]} -bdk_electrum = { version = "0.20.1", default-features = false, features = ["use-rustls"]} -bdk_wallet = { version = "1.0.0", default-features = false, features = ["std", "keys-bip39"]} +bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } +bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} +bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls"]} +bdk_wallet = { version = "2.0.0", default-features = false, features = ["std", "keys-bip39"]} -reqwest = { version = "0.11", default-features = false, features = ["json", "rustls-tls"] } +reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } rusqlite = { version = "0.31.0", features = ["bundled"] } -bitcoin = "0.32.2" +bitcoin = "0.32.4" bip39 = "2.0.0" bip21 = { version = "0.5", features = ["std"], default-features = false } @@ -76,8 +76,13 @@ base64 = { version = "0.22.1", default-features = false, features = ["std"] } rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } -esplora-client = { version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } -electrum-client = { version = "0.22.0", default-features = true } +esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] } + +# FIXME: This was introduced to decouple the `bdk_esplora` and +# `lightning-transaction-sync` APIs. We should drop it as part of the upgrade +# to LDK 0.2. +esplora-client_0_11 = { package = "esplora-client", version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } +electrum-client = { version = "0.23.1", default-features = true } libc = "0.2" uniffi = { version = "0.27.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } @@ -98,10 +103,10 @@ proptest = "1.0.0" regex = "1.5.6" [target.'cfg(not(no_download))'.dev-dependencies] -electrsd = { version = "0.33.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } +electrsd = { version = "0.34.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } [target.'cfg(no_download)'.dev-dependencies] -electrsd = { version = "0.33.0", default-features = false, features = ["legacy"] } +electrsd = { version = "0.34.0", default-features = false, features = ["legacy"] } corepc-node = { version = "0.7.0", default-features = false, features = ["27_2"] } [target.'cfg(cln_test)'.dev-dependencies] diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 62627797e1..915d327baa 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -237,11 +237,18 @@ impl ChainSource { kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { + // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating + // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. + let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); + client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); + let tx_sync = + Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); + let mut client_builder = esplora_client::Builder::new(&server_url); client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); let esplora_client = client_builder.build_async().unwrap(); - let tx_sync = - Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); Self::Esplora { diff --git a/src/wallet/ser.rs b/src/wallet/ser.rs index 2e33992a8a..ae1509bdf0 100644 --- a/src/wallet/ser.rs +++ b/src/wallet/ser.rs @@ -107,7 +107,9 @@ impl<'a> Writeable for ChangeSetSerWrapper<'a, BdkTxGraphChangeSet>, > = RequiredWrapper(None); let mut last_seen: RequiredWrapper> = RequiredWrapper(None); + let mut first_seen = None; + let mut last_evicted = None; decode_tlv_stream!(reader, { (0, txs, required), + (1, first_seen, option), (2, txouts, required), + (3, last_evicted, option), (4, anchors, required), (6, last_seen, required), }); @@ -142,6 +148,8 @@ impl Readable for ChangeSetDeserWrapper Writeable for ChangeSetSerWrapper<'a, BdkIndexerChangeSet> { fn write(&self, writer: &mut W) -> Result<(), lightning::io::Error> { CHANGESET_SERIALIZATION_VERSION.write(writer)?; + // Note we don't persist/use the optional spk_cache currently. encode_tlv_stream!(writer, { (0, ChangeSetSerWrapper(&self.0.last_revealed), required) }); Ok(()) } @@ -275,9 +284,13 @@ impl Readable for ChangeSetDeserWrapper { let mut last_revealed: RequiredWrapper>> = RequiredWrapper(None); + // Note we don't persist/use the optional spk_cache currently. decode_tlv_stream!(reader, { (0, last_revealed, required) }); - Ok(Self(BdkIndexerChangeSet { last_revealed: last_revealed.0.unwrap().0 })) + Ok(Self(BdkIndexerChangeSet { + last_revealed: last_revealed.0.unwrap().0, + spk_cache: Default::default(), + })) } } From e6fa232dd4506d1ef2d23b54b9d7ee87b5786afc Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Jun 2025 11:36:15 +0200 Subject: [PATCH 067/177] Retrieve and apply evicted transactions to BDK wallet With version 2.0, BDK introduced a new `Wallet::apply_evicted_txs` API that requires that we tell the `Wallet` about any mempool evictions. Here, we implement this as part of our mempool polling, which should fix a bug that left the wallet unable to spend funds if previously-canoncical mempool transactions ended up being evicted. --- src/chain/bitcoind_rpc.rs | 31 ++++++++++++++++++++++++++++++- src/chain/mod.rs | 14 ++++++++++---- src/wallet/mod.rs | 15 +++++++++++++-- 3 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/chain/bitcoind_rpc.rs b/src/chain/bitcoind_rpc.rs index e05812fa5a..3ca2c221fd 100644 --- a/src/chain/bitcoind_rpc.rs +++ b/src/chain/bitcoind_rpc.rs @@ -190,12 +190,24 @@ impl BitcoindRpcClient { Ok(()) } + /// Returns two `Vec`s: + /// - mempool transactions, alongside their first-seen unix timestamps. + /// - transactions that have been evicted from the mempool, alongside the last time they were seen absent. + pub(crate) async fn get_updated_mempool_transactions( + &self, best_processed_height: u32, unconfirmed_txids: Vec, + ) -> std::io::Result<(Vec<(Transaction, u64)>, Vec<(Txid, u64)>)> { + let mempool_txs = + self.get_mempool_transactions_and_timestamp_at_height(best_processed_height).await?; + let evicted_txids = self.get_evicted_mempool_txids_and_timestamp(unconfirmed_txids).await?; + Ok((mempool_txs, evicted_txids)) + } + /// Get mempool transactions, alongside their first-seen unix timestamps. /// /// This method is an adapted version of `bdk_bitcoind_rpc::Emitter::mempool`. It emits each /// transaction only once, unless we cannot assume the transaction's ancestors are already /// emitted. - pub(crate) async fn get_mempool_transactions_and_timestamp_at_height( + async fn get_mempool_transactions_and_timestamp_at_height( &self, best_processed_height: u32, ) -> std::io::Result> { let prev_mempool_time = self.latest_mempool_timestamp.load(Ordering::Relaxed); @@ -252,6 +264,23 @@ impl BitcoindRpcClient { } Ok(txs_to_emit) } + + // Retrieve a list of Txids that have been evicted from the mempool. + // + // To this end, we first update our local mempool_entries_cache and then return all unconfirmed + // wallet `Txid`s that don't appear in the mempool still. + async fn get_evicted_mempool_txids_and_timestamp( + &self, unconfirmed_txids: Vec, + ) -> std::io::Result> { + let latest_mempool_timestamp = self.latest_mempool_timestamp.load(Ordering::Relaxed); + let mempool_entries_cache = self.mempool_entries_cache.lock().await; + let evicted_txids = unconfirmed_txids + .into_iter() + .filter(|txid| mempool_entries_cache.contains_key(txid)) + .map(|txid| (txid, latest_mempool_timestamp)) + .collect(); + Ok(evicted_txids) + } } impl BlockSource for BitcoindRpcClient { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 915d327baa..fac8b0e6cc 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -1095,18 +1095,24 @@ impl ChainSource { let cur_height = channel_manager.current_best_block().height; let now = SystemTime::now(); + let unconfirmed_txids = onchain_wallet.get_unconfirmed_txids(); match bitcoind_rpc_client - .get_mempool_transactions_and_timestamp_at_height(cur_height) + .get_updated_mempool_transactions(cur_height, unconfirmed_txids) .await { - Ok(unconfirmed_txs) => { + Ok((unconfirmed_txs, evicted_txids)) => { log_trace!( logger, - "Finished polling mempool of size {} in {}ms", + "Finished polling mempool of size {} and {} evicted transactions in {}ms", unconfirmed_txs.len(), + evicted_txids.len(), now.elapsed().unwrap().as_millis() ); - let _ = onchain_wallet.apply_unconfirmed_txs(unconfirmed_txs); + onchain_wallet + .apply_mempool_txs(unconfirmed_txs, evicted_txids) + .unwrap_or_else(|e| { + log_error!(logger, "Failed to apply mempool transactions: {:?}", e); + }); }, Err(e) => { log_error!(logger, "Failed to poll for mempool transactions: {:?}", e); diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 65fa2e24d5..fbac1d1b6d 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -107,6 +107,16 @@ where self.inner.lock().unwrap().tx_graph().full_txs().map(|tx_node| tx_node.tx).collect() } + pub(crate) fn get_unconfirmed_txids(&self) -> Vec { + self.inner + .lock() + .unwrap() + .transactions() + .filter(|t| t.chain_position.is_unconfirmed()) + .map(|t| t.tx_node.txid) + .collect() + } + pub(crate) fn current_best_block(&self) -> BestBlock { let checkpoint = self.inner.lock().unwrap().latest_checkpoint(); BestBlock { block_hash: checkpoint.hash(), height: checkpoint.height() } @@ -136,11 +146,12 @@ where } } - pub(crate) fn apply_unconfirmed_txs( - &self, unconfirmed_txs: Vec<(Transaction, u64)>, + pub(crate) fn apply_mempool_txs( + &self, unconfirmed_txs: Vec<(Transaction, u64)>, evicted_txids: Vec<(Txid, u64)>, ) -> Result<(), Error> { let mut locked_wallet = self.inner.lock().unwrap(); locked_wallet.apply_unconfirmed_txs(unconfirmed_txs); + locked_wallet.apply_evicted_txs(evicted_txids); let mut locked_persister = self.persister.lock().unwrap(); locked_wallet.persist(&mut locked_persister).map_err(|e| { From 4abe3d86d1b1bf45ae69744fbb6b32aa925643fe Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Jun 2025 11:28:11 +0200 Subject: [PATCH 068/177] Update `CHANGELOG` for v0.6.0 --- CHANGELOG.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 931b98f287..29b6f748c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +# 0.6.0 - Jun. 9, 2025 +This sixth minor release mainly fixes an issue that could have left the +on-chain wallet unable to spend funds if transactions that had previously been +accepted to the mempool ended up being evicted. + +## Feature and API updates +- Onchain addresses are now validated against the expected network before use (#519). +- The API methods on the `Bolt11Invoice` type are now exposed in bindings (#522). +- The `UnifiedQrPayment::receive` flow no longer aborts if we're unable to generate a BOLT12 offer (#548). + +## Bug Fixes and Improvements +- Previously, the node could potentially enter a state that would have left the + onchain wallet unable spend any funds if previously-generated transactions + had been first accepted, and then evicted from the mempool. This has been + fixed in BDK 2.0.0, to which we upgrade as part of this release. (#551) +- A bug that had us fail `OnchainPayment::send_all` in the `retrain_reserves` + mode when requiring sub-dust-limit anchor reserves has been fixed (#540). +- The output of the `log` facade logger has been corrected (#547). + +## Compatibility Notes +- The BDK dependency has been bumped to `bdk_wallet` v2.0 (#551). + +In total, this release features 20 files changed, 1188 insertions, 447 deletions, in 18 commits from 3 authors in alphabetical order: + +- alexanderwiederin +- Camillarhi +- Elias Rohrer + # 0.5.0 - Apr. 29, 2025 Besides numerous API improvements and bugfixes this fifth minor release notably adds support for sourcing chain and fee rate data from an Electrum backend, requesting channels via the [bLIP-51 / LSPS1](https://github.com/lightning/blips/blob/master/blip-0051.md) protocol, as well as experimental support for operating as a [bLIP-52 / LSPS2](https://github.com/lightning/blips/blob/master/blip-0052.md) service. From 35524cb0bc388173e2b697a049d65db0ccac8c49 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Jun 2025 11:31:39 +0200 Subject: [PATCH 069/177] Bump version number post v0.6.0 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 8c91e9b797..bf8bed08c7 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "ldk-node" -version = "0.6.0+git" +version = "0.7.0+git" authors = ["Elias Rohrer "] homepage = "https://lightningdevkit.org/" license = "MIT OR Apache-2.0" From a6697b8559333cec6caa142d47f8b5b0ad5ae707 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Jun 2025 14:41:13 +0200 Subject: [PATCH 070/177] Bump version number for bindings --- Package.swift | 2 +- bindings/kotlin/ldk-node-android/gradle.properties | 2 +- bindings/kotlin/ldk-node-jvm/gradle.properties | 2 +- bindings/python/pyproject.toml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Package.swift b/Package.swift index 61da2d5e73..f6bad5e3c1 100644 --- a/Package.swift +++ b/Package.swift @@ -3,7 +3,7 @@ import PackageDescription -let tag = "v0.5.0" +let tag = "v0.6.0" let checksum = "fd9eb84a478402af8f790519a463b6e1bf6ab3987f5951cd8375afb9d39e7a4b" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" diff --git a/bindings/kotlin/ldk-node-android/gradle.properties b/bindings/kotlin/ldk-node-android/gradle.properties index 1976b71232..578c3308b3 100644 --- a/bindings/kotlin/ldk-node-android/gradle.properties +++ b/bindings/kotlin/ldk-node-android/gradle.properties @@ -2,4 +2,4 @@ org.gradle.jvmargs=-Xmx1536m android.useAndroidX=true android.enableJetifier=true kotlin.code.style=official -libraryVersion=0.5.0 +libraryVersion=0.6.0 diff --git a/bindings/kotlin/ldk-node-jvm/gradle.properties b/bindings/kotlin/ldk-node-jvm/gradle.properties index 62d6602357..913b5caeac 100644 --- a/bindings/kotlin/ldk-node-jvm/gradle.properties +++ b/bindings/kotlin/ldk-node-jvm/gradle.properties @@ -1,3 +1,3 @@ org.gradle.jvmargs=-Xmx1536m kotlin.code.style=official -libraryVersion=0.5.0 +libraryVersion=0.6.0 diff --git a/bindings/python/pyproject.toml b/bindings/python/pyproject.toml index 8af86d2bbe..496781a6a5 100644 --- a/bindings/python/pyproject.toml +++ b/bindings/python/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "ldk_node" -version = "0.5.0" +version = "0.6.0" authors = [ { name="Elias Rohrer", email="dev@tnull.de" }, ] From 415941bb696aea6d644d7ef00e73fe7f877ae3c7 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Jun 2025 14:51:29 +0200 Subject: [PATCH 071/177] Update Swift files for 0.6.0 --- Package.swift | 2 +- bindings/swift/Sources/LDKNode/LDKNode.swift | 612 ++++++++++++++++--- 2 files changed, 539 insertions(+), 75 deletions(-) diff --git a/Package.swift b/Package.swift index f6bad5e3c1..33c5a70b87 100644 --- a/Package.swift +++ b/Package.swift @@ -4,7 +4,7 @@ import PackageDescription let tag = "v0.6.0" -let checksum = "fd9eb84a478402af8f790519a463b6e1bf6ab3987f5951cd8375afb9d39e7a4b" +let checksum = "8bda396624134e0b592bfcc2f977b9aa5ce8c2ee359c032ae3520869ece8851c" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index de5df5e002..442201d31f 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -511,6 +511,252 @@ fileprivate struct FfiConverterData: FfiConverterRustBuffer { +public protocol Bolt11InvoiceProtocol : AnyObject { + + func amountMilliSatoshis() -> UInt64? + + func currency() -> Currency + + func description() -> Bolt11InvoiceDescription + + func expiryTimeSeconds() -> UInt64 + + func fallbackAddresses() -> [Address] + + func isExpired() -> Bool + + func minFinalCltvExpiryDelta() -> UInt64 + + func network() -> Network + + func paymentHash() -> PaymentHash + + func paymentSecret() -> PaymentSecret + + func recoverPayeePubKey() -> PublicKey + + func routeHints() -> [[RouteHintHop]] + + func secondsSinceEpoch() -> UInt64 + + func secondsUntilExpiry() -> UInt64 + + func signableHash() -> [UInt8] + + func wouldExpire(atTimeSeconds: UInt64) -> Bool + +} + +open class Bolt11Invoice: + Bolt11InvoiceProtocol { + fileprivate let pointer: UnsafeMutableRawPointer! + + /// Used to instantiate a [FFIObject] without an actual pointer, for fakes in tests, mostly. + public struct NoPointer { + public init() {} + } + + // TODO: We'd like this to be `private` but for Swifty reasons, + // we can't implement `FfiConverter` without making this `required` and we can't + // make it `required` without making it `public`. + required public init(unsafeFromRawPointer pointer: UnsafeMutableRawPointer) { + self.pointer = pointer + } + + /// This constructor can be used to instantiate a fake object. + /// - Parameter noPointer: Placeholder value so we can have a constructor separate from the default empty one that may be implemented for classes extending [FFIObject]. + /// + /// - Warning: + /// Any object instantiated with this constructor cannot be passed to an actual Rust-backed object. Since there isn't a backing [Pointer] the FFI lower functions will crash. + public init(noPointer: NoPointer) { + self.pointer = nil + } + + public func uniffiClonePointer() -> UnsafeMutableRawPointer { + return try! rustCall { uniffi_ldk_node_fn_clone_bolt11invoice(self.pointer, $0) } + } + // No primary constructor declared for this class. + + deinit { + guard let pointer = pointer else { + return + } + + try! rustCall { uniffi_ldk_node_fn_free_bolt11invoice(pointer, $0) } + } + + +public static func fromStr(invoiceStr: String)throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(try rustCallWithError(FfiConverterTypeNodeError.lift) { + uniffi_ldk_node_fn_constructor_bolt11invoice_from_str( + FfiConverterString.lower(invoiceStr),$0 + ) +}) +} + + + +open func amountMilliSatoshis() -> UInt64? { + return try! FfiConverterOptionUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_amount_milli_satoshis(self.uniffiClonePointer(),$0 + ) +}) +} + +open func currency() -> Currency { + return try! FfiConverterTypeCurrency.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_currency(self.uniffiClonePointer(),$0 + ) +}) +} + +open func description() -> Bolt11InvoiceDescription { + return try! FfiConverterTypeBolt11InvoiceDescription.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_description(self.uniffiClonePointer(),$0 + ) +}) +} + +open func expiryTimeSeconds() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_expiry_time_seconds(self.uniffiClonePointer(),$0 + ) +}) +} + +open func fallbackAddresses() -> [Address] { + return try! FfiConverterSequenceTypeAddress.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_fallback_addresses(self.uniffiClonePointer(),$0 + ) +}) +} + +open func isExpired() -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_is_expired(self.uniffiClonePointer(),$0 + ) +}) +} + +open func minFinalCltvExpiryDelta() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_min_final_cltv_expiry_delta(self.uniffiClonePointer(),$0 + ) +}) +} + +open func network() -> Network { + return try! FfiConverterTypeNetwork.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_network(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentHash() -> PaymentHash { + return try! FfiConverterTypePaymentHash.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func paymentSecret() -> PaymentSecret { + return try! FfiConverterTypePaymentSecret.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_payment_secret(self.uniffiClonePointer(),$0 + ) +}) +} + +open func recoverPayeePubKey() -> PublicKey { + return try! FfiConverterTypePublicKey.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_recover_payee_pub_key(self.uniffiClonePointer(),$0 + ) +}) +} + +open func routeHints() -> [[RouteHintHop]] { + return try! FfiConverterSequenceSequenceTypeRouteHintHop.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_route_hints(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsSinceEpoch() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_since_epoch(self.uniffiClonePointer(),$0 + ) +}) +} + +open func secondsUntilExpiry() -> UInt64 { + return try! FfiConverterUInt64.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_seconds_until_expiry(self.uniffiClonePointer(),$0 + ) +}) +} + +open func signableHash() -> [UInt8] { + return try! FfiConverterSequenceUInt8.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_signable_hash(self.uniffiClonePointer(),$0 + ) +}) +} + +open func wouldExpire(atTimeSeconds: UInt64) -> Bool { + return try! FfiConverterBool.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_would_expire(self.uniffiClonePointer(), + FfiConverterUInt64.lower(atTimeSeconds),$0 + ) +}) +} + + +} + +public struct FfiConverterTypeBolt11Invoice: FfiConverter { + + typealias FfiType = UnsafeMutableRawPointer + typealias SwiftType = Bolt11Invoice + + public static func lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return Bolt11Invoice(unsafeFromRawPointer: pointer) + } + + public static func lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return value.uniffiClonePointer() + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { + let v: UInt64 = try readInt(&buf) + // The Rust code won't compile if a pointer won't fit in a UInt64. + // We have to go via `UInt` because that's the thing that's the size of a pointer. + let ptr = UnsafeMutableRawPointer(bitPattern: UInt(truncatingIfNeeded: v)) + if (ptr == nil) { + throw UniffiInternalError.unexpectedNullPointer + } + return try lift(ptr!) + } + + public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { + // This fiddling is because `Int` is the thing that's the same size as a pointer. + // The Rust code won't compile if a pointer won't fit in a `UInt64`. + writeInt(&buf, UInt64(bitPattern: Int64(Int(bitPattern: lower(value))))) + } +} + + + + +public func FfiConverterTypeBolt11Invoice_lift(_ pointer: UnsafeMutableRawPointer) throws -> Bolt11Invoice { + return try FfiConverterTypeBolt11Invoice.lift(pointer) +} + +public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> UnsafeMutableRawPointer { + return FfiConverterTypeBolt11Invoice.lower(value) +} + + + + public protocol Bolt11PaymentProtocol : AnyObject { func claimForHash(paymentHash: PaymentHash, claimableAmountMsat: UInt64, preimage: PaymentPreimage) throws @@ -2990,36 +3236,6 @@ public struct Bolt11PaymentInfo { -extension Bolt11PaymentInfo: Equatable, Hashable { - public static func ==(lhs: Bolt11PaymentInfo, rhs: Bolt11PaymentInfo) -> Bool { - if lhs.state != rhs.state { - return false - } - if lhs.expiresAt != rhs.expiresAt { - return false - } - if lhs.feeTotalSat != rhs.feeTotalSat { - return false - } - if lhs.orderTotalSat != rhs.orderTotalSat { - return false - } - if lhs.invoice != rhs.invoice { - return false - } - return true - } - - public func hash(into hasher: inout Hasher) { - hasher.combine(state) - hasher.combine(expiresAt) - hasher.combine(feeTotalSat) - hasher.combine(orderTotalSat) - hasher.combine(invoice) - } -} - - public struct FfiConverterTypeBolt11PaymentInfo: FfiConverterRustBuffer { public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11PaymentInfo { return @@ -4883,6 +5099,95 @@ public func FfiConverterTypePeerDetails_lower(_ value: PeerDetails) -> RustBuffe } +public struct RouteHintHop { + public var srcNodeId: PublicKey + public var shortChannelId: UInt64 + public var cltvExpiryDelta: UInt16 + public var htlcMinimumMsat: UInt64? + public var htlcMaximumMsat: UInt64? + public var fees: RoutingFees + + // Default memberwise initializers are never public by default, so we + // declare one manually. + public init(srcNodeId: PublicKey, shortChannelId: UInt64, cltvExpiryDelta: UInt16, htlcMinimumMsat: UInt64?, htlcMaximumMsat: UInt64?, fees: RoutingFees) { + self.srcNodeId = srcNodeId + self.shortChannelId = shortChannelId + self.cltvExpiryDelta = cltvExpiryDelta + self.htlcMinimumMsat = htlcMinimumMsat + self.htlcMaximumMsat = htlcMaximumMsat + self.fees = fees + } +} + + + +extension RouteHintHop: Equatable, Hashable { + public static func ==(lhs: RouteHintHop, rhs: RouteHintHop) -> Bool { + if lhs.srcNodeId != rhs.srcNodeId { + return false + } + if lhs.shortChannelId != rhs.shortChannelId { + return false + } + if lhs.cltvExpiryDelta != rhs.cltvExpiryDelta { + return false + } + if lhs.htlcMinimumMsat != rhs.htlcMinimumMsat { + return false + } + if lhs.htlcMaximumMsat != rhs.htlcMaximumMsat { + return false + } + if lhs.fees != rhs.fees { + return false + } + return true + } + + public func hash(into hasher: inout Hasher) { + hasher.combine(srcNodeId) + hasher.combine(shortChannelId) + hasher.combine(cltvExpiryDelta) + hasher.combine(htlcMinimumMsat) + hasher.combine(htlcMaximumMsat) + hasher.combine(fees) + } +} + + +public struct FfiConverterTypeRouteHintHop: FfiConverterRustBuffer { + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> RouteHintHop { + return + try RouteHintHop( + srcNodeId: FfiConverterTypePublicKey.read(from: &buf), + shortChannelId: FfiConverterUInt64.read(from: &buf), + cltvExpiryDelta: FfiConverterUInt16.read(from: &buf), + htlcMinimumMsat: FfiConverterOptionUInt64.read(from: &buf), + htlcMaximumMsat: FfiConverterOptionUInt64.read(from: &buf), + fees: FfiConverterTypeRoutingFees.read(from: &buf) + ) + } + + public static func write(_ value: RouteHintHop, into buf: inout [UInt8]) { + FfiConverterTypePublicKey.write(value.srcNodeId, into: &buf) + FfiConverterUInt64.write(value.shortChannelId, into: &buf) + FfiConverterUInt16.write(value.cltvExpiryDelta, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMinimumMsat, into: &buf) + FfiConverterOptionUInt64.write(value.htlcMaximumMsat, into: &buf) + FfiConverterTypeRoutingFees.write(value.fees, into: &buf) + } +} + + +public func FfiConverterTypeRouteHintHop_lift(_ buf: RustBuffer) throws -> RouteHintHop { + return try FfiConverterTypeRouteHintHop.lift(buf) +} + +public func FfiConverterTypeRouteHintHop_lower(_ value: RouteHintHop) -> RustBuffer { + return FfiConverterTypeRouteHintHop.lower(value) +} + + public struct RoutingFees { public var baseMsat: UInt32 public var proportionalMillionths: UInt32 @@ -5506,6 +5811,82 @@ extension ConfirmationStatus: Equatable, Hashable {} +// Note that we don't yet support `indirect` for enums. +// See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. + +public enum Currency { + + case bitcoin + case bitcoinTestnet + case regtest + case simnet + case signet +} + + +public struct FfiConverterTypeCurrency: FfiConverterRustBuffer { + typealias SwiftType = Currency + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Currency { + let variant: Int32 = try readInt(&buf) + switch variant { + + case 1: return .bitcoin + + case 2: return .bitcoinTestnet + + case 3: return .regtest + + case 4: return .simnet + + case 5: return .signet + + default: throw UniffiInternalError.unexpectedEnumCase + } + } + + public static func write(_ value: Currency, into buf: inout [UInt8]) { + switch value { + + + case .bitcoin: + writeInt(&buf, Int32(1)) + + + case .bitcoinTestnet: + writeInt(&buf, Int32(2)) + + + case .regtest: + writeInt(&buf, Int32(3)) + + + case .simnet: + writeInt(&buf, Int32(4)) + + + case .signet: + writeInt(&buf, Int32(5)) + + } + } +} + + +public func FfiConverterTypeCurrency_lift(_ buf: RustBuffer) throws -> Currency { + return try FfiConverterTypeCurrency.lift(buf) +} + +public func FfiConverterTypeCurrency_lower(_ value: Currency) -> RustBuffer { + return FfiConverterTypeCurrency.lower(value) +} + + + +extension Currency: Equatable, Hashable {} + + + // Note that we don't yet support `indirect` for enums. // See https://github.com/mozilla/uniffi-rs/issues/396 for further discussion. @@ -8072,6 +8453,28 @@ fileprivate struct FfiConverterSequenceTypePeerDetails: FfiConverterRustBuffer { } } +fileprivate struct FfiConverterSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [RouteHintHop] + + public static func write(_ value: [RouteHintHop], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [RouteHintHop] { + let len: Int32 = try readInt(&buf) + var seq = [RouteHintHop]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeLightningBalance: FfiConverterRustBuffer { typealias SwiftType = [LightningBalance] @@ -8116,6 +8519,50 @@ fileprivate struct FfiConverterSequenceTypePendingSweepBalance: FfiConverterRust } } +fileprivate struct FfiConverterSequenceSequenceTypeRouteHintHop: FfiConverterRustBuffer { + typealias SwiftType = [[RouteHintHop]] + + public static func write(_ value: [[RouteHintHop]], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterSequenceTypeRouteHintHop.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [[RouteHintHop]] { + let len: Int32 = try readInt(&buf) + var seq = [[RouteHintHop]]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterSequenceTypeRouteHintHop.read(from: &buf)) + } + return seq + } +} + +fileprivate struct FfiConverterSequenceTypeAddress: FfiConverterRustBuffer { + typealias SwiftType = [Address] + + public static func write(_ value: [Address], into buf: inout [UInt8]) { + let len = Int32(value.count) + writeInt(&buf, len) + for item in value { + FfiConverterTypeAddress.write(item, into: &buf) + } + } + + public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> [Address] { + let len: Int32 = try readInt(&buf) + var seq = [Address]() + seq.reserveCapacity(Int(len)) + for _ in 0 ..< len { + seq.append(try FfiConverterTypeAddress.read(from: &buf)) + } + return seq + } +} + fileprivate struct FfiConverterSequenceTypeNodeId: FfiConverterRustBuffer { typealias SwiftType = [NodeId] @@ -8274,40 +8721,6 @@ public func FfiConverterTypeBlockHash_lower(_ value: BlockHash) -> RustBuffer { -/** - * Typealias from the type name used in the UDL file to the builtin type. This - * is needed because the UDL type name is used in function/method signatures. - */ -public typealias Bolt11Invoice = String -public struct FfiConverterTypeBolt11Invoice: FfiConverter { - public static func read(from buf: inout (data: Data, offset: Data.Index)) throws -> Bolt11Invoice { - return try FfiConverterString.read(from: &buf) - } - - public static func write(_ value: Bolt11Invoice, into buf: inout [UInt8]) { - return FfiConverterString.write(value, into: &buf) - } - - public static func lift(_ value: RustBuffer) throws -> Bolt11Invoice { - return try FfiConverterString.lift(value) - } - - public static func lower(_ value: Bolt11Invoice) -> RustBuffer { - return FfiConverterString.lower(value) - } -} - - -public func FfiConverterTypeBolt11Invoice_lift(_ value: RustBuffer) throws -> Bolt11Invoice { - return try FfiConverterTypeBolt11Invoice.lift(value) -} - -public func FfiConverterTypeBolt11Invoice_lower(_ value: Bolt11Invoice) -> RustBuffer { - return FfiConverterTypeBolt11Invoice.lower(value) -} - - - /** * Typealias from the type name used in the UDL file to the builtin type. This * is needed because the UDL type name is used in function/method signatures. @@ -9032,40 +9445,88 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_func_generate_entropy_mnemonic() != 59926) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_bolt11invoice_amount_milli_satoshis() != 50823) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_currency() != 32179) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_description() != 9887) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_expiry_time_seconds() != 23625) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_fallback_addresses() != 55276) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_is_expired() != 15932) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_min_final_cltv_expiry_delta() != 8855) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_network() != 10420) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_hash() != 42571) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_payment_secret() != 26081) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_recover_payee_pub_key() != 18874) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_route_hints() != 63051) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_since_epoch() != 53979) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_seconds_until_expiry() != 64193) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_signable_hash() != 30910) { + return InitializationResult.apiChecksumMismatch + } + if (uniffi_ldk_node_checksum_method_bolt11invoice_would_expire() != 30331) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_bolt11payment_claim_for_hash() != 52848) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt11payment_fail_for_hash() != 24516) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 47624) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive() != 6073) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 36395) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_for_hash() != 27050) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 38916) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount() != 4893) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 9075) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_for_hash() != 1402) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 58805) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_variable_amount_via_jit_channel() != 24506) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 30211) { + if (uniffi_ldk_node_checksum_method_bolt11payment_receive_via_jit_channel() != 16532) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 39133) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send() != 63952) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 39625) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes() != 969) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 25010) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_probes_using_amount() != 50136) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 19557) { + if (uniffi_ldk_node_checksum_method_bolt11payment_send_using_amount() != 36530) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt12payment_initiate_refund() != 38039) { @@ -9320,6 +9781,9 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_vssheaderprovider_get_headers() != 7788) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_constructor_bolt11invoice_from_str() != 349) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_constructor_builder_from_config() != 994) { return InitializationResult.apiChecksumMismatch } From 07b5471d10b71e5693fac9dcb9a183da1aaff46e Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Sun, 1 Jun 2025 06:43:02 +0200 Subject: [PATCH 072/177] Restructure FFI bindings with conversion traits for Lightning types This commit reorganizes the FFI architecture by introducing conversion traits for lightning types. Moves code from uniffi_types.rs to a dedicated ffi module for separation of concerns. --- src/ffi/mod.rs | 47 ++++++++++++++++ src/{uniffi_types.rs => ffi/types.rs} | 19 ++++--- src/lib.rs | 5 +- src/liquidity.rs | 2 +- src/payment/bolt11.rs | 80 +++++++++------------------ src/payment/unified_qr.rs | 5 +- 6 files changed, 89 insertions(+), 69 deletions(-) create mode 100644 src/ffi/mod.rs rename src/{uniffi_types.rs => ffi/types.rs} (98%) diff --git a/src/ffi/mod.rs b/src/ffi/mod.rs new file mode 100644 index 0000000000..32464d0445 --- /dev/null +++ b/src/ffi/mod.rs @@ -0,0 +1,47 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in + +#[cfg(feature = "uniffi")] +mod types; + +#[cfg(feature = "uniffi")] +pub use types::*; + +#[cfg(feature = "uniffi")] +pub fn maybe_deref(wrapped_type: &std::sync::Arc) -> &R +where + T: AsRef, +{ + wrapped_type.as_ref().as_ref() +} + +#[cfg(feature = "uniffi")] +pub fn maybe_try_convert_enum(wrapped_type: &T) -> Result +where + for<'a> R: TryFrom<&'a T, Error = crate::error::Error>, +{ + R::try_from(wrapped_type) +} + +#[cfg(feature = "uniffi")] +pub fn maybe_wrap(ldk_type: impl Into) -> std::sync::Arc { + std::sync::Arc::new(ldk_type.into()) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_deref(value: &T) -> &T { + value +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_try_convert_enum(value: &T) -> Result<&T, crate::error::Error> { + Ok(value) +} + +#[cfg(not(feature = "uniffi"))] +pub fn maybe_wrap(value: T) -> T { + value +} diff --git a/src/uniffi_types.rs b/src/ffi/types.rs similarity index 98% rename from src/uniffi_types.rs rename to src/ffi/types.rs index 77f9348cc7..4d30934761 100644 --- a/src/uniffi_types.rs +++ b/src/ffi/types.rs @@ -61,6 +61,7 @@ use lightning::util::ser::Writeable; use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; use std::convert::TryInto; +use std::ops::Deref; use std::str::FromStr; use std::sync::Arc; use std::time::Duration; @@ -475,11 +476,6 @@ impl Bolt11Invoice { invoice_str.parse() } - /// Returns the underlying invoice [`LdkBolt11Invoice`] - pub fn into_inner(self) -> LdkBolt11Invoice { - self.inner - } - /// The hash of the [`RawBolt11Invoice`] that was signed. /// /// [`RawBolt11Invoice`]: lightning_invoice::RawBolt11Invoice @@ -593,9 +589,16 @@ impl From for Bolt11Invoice { } } -impl From for LdkBolt11Invoice { - fn from(wrapper: Bolt11Invoice) -> Self { - wrapper.into_inner() +impl Deref for Bolt11Invoice { + type Target = LdkBolt11Invoice; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Bolt11Invoice { + fn as_ref(&self) -> &LdkBolt11Invoice { + self.deref() } } diff --git a/src/lib.rs b/src/lib.rs index c3bfe16d8f..e80ca964d3 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -84,6 +84,7 @@ mod data_store; mod error; mod event; mod fee_estimator; +mod ffi; mod gossip; pub mod graph; mod hex_utils; @@ -96,8 +97,6 @@ mod peer_store; mod sweep; mod tx_broadcaster; mod types; -#[cfg(feature = "uniffi")] -mod uniffi_types; mod wallet; pub use bip39; @@ -117,7 +116,7 @@ pub use event::Event; pub use io::utils::generate_entropy_mnemonic; #[cfg(feature = "uniffi")] -use uniffi_types::*; +use ffi::*; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; diff --git a/src/liquidity.rs b/src/liquidity.rs index 47f3dcce45..a4516edd07 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -1308,7 +1308,7 @@ type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; #[derive(Clone, Debug, PartialEq, Eq)] pub struct PaymentInfo { /// A Lightning payment using BOLT 11. - pub bolt11: Option, + pub bolt11: Option, /// An onchain payment. pub onchain: Option, } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 0525718182..817a428bdf 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -13,6 +13,7 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::connection::ConnectionManager; use crate::data_store::DataStoreUpdateResult; use crate::error::Error; +use crate::ffi::{maybe_deref, maybe_try_convert_enum, maybe_wrap}; use crate::liquidity::LiquiditySource; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{ @@ -42,43 +43,12 @@ use std::sync::{Arc, RwLock}; #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; #[cfg(feature = "uniffi")] -type Bolt11Invoice = Arc; - -#[cfg(not(feature = "uniffi"))] -pub(crate) fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { - invoice -} -#[cfg(feature = "uniffi")] -pub(crate) fn maybe_wrap_invoice(invoice: LdkBolt11Invoice) -> Bolt11Invoice { - Arc::new(invoice.into()) -} - -#[cfg(not(feature = "uniffi"))] -pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { - invoice -} -#[cfg(feature = "uniffi")] -pub fn maybe_convert_invoice(invoice: &Bolt11Invoice) -> &LdkBolt11Invoice { - &invoice.inner -} +type Bolt11Invoice = Arc; #[cfg(not(feature = "uniffi"))] type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription; #[cfg(feature = "uniffi")] -type Bolt11InvoiceDescription = crate::uniffi_types::Bolt11InvoiceDescription; - -macro_rules! maybe_convert_description { - ($description: expr) => {{ - #[cfg(not(feature = "uniffi"))] - { - $description - } - #[cfg(feature = "uniffi")] - { - &LdkBolt11InvoiceDescription::try_from($description)? - } - }}; -} +type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; /// A payment handler allowing to create and pay [BOLT 11] invoices. /// @@ -125,7 +95,7 @@ impl Bolt11Payment { pub fn send( &self, invoice: &Bolt11Invoice, sending_parameters: Option, ) -> Result { - let invoice = maybe_convert_invoice(invoice); + let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -234,7 +204,7 @@ impl Bolt11Payment { &self, invoice: &Bolt11Invoice, amount_msat: u64, sending_parameters: Option, ) -> Result { - let invoice = maybe_convert_invoice(invoice); + let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -466,9 +436,9 @@ impl Bolt11Payment { pub fn receive( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_convert_description!(description); - let invoice = self.receive_inner(Some(amount_msat), description, expiry_secs, None)?; - Ok(maybe_wrap_invoice(invoice)) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(Some(amount_msat), &description, expiry_secs, None)?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a payment of the amount @@ -489,10 +459,10 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_convert_description!(description); + let description = maybe_try_convert_enum(description)?; let invoice = - self.receive_inner(Some(amount_msat), description, expiry_secs, Some(payment_hash))?; - Ok(maybe_wrap_invoice(invoice)) + self.receive_inner(Some(amount_msat), &description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request and receive a payment for which the @@ -502,9 +472,9 @@ impl Bolt11Payment { pub fn receive_variable_amount( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, ) -> Result { - let description = maybe_convert_description!(description); - let invoice = self.receive_inner(None, description, expiry_secs, None)?; - Ok(maybe_wrap_invoice(invoice)) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(None, &description, expiry_secs, None)?; + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a payment for the given payment hash @@ -524,9 +494,9 @@ impl Bolt11Payment { pub fn receive_variable_amount_for_hash( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, payment_hash: PaymentHash, ) -> Result { - let description = maybe_convert_description!(description); - let invoice = self.receive_inner(None, description, expiry_secs, Some(payment_hash))?; - Ok(maybe_wrap_invoice(invoice)) + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_inner(None, &description, expiry_secs, Some(payment_hash))?; + Ok(maybe_wrap(invoice)) } pub(crate) fn receive_inner( @@ -601,15 +571,15 @@ impl Bolt11Payment { &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, ) -> Result { - let description = maybe_convert_description!(description); + let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( Some(amount_msat), - description, + &description, expiry_secs, max_total_lsp_fee_limit_msat, None, )?; - Ok(maybe_wrap_invoice(invoice)) + Ok(maybe_wrap(invoice)) } /// Returns a payable invoice that can be used to request a variable amount payment (also known @@ -627,15 +597,15 @@ impl Bolt11Payment { &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, max_proportional_lsp_fee_limit_ppm_msat: Option, ) -> Result { - let description = maybe_convert_description!(description); + let description = maybe_try_convert_enum(description)?; let invoice = self.receive_via_jit_channel_inner( None, - description, + &description, expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, )?; - Ok(maybe_wrap_invoice(invoice)) + Ok(maybe_wrap(invoice)) } fn receive_via_jit_channel_inner( @@ -742,7 +712,7 @@ impl Bolt11Payment { /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { - let invoice = maybe_convert_invoice(invoice); + let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -775,7 +745,7 @@ impl Bolt11Payment { pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, ) -> Result<(), Error> { - let invoice = maybe_convert_invoice(invoice); + let invoice = maybe_deref(invoice); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 5e6c1ef602..abfc5b784b 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -12,8 +12,9 @@ //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md use crate::error::Error; +use crate::ffi::maybe_wrap; use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{bolt11::maybe_wrap_invoice, Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; use crate::Config; use lightning::ln::channelmanager::PaymentId; @@ -153,7 +154,7 @@ impl UnifiedQrPayment { } if let Some(invoice) = uri_network_checked.extras.bolt11_invoice { - let invoice = maybe_wrap_invoice(invoice); + let invoice = maybe_wrap(invoice); match self.bolt11_invoice.send(&invoice, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt11 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT11 invoice: {:?}. This is part of a unified QR code payment. Falling back to the on-chain transaction.", e), From ef81e0d65947aa797f5c91e92036e37c735b6737 Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Mon, 5 May 2025 19:49:02 +0200 Subject: [PATCH 073/177] Add Offer wrapper for FFI bindings Implement Offer struct in ffi/types.rs to provide a wrapper around LDK's Offer for cross-language bindings. Modified payment handling in bolt12.rs to: - Support both native and FFI-compatible types via type aliasing - Implement conditional compilation for transparent FFI support - Update payment functions to handle wrapped types Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types. --- bindings/ldk_node.udl | 27 +++- src/ffi/types.rs | 310 +++++++++++++++++++++++++++++++++++++- src/payment/bolt12.rs | 27 +++- src/payment/unified_qr.rs | 17 ++- 4 files changed, 357 insertions(+), 24 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index c2f0166c84..38ab4677c4 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -736,6 +736,30 @@ interface Bolt11Invoice { PublicKey recover_payee_pub_key(); }; +[Enum] +interface OfferAmount { + Bitcoin(u64 amount_msats); + Currency(string iso4217_code, u64 amount); +}; + +[Traits=(Debug, Display, Eq)] +interface Offer { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string offer_str); + OfferId id(); + boolean is_expired(); + string? description(); + string? issuer(); + OfferAmount? amount(); + boolean is_valid_quantity(u64 quantity); + boolean expects_quantity(); + boolean supports_chain(Network chain); + sequence chains(); + sequence? metadata(); + u64? absolute_expiry_seconds(); + PublicKey? issuer_signing_pubkey(); +}; + [Custom] typedef string Txid; @@ -754,9 +778,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Offer; - [Custom] typedef string Refund; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 4d30934761..8b511c8301 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -26,7 +26,7 @@ pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::types::ChannelId; pub use lightning::offers::invoice::Bolt12Invoice; -pub use lightning::offers::offer::{Offer, OfferId}; +pub use lightning::offers::offer::OfferId; pub use lightning::offers::refund::Refund; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::util::string::UntrustedString; @@ -57,6 +57,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; +use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; use lightning::util::ser::Writeable; use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; @@ -114,15 +115,166 @@ impl UniffiCustomTypeConverter for Address { } } -impl UniffiCustomTypeConverter for Offer { - type Builtin = String; +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum OfferAmount { + Bitcoin { amount_msats: u64 }, + Currency { iso4217_code: String, amount: u64 }, +} - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Offer::from_str(&val).map_err(|_| Error::InvalidOffer.into()) +impl From for OfferAmount { + fn from(ldk_amount: LdkAmount) -> Self { + match ldk_amount { + LdkAmount::Bitcoin { amount_msats } => OfferAmount::Bitcoin { amount_msats }, + LdkAmount::Currency { iso4217_code, amount } => OfferAmount::Currency { + iso4217_code: iso4217_code.iter().map(|&b| b as char).collect(), + amount, + }, + } } +} - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() +/// An `Offer` is a potentially long-lived proposal for payment of a good or service. +/// +/// An offer is a precursor to an [`InvoiceRequest`]. A merchant publishes an offer from which a +/// customer may request an [`Bolt12Invoice`] for a specific quantity and using an amount sufficient +/// to cover that quantity (i.e., at least `quantity * amount`). See [`Offer::amount`]. +/// +/// Offers may be denominated in currency other than bitcoin but are ultimately paid using the +/// latter. +/// +/// Through the use of [`BlindedMessagePath`]s, offers provide recipient privacy. +/// +/// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest +/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice +/// [`Offer`]: lightning::offers::Offer:amount +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Offer { + pub(crate) inner: LdkOffer, +} + +impl Offer { + pub fn from_str(offer_str: &str) -> Result { + offer_str.parse() + } + + /// Returns the id of the offer. + pub fn id(&self) -> OfferId { + OfferId(self.inner.id().0) + } + + /// Whether the offer has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// A complete description of the purpose of the payment. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn description(&self) -> Option { + self.inner.description().map(|printable| printable.to_string()) + } + + /// The issuer of the offer, possibly beginning with `user@domain` or `domain`. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// The minimum amount required for a successful payment of a single item. + pub fn amount(&self) -> Option { + self.inner.amount().map(|amount| amount.into()) + } + + /// Returns whether the given quantity is valid for the offer. + pub fn is_valid_quantity(&self, quantity: u64) -> bool { + self.inner.is_valid_quantity(quantity) + } + + /// Returns whether a quantity is expected in an [`InvoiceRequest`] for the offer. + /// + /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest + pub fn expects_quantity(&self) -> bool { + self.inner.expects_quantity() + } + + /// Returns whether the given chain is supported by the offer. + pub fn supports_chain(&self, chain: Network) -> bool { + self.inner.supports_chain(chain.chain_hash()) + } + + /// The chains that may be used when paying a requested invoice (e.g., bitcoin mainnet). + /// + /// Payments must be denominated in units of the minimal lightning-payable unit (e.g., msats) + /// for the selected chain. + pub fn chains(&self) -> Vec { + self.inner.chains().into_iter().filter_map(Network::from_chain_hash).collect() + } + + /// Opaque bytes set by the originator. + /// + /// Useful for authentication and validating fields since it is reflected in `invoice_request` + /// messages along with all the other fields from the `offer`. + pub fn metadata(&self) -> Option> { + self.inner.metadata().cloned() + } + + /// Seconds since the Unix epoch when an invoice should no longer be requested. + /// + /// If `None`, the offer does not expire. + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// The public key corresponding to the key used by the recipient to sign invoices. + /// - If [`Offer::paths`] is empty, MUST be `Some` and contain the recipient's node id for + /// sending an [`InvoiceRequest`]. + /// - If [`Offer::paths`] is not empty, MAY be `Some` and contain a transient id. + /// - If `None`, the signing pubkey will be the final blinded node id from the + /// [`BlindedMessagePath`] in [`Offer::paths`] used to send the [`InvoiceRequest`]. + /// + /// See also [`Bolt12Invoice::signing_pubkey`]. + /// + /// [`InvoiceRequest`]: lightning::offers::invoice_request::InvoiceRequest + /// [`Bolt12Invoice::signing_pubkey`]: lightning::offers::invoice::Bolt12Invoice::signing_pubkey + pub fn issuer_signing_pubkey(&self) -> Option { + self.inner.issuer_signing_pubkey() + } +} + +impl std::str::FromStr for Offer { + type Err = Error; + + fn from_str(offer_str: &str) -> Result { + offer_str + .parse::() + .map(|offer| Offer { inner: offer }) + .map_err(|_| Error::InvalidOffer) + } +} + +impl From for Offer { + fn from(offer: LdkOffer) -> Self { + Offer { inner: offer } + } +} + +impl Deref for Offer { + type Target = LdkOffer; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Offer { + fn as_ref(&self) -> &LdkOffer { + self.deref() + } +} + +impl std::fmt::Display for Offer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) } } @@ -661,6 +813,13 @@ impl UniffiCustomTypeConverter for DateTime { #[cfg(test)] mod tests { + use std::{ + num::NonZeroU64, + time::{SystemTime, UNIX_EPOCH}, + }; + + use lightning::offers::offer::{OfferBuilder, Quantity}; + use super::*; fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { @@ -670,6 +829,36 @@ mod tests { (ldk_invoice, wrapped_invoice) } + fn create_test_offer() -> (LdkOffer, Offer) { + let pubkey = bitcoin::secp256k1::PublicKey::from_str( + "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + ) + .unwrap(); + + let expiry = + (SystemTime::now() + Duration::from_secs(3600)).duration_since(UNIX_EPOCH).unwrap(); + + let quantity = NonZeroU64::new(10_000).unwrap(); + + let builder = OfferBuilder::new(pubkey) + .description("Test offer description".to_string()) + .amount_msats(100_000) + .issuer("Offer issuer".to_string()) + .absolute_expiry(expiry) + .chain(Network::Bitcoin) + .supported_quantity(Quantity::Bounded(quantity)) + .metadata(vec![ + 0xde, 0xad, 0xbe, 0xef, 0xca, 0xfe, 0xba, 0xbe, 0x12, 0x34, 0x56, 0x78, 0x90, 0xab, + 0xcd, 0xef, + ]) + .unwrap(); + + let ldk_offer = builder.build().unwrap(); + let wrapped_offer = Offer::from(ldk_offer.clone()); + + (ldk_offer, wrapped_offer) + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); @@ -779,4 +968,111 @@ mod tests { parsed_invoice.payment_hash().to_byte_array().to_vec() ); } + + #[test] + fn test_offer() { + let (ldk_offer, wrapped_offer) = create_test_offer(); + match (ldk_offer.description(), wrapped_offer.description()) { + (Some(ldk_desc), Some(wrapped_desc)) => { + assert_eq!(ldk_desc.to_string(), wrapped_desc); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had a description but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had a description but LDK offer did not!"); + }, + } + + match (ldk_offer.amount(), wrapped_offer.amount()) { + (Some(ldk_amount), Some(wrapped_amount)) => { + let ldk_amount: OfferAmount = ldk_amount.into(); + assert_eq!(ldk_amount, wrapped_amount); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an amount but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an amount but LDK offer did not!"); + }, + } + + match (ldk_offer.issuer(), wrapped_offer.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an issuer but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an issuer but LDK offer did not!"); + }, + } + + assert_eq!(ldk_offer.is_expired(), wrapped_offer.is_expired()); + assert_eq!(ldk_offer.id(), wrapped_offer.id()); + assert_eq!(ldk_offer.is_valid_quantity(10_000), wrapped_offer.is_valid_quantity(10_000)); + assert_eq!(ldk_offer.expects_quantity(), wrapped_offer.expects_quantity()); + assert_eq!( + ldk_offer.supports_chain(Network::Bitcoin.chain_hash()), + wrapped_offer.supports_chain(Network::Bitcoin) + ); + assert_eq!( + ldk_offer.chains(), + wrapped_offer.chains().iter().map(|c| c.chain_hash()).collect::>() + ); + match (ldk_offer.metadata(), wrapped_offer.metadata()) { + (Some(ldk_metadata), Some(wrapped_metadata)) => { + assert_eq!(ldk_metadata.clone(), wrapped_metadata); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had metadata but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had metadata but LDK offer did not!"); + }, + } + + match (ldk_offer.absolute_expiry(), wrapped_offer.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an absolute expiry but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an absolute expiry but LDK offer did not!"); + }, + } + + match (ldk_offer.issuer_signing_pubkey(), wrapped_offer.issuer_signing_pubkey()) { + (Some(ldk_expiry_signing_pubkey), Some(wrapped_issuer_signing_pubkey)) => { + assert_eq!(ldk_expiry_signing_pubkey, wrapped_issuer_signing_pubkey); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK offer had an issuer signing pubkey but wrapped offer did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped offer had an issuer signing pubkey but LDK offer did not!"); + }, + } + } } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 8006f4bb94..aa642a0844 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -11,13 +11,14 @@ use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; use crate::error::Error; +use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::invoice::Bolt12Invoice; -use lightning::offers::offer::{Amount, Offer, Quantity}; +use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::offers::refund::Refund; use lightning::util::string::UntrustedString; @@ -28,6 +29,11 @@ use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(not(feature = "uniffi"))] +type Offer = LdkOffer; +#[cfg(feature = "uniffi")] +type Offer = Arc; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -59,6 +65,7 @@ impl Bolt12Payment { pub fn send( &self, offer: &Offer, quantity: Option, payer_note: Option, ) -> Result { + let offer = maybe_deref(offer); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -160,6 +167,7 @@ impl Bolt12Payment { pub fn send_using_amount( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, ) -> Result { + let offer = maybe_deref(offer); let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); @@ -254,11 +262,9 @@ impl Bolt12Payment { } } - /// Returns a payable offer that can be used to request and receive a payment of the amount - /// given. - pub fn receive( + pub(crate) fn receive_inner( &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, - ) -> Result { + ) -> Result { let absolute_expiry = expiry_secs.map(|secs| { (SystemTime::now() + Duration::from_secs(secs as u64)) .duration_since(UNIX_EPOCH) @@ -291,6 +297,15 @@ impl Bolt12Payment { Ok(finalized_offer) } + /// Returns a payable offer that can be used to request and receive a payment of the amount + /// given. + pub fn receive( + &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, + ) -> Result { + let offer = self.receive_inner(amount_msat, description, expiry_secs, quantity)?; + Ok(maybe_wrap(offer)) + } + /// Returns a payable offer that can be used to request and receive a payment for which the /// amount is to be determined by the user, also known as a "zero-amount" offer. pub fn receive_variable_amount( @@ -312,7 +327,7 @@ impl Bolt12Payment { Error::OfferCreationFailed })?; - Ok(offer) + Ok(maybe_wrap(offer)) } /// Requests a refund payment for the given [`Refund`]. diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index abfc5b784b..125e1d09be 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -95,14 +95,14 @@ impl UnifiedQrPayment { let amount_msats = amount_sats * 1_000; - let bolt12_offer = match self.bolt12_payment.receive(amount_msats, description, None, None) - { - Ok(offer) => Some(offer), - Err(e) => { - log_error!(self.logger, "Failed to create offer: {}", e); - None - }, - }; + let bolt12_offer = + match self.bolt12_payment.receive_inner(amount_msats, description, None, None) { + Ok(offer) => Some(offer), + Err(e) => { + log_error!(self.logger, "Failed to create offer: {}", e); + None + }, + }; let invoice_description = Bolt11InvoiceDescription::Direct( Description::new(description.to_string()).map_err(|_| Error::InvoiceCreationFailed)?, @@ -147,6 +147,7 @@ impl UnifiedQrPayment { uri.clone().require_network(self.config.network).map_err(|_| Error::InvalidNetwork)?; if let Some(offer) = uri_network_checked.extras.bolt12_offer { + let offer = maybe_wrap(offer); match self.bolt12_payment.send(&offer, None, None) { Ok(payment_id) => return Ok(QrPaymentResult::Bolt12 { payment_id }), Err(e) => log_error!(self.logger, "Failed to send BOLT12 offer: {:?}. This is part of a unified QR code payment. Falling back to the BOLT11 invoice.", e), From dc6a8f20548a21d38d1906c2a90100ae82e78861 Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Mon, 5 May 2025 19:50:24 +0200 Subject: [PATCH 074/177] Add Refund wrapper for FFI bindings Implement Refund struct in ffi/types.rs to provide a wrapper around LDK's Refund for cross-language bindings. Modified payment handling in bolt12.rs to: - Support both native and FFI-compatible types via type aliasing - Implement conditional compilation for transparent FFI support - Update payment functions to handle wrapped types Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types. --- bindings/ldk_node.udl | 19 +++- src/ffi/types.rs | 227 ++++++++++++++++++++++++++++++++++++++++-- src/payment/bolt12.rs | 15 ++- 3 files changed, 246 insertions(+), 15 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 38ab4677c4..d489935328 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -760,6 +760,22 @@ interface Offer { PublicKey? issuer_signing_pubkey(); }; +[Traits=(Debug, Display, Eq)] +interface Refund { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string refund_str); + string description(); + u64? absolute_expiry_seconds(); + boolean is_expired(); + string? issuer(); + sequence payer_metadata(); + Network? chain(); + u64 amount_msats(); + u64? quantity(); + PublicKey payer_signing_pubkey(); + string? payer_note(); +}; + [Custom] typedef string Txid; @@ -778,9 +794,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Refund; - [Custom] typedef string Bolt12Invoice; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 8b511c8301..6c0aaf8804 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -27,7 +27,6 @@ pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::types::ChannelId; pub use lightning::offers::invoice::Bolt12Invoice; pub use lightning::offers::offer::OfferId; -pub use lightning::offers::refund::Refund; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::util::string::UntrustedString; @@ -58,6 +57,7 @@ use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; +use lightning::offers::refund::Refund as LdkRefund; use lightning::util::ser::Writeable; use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; @@ -278,15 +278,123 @@ impl std::fmt::Display for Offer { } } -impl UniffiCustomTypeConverter for Refund { - type Builtin = String; +/// A `Refund` is a request to send an [`Bolt12Invoice`] without a preceding [`Offer`]. +/// +/// Typically, after an invoice is paid, the recipient may publish a refund allowing the sender to +/// recoup their funds. A refund may be used more generally as an "offer for money", such as with a +/// bitcoin ATM. +/// +/// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice +/// [`Offer`]: lightning::offers::offer::Offer +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Refund { + pub(crate) inner: LdkRefund, +} - fn into_custom(val: Self::Builtin) -> uniffi::Result { - Refund::from_str(&val).map_err(|_| Error::InvalidRefund.into()) +impl Refund { + pub fn from_str(refund_str: &str) -> Result { + refund_str.parse() } - fn from_custom(obj: Self) -> Self::Builtin { - obj.to_string() + /// A complete description of the purpose of the refund. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn description(&self) -> String { + self.inner.description().to_string() + } + + /// Seconds since the Unix epoch when an invoice should no longer be sent. + /// + /// If `None`, the refund does not expire. + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// Whether the refund has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// The issuer of the refund, possibly beginning with `user@domain` or `domain`. + /// + /// Intended to be displayed to the user but with the caveat that it has not been verified in any way. + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// An unpredictable series of bytes, typically containing information about the derivation of + /// [`payer_signing_pubkey`]. + /// + /// [`payer_signing_pubkey`]: Self::payer_signing_pubkey + pub fn payer_metadata(&self) -> Vec { + self.inner.payer_metadata().to_vec() + } + + /// A chain that the refund is valid for. + pub fn chain(&self) -> Option { + Network::try_from(self.inner.chain()).ok() + } + + /// The amount to refund in msats (i.e., the minimum lightning-payable unit for [`chain`]). + /// + /// [`chain`]: Self::chain + pub fn amount_msats(&self) -> u64 { + self.inner.amount_msats() + } + + /// The quantity of an item that refund is for. + pub fn quantity(&self) -> Option { + self.inner.quantity() + } + + /// A public node id to send to in the case where there are no [`paths`]. + /// + /// Otherwise, a possibly transient pubkey. + /// + /// [`paths`]: lightning::offers::refund::Refund::paths + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.inner.payer_signing_pubkey() + } + + /// Payer provided note to include in the invoice. + pub fn payer_note(&self) -> Option { + self.inner.payer_note().map(|printable| printable.to_string()) + } +} + +impl std::str::FromStr for Refund { + type Err = Error; + + fn from_str(refund_str: &str) -> Result { + refund_str + .parse::() + .map(|refund| Refund { inner: refund }) + .map_err(|_| Error::InvalidRefund) + } +} + +impl From for Refund { + fn from(refund: LdkRefund) -> Self { + Refund { inner: refund } + } +} + +impl Deref for Refund { + type Target = LdkRefund; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Refund { + fn as_ref(&self) -> &LdkRefund { + self.deref() + } +} + +impl std::fmt::Display for Refund { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.inner) } } @@ -818,9 +926,11 @@ mod tests { time::{SystemTime, UNIX_EPOCH}, }; - use lightning::offers::offer::{OfferBuilder, Quantity}; - use super::*; + use lightning::offers::{ + offer::{OfferBuilder, Quantity}, + refund::RefundBuilder, + }; fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; @@ -859,6 +969,28 @@ mod tests { (ldk_offer, wrapped_offer) } + fn create_test_refund() -> (LdkRefund, Refund) { + let payer_key = bitcoin::secp256k1::PublicKey::from_str( + "02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619", + ) + .unwrap(); + + let expiry = + (SystemTime::now() + Duration::from_secs(3600)).duration_since(UNIX_EPOCH).unwrap(); + + let builder = RefundBuilder::new("Test refund".to_string().into(), payer_key, 100_000) + .unwrap() + .description("Test refund description".to_string()) + .absolute_expiry(expiry) + .quantity(3) + .issuer("test_issuer".to_string()); + + let ldk_refund = builder.build().unwrap(); + let wrapped_refund = Refund::from(ldk_refund.clone()); + + (ldk_refund, wrapped_refund) + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); @@ -1075,4 +1207,81 @@ mod tests { }, } } + + #[test] + fn test_refund_roundtrip() { + let (ldk_refund, _) = create_test_refund(); + + let refund_str = ldk_refund.to_string(); + + let parsed_refund = Refund::from_str(&refund_str); + assert!(parsed_refund.is_ok(), "Failed to parse refund from string!"); + + let invalid_result = Refund::from_str("invalid_refund_string"); + assert!(invalid_result.is_err()); + assert!(matches!(invalid_result.err().unwrap(), Error::InvalidRefund)); + } + + #[test] + fn test_refund_properties() { + let (ldk_refund, wrapped_refund) = create_test_refund(); + + assert_eq!(ldk_refund.description().to_string(), wrapped_refund.description()); + assert_eq!(ldk_refund.amount_msats(), wrapped_refund.amount_msats()); + assert_eq!(ldk_refund.is_expired(), wrapped_refund.is_expired()); + + match (ldk_refund.absolute_expiry(), wrapped_refund.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an expiry but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an expiry but LDK refund did not!"); + }, + } + + match (ldk_refund.quantity(), wrapped_refund.quantity()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry, wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an quantity but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an quantity but LDK refund did not!"); + }, + } + + match (ldk_refund.issuer(), wrapped_refund.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK refund had an issuer but wrapped refund did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped refund had an issuer but LDK refund did not!"); + }, + } + + assert_eq!(ldk_refund.payer_metadata().to_vec(), wrapped_refund.payer_metadata()); + assert_eq!(ldk_refund.payer_signing_pubkey(), wrapped_refund.payer_signing_pubkey()); + + if let Ok(network) = Network::try_from(ldk_refund.chain()) { + assert_eq!(wrapped_refund.chain(), Some(network)); + } + + assert_eq!(ldk_refund.payer_note().map(|p| p.to_string()), wrapped_refund.payer_note()); + } } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index aa642a0844..74c3ac45fd 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -20,7 +20,6 @@ use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; -use lightning::offers::refund::Refund; use lightning::util::string::UntrustedString; use rand::RngCore; @@ -34,6 +33,11 @@ type Offer = LdkOffer; #[cfg(feature = "uniffi")] type Offer = Arc; +#[cfg(not(feature = "uniffi"))] +type Refund = lightning::offers::refund::Refund; +#[cfg(feature = "uniffi")] +type Refund = Arc; + /// A payment handler allowing to create and pay [BOLT 12] offers and refunds. /// /// Should be retrieved by calling [`Node::bolt12_payment`]. @@ -334,8 +338,11 @@ impl Bolt12Payment { /// /// The returned [`Bolt12Invoice`] is for informational purposes only (i.e., isn't needed to /// retrieve the refund). + /// + /// [`Refund`]: lightning::offers::refund::Refund pub fn request_refund_payment(&self, refund: &Refund) -> Result { - let invoice = self.channel_manager.request_refund_payment(refund).map_err(|e| { + let refund = maybe_deref(refund); + let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| { log_error!(self.logger, "Failed to request refund payment: {:?}", e); Error::InvoiceRequestCreationFailed })?; @@ -366,6 +373,8 @@ impl Bolt12Payment { } /// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given. + /// + /// [`Refund`]: lightning::offers::refund::Refund pub fn initiate_refund( &self, amount_msat: u64, expiry_secs: u32, quantity: Option, payer_note: Option, @@ -427,6 +436,6 @@ impl Bolt12Payment { self.payment_store.insert(payment)?; - Ok(refund) + Ok(maybe_wrap(refund)) } } From f01d0219efc1fb7542e1e760f1a10504806edd5e Mon Sep 17 00:00:00 2001 From: alexanderwiederin Date: Mon, 12 May 2025 22:50:49 +0200 Subject: [PATCH 075/177] Add Bolt12Invoice wrapper for FFI bindings Implement Bolt12Invoice struct in ffi/types.rs to provide a wrapper around LDK's Bolt12Invoice for cross-language bindings. Modified payment handling in bolt12.rs to: - Support both native and FFI-compatible types via type aliasing - Implement conditional compilation for transparent FFI support - Update payment functions to handle wrapped types Added testing to verify that properties are preserved when wrapping/unwrapping between native and FFI types. --- bindings/ldk_node.udl | 28 +++- src/ffi/types.rs | 369 ++++++++++++++++++++++++++++++++++++++++-- src/payment/bolt12.rs | 9 +- 3 files changed, 385 insertions(+), 21 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index d489935328..505f0db8d5 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -776,6 +776,31 @@ interface Refund { string? payer_note(); }; +interface Bolt12Invoice { + [Throws=NodeError, Name=from_str] + constructor([ByRef] string invoice_str); + PaymentHash payment_hash(); + u64 amount_msats(); + OfferAmount? amount(); + PublicKey signing_pubkey(); + u64 created_at(); + u64? absolute_expiry_seconds(); + u64 relative_expiry(); + boolean is_expired(); + string? description(); + string? issuer(); + string? payer_note(); + sequence? metadata(); + u64? quantity(); + sequence signable_hash(); + PublicKey payer_signing_pubkey(); + PublicKey? issuer_signing_pubkey(); + sequence chain(); + sequence>? offer_chains(); + sequence
fallback_addresses(); + sequence encode(); +}; + [Custom] typedef string Txid; @@ -794,9 +819,6 @@ typedef string NodeId; [Custom] typedef string Address; -[Custom] -typedef string Bolt12Invoice; - [Custom] typedef string OfferId; diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 6c0aaf8804..bbf7302110 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -25,7 +25,6 @@ pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParame pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::types::ChannelId; -pub use lightning::offers::invoice::Bolt12Invoice; pub use lightning::offers::offer::OfferId; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::util::string::UntrustedString; @@ -56,6 +55,7 @@ use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::PaymentId; +use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; use lightning::offers::refund::Refund as LdkRefund; use lightning::util::ser::Writeable; @@ -398,20 +398,218 @@ impl std::fmt::Display for Refund { } } -impl UniffiCustomTypeConverter for Bolt12Invoice { - type Builtin = String; +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Bolt12Invoice { + pub(crate) inner: LdkBolt12Invoice, +} - fn into_custom(val: Self::Builtin) -> uniffi::Result { - if let Some(bytes_vec) = hex_utils::to_vec(&val) { - if let Ok(invoice) = Bolt12Invoice::try_from(bytes_vec) { - return Ok(invoice); +impl Bolt12Invoice { + pub fn from_str(invoice_str: &str) -> Result { + invoice_str.parse() + } + + /// SHA256 hash of the payment preimage that will be given in return for paying the invoice. + pub fn payment_hash(&self) -> PaymentHash { + PaymentHash(self.inner.payment_hash().0) + } + + /// The minimum amount required for a successful payment of the invoice. + pub fn amount_msats(&self) -> u64 { + self.inner.amount_msats() + } + + /// The minimum amount required for a successful payment of a single item. + /// + /// From [`Offer::amount`]; `None` if the invoice was created in response to a [`Refund`] or if + /// the [`Offer`] did not set it. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::amount`]: lightning::offers::offer::Offer::amount + /// [`Refund`]: lightning::offers::refund::Refund + pub fn amount(&self) -> Option { + self.inner.amount().map(|amount| amount.into()) + } + + /// A typically transient public key corresponding to the key used to sign the invoice. + /// + /// If the invoices was created in response to an [`Offer`], then this will be: + /// - [`Offer::issuer_signing_pubkey`] if it's `Some`, otherwise + /// - the final blinded node id from a [`BlindedMessagePath`] in [`Offer::paths`] if `None`. + /// + /// If the invoice was created in response to a [`Refund`], then it is a valid pubkey chosen by + /// the recipient. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::issuer_signing_pubkey`]: lightning::offers::offer::Offer::issuer_signing_pubkey + /// [`Offer::paths`]: lightning::offers::offer::Offer::paths + /// [`Refund`]: lightning::offers::refund::Refund + pub fn signing_pubkey(&self) -> PublicKey { + self.inner.signing_pubkey() + } + + /// Duration since the Unix epoch when the invoice was created. + pub fn created_at(&self) -> u64 { + self.inner.created_at().as_secs() + } + + /// Seconds since the Unix epoch when an invoice should no longer be requested. + /// + /// From [`Offer::absolute_expiry`] or [`Refund::absolute_expiry`]. + /// + /// [`Offer::absolute_expiry`]: lightning::offers::offer::Offer::absolute_expiry + pub fn absolute_expiry_seconds(&self) -> Option { + self.inner.absolute_expiry().map(|duration| duration.as_secs()) + } + + /// When the invoice has expired and therefore should no longer be paid. + pub fn relative_expiry(&self) -> u64 { + self.inner.relative_expiry().as_secs() + } + + /// Whether the invoice has expired. + pub fn is_expired(&self) -> bool { + self.inner.is_expired() + } + + /// A complete description of the purpose of the originating offer or refund. + /// + /// From [`Offer::description`] or [`Refund::description`]. + /// + /// [`Offer::description`]: lightning::offers::offer::Offer::description + /// [`Refund::description`]: lightning::offers::refund::Refund::description + pub fn description(&self) -> Option { + self.inner.description().map(|printable| printable.to_string()) + } + + /// The issuer of the offer or refund. + /// + /// From [`Offer::issuer`] or [`Refund::issuer`]. + /// + /// [`Offer::issuer`]: lightning::offers::offer::Offer::issuer + /// [`Refund::issuer`]: lightning::offers::refund::Refund::issuer + pub fn issuer(&self) -> Option { + self.inner.issuer().map(|printable| printable.to_string()) + } + + /// A payer-provided note reflected back in the invoice. + /// + /// From [`InvoiceRequest::payer_note`] or [`Refund::payer_note`]. + /// + /// [`Refund::payer_note`]: lightning::offers::refund::Refund::payer_note + pub fn payer_note(&self) -> Option { + self.inner.payer_note().map(|note| note.to_string()) + } + + /// Opaque bytes set by the originating [`Offer`]. + /// + /// From [`Offer::metadata`]; `None` if the invoice was created in response to a [`Refund`] or + /// if the [`Offer`] did not set it. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`Offer::metadata`]: lightning::offers::offer::Offer::metadata + /// [`Refund`]: lightning::offers::refund::Refund + pub fn metadata(&self) -> Option> { + self.inner.metadata().cloned() + } + + /// The quantity of items requested or refunded for. + /// + /// From [`InvoiceRequest::quantity`] or [`Refund::quantity`]. + /// + /// [`Refund::quantity`]: lightning::offers::refund::Refund::quantity + pub fn quantity(&self) -> Option { + self.inner.quantity() + } + + /// Hash that was used for signing the invoice. + pub fn signable_hash(&self) -> Vec { + self.inner.signable_hash().to_vec() + } + + /// A possibly transient pubkey used to sign the invoice request or to send an invoice for a + /// refund in case there are no [`message_paths`]. + /// + /// [`message_paths`]: lightning::offers::invoice::Bolt12Invoice + pub fn payer_signing_pubkey(&self) -> PublicKey { + self.inner.payer_signing_pubkey() + } + + /// The public key used by the recipient to sign invoices. + /// + /// From [`Offer::issuer_signing_pubkey`] and may be `None`; also `None` if the invoice was + /// created in response to a [`Refund`]. + /// + /// [`Offer::issuer_signing_pubkey`]: lightning::offers::offer::Offer::issuer_signing_pubkey + /// [`Refund`]: lightning::offers::refund::Refund + pub fn issuer_signing_pubkey(&self) -> Option { + self.inner.issuer_signing_pubkey() + } + + /// The chain that must be used when paying the invoice; selected from [`offer_chains`] if the + /// invoice originated from an offer. + /// + /// From [`InvoiceRequest::chain`] or [`Refund::chain`]. + /// + /// [`offer_chains`]: lightning::offers::invoice::Bolt12Invoice::offer_chains + /// [`InvoiceRequest::chain`]: lightning::offers::invoice_request::InvoiceRequest::chain + /// [`Refund::chain`]: lightning::offers::refund::Refund::chain + pub fn chain(&self) -> Vec { + self.inner.chain().to_bytes().to_vec() + } + + /// The chains that may be used when paying a requested invoice. + /// + /// From [`Offer::chains`]; `None` if the invoice was created in response to a [`Refund`]. + /// + /// [`Offer::chains`]: lightning::offers::offer::Offer::chains + /// [`Refund`]: lightning::offers::refund::Refund + pub fn offer_chains(&self) -> Option>> { + self.inner + .offer_chains() + .map(|chains| chains.iter().map(|chain| chain.to_bytes().to_vec()).collect()) + } + + /// Fallback addresses for paying the invoice on-chain, in order of most-preferred to + /// least-preferred. + pub fn fallback_addresses(&self) -> Vec
{ + self.inner.fallbacks() + } + + /// Writes `self` out to a `Vec`. + pub fn encode(&self) -> Vec { + self.inner.encode() + } +} + +impl std::str::FromStr for Bolt12Invoice { + type Err = Error; + + fn from_str(invoice_str: &str) -> Result { + if let Some(bytes_vec) = hex_utils::to_vec(invoice_str) { + if let Ok(invoice) = LdkBolt12Invoice::try_from(bytes_vec) { + return Ok(Bolt12Invoice { inner: invoice }); } } - Err(Error::InvalidInvoice.into()) + Err(Error::InvalidInvoice) } +} - fn from_custom(obj: Self) -> Self::Builtin { - hex_utils::to_string(&obj.encode()) +impl From for Bolt12Invoice { + fn from(invoice: LdkBolt12Invoice) -> Self { + Bolt12Invoice { inner: invoice } + } +} + +impl Deref for Bolt12Invoice { + type Target = LdkBolt12Invoice; + fn deref(&self) -> &Self::Target { + &self.inner + } +} + +impl AsRef for Bolt12Invoice { + fn as_ref(&self) -> &LdkBolt12Invoice { + self.deref() } } @@ -932,7 +1130,7 @@ mod tests { refund::RefundBuilder, }; - fn create_test_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { + fn create_test_bolt11_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; let ldk_invoice: LdkBolt11Invoice = invoice_string.parse().unwrap(); let wrapped_invoice = Bolt11Invoice::from(ldk_invoice.clone()); @@ -991,6 +1189,19 @@ mod tests { (ldk_refund, wrapped_refund) } + fn create_test_bolt12_invoice() -> (LdkBolt12Invoice, Bolt12Invoice) { + let invoice_hex = "0020a5b7104b95f17442d6638143ded62b02c2fda98cdf35841713fd0f44b59286560a000e04682cb028502006226e46111a0b59caaf126043eb5bbf28c34f3a5e332a1fc7b2b73cf188910f520227105601015821034b4f0765a115caeff6787a8fb2d976c02467a36aea32901539d76473817937c65904546573745a9c00000068000001000003e75203dee4b3e5d48650caf1faadda53ac6e0dc3f509cc5e9c46defb8aeeec14010348e84fab39b226b1e0696cb6fb40bdb293952c184cf02007fa6e983cd311d189004e7bd75ff9ef069642f2abfa5916099e5a16144e1a6d9b4f246624d3b57d2895d5d2e46fe8661e49717d1663ad2c07b023738370a3e44a960f683040b1862fe36e22347c2dbe429c51af377bdbe01ca0e103f295d1678c68b628957a53a820afcc25763cc67b38aca82067bdf52dc68c061a02575d91c01beca64cc09735c395e91d034841d3e61b58948da631192ce556b85b01028e2284ead4ce184981f4d0f387f8d47295d4fa1dab6a6ae3a417550ac1c8b1aa007b38c926212fbf23154c6ff707621d6eedafc4298b133111d90934bb9d5a2103f0c8e4a3f3daa992334aad300677f23b4285db2ee5caf0a0ecc39c6596c3c4e42318040bec46add3626501f6e422be9c791adc81ea5c83ff0bfa91b7d42bcac0ed128a640fe970da584cff80fd5c12a8ea9b546a2d63515343a933daa21c0000000000000000001800000000000000011d24b2dfac5200000000a404682ca218a820a4a878fb352e63673c05eb07e53563fc8022ff039ad4c66e65848a7cde7ee780aa022710ae03020000b02103800fd75bf6b1e7c5f3fab33a372f6599730e0fae7a30fa4e5c8fbc69c3a87981f0403c9a40e6c9d08e12b0a155101d23a170b4f5b38051b0a0a09a794ce49e820f65d50c8fad7518200d3a28331aa5c668a8f7d70206aaf8bea2e8f05f0904b6e033"; + + let invoice_bytes = hex_utils::to_vec(invoice_hex).expect("Valid hex string"); + + let ldk_invoice = + LdkBolt12Invoice::try_from(invoice_bytes).expect("Valid Bolt12Invoice bytes"); + + let wrapped_invoice = Bolt12Invoice { inner: ldk_invoice.clone() }; + + (ldk_invoice, wrapped_invoice) + } + #[test] fn test_invoice_description_conversion() { let hash = "09d08d4865e8af9266f6cc7c0ae23a1d6bf868207cf8f7c5979b9f6ed850dfb0".to_string(); @@ -1003,7 +1214,7 @@ mod tests { #[test] fn test_bolt11_invoice_basic_properties() { - let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); assert_eq!( ldk_invoice.payment_hash().to_string(), @@ -1029,7 +1240,7 @@ mod tests { #[test] fn test_bolt11_invoice_time_related_fields() { - let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); assert_eq!(ldk_invoice.expiry_time().as_secs(), wrapped_invoice.expiry_time_seconds()); assert_eq!( @@ -1048,7 +1259,7 @@ mod tests { #[test] fn test_bolt11_invoice_description() { - let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); let ldk_description = ldk_invoice.description(); let wrapped_description = wrapped_invoice.description(); @@ -1072,7 +1283,7 @@ mod tests { #[test] fn test_bolt11_invoice_route_hints() { - let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); let wrapped_route_hints = wrapped_invoice.route_hints(); let ldk_route_hints = ldk_invoice.route_hints(); @@ -1091,7 +1302,7 @@ mod tests { #[test] fn test_bolt11_invoice_roundtrip() { - let (ldk_invoice, wrapped_invoice) = create_test_invoice(); + let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); let invoice_str = wrapped_invoice.to_string(); let parsed_invoice: LdkBolt11Invoice = invoice_str.parse().unwrap(); @@ -1284,4 +1495,130 @@ mod tests { assert_eq!(ldk_refund.payer_note().map(|p| p.to_string()), wrapped_refund.payer_note()); } + + #[test] + fn test_bolt12_invoice_properties() { + let (ldk_invoice, wrapped_invoice) = create_test_bolt12_invoice(); + + assert_eq!( + ldk_invoice.payment_hash().0.to_vec(), + wrapped_invoice.payment_hash().0.to_vec() + ); + assert_eq!(ldk_invoice.amount_msats(), wrapped_invoice.amount_msats()); + assert_eq!(ldk_invoice.is_expired(), wrapped_invoice.is_expired()); + + assert_eq!(ldk_invoice.signing_pubkey(), wrapped_invoice.signing_pubkey()); + + assert_eq!(ldk_invoice.created_at().as_secs(), wrapped_invoice.created_at()); + + match (ldk_invoice.absolute_expiry(), wrapped_invoice.absolute_expiry_seconds()) { + (Some(ldk_expiry), Some(wrapped_expiry)) => { + assert_eq!(ldk_expiry.as_secs(), wrapped_expiry); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had an absolute expiry but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had an absolute expiry but LDK invoice did not!"); + }, + } + + assert_eq!(ldk_invoice.relative_expiry().as_secs(), wrapped_invoice.relative_expiry()); + + match (ldk_invoice.description(), wrapped_invoice.description()) { + (Some(ldk_desc), Some(wrapped_desc)) => { + assert_eq!(ldk_desc.to_string(), wrapped_desc); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had a description but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had a description but LDK invoice did not!"); + }, + } + + match (ldk_invoice.issuer(), wrapped_invoice.issuer()) { + (Some(ldk_issuer), Some(wrapped_issuer)) => { + assert_eq!(ldk_issuer.to_string(), wrapped_issuer); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had an issuer but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had an issuer but LDK invoice did not!"); + }, + } + + match (ldk_invoice.payer_note(), wrapped_invoice.payer_note()) { + (Some(ldk_note), Some(wrapped_note)) => { + assert_eq!(ldk_note.to_string(), wrapped_note); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had a payer note but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had a payer note but LDK invoice did not!"); + }, + } + + match (ldk_invoice.metadata(), wrapped_invoice.metadata()) { + (Some(ldk_metadata), Some(wrapped_metadata)) => { + assert_eq!(ldk_metadata.as_slice(), wrapped_metadata.as_slice()); + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had metadata but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had metadata but LDK invoice did not!"); + }, + } + + assert_eq!(ldk_invoice.quantity(), wrapped_invoice.quantity()); + + assert_eq!(ldk_invoice.chain().to_bytes().to_vec(), wrapped_invoice.chain()); + + match (ldk_invoice.offer_chains(), wrapped_invoice.offer_chains()) { + (Some(ldk_chains), Some(wrapped_chains)) => { + assert_eq!(ldk_chains.len(), wrapped_chains.len()); + for (i, ldk_chain) in ldk_chains.iter().enumerate() { + assert_eq!(ldk_chain.to_bytes().to_vec(), wrapped_chains[i]); + } + }, + (None, None) => { + // Both fields are missing which is expected behaviour when converting + }, + (Some(_), None) => { + panic!("LDK invoice had offer chains but wrapped invoice did not!"); + }, + (None, Some(_)) => { + panic!("Wrapped invoice had offer chains but LDK invoice did not!"); + }, + } + + let ldk_fallbacks = ldk_invoice.fallbacks(); + let wrapped_fallbacks = wrapped_invoice.fallback_addresses(); + assert_eq!(ldk_fallbacks.len(), wrapped_fallbacks.len()); + for (i, ldk_fallback) in ldk_fallbacks.iter().enumerate() { + assert_eq!(*ldk_fallback, wrapped_fallbacks[i]); + } + + assert_eq!(ldk_invoice.encode(), wrapped_invoice.encode()); + + assert_eq!(ldk_invoice.signable_hash().to_vec(), wrapped_invoice.signable_hash()); + } } diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 74c3ac45fd..b9efa3241d 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -17,7 +17,6 @@ use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, Payme use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; -use lightning::offers::invoice::Bolt12Invoice; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::util::string::UntrustedString; @@ -28,6 +27,11 @@ use std::num::NonZeroU64; use std::sync::{Arc, RwLock}; use std::time::{Duration, SystemTime, UNIX_EPOCH}; +#[cfg(not(feature = "uniffi"))] +type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; +#[cfg(feature = "uniffi")] +type Bolt12Invoice = Arc; + #[cfg(not(feature = "uniffi"))] type Offer = LdkOffer; #[cfg(feature = "uniffi")] @@ -340,6 +344,7 @@ impl Bolt12Payment { /// retrieve the refund). /// /// [`Refund`]: lightning::offers::refund::Refund + /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice pub fn request_refund_payment(&self, refund: &Refund) -> Result { let refund = maybe_deref(refund); let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| { @@ -369,7 +374,7 @@ impl Bolt12Payment { self.payment_store.insert(payment)?; - Ok(invoice) + Ok(maybe_wrap(invoice)) } /// Returns a [`Refund`] object that can be used to offer a refund payment of the amount given. From 520e5aa05f686b5ffd365f1769d96bbe26a0b08c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Jun 2025 13:37:02 +0200 Subject: [PATCH 076/177] Export trait impls in FFI Previously, we moved from a `String` representation to a 'full' `Bolt11Invoice` type. However, we forgot to expose the `Display` implementation in the FFI, leaving now way to retrieve the invoice string. Here, we fix this oversight, and also make a few related changes. --- bindings/ldk_node.udl | 1 + src/ffi/types.rs | 2 +- src/payment/unified_qr.rs | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 505f0db8d5..e914fd00ed 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -715,6 +715,7 @@ dictionary RouteHintHop { RoutingFees fees; }; +[Traits=(Debug, Display, Eq)] interface Bolt11Invoice { [Throws=NodeError, Name=from_str] constructor([ByRef] string invoice_str); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index bbf7302110..d35f2aa2ee 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -926,7 +926,7 @@ impl From for RouteHintHop { /// Represents a syntactically and semantically correct lightning BOLT11 invoice. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Bolt11Invoice { - pub inner: LdkBolt11Invoice, + pub(crate) inner: LdkBolt11Invoice, } impl Bolt11Invoice { diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index 125e1d09be..af5ee1c7b2 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -188,6 +188,7 @@ impl UnifiedQrPayment { /// [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki /// [`PaymentId`]: lightning::ln::channelmanager::PaymentId /// [`Txid`]: bitcoin::hash_types::Txid +#[derive(Debug)] pub enum QrPaymentResult { /// An on-chain payment. Onchain { From eff4d77962c652595f5b4d4a80e6e59bbc7c5ada Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 17 Jun 2025 15:15:43 +0200 Subject: [PATCH 077/177] Avoid collision in `Bolt11Invoice::description` When exporting `Display` to bindings, Swift will add a `description` method to the respective object. Unfortunately, this collides with `Bolt11Invoice::description`, which we therefore rename to `Bolt11Invoice::invoice_description`. --- bindings/ldk_node.udl | 2 +- src/ffi/types.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index e914fd00ed..36767b7901 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -728,7 +728,7 @@ interface Bolt11Invoice { u64 seconds_until_expiry(); boolean is_expired(); boolean would_expire(u64 at_time_seconds); - Bolt11InvoiceDescription description(); + Bolt11InvoiceDescription invoice_description(); u64 min_final_cltv_expiry_delta(); Network network(); Currency currency(); diff --git a/src/ffi/types.rs b/src/ffi/types.rs index d35f2aa2ee..c65bb05992 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -985,7 +985,7 @@ impl Bolt11Invoice { } /// Return the description or a hash of it for longer ones - pub fn description(&self) -> Bolt11InvoiceDescription { + pub fn invoice_description(&self) -> Bolt11InvoiceDescription { self.inner.description().into() } From 9a8254d937a9f3b24126b3527645744713323960 Mon Sep 17 00:00:00 2001 From: Andrei Date: Thu, 19 Jun 2025 00:00:00 +0000 Subject: [PATCH 078/177] Log background sync of RGS message with info level Other messages like on-chain wallet sync or fee rate cache update logged with info level --- src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e80ca964d3..b09f9a9f77 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,7 +146,9 @@ use types::{ }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; -use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +#[cfg(tokio_unstable)] +use logger::log_trace; +use logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use lightning::chain::BestBlock; use lightning::events::bump_transaction::Wallet as LdkWallet; @@ -285,7 +287,7 @@ impl Node { let now = Instant::now(); match gossip_source.update_rgs_snapshot().await { Ok(updated_timestamp) => { - log_trace!( + log_info!( gossip_sync_logger, "Background sync of RGS gossip data finished in {}ms.", now.elapsed().as_millis() From 6b9751bd140606e5780224fa3ec8e9dea523aa08 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 19 Jun 2025 14:06:43 +0200 Subject: [PATCH 079/177] Fix test code after `Bolt11Invoice::invoice_description` rename .. which we forgot when we made the change. --- src/ffi/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/ffi/types.rs b/src/ffi/types.rs index c65bb05992..984e4da8fb 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -1262,7 +1262,7 @@ mod tests { let (ldk_invoice, wrapped_invoice) = create_test_bolt11_invoice(); let ldk_description = ldk_invoice.description(); - let wrapped_description = wrapped_invoice.description(); + let wrapped_description = wrapped_invoice.invoice_description(); match (ldk_description, &wrapped_description) { ( From efa5b735de912e2a3c06d2474ca5186701e5c07c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 19 Jun 2025 13:09:58 +0200 Subject: [PATCH 080/177] Update `CHANGELOG.md` for v0.6.1 --- CHANGELOG.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 29b6f748c2..fe613a07ba 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,21 @@ +# 0.6.1 - Jun. 19, 2025 +This patch release fixes minor issues with the recently-exposed `Bolt11Invoice` +type in bindings. + +## Feature and API updates +- The `Bolt11Invoice::description` method is now exposed as + `Bolt11Invoice::invoice_description` in bindings, to avoid collisions with a + Swift standard method of same name (#576) + +## Bug Fixes and Improvements +- The `Display` implementation of `Bolt11Invoice` is now exposed in bindings, + (re-)allowing to render the invoice as a string. (#574) + +In total, this release features 9 files changed, 549 insertions, 83 deletions, +in 8 commits from 1 author in alphabetical order: + +- Elias Rohrer + # 0.6.0 - Jun. 9, 2025 This sixth minor release mainly fixes an issue that could have left the on-chain wallet unable to spend funds if transactions that had previously been From ec2b24d664b7a3186418526e3062d53d0b88a1f7 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 19 Jun 2025 13:25:13 +0200 Subject: [PATCH 081/177] Update Swift files for v0.6.1 --- Package.swift | 4 +- bindings/swift/Sources/LDKNode/LDKNode.swift | 52 +++++++++++++++----- 2 files changed, 42 insertions(+), 14 deletions(-) diff --git a/Package.swift b/Package.swift index 33c5a70b87..78a38f294c 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.6.0" -let checksum = "8bda396624134e0b592bfcc2f977b9aa5ce8c2ee359c032ae3520869ece8851c" +let tag = "v0.6.1" +let checksum = "73f53b615d5bfdf76f2e7233bde17a2a62631292ce506763a7150344230859c8" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( diff --git a/bindings/swift/Sources/LDKNode/LDKNode.swift b/bindings/swift/Sources/LDKNode/LDKNode.swift index 442201d31f..20ad658d7b 100644 --- a/bindings/swift/Sources/LDKNode/LDKNode.swift +++ b/bindings/swift/Sources/LDKNode/LDKNode.swift @@ -517,12 +517,12 @@ public protocol Bolt11InvoiceProtocol : AnyObject { func currency() -> Currency - func description() -> Bolt11InvoiceDescription - func expiryTimeSeconds() -> UInt64 func fallbackAddresses() -> [Address] + func invoiceDescription() -> Bolt11InvoiceDescription + func isExpired() -> Bool func minFinalCltvExpiryDelta() -> UInt64 @@ -548,6 +548,9 @@ public protocol Bolt11InvoiceProtocol : AnyObject { } open class Bolt11Invoice: + CustomDebugStringConvertible, + CustomStringConvertible, + Equatable, Bolt11InvoiceProtocol { fileprivate let pointer: UnsafeMutableRawPointer! @@ -610,13 +613,6 @@ open func currency() -> Currency { }) } -open func description() -> Bolt11InvoiceDescription { - return try! FfiConverterTypeBolt11InvoiceDescription.lift(try! rustCall() { - uniffi_ldk_node_fn_method_bolt11invoice_description(self.uniffiClonePointer(),$0 - ) -}) -} - open func expiryTimeSeconds() -> UInt64 { return try! FfiConverterUInt64.lift(try! rustCall() { uniffi_ldk_node_fn_method_bolt11invoice_expiry_time_seconds(self.uniffiClonePointer(),$0 @@ -631,6 +627,13 @@ open func fallbackAddresses() -> [Address] { }) } +open func invoiceDescription() -> Bolt11InvoiceDescription { + return try! FfiConverterTypeBolt11InvoiceDescription.lift(try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_invoice_description(self.uniffiClonePointer(),$0 + ) +}) +} + open func isExpired() -> Bool { return try! FfiConverterBool.lift(try! rustCall() { uniffi_ldk_node_fn_method_bolt11invoice_is_expired(self.uniffiClonePointer(),$0 @@ -709,6 +712,31 @@ open func wouldExpire(atTimeSeconds: UInt64) -> Bool { }) } + open var debugDescription: String { + return try! FfiConverterString.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_debug(self.uniffiClonePointer(),$0 + ) +} + ) + } + open var description: String { + return try! FfiConverterString.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_display(self.uniffiClonePointer(),$0 + ) +} + ) + } + public static func == (self: Bolt11Invoice, other: Bolt11Invoice) -> Bool { + return try! FfiConverterBool.lift( + try! rustCall() { + uniffi_ldk_node_fn_method_bolt11invoice_uniffi_trait_eq_eq(self.uniffiClonePointer(), + FfiConverterTypeBolt11Invoice.lower(other),$0 + ) +} + ) + } } @@ -9451,15 +9479,15 @@ private var initializationResult: InitializationResult { if (uniffi_ldk_node_checksum_method_bolt11invoice_currency() != 32179) { return InitializationResult.apiChecksumMismatch } - if (uniffi_ldk_node_checksum_method_bolt11invoice_description() != 9887) { - return InitializationResult.apiChecksumMismatch - } if (uniffi_ldk_node_checksum_method_bolt11invoice_expiry_time_seconds() != 23625) { return InitializationResult.apiChecksumMismatch } if (uniffi_ldk_node_checksum_method_bolt11invoice_fallback_addresses() != 55276) { return InitializationResult.apiChecksumMismatch } + if (uniffi_ldk_node_checksum_method_bolt11invoice_invoice_description() != 395) { + return InitializationResult.apiChecksumMismatch + } if (uniffi_ldk_node_checksum_method_bolt11invoice_is_expired() != 15932) { return InitializationResult.apiChecksumMismatch } From 1199f7d47d42ced6938ddbaf534db09ba7862c46 Mon Sep 17 00:00:00 2001 From: Andrei Date: Fri, 27 Jun 2025 00:00:00 +0000 Subject: [PATCH 082/177] Set log target to module path The `log!()` macro defaults to using the module path as the log target when no target is specified. Explicitly setting the log target to the module path enables better integration with other logging crates, such as `env_logger`, allowing them to filter logs by target (e.g., via `env_logger::Builder::parse_filters()`). --- src/logger.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/logger.rs b/src/logger.rs index d357f018d9..bbd24ec207 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -153,6 +153,7 @@ impl LogWriter for Writer { #[cfg(not(feature = "uniffi"))] log::logger().log( &builder + .target(record.module_path) .module_path(Some(record.module_path)) .line(Some(record.line)) .args(format_args!("{}", record.args)) @@ -161,6 +162,7 @@ impl LogWriter for Writer { #[cfg(feature = "uniffi")] log::logger().log( &builder + .target(&record.module_path) .module_path(Some(&record.module_path)) .line(Some(record.line)) .args(format_args!("{}", record.args)) From 3954355a7535811378be8d386d4dad2e18add8db Mon Sep 17 00:00:00 2001 From: tosynthegeek Date: Mon, 30 Jun 2025 09:04:34 +0100 Subject: [PATCH 083/177] Add exponential backoff for sync failures Previously, chain synchronization failures would retry immediately without any delay, which could lead to tight retry loops and high CPU usage during failures. This change introduces exponential backoff for transient errors, starting at 2 seconds and doubling each time up to a maximum of 300 seconds. Persistent errors also now delay retries by the maximum backoff duration to prevent rapid loops while maintaining eventual recovery. Fixes #587 --- src/chain/mod.rs | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index fac8b0e6cc..df10ecac21 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -36,7 +36,7 @@ use lightning_transaction_sync::EsploraSyncClient; use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; -use lightning_block_sync::SpvClient; +use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; use bdk_esplora::EsploraAsyncExt; use bdk_wallet::Update as BdkUpdate; @@ -425,6 +425,9 @@ impl ChainSource { "Starting initial synchronization of chain listeners. This might take a while..", ); + let mut backoff = CHAIN_POLLING_INTERVAL_SECS; + const MAX_BACKOFF_SECS: u64 = 300; + loop { let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; @@ -504,8 +507,24 @@ impl ChainSource { Err(e) => { log_error!(logger, "Failed to synchronize chain listeners: {:?}", e); - tokio::time::sleep(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)) - .await; + if e.kind() == BlockSourceErrorKind::Transient { + log_info!( + logger, + "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + backoff + ); + tokio::time::sleep(Duration::from_secs(backoff)).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); + } else { + log_error!( + logger, + "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + MAX_BACKOFF_SECS + ); + tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; + } }, } } From 948f289d7756c98d21f43682c193852c039bd2b9 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Mon, 30 Jun 2025 21:52:19 -0300 Subject: [PATCH 084/177] Fix CLN crash by waiting for block height sync before channel open (#527) --- tests/integration_tests_cln.rs | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index b6300576c0..f77311fb2b 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -64,7 +64,17 @@ fn test_cln() { // Setup CLN let sock = "/tmp/lightning-rpc"; let cln_client = LightningRPC::new(&sock); - let cln_info = cln_client.getinfo().unwrap(); + let cln_info = { + loop { + let info = cln_client.getinfo().unwrap(); + // Wait for CLN to sync block height before channel open. + // Prevents crash due to unset blockheight (see LDK Node issue #527). + if info.blockheight > 0 { + break info; + } + std::thread::sleep(std::time::Duration::from_millis(250)); + } + }; let cln_node_id = PublicKey::from_str(&cln_info.id).unwrap(); let cln_address: SocketAddress = match cln_info.binding.first().unwrap() { NetworkAddress::Ipv4 { address, port } => { From c1f27ef4f22b390138d5a4cf30a5f7aa218eee91 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Thu, 19 Jun 2025 18:00:00 +0100 Subject: [PATCH 085/177] prefactor: rename bitcoind_rpc to bitcoind Since we unify RPC and REST sync clients in the following commit, we rename bitcoind_rpc to bitcoind as this appropriately captures the unified client. --- src/chain/{bitcoind_rpc.rs => bitcoind.rs} | 0 src/chain/mod.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/chain/{bitcoind_rpc.rs => bitcoind.rs} (100%) diff --git a/src/chain/bitcoind_rpc.rs b/src/chain/bitcoind.rs similarity index 100% rename from src/chain/bitcoind_rpc.rs rename to src/chain/bitcoind.rs diff --git a/src/chain/mod.rs b/src/chain/mod.rs index df10ecac21..4d91ffe869 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -5,10 +5,10 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -mod bitcoind_rpc; +mod bitcoind; mod electrum; -use crate::chain::bitcoind_rpc::{ +use crate::chain::bitcoind::{ BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; use crate::chain::electrum::ElectrumRuntimeClient; From b7de5f82e087b27274c02988fd8364e8a778b0d0 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Mon, 30 Jun 2025 23:08:27 +0100 Subject: [PATCH 086/177] prefactor: prepare client for REST interface support In the next commit, we'll introduce a unified bitcoind client with two variant - RPC and REST. In preparation to support chain syncing via the REST interface, we refactor some methods into helper functions specific to the interface client. --- src/chain/bitcoind.rs | 108 ++++++++++++++++++++++++++++++++++-------- 1 file changed, 89 insertions(+), 19 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 3ca2c221fd..bcef474cf6 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -54,17 +54,31 @@ impl BitcoindRpcClient { } pub(crate) async fn broadcast_transaction(&self, tx: &Transaction) -> std::io::Result { + Self::broadcast_transaction_inner(self.rpc_client(), tx).await + } + + async fn broadcast_transaction_inner( + rpc_client: Arc, tx: &Transaction, + ) -> std::io::Result { let tx_serialized = bitcoin::consensus::encode::serialize_hex(tx); let tx_json = serde_json::json!(tx_serialized); - self.rpc_client.call_method::("sendrawtransaction", &[tx_json]).await + rpc_client.call_method::("sendrawtransaction", &[tx_json]).await } pub(crate) async fn get_fee_estimate_for_target( &self, num_blocks: usize, estimation_mode: FeeRateEstimationMode, + ) -> std::io::Result { + Self::get_fee_estimate_for_target_inner(self.rpc_client(), num_blocks, estimation_mode) + .await + } + + /// Estimate the fee rate for the provided target number of blocks. + async fn get_fee_estimate_for_target_inner( + rpc_client: Arc, num_blocks: usize, estimation_mode: FeeRateEstimationMode, ) -> std::io::Result { let num_blocks_json = serde_json::json!(num_blocks); let estimation_mode_json = serde_json::json!(estimation_mode); - self.rpc_client + rpc_client .call_method::( "estimatesmartfee", &[num_blocks_json, estimation_mode_json], @@ -74,7 +88,14 @@ impl BitcoindRpcClient { } pub(crate) async fn get_mempool_minimum_fee_rate(&self) -> std::io::Result { - self.rpc_client + Self::get_mempool_minimum_fee_rate_rpc(self.rpc_client()).await + } + + /// Get the minimum mempool fee rate via RPC interface. + async fn get_mempool_minimum_fee_rate_rpc( + rpc_client: Arc, + ) -> std::io::Result { + rpc_client .call_method::("getmempoolinfo", &[]) .await .map(|resp| resp.0) @@ -82,11 +103,17 @@ impl BitcoindRpcClient { pub(crate) async fn get_raw_transaction( &self, txid: &Txid, + ) -> std::io::Result> { + Self::get_raw_transaction_rpc(self.rpc_client(), txid).await + } + + /// Retrieve raw transaction for provided transaction ID via the RPC interface. + async fn get_raw_transaction_rpc( + rpc_client: Arc, txid: &Txid, ) -> std::io::Result> { let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); let txid_json = serde_json::json!(txid_hex); - match self - .rpc_client + match rpc_client .call_method::("getrawtransaction", &[txid_json]) .await { @@ -119,8 +146,13 @@ impl BitcoindRpcClient { } pub(crate) async fn get_raw_mempool(&self) -> std::io::Result> { + Self::get_raw_mempool_rpc(self.rpc_client()).await + } + + /// Retrieves the raw mempool via the RPC interface. + async fn get_raw_mempool_rpc(rpc_client: Arc) -> std::io::Result> { let verbose_flag_json = serde_json::json!(false); - self.rpc_client + rpc_client .call_method::("getrawmempool", &[verbose_flag_json]) .await .map(|resp| resp.0) @@ -128,15 +160,19 @@ impl BitcoindRpcClient { pub(crate) async fn get_mempool_entry( &self, txid: Txid, + ) -> std::io::Result> { + Self::get_mempool_entry_inner(self.rpc_client(), txid).await + } + + /// Retrieves the mempool entry of the provided transaction ID. + async fn get_mempool_entry_inner( + client: Arc, txid: Txid, ) -> std::io::Result> { let txid_hex = bitcoin::consensus::encode::serialize_hex(&txid); let txid_json = serde_json::json!(txid_hex); - match self - .rpc_client - .call_method::("getmempoolentry", &[txid_json]) - .await - { - Ok(resp) => Ok(Some(MempoolEntry { txid, height: resp.height, time: resp.time })), + + match client.call_method::("getmempoolentry", &[txid_json]).await { + Ok(resp) => Ok(Some(MempoolEntry { txid, time: resp.time, height: resp.height })), Err(e) => match e.into_inner() { Some(inner) => { let rpc_error_res: Result, _> = inner.downcast(); @@ -165,9 +201,15 @@ impl BitcoindRpcClient { } pub(crate) async fn update_mempool_entries_cache(&self) -> std::io::Result<()> { + self.update_mempool_entries_cache_inner(&self.mempool_entries_cache).await + } + + async fn update_mempool_entries_cache_inner( + &self, mempool_entries_cache: &tokio::sync::Mutex>, + ) -> std::io::Result<()> { let mempool_txids = self.get_raw_mempool().await?; - let mut mempool_entries_cache = self.mempool_entries_cache.lock().await; + let mut mempool_entries_cache = mempool_entries_cache.lock().await; mempool_entries_cache.retain(|txid, _| mempool_txids.contains(txid)); if let Some(difference) = mempool_txids.len().checked_sub(mempool_entries_cache.capacity()) @@ -210,13 +252,28 @@ impl BitcoindRpcClient { async fn get_mempool_transactions_and_timestamp_at_height( &self, best_processed_height: u32, ) -> std::io::Result> { - let prev_mempool_time = self.latest_mempool_timestamp.load(Ordering::Relaxed); + self.get_mempool_transactions_and_timestamp_at_height_inner( + &self.latest_mempool_timestamp, + &self.mempool_entries_cache, + &self.mempool_txs_cache, + best_processed_height, + ) + .await + } + + async fn get_mempool_transactions_and_timestamp_at_height_inner( + &self, latest_mempool_timestamp: &AtomicU64, + mempool_entries_cache: &tokio::sync::Mutex>, + mempool_txs_cache: &tokio::sync::Mutex>, + best_processed_height: u32, + ) -> std::io::Result> { + let prev_mempool_time = latest_mempool_timestamp.load(Ordering::Relaxed); let mut latest_time = prev_mempool_time; self.update_mempool_entries_cache().await?; - let mempool_entries_cache = self.mempool_entries_cache.lock().await; - let mut mempool_txs_cache = self.mempool_txs_cache.lock().await; + let mempool_entries_cache = mempool_entries_cache.lock().await; + let mut mempool_txs_cache = mempool_txs_cache.lock().await; mempool_txs_cache.retain(|txid, _| mempool_entries_cache.contains_key(txid)); if let Some(difference) = @@ -260,7 +317,7 @@ impl BitcoindRpcClient { } if !txs_to_emit.is_empty() { - self.latest_mempool_timestamp.store(latest_time, Ordering::Release); + latest_mempool_timestamp.store(latest_time, Ordering::Release); } Ok(txs_to_emit) } @@ -272,8 +329,21 @@ impl BitcoindRpcClient { async fn get_evicted_mempool_txids_and_timestamp( &self, unconfirmed_txids: Vec, ) -> std::io::Result> { - let latest_mempool_timestamp = self.latest_mempool_timestamp.load(Ordering::Relaxed); - let mempool_entries_cache = self.mempool_entries_cache.lock().await; + self.get_evicted_mempool_txids_and_timestamp_inner( + &self.latest_mempool_timestamp, + &self.mempool_entries_cache, + unconfirmed_txids, + ) + .await + } + + async fn get_evicted_mempool_txids_and_timestamp_inner( + &self, latest_mempool_timestamp: &AtomicU64, + mempool_entries_cache: &tokio::sync::Mutex>, + unconfirmed_txids: Vec, + ) -> std::io::Result> { + let latest_mempool_timestamp = latest_mempool_timestamp.load(Ordering::Relaxed); + let mempool_entries_cache = mempool_entries_cache.lock().await; let evicted_txids = unconfirmed_txids .into_iter() .filter(|txid| mempool_entries_cache.contains_key(txid)) From 36e4c7f5e10a564c71ca861e8d2b38013c6db244 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Tue, 1 Jul 2025 01:49:36 +0100 Subject: [PATCH 087/177] feat: support chain sourcing via REST interface This commit: - Adds support for syncing data via Bitcoin Core's REST interface. - Unifies the REST and RPC Bitcoin Core API clients --- Cargo.toml | 2 +- bindings/ldk_node.udl | 1 + docker-compose.yml | 5 +- src/builder.rs | 126 +++++++++-- src/chain/bitcoind.rs | 369 +++++++++++++++++++++++++++----- src/chain/mod.rs | 118 ++++++---- src/config.rs | 9 + src/lib.rs | 2 +- tests/common/mod.rs | 23 +- tests/integration_tests_rust.rs | 12 +- 10 files changed, 553 insertions(+), 114 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf8bed08c7..96e47b2602 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -35,7 +35,7 @@ lightning-net-tokio = { version = "0.1.0" } lightning-persister = { version = "0.1.0" } lightning-background-processor = { version = "0.1.0", features = ["futures"] } lightning-rapid-gossip-sync = { version = "0.1.0" } -lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "tokio"] } +lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "rest-client", "tokio"] } lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } lightning-liquidity = { version = "0.1.0", features = ["std"] } diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 36767b7901..3c240b43c3 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -78,6 +78,7 @@ interface Builder { void set_chain_source_esplora(string server_url, EsploraSyncConfig? config); void set_chain_source_electrum(string server_url, ElectrumSyncConfig? config); void set_chain_source_bitcoind_rpc(string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); + void set_chain_source_bitcoind_rest(string rest_host, u16 rest_port, string rpc_host, u16 rpc_port, string rpc_user, string rpc_password); void set_gossip_source_p2p(); void set_gossip_source_rgs(string rgs_server_url); void set_liquidity_source_lsps1(PublicKey node_id, SocketAddress address, string? token); diff --git a/docker-compose.yml b/docker-compose.yml index 425dc129a0..e71fd70fba 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -13,10 +13,11 @@ services: "-rpcbind=0.0.0.0", "-rpcuser=user", "-rpcpassword=pass", - "-fallbackfee=0.00001" + "-fallbackfee=0.00001", + "-rest" ] ports: - - "18443:18443" # Regtest RPC port + - "18443:18443" # Regtest REST and RPC port - "18444:18444" # Regtest P2P port networks: - bitcoin-electrs diff --git a/src/builder.rs b/src/builder.rs index 31a0fee456..a177768f63 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,8 +7,9 @@ use crate::chain::{ChainSource, DEFAULT_ESPLORA_SERVER_URL}; use crate::config::{ - default_user_config, may_announce_channel, AnnounceError, Config, ElectrumSyncConfig, - EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AnnounceError, BitcoindRestClientConfig, Config, + ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, + WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; @@ -84,9 +85,21 @@ const LSPS_HARDENED_CHILD_INDEX: u32 = 577; #[derive(Debug, Clone)] enum ChainDataSourceConfig { - Esplora { server_url: String, sync_config: Option }, - Electrum { server_url: String, sync_config: Option }, - BitcoindRpc { rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String }, + Esplora { + server_url: String, + sync_config: Option, + }, + Electrum { + server_url: String, + sync_config: Option, + }, + Bitcoind { + rpc_host: String, + rpc_port: u16, + rpc_user: String, + rpc_password: String, + rest_client_config: Option, + }, } #[derive(Debug, Clone)] @@ -299,13 +312,48 @@ impl NodeBuilder { self } - /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC - /// endpoint. + /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. + /// + /// This method establishes an RPC connection that enables all essential chain operations including + /// transaction broadcasting and chain data synchronization. + /// + /// ## Parameters: + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection. pub fn set_chain_source_bitcoind_rpc( &mut self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, ) -> &mut Self { - self.chain_data_source_config = - Some(ChainDataSourceConfig::BitcoindRpc { rpc_host, rpc_port, rpc_user, rpc_password }); + self.chain_data_source_config = Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config: None, + }); + self + } + + /// Configures the [`Node`] instance to synchronize chain data from a Bitcoin Core REST endpoint. + /// + /// This method enables chain data synchronization via Bitcoin Core's REST interface. We pass + /// additional RPC configuration to non-REST-supported API calls like transaction broadcasting. + /// + /// ## Parameters: + /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection. + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection + pub fn set_chain_source_bitcoind_rest( + &mut self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, + rpc_user: String, rpc_password: String, + ) -> &mut Self { + self.chain_data_source_config = Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config: Some(BitcoindRestClientConfig { rest_host, rest_port }), + }); + self } @@ -716,8 +764,14 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_chain_source_electrum(server_url, sync_config); } - /// Configures the [`Node`] instance to source its chain data from the given Bitcoin Core RPC - /// endpoint. + /// Configures the [`Node`] instance to connect to a Bitcoin Core node via RPC. + /// + /// This method establishes an RPC connection that enables all essential chain operations including + /// transaction broadcasting and chain data synchronization. + /// + /// ## Parameters: + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection. pub fn set_chain_source_bitcoind_rpc( &self, rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, ) { @@ -729,6 +783,29 @@ impl ArcedNodeBuilder { ); } + /// Configures the [`Node`] instance to synchronize chain data from a Bitcoin Core REST endpoint. + /// + /// This method enables chain data synchronization via Bitcoin Core's REST interface. We pass + /// additional RPC configuration to non-REST-supported API calls like transaction broadcasting. + /// + /// ## Parameters: + /// * `rest_host`, `rest_port` - Required parameters for the Bitcoin Core REST connection. + /// * `rpc_host`, `rpc_port`, `rpc_user`, `rpc_password` - Required parameters for the Bitcoin Core RPC + /// connection + pub fn set_chain_source_bitcoind_rest( + &self, rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, + rpc_user: String, rpc_password: String, + ) { + self.inner.write().unwrap().set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + ); + } + /// Configures the [`Node`] instance to source its gossip data from the Lightning peer-to-peer /// network. pub fn set_gossip_source_p2p(&self) { @@ -1068,8 +1145,14 @@ fn build_with_store_internal( Arc::clone(&node_metrics), )) }, - Some(ChainDataSourceConfig::BitcoindRpc { rpc_host, rpc_port, rpc_user, rpc_password }) => { - Arc::new(ChainSource::new_bitcoind_rpc( + Some(ChainDataSourceConfig::Bitcoind { + rpc_host, + rpc_port, + rpc_user, + rpc_password, + rest_client_config, + }) => match rest_client_config { + Some(rest_client_config) => Arc::new(ChainSource::new_bitcoind_rest( rpc_host.clone(), *rpc_port, rpc_user.clone(), @@ -1079,10 +1162,25 @@ fn build_with_store_internal( Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), + rest_client_config.clone(), Arc::clone(&logger), Arc::clone(&node_metrics), - )) + )), + None => Arc::new(ChainSource::new_bitcoind_rpc( + rpc_host.clone(), + *rpc_port, + rpc_user.clone(), + rpc_password.clone(), + Arc::clone(&wallet), + Arc::clone(&fee_estimator), + Arc::clone(&tx_broadcaster), + Arc::clone(&kv_store), + Arc::clone(&config), + Arc::clone(&logger), + Arc::clone(&node_metrics), + )), }, + None => { // Default to Esplora client. let server_url = DEFAULT_ESPLORA_SERVER_URL.to_string(); diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index bcef474cf6..98e77cac7c 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -7,11 +7,14 @@ use crate::types::{ChainMonitor, ChannelManager, Sweeper, Wallet}; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; use lightning::chain::Listen; - -use lightning_block_sync::http::HttpEndpoint; -use lightning_block_sync::http::JsonResponse; +use lightning_block_sync::gossip::UtxoSource; +use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::poll::ValidatedBlockHeader; +use lightning_block_sync::rest::RestClient; use lightning_block_sync::rpc::{RpcClient, RpcError}; use lightning_block_sync::{ AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache, @@ -19,26 +22,31 @@ use lightning_block_sync::{ use serde::Serialize; -use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; - -use base64::prelude::{Engine, BASE64_STANDARD}; - use std::collections::{HashMap, VecDeque}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; -pub struct BitcoindRpcClient { - rpc_client: Arc, - latest_mempool_timestamp: AtomicU64, - mempool_entries_cache: tokio::sync::Mutex>, - mempool_txs_cache: tokio::sync::Mutex>, +pub enum BitcoindClient { + Rpc { + rpc_client: Arc, + latest_mempool_timestamp: AtomicU64, + mempool_entries_cache: tokio::sync::Mutex>, + mempool_txs_cache: tokio::sync::Mutex>, + }, + Rest { + rest_client: Arc, + rpc_client: Arc, + latest_mempool_timestamp: AtomicU64, + mempool_entries_cache: tokio::sync::Mutex>, + mempool_txs_cache: tokio::sync::Mutex>, + }, } -impl BitcoindRpcClient { - pub(crate) fn new(host: String, port: u16, rpc_user: String, rpc_password: String) -> Self { - let http_endpoint = HttpEndpoint::for_host(host.clone()).with_port(port); - let rpc_credentials = - BASE64_STANDARD.encode(format!("{}:{}", rpc_user.clone(), rpc_password.clone())); +impl BitcoindClient { + /// Creates a new RPC API client for the chain interactions with Bitcoin Core. + pub(crate) fn new_rpc(host: String, port: u16, rpc_user: String, rpc_password: String) -> Self { + let http_endpoint = endpoint(host, port); + let rpc_credentials = rpc_credentials(rpc_user, rpc_password); let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, http_endpoint)); @@ -46,15 +54,60 @@ impl BitcoindRpcClient { let mempool_entries_cache = tokio::sync::Mutex::new(HashMap::new()); let mempool_txs_cache = tokio::sync::Mutex::new(HashMap::new()); - Self { rpc_client, latest_mempool_timestamp, mempool_entries_cache, mempool_txs_cache } + Self::Rpc { rpc_client, latest_mempool_timestamp, mempool_entries_cache, mempool_txs_cache } } - pub(crate) fn rpc_client(&self) -> Arc { - Arc::clone(&self.rpc_client) + /// Creates a new, primarily REST API client for the chain interactions + /// with Bitcoin Core. + /// + /// Aside the required REST host and port, we provide RPC configuration + /// options for necessary calls not supported by the REST interface. + pub(crate) fn new_rest( + rest_host: String, rest_port: u16, rpc_host: String, rpc_port: u16, rpc_user: String, + rpc_password: String, + ) -> Self { + let rest_endpoint = endpoint(rest_host, rest_port).with_path("/rest".to_string()); + let rest_client = Arc::new(RestClient::new(rest_endpoint)); + + let rpc_endpoint = endpoint(rpc_host, rpc_port); + let rpc_credentials = rpc_credentials(rpc_user, rpc_password); + let rpc_client = Arc::new(RpcClient::new(&rpc_credentials, rpc_endpoint)); + + let latest_mempool_timestamp = AtomicU64::new(0); + + let mempool_entries_cache = tokio::sync::Mutex::new(HashMap::new()); + let mempool_txs_cache = tokio::sync::Mutex::new(HashMap::new()); + + Self::Rest { + rest_client, + rpc_client, + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + } } + pub(crate) fn utxo_source(&self) -> Arc { + match self { + BitcoindClient::Rpc { rpc_client, .. } => Arc::clone(rpc_client) as Arc, + BitcoindClient::Rest { rest_client, .. } => { + Arc::clone(rest_client) as Arc + }, + } + } + + /// Broadcasts the provided transaction. pub(crate) async fn broadcast_transaction(&self, tx: &Transaction) -> std::io::Result { - Self::broadcast_transaction_inner(self.rpc_client(), tx).await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::broadcast_transaction_inner(Arc::clone(rpc_client), tx).await + }, + BitcoindClient::Rest { rpc_client, .. } => { + // Bitcoin Core's REST interface does not support broadcasting transactions + // so we use the RPC client. + Self::broadcast_transaction_inner(Arc::clone(rpc_client), tx).await + }, + } } async fn broadcast_transaction_inner( @@ -65,11 +118,31 @@ impl BitcoindRpcClient { rpc_client.call_method::("sendrawtransaction", &[tx_json]).await } + /// Retrieve the fee estimate needed for a transaction to begin + /// confirmation within the provided `num_blocks`. pub(crate) async fn get_fee_estimate_for_target( &self, num_blocks: usize, estimation_mode: FeeRateEstimationMode, ) -> std::io::Result { - Self::get_fee_estimate_for_target_inner(self.rpc_client(), num_blocks, estimation_mode) - .await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_fee_estimate_for_target_inner( + Arc::clone(rpc_client), + num_blocks, + estimation_mode, + ) + .await + }, + BitcoindClient::Rest { rpc_client, .. } => { + // We rely on the internal RPC client to make this call, as this + // operation is not supported by Bitcoin Core's REST interface. + Self::get_fee_estimate_for_target_inner( + Arc::clone(rpc_client), + num_blocks, + estimation_mode, + ) + .await + }, + } } /// Estimate the fee rate for the provided target number of blocks. @@ -87,11 +160,19 @@ impl BitcoindRpcClient { .map(|resp| resp.0) } + /// Gets the mempool minimum fee rate. pub(crate) async fn get_mempool_minimum_fee_rate(&self) -> std::io::Result { - Self::get_mempool_minimum_fee_rate_rpc(self.rpc_client()).await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_mempool_minimum_fee_rate_rpc(Arc::clone(rpc_client)).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_mempool_minimum_fee_rate_rest(Arc::clone(rest_client)).await + }, + } } - /// Get the minimum mempool fee rate via RPC interface. + /// Get the mempool minimum fee rate via RPC interface. async fn get_mempool_minimum_fee_rate_rpc( rpc_client: Arc, ) -> std::io::Result { @@ -101,10 +182,28 @@ impl BitcoindRpcClient { .map(|resp| resp.0) } + /// Get the mempool minimum fee rate via REST interface. + async fn get_mempool_minimum_fee_rate_rest( + rest_client: Arc, + ) -> std::io::Result { + rest_client + .request_resource::("mempool/info.json") + .await + .map(|resp| resp.0) + } + + /// Gets the raw transaction for the provided transaction ID. Returns `None` if not found. pub(crate) async fn get_raw_transaction( &self, txid: &Txid, ) -> std::io::Result> { - Self::get_raw_transaction_rpc(self.rpc_client(), txid).await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_raw_transaction_rpc(Arc::clone(rpc_client), txid).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_raw_transaction_rest(Arc::clone(rest_client), txid).await + }, + } } /// Retrieve raw transaction for provided transaction ID via the RPC interface. @@ -145,8 +244,68 @@ impl BitcoindRpcClient { } } + /// Retrieve raw transaction for provided transaction ID via the REST interface. + async fn get_raw_transaction_rest( + rest_client: Arc, txid: &Txid, + ) -> std::io::Result> { + let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); + let tx_path = format!("tx/{}.json", txid_hex); + match rest_client + .request_resource::(&tx_path) + .await + { + Ok(resp) => Ok(Some(resp.0)), + Err(e) => match e.kind() { + std::io::ErrorKind::Other => { + match e.into_inner() { + Some(inner) => { + let http_error_res: Result, _> = inner.downcast(); + match http_error_res { + Ok(http_error) => { + // Check if it's the HTTP NOT_FOUND error code. + if &http_error.status_code == "404" { + Ok(None) + } else { + Err(std::io::Error::new( + std::io::ErrorKind::Other, + http_error, + )) + } + }, + Err(_) => { + let error_msg = + format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new( + std::io::ErrorKind::Other, + error_msg.as_str(), + )) + }, + } + }, + None => { + let error_msg = format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) + }, + } + }, + _ => { + let error_msg = format!("Failed to process {} response.", tx_path); + Err(std::io::Error::new(std::io::ErrorKind::Other, error_msg.as_str())) + }, + }, + } + } + + /// Retrieves the raw mempool. pub(crate) async fn get_raw_mempool(&self) -> std::io::Result> { - Self::get_raw_mempool_rpc(self.rpc_client()).await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_raw_mempool_rpc(Arc::clone(rpc_client)).await + }, + BitcoindClient::Rest { rest_client, .. } => { + Self::get_raw_mempool_rest(Arc::clone(rest_client)).await + }, + } } /// Retrieves the raw mempool via the RPC interface. @@ -158,10 +317,28 @@ impl BitcoindRpcClient { .map(|resp| resp.0) } + /// Retrieves the raw mempool via the REST interface. + async fn get_raw_mempool_rest(rest_client: Arc) -> std::io::Result> { + rest_client + .request_resource::( + "mempool/contents.json?verbose=false", + ) + .await + .map(|resp| resp.0) + } + + /// Retrieves an entry from the mempool if it exists, else return `None`. pub(crate) async fn get_mempool_entry( &self, txid: Txid, ) -> std::io::Result> { - Self::get_mempool_entry_inner(self.rpc_client(), txid).await + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Self::get_mempool_entry_inner(Arc::clone(rpc_client), txid).await + }, + BitcoindClient::Rest { rpc_client, .. } => { + Self::get_mempool_entry_inner(Arc::clone(rpc_client), txid).await + }, + } } /// Retrieves the mempool entry of the provided transaction ID. @@ -201,7 +378,14 @@ impl BitcoindRpcClient { } pub(crate) async fn update_mempool_entries_cache(&self) -> std::io::Result<()> { - self.update_mempool_entries_cache_inner(&self.mempool_entries_cache).await + match self { + BitcoindClient::Rpc { mempool_entries_cache, .. } => { + self.update_mempool_entries_cache_inner(mempool_entries_cache).await + }, + BitcoindClient::Rest { mempool_entries_cache, .. } => { + self.update_mempool_entries_cache_inner(mempool_entries_cache).await + }, + } } async fn update_mempool_entries_cache_inner( @@ -249,16 +433,39 @@ impl BitcoindRpcClient { /// This method is an adapted version of `bdk_bitcoind_rpc::Emitter::mempool`. It emits each /// transaction only once, unless we cannot assume the transaction's ancestors are already /// emitted. - async fn get_mempool_transactions_and_timestamp_at_height( + pub(crate) async fn get_mempool_transactions_and_timestamp_at_height( &self, best_processed_height: u32, ) -> std::io::Result> { - self.get_mempool_transactions_and_timestamp_at_height_inner( - &self.latest_mempool_timestamp, - &self.mempool_entries_cache, - &self.mempool_txs_cache, - best_processed_height, - ) - .await + match self { + BitcoindClient::Rpc { + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + .. + } => { + self.get_mempool_transactions_and_timestamp_at_height_inner( + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + best_processed_height, + ) + .await + }, + BitcoindClient::Rest { + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + .. + } => { + self.get_mempool_transactions_and_timestamp_at_height_inner( + latest_mempool_timestamp, + mempool_entries_cache, + mempool_txs_cache, + best_processed_height, + ) + .await + }, + } } async fn get_mempool_transactions_and_timestamp_at_height_inner( @@ -329,16 +536,28 @@ impl BitcoindRpcClient { async fn get_evicted_mempool_txids_and_timestamp( &self, unconfirmed_txids: Vec, ) -> std::io::Result> { - self.get_evicted_mempool_txids_and_timestamp_inner( - &self.latest_mempool_timestamp, - &self.mempool_entries_cache, - unconfirmed_txids, - ) - .await + match self { + BitcoindClient::Rpc { latest_mempool_timestamp, mempool_entries_cache, .. } => { + Self::get_evicted_mempool_txids_and_timestamp_inner( + latest_mempool_timestamp, + mempool_entries_cache, + unconfirmed_txids, + ) + .await + }, + BitcoindClient::Rest { latest_mempool_timestamp, mempool_entries_cache, .. } => { + Self::get_evicted_mempool_txids_and_timestamp_inner( + latest_mempool_timestamp, + mempool_entries_cache, + unconfirmed_txids, + ) + .await + }, + } } async fn get_evicted_mempool_txids_and_timestamp_inner( - &self, latest_mempool_timestamp: &AtomicU64, + latest_mempool_timestamp: &AtomicU64, mempool_entries_cache: &tokio::sync::Mutex>, unconfirmed_txids: Vec, ) -> std::io::Result> { @@ -353,21 +572,42 @@ impl BitcoindRpcClient { } } -impl BlockSource for BitcoindRpcClient { +impl BlockSource for BitcoindClient { fn get_header<'a>( - &'a self, header_hash: &'a BlockHash, height_hint: Option, + &'a self, header_hash: &'a bitcoin::BlockHash, height_hint: Option, ) -> AsyncBlockSourceResult<'a, BlockHeaderData> { - Box::pin(async move { self.rpc_client.get_header(header_hash, height_hint).await }) + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_header(header_hash, height_hint).await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_header(header_hash, height_hint).await }) + }, + } } fn get_block<'a>( - &'a self, header_hash: &'a BlockHash, + &'a self, header_hash: &'a bitcoin::BlockHash, ) -> AsyncBlockSourceResult<'a, BlockData> { - Box::pin(async move { self.rpc_client.get_block(header_hash).await }) + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_block(header_hash).await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_block(header_hash).await }) + }, + } } - fn get_best_block(&self) -> AsyncBlockSourceResult<(BlockHash, Option)> { - Box::pin(async move { self.rpc_client.get_best_block().await }) + fn get_best_block(&self) -> AsyncBlockSourceResult<(bitcoin::BlockHash, Option)> { + match self { + BitcoindClient::Rpc { rpc_client, .. } => { + Box::pin(async move { rpc_client.get_best_block().await }) + }, + BitcoindClient::Rest { rest_client, .. } => { + Box::pin(async move { rest_client.get_best_block().await }) + }, + } } } @@ -395,7 +635,7 @@ impl TryInto for JsonResponse { } } -pub struct MempoolMinFeeResponse(pub FeeRate); +pub(crate) struct MempoolMinFeeResponse(pub FeeRate); impl TryInto for JsonResponse { type Error = std::io::Error; @@ -413,7 +653,7 @@ impl TryInto for JsonResponse { } } -pub struct GetRawTransactionResponse(pub Transaction); +pub(crate) struct GetRawTransactionResponse(pub Transaction); impl TryInto for JsonResponse { type Error = std::io::Error; @@ -600,3 +840,26 @@ impl Listen for ChainListener { self.output_sweeper.block_disconnected(header, height); } } + +pub(crate) fn rpc_credentials(rpc_user: String, rpc_password: String) -> String { + BASE64_STANDARD.encode(format!("{}:{}", rpc_user, rpc_password)) +} + +pub(crate) fn endpoint(host: String, port: u16) -> HttpEndpoint { + HttpEndpoint::for_host(host).with_port(port) +} + +#[derive(Debug)] +pub struct HttpError { + pub(crate) status_code: String, + pub(crate) contents: Vec, +} + +impl std::error::Error for HttpError {} + +impl std::fmt::Display for HttpError { + fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { + let contents = String::from_utf8_lossy(&self.contents); + write!(f, "status_code: {}, contents: {}", self.status_code, contents) + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 4d91ffe869..c3d5fdedc5 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -9,14 +9,15 @@ mod bitcoind; mod electrum; use crate::chain::bitcoind::{ - BitcoindRpcClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, + BitcoindClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; use crate::chain::electrum::ElectrumRuntimeClient; use crate::config::{ - BackgroundSyncConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, - BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, - LDK_WALLET_SYNC_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, - TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, + BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, + RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, + WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use crate::fee_estimator::{ apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, @@ -215,8 +216,8 @@ pub(crate) enum ChainSource { logger: Arc, node_metrics: Arc>, }, - BitcoindRpc { - bitcoind_rpc_client: Arc, + Bitcoind { + api_client: Arc, header_cache: tokio::sync::Mutex, latest_chain_tip: RwLock>, onchain_wallet: Arc, @@ -293,18 +294,23 @@ impl ChainSource { } pub(crate) fn new_bitcoind_rpc( - host: String, port: u16, rpc_user: String, rpc_password: String, + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, onchain_wallet: Arc, fee_estimator: Arc, tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - let bitcoind_rpc_client = - Arc::new(BitcoindRpcClient::new(host, port, rpc_user, rpc_password)); + let api_client = Arc::new(BitcoindClient::new_rpc( + rpc_host.clone(), + rpc_port.clone(), + rpc_user.clone(), + rpc_password.clone(), + )); + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self::BitcoindRpc { - bitcoind_rpc_client, + Self::Bitcoind { + api_client, header_cache, latest_chain_tip, onchain_wallet, @@ -318,6 +324,41 @@ impl ChainSource { } } + pub(crate) fn new_bitcoind_rest( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + rest_client_config: BitcoindRestClientConfig, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rest( + rest_client_config.rest_host, + rest_client_config.rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + + Self::Bitcoind { + api_client, + header_cache, + latest_chain_tip, + wallet_polling_status, + onchain_wallet, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger, + node_metrics, + } + } + pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { match self { Self::Electrum { server_url, electrum_runtime_status, config, logger, .. } => { @@ -348,7 +389,7 @@ impl ChainSource { pub(crate) fn as_utxo_source(&self) -> Option> { match self { - Self::BitcoindRpc { bitcoind_rpc_client, .. } => Some(bitcoind_rpc_client.rpc_client()), + Self::Bitcoind { api_client, .. } => Some(api_client.utxo_source()), _ => None, } } @@ -399,8 +440,8 @@ impl ChainSource { return; } }, - Self::BitcoindRpc { - bitcoind_rpc_client, + Self::Bitcoind { + api_client, header_cache, latest_chain_tip, onchain_wallet, @@ -469,7 +510,7 @@ impl ChainSource { let mut locked_header_cache = header_cache.lock().await; let now = SystemTime::now(); match synchronize_listeners( - bitcoind_rpc_client.as_ref(), + api_client.as_ref(), config.network, &mut *locked_header_cache, chain_listeners.clone(), @@ -836,8 +877,8 @@ impl ChainSource { res }, - Self::BitcoindRpc { .. } => { - // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via + Self::Bitcoind { .. } => { + // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Onchain wallet will be synced via chain polling") }, @@ -1006,8 +1047,8 @@ impl ChainSource { res }, - Self::BitcoindRpc { .. } => { - // In BitcoindRpc mode we sync lightning and onchain wallet in one go by via + Self::Bitcoind { .. } => { + // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Lightning wallet will be synced via chain polling") }, @@ -1029,8 +1070,8 @@ impl ChainSource { // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, - Self::BitcoindRpc { - bitcoind_rpc_client, + Self::Bitcoind { + api_client, header_cache, latest_chain_tip, onchain_wallet, @@ -1059,7 +1100,7 @@ impl ChainSource { let chain_tip = if let Some(tip) = latest_chain_tip_opt { tip } else { - match validate_best_block_header(bitcoind_rpc_client.as_ref()).await { + match validate_best_block_header(api_client.as_ref()).await { Ok(tip) => { *latest_chain_tip.write().unwrap() = Some(tip); tip @@ -1077,8 +1118,7 @@ impl ChainSource { }; let mut locked_header_cache = header_cache.lock().await; - let chain_poller = - ChainPoller::new(Arc::clone(&bitcoind_rpc_client), config.network); + let chain_poller = ChainPoller::new(Arc::clone(&api_client), config.network); let chain_listener = ChainListener { onchain_wallet: Arc::clone(&onchain_wallet), channel_manager: Arc::clone(&channel_manager), @@ -1115,7 +1155,7 @@ impl ChainSource { let now = SystemTime::now(); let unconfirmed_txids = onchain_wallet.get_unconfirmed_txids(); - match bitcoind_rpc_client + match api_client .get_updated_mempool_transactions(cur_height, unconfirmed_txids) .await { @@ -1300,8 +1340,8 @@ impl ChainSource { Ok(()) }, - Self::BitcoindRpc { - bitcoind_rpc_client, + Self::Bitcoind { + api_client, fee_estimator, config, kv_store, @@ -1332,7 +1372,7 @@ impl ChainSource { ConfirmationTarget::Lightning( LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, ) => { - let estimation_fut = bitcoind_rpc_client.get_mempool_minimum_fee_rate(); + let estimation_fut = api_client.get_mempool_minimum_fee_rate(); get_fee_rate_update!(estimation_fut) }, ConfirmationTarget::Lightning( @@ -1340,8 +1380,8 @@ impl ChainSource { ) => { let num_blocks = get_num_block_defaults_for_target(target); let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); + let estimation_fut = + api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); get_fee_rate_update!(estimation_fut) }, ConfirmationTarget::Lightning( @@ -1349,16 +1389,16 @@ impl ChainSource { ) => { let num_blocks = get_num_block_defaults_for_target(target); let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); + let estimation_fut = + api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); get_fee_rate_update!(estimation_fut) }, _ => { // Otherwise, we default to economical block-target estimate. let num_blocks = get_num_block_defaults_for_target(target); let estimation_mode = FeeRateEstimationMode::Economical; - let estimation_fut = bitcoind_rpc_client - .get_fee_estimate_for_target(num_blocks, estimation_mode); + let estimation_fut = + api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); get_fee_rate_update!(estimation_fut) }, }; @@ -1530,7 +1570,7 @@ impl ChainSource { } } }, - Self::BitcoindRpc { bitcoind_rpc_client, tx_broadcaster, logger, .. } => { + Self::Bitcoind { api_client, tx_broadcaster, logger, .. } => { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual @@ -1541,7 +1581,7 @@ impl ChainSource { let txid = tx.compute_txid(); let timeout_fut = tokio::time::timeout( Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - bitcoind_rpc_client.broadcast_transaction(tx), + api_client.broadcast_transaction(tx), ); match timeout_fut.await { Ok(res) => match res { @@ -1595,7 +1635,7 @@ impl Filter for ChainSource { Self::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) }, - Self::BitcoindRpc { .. } => (), + Self::Bitcoind { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { @@ -1604,7 +1644,7 @@ impl Filter for ChainSource { Self::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_output(output) }, - Self::BitcoindRpc { .. } => (), + Self::Bitcoind { .. } => (), } } } diff --git a/src/config.rs b/src/config.rs index 4a39c1b561..a2930ea5a9 100644 --- a/src/config.rs +++ b/src/config.rs @@ -397,6 +397,15 @@ impl Default for ElectrumSyncConfig { } } +/// Configuration for syncing with Bitcoin Core backend via REST. +#[derive(Debug, Clone)] +pub struct BitcoindRestClientConfig { + /// Host URL. + pub rest_host: String, + /// Host port. + pub rest_port: u16, +} + /// Options which apply on a per-channel basis and may change at runtime or based on negotiation /// with our counterparty. #[derive(Copy, Clone, Debug, PartialEq, Eq)] diff --git a/src/lib.rs b/src/lib.rs index b09f9a9f77..a75da763ac 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1277,7 +1277,7 @@ impl Node { .await?; chain_source.sync_onchain_wallet().await?; }, - ChainSource::BitcoindRpc { .. } => { + ChainSource::Bitcoind { .. } => { chain_source.update_fee_rate_estimates().await?; chain_source .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3258df791a..daed864754 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -174,6 +174,7 @@ pub(crate) fn setup_bitcoind_and_electrsd() -> (BitcoinD, ElectrsD) { ); let mut bitcoind_conf = corepc_node::Conf::default(); bitcoind_conf.network = "regtest"; + bitcoind_conf.args.push("-rest"); let bitcoind = BitcoinD::with_conf(bitcoind_exe, &bitcoind_conf).unwrap(); let electrs_exe = env::var("ELECTRS_EXE") @@ -256,7 +257,8 @@ type TestNode = Node; pub(crate) enum TestChainSource<'a> { Esplora(&'a ElectrsD), Electrum(&'a ElectrsD), - BitcoindRpc(&'a BitcoinD), + BitcoindRpcSync(&'a BitcoinD), + BitcoindRestSync(&'a BitcoinD), } #[derive(Clone, Default)] @@ -317,7 +319,7 @@ pub(crate) fn setup_node( let sync_config = ElectrumSyncConfig { background_sync_config: None }; builder.set_chain_source_electrum(electrum_url.clone(), Some(sync_config)); }, - TestChainSource::BitcoindRpc(bitcoind) => { + TestChainSource::BitcoindRpcSync(bitcoind) => { let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); let rpc_port = bitcoind.params.rpc_socket.port(); let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); @@ -325,6 +327,23 @@ pub(crate) fn setup_node( let rpc_password = values.password; builder.set_chain_source_bitcoind_rpc(rpc_host, rpc_port, rpc_user, rpc_password); }, + TestChainSource::BitcoindRestSync(bitcoind) => { + let rpc_host = bitcoind.params.rpc_socket.ip().to_string(); + let rpc_port = bitcoind.params.rpc_socket.port(); + let values = bitcoind.params.get_cookie_values().unwrap().unwrap(); + let rpc_user = values.user; + let rpc_password = values.password; + let rest_host = bitcoind.params.rpc_socket.ip().to_string(); + let rest_port = bitcoind.params.rpc_socket.port(); + builder.set_chain_source_bitcoind_rest( + rest_host, + rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + ); + }, } match &config.log_writer { diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index db48eca236..fbd95ef502 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -56,9 +56,17 @@ fn channel_full_cycle_electrum() { } #[test] -fn channel_full_cycle_bitcoind() { +fn channel_full_cycle_bitcoind_rpc_sync() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); - let chain_source = TestChainSource::BitcoindRpc(&bitcoind); + let chain_source = TestChainSource::BitcoindRpcSync(&bitcoind); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); +} + +#[test] +fn channel_full_cycle_bitcoind_rest_sync() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::BitcoindRestSync(&bitcoind); let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); do_channel_full_cycle(node_a, node_b, &bitcoind.client, &electrsd.client, false, true, false); } From ed96316bc7b20b27d395f2e18270d047962f4cf5 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 1 Jul 2025 13:29:49 +0200 Subject: [PATCH 088/177] Bump `uniffi` dependency to v0.28.3 We bump our `uniffi` dependency to v0.28.3 to unlock some of the nicer features `uniffi` added since the previously-used v0.27.3. However, we can't bump it further to v0.29.3, as we have users requiring compatibility with `uniffi-bindgen-go`, which only supports v0.28.3 at the time of writing. --- Cargo.toml | 4 ++-- bindings/uniffi-bindgen/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bf8bed08c7..7fc1123e1a 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -84,7 +84,7 @@ esplora-client = { version = "0.12", default-features = false, features = ["toki esplora-client_0_11 = { package = "esplora-client", version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } electrum-client = { version = "0.23.1", default-features = true } libc = "0.2" -uniffi = { version = "0.27.3", features = ["build"], optional = true } +uniffi = { version = "0.28.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } serde_json = { version = "1.0.128", default-features = false, features = ["std"] } log = { version = "0.4.22", default-features = false, features = ["std"]} @@ -117,7 +117,7 @@ lnd_grpc_rust = { version = "2.10.0", default-features = false } tokio = { version = "1.37", features = ["fs"] } [build-dependencies] -uniffi = { version = "0.27.3", features = ["build"], optional = true } +uniffi = { version = "0.28.3", features = ["build"], optional = true } [profile.release] panic = "abort" diff --git a/bindings/uniffi-bindgen/Cargo.toml b/bindings/uniffi-bindgen/Cargo.toml index 9a4c9d5daa..a33c0f9ae0 100644 --- a/bindings/uniffi-bindgen/Cargo.toml +++ b/bindings/uniffi-bindgen/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -uniffi = { version = "0.27.3", features = ["cli"] } +uniffi = { version = "0.28.3", features = ["cli"] } From 6fe2d30c76107f4968b712ac809cfaae38736ed0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 13 May 2025 14:04:46 +0200 Subject: [PATCH 089/177] Reduce syncing and shutdown timeouts considerably Previously, we had to configure enormous syncing timeouts as the BDK wallet syncing would hold a central mutex that could lead to large parts of event handling and syncing locking up. Here, we drop the configured timeouts considerably across the board, since such huge values are hopefully not required anymore. --- src/chain/electrum.rs | 2 +- src/config.rs | 7 +++++-- src/lib.rs | 10 ++++------ 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 6e62d9c081..9882e652b0 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -40,7 +40,7 @@ use std::time::{Duration, Instant}; const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; -const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 20; +const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 10; pub(crate) struct ElectrumRuntimeClient { electrum_client: Arc, diff --git a/src/config.rs b/src/config.rs index a2930ea5a9..7b7ed8156d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -65,10 +65,13 @@ pub(crate) const NODE_ANN_BCAST_INTERVAL: Duration = Duration::from_secs(60 * 60 pub(crate) const WALLET_SYNC_INTERVAL_MINIMUM_SECS: u64 = 10; // The timeout after which we abort a wallet syncing operation. -pub(crate) const BDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 90; +pub(crate) const BDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 20; // The timeout after which we abort a wallet syncing operation. -pub(crate) const LDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 30; +pub(crate) const LDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 10; + +// The timeout after which we give up waiting on LDK's event handler to exit on shutdown. +pub(crate) const LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS: u64 = 30; // The timeout after which we abort a fee rate cache update operation. pub(crate) const FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS: u64 = 5; diff --git a/src/lib.rs b/src/lib.rs index a75da763ac..8579c29fc8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -126,8 +126,9 @@ pub use builder::NodeBuilder as Builder; use chain::ChainSource; use config::{ - default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, - PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + default_user_config, may_announce_channel, ChannelConfig, Config, + LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, + RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; @@ -673,13 +674,10 @@ impl Node { let event_handling_stopped_logger = Arc::clone(&self.logger); let mut event_handling_stopped_receiver = self.event_handling_stopped_sender.subscribe(); - // FIXME: For now, we wait up to 100 secs (BDK_WALLET_SYNC_TIMEOUT_SECS + 10) to allow - // event handling to exit gracefully even if it was blocked on the BDK wallet syncing. We - // should drop this considerably post upgrading to BDK 1.0. let timeout_res = tokio::task::block_in_place(move || { runtime.block_on(async { tokio::time::timeout( - Duration::from_secs(100), + Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), event_handling_stopped_receiver.changed(), ) .await From 9eae61dc0a4b0f14a728d2019cd10d20c9b35a2a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 7 Jul 2025 11:37:52 +0200 Subject: [PATCH 090/177] Await on the background processing task's `JoinHandle` Previously, we used to a channel to indicate that the background processor task has been stopped. Here, we rather just await the task's `JoinHandle` which is more robust in that it avoids a race condition. --- src/builder.rs | 4 +-- src/lib.rs | 80 +++++++++++++++++++++++--------------------------- 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index a177768f63..66b160e312 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1591,12 +1591,12 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); - let (event_handling_stopped_sender, _) = tokio::sync::watch::channel(()); + let background_processor_task = Mutex::new(None); Ok(Node { runtime, stop_sender, - event_handling_stopped_sender, + background_processor_task, config, wallet, chain_source, diff --git a/src/lib.rs b/src/lib.rs index 8579c29fc8..e0f8ff2361 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,7 +180,7 @@ uniffi::include_scaffolding!("ldk_node"); pub struct Node { runtime: Arc>>>, stop_sender: tokio::sync::watch::Sender<()>, - event_handling_stopped_sender: tokio::sync::watch::Sender<()>, + background_processor_task: Mutex>>, config: Arc, wallet: Arc, chain_source: Arc, @@ -579,8 +579,7 @@ impl Node { }; let background_stop_logger = Arc::clone(&self.logger); - let event_handling_stopped_sender = self.event_handling_stopped_sender.clone(); - runtime.spawn(async move { + let handle = runtime.spawn(async move { process_events_async( background_persister, |e| background_event_handler.handle_event(e), @@ -601,19 +600,9 @@ impl Node { panic!("Failed to process events"); }); log_debug!(background_stop_logger, "Events processing stopped.",); - - match event_handling_stopped_sender.send(()) { - Ok(_) => (), - Err(e) => { - log_error!( - background_stop_logger, - "Failed to send 'events handling stopped' signal. This should never happen: {}", - e - ); - debug_assert!(false); - }, - } }); + debug_assert!(self.background_processor_task.lock().unwrap().is_none()); + *self.background_processor_task.lock().unwrap() = Some(handle); if let Some(liquidity_source) = self.liquidity_source.as_ref() { let mut stop_liquidity_handler = self.stop_sender.subscribe(); @@ -670,39 +659,42 @@ impl Node { // Disconnect all peers. self.peer_manager.disconnect_all_peers(); - // Wait until event handling stopped, at least until a timeout is reached. - let event_handling_stopped_logger = Arc::clone(&self.logger); - let mut event_handling_stopped_receiver = self.event_handling_stopped_sender.subscribe(); + // Stop any runtime-dependant chain sources. + self.chain_source.stop(); - let timeout_res = tokio::task::block_in_place(move || { - runtime.block_on(async { - tokio::time::timeout( - Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), - event_handling_stopped_receiver.changed(), - ) - .await - }) - }); + // Wait until background processing stopped, at least until a timeout is reached. + if let Some(background_processor_task) = + self.background_processor_task.lock().unwrap().take() + { + let abort_handle = background_processor_task.abort_handle(); + let timeout_res = tokio::task::block_in_place(move || { + runtime.block_on(async { + tokio::time::timeout( + Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), + background_processor_task, + ) + .await + }) + }); - match timeout_res { - Ok(stop_res) => match stop_res { - Ok(()) => {}, + match timeout_res { + Ok(stop_res) => match stop_res { + Ok(()) => {}, + Err(e) => { + abort_handle.abort(); + log_error!( + self.logger, + "Stopping event handling failed. This should never happen: {}", + e + ); + panic!("Stopping event handling failed. This should never happen."); + }, + }, Err(e) => { - log_error!( - event_handling_stopped_logger, - "Stopping event handling failed. This should never happen: {}", - e - ); - panic!("Stopping event handling failed. This should never happen."); + abort_handle.abort(); + log_error!(self.logger, "Stopping event handling timed out: {}", e); }, - }, - Err(e) => { - log_error!( - event_handling_stopped_logger, - "Stopping event handling timed out: {}", - e - ); - }, + } } #[cfg(tokio_unstable)] From c3d6161b7e46ef20db6530827aeef3a21c57703a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 7 Jul 2025 13:28:03 +0200 Subject: [PATCH 091/177] Improve logging in `stop` .. we provide finer-grained logging after each step of `stop`. --- src/lib.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e0f8ff2361..0a53fbbb30 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -147,9 +147,7 @@ use types::{ }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; -#[cfg(tokio_unstable)] -use logger::log_trace; -use logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use lightning::chain::BestBlock; use lightning::events::bump_transaction::Wallet as LdkWallet; @@ -578,7 +576,6 @@ impl Node { }) }; - let background_stop_logger = Arc::clone(&self.logger); let handle = runtime.spawn(async move { process_events_async( background_persister, @@ -599,7 +596,6 @@ impl Node { log_error!(background_error_logger, "Failed to process events: {}", e); panic!("Failed to process events"); }); - log_debug!(background_stop_logger, "Events processing stopped.",); }); debug_assert!(self.background_processor_task.lock().unwrap().is_none()); *self.background_processor_task.lock().unwrap() = Some(handle); @@ -645,7 +641,7 @@ impl Node { // Stop the runtime. match self.stop_sender.send(()) { - Ok(_) => (), + Ok(_) => log_trace!(self.logger, "Sent shutdown signal to background tasks."), Err(e) => { log_error!( self.logger, @@ -658,9 +654,11 @@ impl Node { // Disconnect all peers. self.peer_manager.disconnect_all_peers(); + log_debug!(self.logger, "Disconnected all network peers."); // Stop any runtime-dependant chain sources. self.chain_source.stop(); + log_debug!(self.logger, "Stopped chain sources."); // Wait until background processing stopped, at least until a timeout is reached. if let Some(background_processor_task) = @@ -679,7 +677,7 @@ impl Node { match timeout_res { Ok(stop_res) => match stop_res { - Ok(()) => {}, + Ok(()) => log_debug!(self.logger, "Stopped background processing of events."), Err(e) => { abort_handle.abort(); log_error!( From 8c157c3b9f80d02d0918933c2fa141bcdc4117e7 Mon Sep 17 00:00:00 2001 From: Enigbe Date: Thu, 3 Jul 2025 14:46:34 +0100 Subject: [PATCH 092/177] ci(vss): use latest gradle version update to Gradle 9.0 and auto-extract version number. --- .github/workflows/vss-integration.yml | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index 2a6c63704e..f7a2307803 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -62,9 +62,16 @@ jobs: # Print Info java -version gradle --version + + GRADLE_VERSION=$(gradle --version | awk '/^Gradle/ {print $2}' | head -1) + if [ -z "$GRADLE_VERSION" ]; then + echo "Error: Failed to extract Gradle version." >&2 + exit 1 + fi + echo "Extracted Gradle Version: $GRADLE_VERSION" cd vss-server/java - gradle wrapper --gradle-version 8.1.1 + gradle wrapper --gradle-version $GRADLE_VERSION ./gradlew --version ./gradlew build From d5df3d0c739e1022006aef8c52a4562aa5fc6852 Mon Sep 17 00:00:00 2001 From: benthecarman Date: Fri, 18 Jul 2025 13:34:47 -0500 Subject: [PATCH 093/177] Add option to include headers in Esplora config This is useful if the esplora server has a form of authentication in front of it --- src/builder.rs | 44 ++++++++++++++++++++++++++++++++++++++++++-- src/chain/mod.rs | 18 ++++++++++++++---- 2 files changed, 56 insertions(+), 6 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 66b160e312..30a1649d2e 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -87,6 +87,7 @@ const LSPS_HARDENED_CHILD_INDEX: u32 = 577; enum ChainDataSourceConfig { Esplora { server_url: String, + headers: HashMap, sync_config: Option, }, Electrum { @@ -294,9 +295,28 @@ impl NodeBuilder { /// information. pub fn set_chain_source_esplora( &mut self, server_url: String, sync_config: Option, + ) -> &mut Self { + self.chain_data_source_config = Some(ChainDataSourceConfig::Esplora { + server_url, + headers: Default::default(), + sync_config, + }); + self + } + + /// Configures the [`Node`] instance to source its chain data from the given Esplora server. + /// + /// The given `headers` will be included in all requests to the Esplora server, typically used for + /// authentication purposes. + /// + /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more + /// information. + pub fn set_chain_source_esplora_with_headers( + &mut self, server_url: String, headers: HashMap, + sync_config: Option, ) -> &mut Self { self.chain_data_source_config = - Some(ChainDataSourceConfig::Esplora { server_url, sync_config }); + Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }); self } @@ -754,6 +774,24 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_chain_source_esplora(server_url, sync_config); } + /// Configures the [`Node`] instance to source its chain data from the given Esplora server. + /// + /// The given `headers` will be included in all requests to the Esplora server, typically used for + /// authentication purposes. + /// + /// If no `sync_config` is given, default values are used. See [`EsploraSyncConfig`] for more + /// information. + pub fn set_chain_source_esplora_with_headers( + &self, server_url: String, headers: HashMap, + sync_config: Option, + ) { + self.inner.write().unwrap().set_chain_source_esplora_with_headers( + server_url, + headers, + sync_config, + ); + } + /// Configures the [`Node`] instance to source its chain data from the given Electrum server. /// /// If no `sync_config` is given, default values are used. See [`ElectrumSyncConfig`] for more @@ -1117,10 +1155,11 @@ fn build_with_store_internal( )); let chain_source = match chain_data_source_config { - Some(ChainDataSourceConfig::Esplora { server_url, sync_config }) => { + Some(ChainDataSourceConfig::Esplora { server_url, headers, sync_config }) => { let sync_config = sync_config.unwrap_or(EsploraSyncConfig::default()); Arc::new(ChainSource::new_esplora( server_url.clone(), + headers.clone(), sync_config, Arc::clone(&wallet), Arc::clone(&fee_estimator), @@ -1187,6 +1226,7 @@ fn build_with_store_internal( let sync_config = EsploraSyncConfig::default(); Arc::new(ChainSource::new_esplora( server_url.clone(), + HashMap::new(), sync_config, Arc::clone(&wallet), Arc::clone(&fee_estimator), diff --git a/src/chain/mod.rs b/src/chain/mod.rs index c3d5fdedc5..2f8eeaac41 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -233,21 +233,31 @@ pub(crate) enum ChainSource { impl ChainSource { pub(crate) fn new_esplora( - server_url: String, sync_config: EsploraSyncConfig, onchain_wallet: Arc, - fee_estimator: Arc, tx_broadcaster: Arc, - kv_store: Arc, config: Arc, logger: Arc, - node_metrics: Arc>, + server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, ) -> Self { // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder_0_11 = client_builder_0_11.header(header_name, header_value); + } + let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); let tx_sync = Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); let mut client_builder = esplora_client::Builder::new(&server_url); client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder = client_builder.header(header_name, header_value); + } + let esplora_client = client_builder.build_async().unwrap(); let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); From 7e93a8bc60bf188373f70b65c5f4b30e334be9c4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 24 Jul 2025 15:15:52 +0200 Subject: [PATCH 094/177] Introduce `ChainSourceKind` type We introduce a new `ChainSourceKind` that is held as a field by `ChainSource`, which better encapsulates the chain syncing logic, and in future commits allows us to move some common fields to `ChainSource`. --- src/chain/mod.rs | 117 ++++++++++++++++++++++++++++------------------- src/lib.rs | 32 +++++-------- 2 files changed, 81 insertions(+), 68 deletions(-) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 2f8eeaac41..ef844264b8 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -187,7 +187,11 @@ impl ElectrumRuntimeStatus { } } -pub(crate) enum ChainSource { +pub(crate) struct ChainSource { + kind: ChainSourceKind, +} + +enum ChainSourceKind { Esplora { sync_config: EsploraSyncConfig, esplora_client: EsploraAsyncClient, @@ -262,7 +266,7 @@ impl ChainSource { let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - Self::Esplora { + let kind = ChainSourceKind::Esplora { sync_config, esplora_client, onchain_wallet, @@ -275,7 +279,9 @@ impl ChainSource { config, logger, node_metrics, - } + }; + + Self { kind } } pub(crate) fn new_electrum( @@ -287,7 +293,7 @@ impl ChainSource { let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - Self::Electrum { + let kind = ChainSourceKind::Electrum { server_url, sync_config, electrum_runtime_status, @@ -300,7 +306,8 @@ impl ChainSource { config, logger, node_metrics, - } + }; + Self { kind } } pub(crate) fn new_bitcoind_rpc( @@ -319,7 +326,7 @@ impl ChainSource { let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self::Bitcoind { + let kind = ChainSourceKind::Bitcoind { api_client, header_cache, latest_chain_tip, @@ -331,7 +338,8 @@ impl ChainSource { config, logger, node_metrics, - } + }; + Self { kind } } pub(crate) fn new_bitcoind_rest( @@ -354,7 +362,7 @@ impl ChainSource { let latest_chain_tip = RwLock::new(None); let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self::Bitcoind { + let kind = ChainSourceKind::Bitcoind { api_client, header_cache, latest_chain_tip, @@ -366,12 +374,19 @@ impl ChainSource { config, logger, node_metrics, - } + }; + Self { kind } } pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { - match self { - Self::Electrum { server_url, electrum_runtime_status, config, logger, .. } => { + match &self.kind { + ChainSourceKind::Electrum { + server_url, + electrum_runtime_status, + config, + logger, + .. + } => { electrum_runtime_status.write().unwrap().start( server_url.clone(), Arc::clone(&runtime), @@ -387,8 +402,8 @@ impl ChainSource { } pub(crate) fn stop(&self) { - match self { - Self::Electrum { electrum_runtime_status, .. } => { + match &self.kind { + ChainSourceKind::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().stop(); }, _ => { @@ -398,19 +413,27 @@ impl ChainSource { } pub(crate) fn as_utxo_source(&self) -> Option> { - match self { - Self::Bitcoind { api_client, .. } => Some(api_client.utxo_source()), + match &self.kind { + ChainSourceKind::Bitcoind { api_client, .. } => Some(api_client.utxo_source()), _ => None, } } + pub(crate) fn is_transaction_based(&self) -> bool { + match &self.kind { + ChainSourceKind::Esplora { .. } => true, + ChainSourceKind::Electrum { .. } => true, + ChainSourceKind::Bitcoind { .. } => false, + } + } + pub(crate) async fn continuously_sync_wallets( &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) { - match self { - Self::Esplora { sync_config, logger, .. } => { + match &self.kind { + ChainSourceKind::Esplora { sync_config, logger, .. } => { if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { self.start_tx_based_sync_loop( stop_sync_receiver, @@ -430,7 +453,7 @@ impl ChainSource { return; } }, - Self::Electrum { sync_config, logger, .. } => { + ChainSourceKind::Electrum { sync_config, logger, .. } => { if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { self.start_tx_based_sync_loop( stop_sync_receiver, @@ -450,7 +473,7 @@ impl ChainSource { return; } }, - Self::Bitcoind { + ChainSourceKind::Bitcoind { api_client, header_cache, latest_chain_tip, @@ -681,8 +704,8 @@ impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - match self { - Self::Esplora { + match &self.kind { + ChainSourceKind::Esplora { esplora_client, onchain_wallet, onchain_wallet_sync_status, @@ -795,7 +818,7 @@ impl ChainSource { res }, - Self::Electrum { + ChainSourceKind::Electrum { electrum_runtime_status, onchain_wallet, onchain_wallet_sync_status, @@ -887,7 +910,7 @@ impl ChainSource { res }, - Self::Bitcoind { .. } => { + ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Onchain wallet will be synced via chain polling") @@ -901,8 +924,8 @@ impl ChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - match self { - Self::Esplora { + match &self.kind { + ChainSourceKind::Esplora { tx_sync, lightning_wallet_sync_status, kv_store, @@ -986,7 +1009,7 @@ impl ChainSource { res }, - Self::Electrum { + ChainSourceKind::Electrum { electrum_runtime_status, lightning_wallet_sync_status, kv_store, @@ -1057,7 +1080,7 @@ impl ChainSource { res }, - Self::Bitcoind { .. } => { + ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via // `ChainPoller`. So nothing to do here. unreachable!("Lightning wallet will be synced via chain polling") @@ -1069,18 +1092,18 @@ impl ChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - match self { - Self::Esplora { .. } => { + match &self.kind { + ChainSourceKind::Esplora { .. } => { // In Esplora mode we sync lightning and onchain wallets via // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, - Self::Electrum { .. } => { + ChainSourceKind::Electrum { .. } => { // In Electrum mode we sync lightning and onchain wallets via // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, - Self::Bitcoind { + ChainSourceKind::Bitcoind { api_client, header_cache, latest_chain_tip, @@ -1220,8 +1243,8 @@ impl ChainSource { } pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - match self { - Self::Esplora { + match &self.kind { + ChainSourceKind::Esplora { esplora_client, fee_estimator, config, @@ -1305,7 +1328,7 @@ impl ChainSource { Ok(()) }, - Self::Electrum { + ChainSourceKind::Electrum { electrum_runtime_status, fee_estimator, kv_store, @@ -1350,7 +1373,7 @@ impl ChainSource { Ok(()) }, - Self::Bitcoind { + ChainSourceKind::Bitcoind { api_client, fee_estimator, config, @@ -1483,8 +1506,8 @@ impl ChainSource { } pub(crate) async fn process_broadcast_queue(&self) { - match self { - Self::Esplora { esplora_client, tx_broadcaster, logger, .. } => { + match &self.kind { + ChainSourceKind::Esplora { esplora_client, tx_broadcaster, logger, .. } => { let mut receiver = tx_broadcaster.get_broadcast_queue().await; while let Some(next_package) = receiver.recv().await { for tx in &next_package { @@ -1560,7 +1583,7 @@ impl ChainSource { } } }, - Self::Electrum { electrum_runtime_status, tx_broadcaster, .. } => { + ChainSourceKind::Electrum { electrum_runtime_status, tx_broadcaster, .. } => { let electrum_client: Arc = if let Some(client) = electrum_runtime_status.read().unwrap().client().as_ref() { @@ -1580,7 +1603,7 @@ impl ChainSource { } } }, - Self::Bitcoind { api_client, tx_broadcaster, logger, .. } => { + ChainSourceKind::Bitcoind { api_client, tx_broadcaster, logger, .. } => { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual @@ -1640,21 +1663,21 @@ impl ChainSource { impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { - match self { - Self::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), - Self::Electrum { electrum_runtime_status, .. } => { + match &self.kind { + ChainSourceKind::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), + ChainSourceKind::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) }, - Self::Bitcoind { .. } => (), + ChainSourceKind::Bitcoind { .. } => (), } } fn register_output(&self, output: lightning::chain::WatchedOutput) { - match self { - Self::Esplora { tx_sync, .. } => tx_sync.register_output(output), - Self::Electrum { electrum_runtime_status, .. } => { + match &self.kind { + ChainSourceKind::Esplora { tx_sync, .. } => tx_sync.register_output(output), + ChainSourceKind::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_output(output) }, - Self::Bitcoind { .. } => (), + ChainSourceKind::Bitcoind { .. } => (), } } } diff --git a/src/lib.rs b/src/lib.rs index 0a53fbbb30..89a17ab03d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1250,27 +1250,17 @@ impl Node { tokio::task::block_in_place(move || { tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( async move { - match chain_source.as_ref() { - ChainSource::Esplora { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - }, - ChainSource::Electrum { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - }, - ChainSource::Bitcoind { .. } => { - chain_source.update_fee_rate_estimates().await?; - chain_source - .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) - .await?; - }, + if chain_source.is_transaction_based() { + chain_source.update_fee_rate_estimates().await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) + .await?; + chain_source.sync_onchain_wallet().await?; + } else { + chain_source.update_fee_rate_estimates().await?; + chain_source + .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) + .await?; } Ok(()) }, From 5afe490b4f7bd73048d3b7c4d9d506af7eef8fd1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Jul 2025 08:58:15 +0200 Subject: [PATCH 095/177] Intermittently introduce additional `impl` blocks .. in the hopes of making the git diff more readable going forward, we break up the `ChainSource` impl block. --- src/chain/mod.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index ef844264b8..f75837cde0 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -700,7 +700,9 @@ impl ChainSource { } } } +} +impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { @@ -917,7 +919,9 @@ impl ChainSource { }, } } +} +impl ChainSource { // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_lightning_wallet( @@ -1087,7 +1091,9 @@ impl ChainSource { }, } } +} +impl ChainSource { pub(crate) async fn poll_and_update_listeners( &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, @@ -1241,7 +1247,9 @@ impl ChainSource { }, } } +} +impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { ChainSourceKind::Esplora { @@ -1504,7 +1512,9 @@ impl ChainSource { }, } } +} +impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { ChainSourceKind::Esplora { esplora_client, tx_broadcaster, logger, .. } => { From 56167cecc03814fa42e440cfd1f90e771aaa4d57 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 24 Jul 2025 15:01:18 +0200 Subject: [PATCH 096/177] Move Esplora sync logic to a `EsploraChainSource` type We refactor our `ChainSource` logic and move out the Esplora code into a new object. --- src/builder.rs | 6 +- src/chain/mod.rs | 749 +++++++++++++++++++++++++---------------------- src/config.rs | 6 + 3 files changed, 403 insertions(+), 358 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 30a1649d2e..85ec70d18a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,11 +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. -use crate::chain::{ChainSource, DEFAULT_ESPLORA_SERVER_URL}; +use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, BitcoindRestClientConfig, Config, - ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, - WALLET_KEYS_SEED_LEN, + ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, + DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index f75837cde0..4544a6d3a1 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -15,9 +15,9 @@ use crate::chain::electrum::ElectrumRuntimeClient; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, - FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, - RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, - WALLET_SYNC_INTERVAL_MINIMUM_SECS, + DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, + LDK_WALLET_SYNC_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, + TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use crate::fee_estimator::{ apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, @@ -32,30 +32,24 @@ use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarge use lightning::chain::{Confirm, Filter, Listen, WatchedOutput}; use lightning::util::ser::Writeable; -use lightning_transaction_sync::EsploraSyncClient; - use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; -use bdk_esplora::EsploraAsyncExt; -use bdk_wallet::Update as BdkUpdate; +use lightning_transaction_sync::EsploraSyncClient; +use bdk_esplora::EsploraAsyncExt; use esplora_client::AsyncClient as EsploraAsyncClient; +use bdk_wallet::Update as BdkUpdate; + use bitcoin::{FeeRate, Network, Script, ScriptBuf, Txid}; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -// The default Esplora server we're using. -pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; - -// The default Esplora client timeout we're using. -pub(crate) const DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS: u64 = 10; - const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; pub(crate) enum WalletSyncStatus { @@ -187,25 +181,77 @@ impl ElectrumRuntimeStatus { } } +pub(super) struct EsploraChainSource { + pub(super) sync_config: EsploraSyncConfig, + esplora_client: EsploraAsyncClient, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + tx_sync: Arc>>, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl EsploraChainSource { + pub(crate) fn new( + server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating + // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. + let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); + client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder_0_11 = client_builder_0_11.header(header_name, header_value); + } + + let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); + let tx_sync = + Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); + + let mut client_builder = esplora_client::Builder::new(&server_url); + client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder = client_builder.header(header_name, header_value); + } + + let esplora_client = client_builder.build_async().unwrap(); + + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + sync_config, + esplora_client, + onchain_wallet, + onchain_wallet_sync_status, + tx_sync, + lightning_wallet_sync_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger, + node_metrics, + } + } +} + + pub(crate) struct ChainSource { kind: ChainSourceKind, + logger: Arc, } enum ChainSourceKind { - Esplora { - sync_config: EsploraSyncConfig, - esplora_client: EsploraAsyncClient, - onchain_wallet: Arc, - onchain_wallet_sync_status: Mutex, - tx_sync: Arc>>, - lightning_wallet_sync_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, - }, + Esplora(EsploraChainSource), Electrum { server_url: String, sync_config: ElectrumSyncConfig, @@ -242,46 +288,20 @@ impl ChainSource { tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating - // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. - let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); - client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - - for (header_name, header_value) in &headers { - client_builder_0_11 = client_builder_0_11.header(header_name, header_value); - } - - let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); - let tx_sync = - Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); - - let mut client_builder = esplora_client::Builder::new(&server_url); - client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - - for (header_name, header_value) in &headers { - client_builder = client_builder.header(header_name, header_value); - } - - let esplora_client = client_builder.build_async().unwrap(); - - let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let kind = ChainSourceKind::Esplora { + let esplora_chain_source = EsploraChainSource::new( + server_url, + headers, sync_config, - esplora_client, onchain_wallet, - onchain_wallet_sync_status, - tx_sync, - lightning_wallet_sync_status, fee_estimator, tx_broadcaster, kv_store, config, - logger, + Arc::clone(&logger), node_metrics, - }; - - Self { kind } + ); + let kind = ChainSourceKind::Esplora(esplora_chain_source); + Self { kind, logger } } pub(crate) fn new_electrum( @@ -304,10 +324,10 @@ impl ChainSource { tx_broadcaster, kv_store, config, - logger, + logger: Arc::clone(&logger), node_metrics, }; - Self { kind } + Self { kind, logger } } pub(crate) fn new_bitcoind_rpc( @@ -336,10 +356,10 @@ impl ChainSource { tx_broadcaster, kv_store, config, - logger, + logger: Arc::clone(&logger), node_metrics, }; - Self { kind } + Self { kind, logger } } pub(crate) fn new_bitcoind_rest( @@ -372,10 +392,10 @@ impl ChainSource { tx_broadcaster, kv_store, config, - logger, + logger: Arc::clone(&logger), node_metrics, }; - Self { kind } + Self { kind, logger } } pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { @@ -421,7 +441,7 @@ impl ChainSource { pub(crate) fn is_transaction_based(&self) -> bool { match &self.kind { - ChainSourceKind::Esplora { .. } => true, + ChainSourceKind::Esplora(_) => true, ChainSourceKind::Electrum { .. } => true, ChainSourceKind::Bitcoind { .. } => false, } @@ -433,21 +453,23 @@ impl ChainSource { output_sweeper: Arc, ) { match &self.kind { - ChainSourceKind::Esplora { sync_config, logger, .. } => { - if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { + ChainSourceKind::Esplora(esplora_chain_source) => { + if let Some(background_sync_config) = + esplora_chain_source.sync_config.background_sync_config.as_ref() + { self.start_tx_based_sync_loop( stop_sync_receiver, channel_manager, chain_monitor, output_sweeper, background_sync_config, - Arc::clone(&logger), + Arc::clone(&self.logger), ) .await } else { // Background syncing is disabled log_info!( - logger, + self.logger, "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", ); return; @@ -467,7 +489,7 @@ impl ChainSource { } else { // Background syncing is disabled log_info!( - logger, + self.logger, "Background syncing is disabled. Manual syncing required for onchain wallet, lightning wallet, and fee rate updates.", ); return; @@ -702,48 +724,36 @@ impl ChainSource { } } -impl ChainSource { - // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, - // etc.) - pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - match &self.kind { - ChainSourceKind::Esplora { - esplora_client, - onchain_wallet, - onchain_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let receiver_res = { - let mut status_lock = onchain_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } +impl EsploraChainSource { + pub(super) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } - let res = { - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + let res = { + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - macro_rules! get_and_apply_wallet_update { + macro_rules! get_and_apply_wallet_update { ($sync_future: expr) => {{ let now = Instant::now(); match $sync_future.await { Ok(res) => match res { - Ok(update) => match onchain_wallet.apply_update(update) { + Ok(update) => match self.onchain_wallet.apply_update(update) { Ok(()) => { log_info!( - logger, + self.logger, "{} of on-chain wallet finished in {}ms.", if incremental_sync { "Incremental sync" } else { "Sync" }, now.elapsed().as_millis() @@ -753,9 +763,13 @@ impl ChainSource { .ok() .map(|d| d.as_secs()); { - let mut locked_node_metrics = node_metrics.write().unwrap(); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - write_node_metrics(&*locked_node_metrics, Arc::clone(&kv_store), Arc::clone(&logger))?; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger) + )?; } Ok(()) }, @@ -764,7 +778,7 @@ impl ChainSource { Err(e) => match *e { esplora_client::Error::Reqwest(he) => { log_error!( - logger, + self.logger, "{} of on-chain wallet failed due to HTTP connection error: {}", if incremental_sync { "Incremental sync" } else { "Sync" }, he @@ -773,7 +787,7 @@ impl ChainSource { }, _ => { log_error!( - logger, + self.logger, "{} of on-chain wallet failed due to Esplora error: {}", if incremental_sync { "Incremental sync" } else { "Sync" }, e @@ -784,7 +798,7 @@ impl ChainSource { }, Err(e) => { log_error!( - logger, + self.logger, "{} of on-chain wallet timed out: {}", if incremental_sync { "Incremental sync" } else { "Sync" }, e @@ -795,30 +809,40 @@ impl ChainSource { }} } - if incremental_sync { - let sync_request = onchain_wallet.get_incremental_sync_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } else { - let full_scan_request = onchain_wallet.get_full_scan_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - esplora_client.full_scan( - full_scan_request, - BDK_CLIENT_STOP_GAP, - BDK_CLIENT_CONCURRENCY, - ), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } - }; + if incremental_sync { + let sync_request = self.onchain_wallet.get_incremental_sync_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.full_scan( + full_scan_request, + BDK_CLIENT_STOP_GAP, + BDK_CLIENT_CONCURRENCY, + ), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } + }; - onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - res + res + } +} + +impl ChainSource { + // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, + // etc.) + pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.sync_onchain_wallet().await }, ChainSourceKind::Electrum { electrum_runtime_status, @@ -921,97 +945,99 @@ impl ChainSource { } } -impl ChainSource { - // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, - // etc.) - pub(crate) async fn sync_lightning_wallet( +impl EsploraChainSource { + pub(super) async fn sync_lightning_wallet( &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - match &self.kind { - ChainSourceKind::Esplora { - tx_sync, - lightning_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + let res = { + let timeout_fut = tokio::time::timeout( + Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), + self.tx_sync.sync(confirmables), + ); + let now = Instant::now(); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); - let receiver_res = { - let mut status_lock = lightning_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - let res = { - let timeout_fut = tokio::time::timeout( - Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), - tx_sync.sync(confirmables), - ); - let now = Instant::now(); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_info!( - logger, - "Sync of Lightning wallet finished in {}ms.", - now.elapsed().as_millis() - ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + Ok(()) + }, + Err(e) => { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Err(e.into()) + }, + }, + Err(e) => { + log_error!(self.logger, "Lightning wallet sync timed out: {}", e); + Err(Error::TxSyncTimeout) + }, + } + }; - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&kv_store), - Arc::clone(&logger), - Arc::clone(&node_metrics), - )?; - Ok(()) - }, - Err(e) => { - log_error!(logger, "Sync of Lightning wallet failed: {}", e); - Err(e.into()) - }, - }, - Err(e) => { - log_error!(logger, "Lightning wallet sync timed out: {}", e); - Err(Error::TxSyncTimeout) - }, - } - }; + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + res + } +} - res +impl ChainSource { + // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, + // etc.) + pub(crate) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source + .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) + .await }, ChainSourceKind::Electrum { electrum_runtime_status, @@ -1249,92 +1275,89 @@ impl ChainSource { } } -impl ChainSource { +impl EsploraChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - match &self.kind { - ChainSourceKind::Esplora { - esplora_client, - fee_estimator, - config, - kv_store, - logger, - node_metrics, - .. - } => { - let now = Instant::now(); - let estimates = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - esplora_client.get_fee_estimates(), - ) - .await - .map_err(|e| { - log_error!(logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })? - .map_err(|e| { - log_error!(logger, "Failed to retrieve fee rate estimates: {}", e); - Error::FeerateEstimationUpdateFailed - })?; - - if estimates.is_empty() && config.network == Network::Bitcoin { - // Ensure we fail if we didn't receive any estimates. - log_error!( - logger, + let now = Instant::now(); + let estimates = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + self.esplora_client.get_fee_estimates(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if estimates.is_empty() && self.config.network == Network::Bitcoin { + // Ensure we fail if we didn't receive any estimates. + log_error!( + self.logger, "Failed to retrieve fee rate estimates: empty fee estimates are dissallowed on Mainnet.", ); - return Err(Error::FeerateEstimationUpdateFailed); - } + return Err(Error::FeerateEstimationUpdateFailed); + } - let confirmation_targets = get_all_conf_targets(); + let confirmation_targets = get_all_conf_targets(); - let mut new_fee_rate_cache = HashMap::with_capacity(10); - for target in confirmation_targets { - let num_blocks = get_num_block_defaults_for_target(target); + let mut new_fee_rate_cache = HashMap::with_capacity(10); + for target in confirmation_targets { + let num_blocks = get_num_block_defaults_for_target(target); - // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it - // yields less than that. This is mostly necessary to continue on - // `signet`/`regtest` where we might not get estimates (or bogus values). - let converted_estimate_sat_vb = - esplora_client::convert_fee_rate(num_blocks, estimates.clone()) - .map_or(1.0, |converted| converted.max(1.0)); + // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it + // yields less than that. This is mostly necessary to continue on + // `signet`/`regtest` where we might not get estimates (or bogus values). + let converted_estimate_sat_vb = + esplora_client::convert_fee_rate(num_blocks, estimates.clone()) + .map_or(1.0, |converted| converted.max(1.0)); - let fee_rate = - FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); + let fee_rate = FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - new_fee_rate_cache.insert(target, adjusted_fee_rate); + new_fee_rate_cache.insert(target, adjusted_fee_rate); - log_trace!( - logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } - fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); - log_info!( - logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } - Ok(()) + Ok(()) + } +} + +impl ChainSource { + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.update_fee_rate_estimates().await }, ChainSourceKind::Electrum { electrum_runtime_status, @@ -1514,84 +1537,87 @@ impl ChainSource { } } -impl ChainSource { +impl EsploraChainSource { pub(crate) async fn process_broadcast_queue(&self) { - match &self.kind { - ChainSourceKind::Esplora { esplora_client, tx_broadcaster, logger, .. } => { - let mut receiver = tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in &next_package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.esplora_client.broadcast(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. log_trace!( - logger, - "Successfully broadcast transaction {}", - txid + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message ); - }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, message - ); - } - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - _ => { - log_error!( - logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + } + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); }, - Err(e) => { + _ => { log_error!( - logger, - "Failed to broadcast transaction due to timeout {}: {}", + self.logger, + "Failed to broadcast transaction {}: {}", txid, e ); log_trace!( - logger, + self.logger, "Failed broadcast transaction bytes: {}", log_bytes!(tx.encode()) ); }, - } - } + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, } + } + } + } +} + +impl ChainSource { + pub(crate) async fn process_broadcast_queue(&self) { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.process_broadcast_queue().await }, ChainSourceKind::Electrum { electrum_runtime_status, tx_broadcaster, .. } => { let electrum_client: Arc = if let Some(client) = @@ -1671,10 +1697,21 @@ impl ChainSource { } } +impl Filter for EsploraChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.tx_sync.register_tx(txid, script_pubkey); + } + fn register_output(&self, output: WatchedOutput) { + self.tx_sync.register_output(output); + } +} + impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { match &self.kind { - ChainSourceKind::Esplora { tx_sync, .. } => tx_sync.register_tx(txid, script_pubkey), + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.register_tx(txid, script_pubkey) + }, ChainSourceKind::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) }, @@ -1683,7 +1720,9 @@ impl Filter for ChainSource { } fn register_output(&self, output: lightning::chain::WatchedOutput) { match &self.kind { - ChainSourceKind::Esplora { tx_sync, .. } => tx_sync.register_output(output), + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.register_output(output) + }, ChainSourceKind::Electrum { electrum_runtime_status, .. } => { electrum_runtime_status.write().unwrap().register_output(output) }, diff --git a/src/config.rs b/src/config.rs index 7b7ed8156d..a5048e64fe 100644 --- a/src/config.rs +++ b/src/config.rs @@ -39,6 +39,12 @@ pub const DEFAULT_LOG_FILENAME: &'static str = "ldk_node.log"; /// The default storage directory. pub const DEFAULT_STORAGE_DIR_PATH: &str = "/tmp/ldk_node"; +// The default Esplora server we're using. +pub(crate) const DEFAULT_ESPLORA_SERVER_URL: &str = "https://blockstream.info/api"; + +// The default Esplora client timeout we're using. +pub(crate) const DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS: u64 = 10; + // The 'stop gap' parameter used by BDK's wallet sync. This seems to configure the threshold // number of derivation indexes after which BDK stops looking for new scripts belonging to the wallet. pub(crate) const BDK_CLIENT_STOP_GAP: usize = 20; From 8a01e17c9d3a7b74838c0ba0bfe7d15da2074e01 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Jul 2025 09:09:21 +0200 Subject: [PATCH 097/177] Move `EsploraChainSource` to a new module `chain::esplora` --- src/chain/esplora.rs | 448 +++++++++++++++++++++++++++++++++++++++++++ src/chain/mod.rs | 431 +---------------------------------------- 2 files changed, 451 insertions(+), 428 deletions(-) create mode 100644 src/chain/esplora.rs diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs new file mode 100644 index 0000000000..3a911394ce --- /dev/null +++ b/src/chain/esplora.rs @@ -0,0 +1,448 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; + +use crate::config::{ + Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, + BDK_WALLET_SYNC_TIMEOUT_SECS, DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; + +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::util::ser::Writeable; + +use lightning_transaction_sync::EsploraSyncClient; + +use bdk_esplora::EsploraAsyncExt; + +use esplora_client::AsyncClient as EsploraAsyncClient; + +use bitcoin::{FeeRate, Network, Script, Txid}; + +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +pub(super) struct EsploraChainSource { + pub(super) sync_config: EsploraSyncConfig, + esplora_client: EsploraAsyncClient, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + tx_sync: Arc>>, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl EsploraChainSource { + pub(crate) fn new( + server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating + // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. + let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); + client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder_0_11 = client_builder_0_11.header(header_name, header_value); + } + + let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); + let tx_sync = + Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); + + let mut client_builder = esplora_client::Builder::new(&server_url); + client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); + + for (header_name, header_value) in &headers { + client_builder = client_builder.header(header_name, header_value); + } + + let esplora_client = client_builder.build_async().unwrap(); + + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + sync_config, + esplora_client, + onchain_wallet, + onchain_wallet_sync_status, + tx_sync, + lightning_wallet_sync_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger, + node_metrics, + } + } + + pub(super) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let res = { + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + macro_rules! get_and_apply_wallet_update { + ($sync_future: expr) => {{ + let now = Instant::now(); + match $sync_future.await { + Ok(res) => match res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger) + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => match *e { + esplora_client::Error::Reqwest(he) => { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP connection error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + he + ); + Err(Error::WalletOperationFailed) + }, + _ => { + log_error!( + self.logger, + "{} of on-chain wallet failed due to Esplora error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationFailed) + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "{} of on-chain wallet timed out: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationTimeout) + }, + } + }} + } + + if incremental_sync { + let sync_request = self.onchain_wallet.get_incremental_sync_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.full_scan( + full_scan_request, + BDK_CLIENT_STOP_GAP, + BDK_CLIENT_CONCURRENCY, + ), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } + }; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + pub(super) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + let res = { + let timeout_fut = tokio::time::timeout( + Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), + self.tx_sync.sync(confirmables), + ); + let now = Instant::now(); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + Ok(()) + }, + Err(e) => { + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Err(e.into()) + }, + }, + Err(e) => { + log_error!(self.logger, "Lightning wallet sync timed out: {}", e); + Err(Error::TxSyncTimeout) + }, + } + }; + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + let now = Instant::now(); + let estimates = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + self.esplora_client.get_fee_estimates(), + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })? + .map_err(|e| { + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + Error::FeerateEstimationUpdateFailed + })?; + + if estimates.is_empty() && self.config.network == Network::Bitcoin { + // Ensure we fail if we didn't receive any estimates. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: empty fee estimates are dissallowed on Mainnet.", + ); + return Err(Error::FeerateEstimationUpdateFailed); + } + + let confirmation_targets = get_all_conf_targets(); + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + for target in confirmation_targets { + let num_blocks = get_num_block_defaults_for_target(target); + + // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it + // yields less than that. This is mostly necessary to continue on + // `signet`/`regtest` where we might not get estimates (or bogus values). + let converted_estimate_sat_vb = + esplora_client::convert_fee_rate(num_blocks, estimates.clone()) + .map_or(1.0, |converted| converted.max(1.0)); + + let fee_rate = FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_queue(&self) { + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in &next_package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.esplora_client.broadcast(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. + log_trace!( + self.logger, + "Failed to broadcast due to HTTP connection error: {}", + message + ); + } else { + log_error!( + self.logger, + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message + ); + } + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + _ => { + log_error!( + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + } + } + } + } +} + +impl Filter for EsploraChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.tx_sync.register_tx(txid, script_pubkey); + } + fn register_output(&self, output: WatchedOutput) { + self.tx_sync.register_output(output); + } +} diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 4544a6d3a1..5674bad8b4 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -7,16 +7,16 @@ mod bitcoind; mod electrum; +mod esplora; use crate::chain::bitcoind::{ BitcoindClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; use crate::chain::electrum::ElectrumRuntimeClient; +use crate::chain::esplora::EsploraChainSource; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, - BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, - DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, - LDK_WALLET_SYNC_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; use crate::fee_estimator::{ @@ -37,11 +37,6 @@ use lightning_block_sync::init::{synchronize_listeners, validate_best_block_head use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; -use lightning_transaction_sync::EsploraSyncClient; - -use bdk_esplora::EsploraAsyncExt; -use esplora_client::AsyncClient as EsploraAsyncClient; - use bdk_wallet::Update as BdkUpdate; use bitcoin::{FeeRate, Network, Script, ScriptBuf, Txid}; @@ -181,70 +176,6 @@ impl ElectrumRuntimeStatus { } } -pub(super) struct EsploraChainSource { - pub(super) sync_config: EsploraSyncConfig, - esplora_client: EsploraAsyncClient, - onchain_wallet: Arc, - onchain_wallet_sync_status: Mutex, - tx_sync: Arc>>, - lightning_wallet_sync_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, -} - -impl EsploraChainSource { - pub(crate) fn new( - server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, - onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, - ) -> Self { - // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating - // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. - let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); - client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - - for (header_name, header_value) in &headers { - client_builder_0_11 = client_builder_0_11.header(header_name, header_value); - } - - let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); - let tx_sync = - Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); - - let mut client_builder = esplora_client::Builder::new(&server_url); - client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - - for (header_name, header_value) in &headers { - client_builder = client_builder.header(header_name, header_value); - } - - let esplora_client = client_builder.build_async().unwrap(); - - let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - Self { - sync_config, - esplora_client, - onchain_wallet, - onchain_wallet_sync_status, - tx_sync, - lightning_wallet_sync_status, - fee_estimator, - tx_broadcaster, - kv_store, - config, - logger, - node_metrics, - } - } -} - - pub(crate) struct ChainSource { kind: ChainSourceKind, logger: Arc, @@ -724,118 +655,6 @@ impl ChainSource { } } -impl EsploraChainSource { - pub(super) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - let receiver_res = { - let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let res = { - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - - macro_rules! get_and_apply_wallet_update { - ($sync_future: expr) => {{ - let now = Instant::now(); - match $sync_future.await { - Ok(res) => match res { - Ok(update) => match self.onchain_wallet.apply_update(update) { - Ok(()) => { - log_info!( - self.logger, - "{} of on-chain wallet finished in {}ms.", - if incremental_sync { "Incremental sync" } else { "Sync" }, - now.elapsed().as_millis() - ); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger) - )?; - } - Ok(()) - }, - Err(e) => Err(e), - }, - Err(e) => match *e { - esplora_client::Error::Reqwest(he) => { - log_error!( - self.logger, - "{} of on-chain wallet failed due to HTTP connection error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - he - ); - Err(Error::WalletOperationFailed) - }, - _ => { - log_error!( - self.logger, - "{} of on-chain wallet failed due to Esplora error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationFailed) - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "{} of on-chain wallet timed out: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationTimeout) - }, - } - }} - } - - if incremental_sync { - let sync_request = self.onchain_wallet.get_incremental_sync_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } else { - let full_scan_request = self.onchain_wallet.get_full_scan_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - self.esplora_client.full_scan( - full_scan_request, - BDK_CLIENT_STOP_GAP, - BDK_CLIENT_CONCURRENCY, - ), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } - }; - - self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res - } -} - impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -945,87 +764,6 @@ impl ChainSource { } } -impl EsploraChainSource { - pub(super) async fn sync_lightning_wallet( - &self, channel_manager: Arc, chain_monitor: Arc, - output_sweeper: Arc, - ) -> Result<(), Error> { - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; - - let receiver_res = { - let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - let res = { - let timeout_fut = tokio::time::timeout( - Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), - self.tx_sync.sync(confirmables), - ); - let now = Instant::now(); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_info!( - self.logger, - "Sync of Lightning wallet finished in {}ms.", - now.elapsed().as_millis() - ); - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - Arc::clone(&self.node_metrics), - )?; - Ok(()) - }, - Err(e) => { - log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); - Err(e.into()) - }, - }, - Err(e) => { - log_error!(self.logger, "Lightning wallet sync timed out: {}", e); - Err(Error::TxSyncTimeout) - }, - } - }; - - self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res - } -} - impl ChainSource { // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -1275,84 +1013,6 @@ impl ChainSource { } } -impl EsploraChainSource { - pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - let now = Instant::now(); - let estimates = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - self.esplora_client.get_fee_estimates(), - ) - .await - .map_err(|e| { - log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })? - .map_err(|e| { - log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); - Error::FeerateEstimationUpdateFailed - })?; - - if estimates.is_empty() && self.config.network == Network::Bitcoin { - // Ensure we fail if we didn't receive any estimates. - log_error!( - self.logger, - "Failed to retrieve fee rate estimates: empty fee estimates are dissallowed on Mainnet.", - ); - return Err(Error::FeerateEstimationUpdateFailed); - } - - let confirmation_targets = get_all_conf_targets(); - - let mut new_fee_rate_cache = HashMap::with_capacity(10); - for target in confirmation_targets { - let num_blocks = get_num_block_defaults_for_target(target); - - // Convert the retrieved fee rate and fall back to 1 sat/vb if we fail or it - // yields less than that. This is mostly necessary to continue on - // `signet`/`regtest` where we might not get estimates (or bogus values). - let converted_estimate_sat_vb = - esplora_client::convert_fee_rate(num_blocks, estimates.clone()) - .map_or(1.0, |converted| converted.max(1.0)); - - let fee_rate = FeeRate::from_sat_per_kwu((converted_estimate_sat_vb * 250.0) as u64); - - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - - new_fee_rate_cache.insert(target, adjusted_fee_rate); - - log_trace!( - self.logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } - - self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); - - log_info!( - self.logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - Ok(()) - } -} - impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { @@ -1537,82 +1197,6 @@ impl ChainSource { } } -impl EsploraChainSource { - pub(crate) async fn process_broadcast_queue(&self) { - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - self.esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); - }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message - ); - } - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - _ => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } - } - } - } -} - impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { @@ -1697,15 +1281,6 @@ impl ChainSource { } } -impl Filter for EsploraChainSource { - fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { - self.tx_sync.register_tx(txid, script_pubkey); - } - fn register_output(&self, output: WatchedOutput) { - self.tx_sync.register_output(output); - } -} - impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { match &self.kind { From fd517ed951905dec2c2b72448a9cc2229a636dd5 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 24 Jul 2025 16:01:45 +0200 Subject: [PATCH 098/177] Move Electrum sync logic to a `ElectrumChainSource` type We refactor our `ChainSource` logic and move out the Electrum code into a new object. --- src/chain/electrum.rs | 92 +++++- src/chain/mod.rs | 633 ++++++++++++++++++++---------------------- 2 files changed, 378 insertions(+), 347 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 9882e652b0..844e461879 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -32,7 +32,7 @@ use electrum_client::Client as ElectrumClient; use electrum_client::ConfigBuilder as ElectrumConfigBuilder; use electrum_client::{Batch, ElectrumApi}; -use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; use std::collections::HashMap; use std::sync::Arc; @@ -42,7 +42,83 @@ const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 10; -pub(crate) struct ElectrumRuntimeClient { +pub(super) enum ElectrumRuntimeStatus { + Started(Arc), + Stopped { + pending_registered_txs: Vec<(Txid, ScriptBuf)>, + pending_registered_outputs: Vec, + }, +} + +impl ElectrumRuntimeStatus { + pub(super) fn new() -> Self { + let pending_registered_txs = Vec::new(); + let pending_registered_outputs = Vec::new(); + Self::Stopped { pending_registered_txs, pending_registered_outputs } + } + + pub(super) fn start( + &mut self, server_url: String, runtime: Arc, config: Arc, + logger: Arc, + ) -> Result<(), Error> { + match self { + Self::Stopped { pending_registered_txs, pending_registered_outputs } => { + let client = Arc::new(ElectrumRuntimeClient::new( + server_url.clone(), + runtime, + config, + logger, + )?); + + // Apply any pending `Filter` entries + for (txid, script_pubkey) in pending_registered_txs.drain(..) { + client.register_tx(&txid, &script_pubkey); + } + + for output in pending_registered_outputs.drain(..) { + client.register_output(output) + } + + *self = Self::Started(client); + }, + Self::Started(_) => { + debug_assert!(false, "We shouldn't call start if we're already started") + }, + } + Ok(()) + } + + pub(super) fn stop(&mut self) { + *self = Self::new() + } + + pub(super) fn client(&self) -> Option> { + match self { + Self::Started(client) => Some(Arc::clone(&client)), + Self::Stopped { .. } => None, + } + } + + pub(super) fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { + match self { + Self::Started(client) => client.register_tx(txid, script_pubkey), + Self::Stopped { pending_registered_txs, .. } => { + pending_registered_txs.push((*txid, script_pubkey.to_owned())) + }, + } + } + + pub(super) fn register_output(&mut self, output: WatchedOutput) { + match self { + Self::Started(client) => client.register_output(output), + Self::Stopped { pending_registered_outputs, .. } => { + pending_registered_outputs.push(output) + }, + } + } +} + +pub(super) struct ElectrumRuntimeClient { electrum_client: Arc, bdk_electrum_client: Arc>, tx_sync: Arc>>, @@ -52,7 +128,7 @@ pub(crate) struct ElectrumRuntimeClient { } impl ElectrumRuntimeClient { - pub(crate) fn new( + pub(super) fn new( server_url: String, runtime: Arc, config: Arc, logger: Arc, ) -> Result { @@ -82,7 +158,7 @@ impl ElectrumRuntimeClient { Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger }) } - pub(crate) async fn sync_confirmables( + pub(super) async fn sync_confirmables( &self, confirmables: Vec>, ) -> Result<(), Error> { let now = Instant::now(); @@ -116,7 +192,7 @@ impl ElectrumRuntimeClient { Ok(res) } - pub(crate) async fn get_full_scan_wallet_update( + pub(super) async fn get_full_scan_wallet_update( &self, request: BdkFullScanRequest, cached_txs: impl IntoIterator>>, ) -> Result, Error> { @@ -150,7 +226,7 @@ impl ElectrumRuntimeClient { }) } - pub(crate) async fn get_incremental_sync_wallet_update( + pub(super) async fn get_incremental_sync_wallet_update( &self, request: BdkSyncRequest<(BdkKeyChainKind, u32)>, cached_txs: impl IntoIterator>>, ) -> Result { @@ -179,7 +255,7 @@ impl ElectrumRuntimeClient { }) } - pub(crate) async fn broadcast(&self, tx: Transaction) { + pub(super) async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); let txid = tx.compute_txid(); @@ -221,7 +297,7 @@ impl ElectrumRuntimeClient { } } - pub(crate) async fn get_fee_rate_cache_update( + pub(super) async fn get_fee_rate_cache_update( &self, ) -> Result, Error> { let electrum_client = Arc::clone(&self.electrum_client); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 5674bad8b4..9119751733 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -9,10 +9,11 @@ mod bitcoind; mod electrum; mod esplora; +use electrum::{ElectrumRuntimeClient, ElectrumRuntimeStatus}; + use crate::chain::bitcoind::{ BitcoindClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; -use crate::chain::electrum::ElectrumRuntimeClient; use crate::chain::esplora::EsploraChainSource; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, @@ -39,7 +40,7 @@ use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; use bdk_wallet::Update as BdkUpdate; -use bitcoin::{FeeRate, Network, Script, ScriptBuf, Txid}; +use bitcoin::{FeeRate, Network, Script, Txid}; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; @@ -100,79 +101,58 @@ impl WalletSyncStatus { } } -pub(crate) enum ElectrumRuntimeStatus { - Started(Arc), - Stopped { - pending_registered_txs: Vec<(Txid, ScriptBuf)>, - pending_registered_outputs: Vec, - }, +pub(super) struct ElectrumChainSource { + server_url: String, + pub(super) sync_config: ElectrumSyncConfig, + electrum_runtime_status: RwLock, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, } -impl ElectrumRuntimeStatus { - pub(crate) fn new() -> Self { - let pending_registered_txs = Vec::new(); - let pending_registered_outputs = Vec::new(); - Self::Stopped { pending_registered_txs, pending_registered_outputs } - } - - pub(crate) fn start( - &mut self, server_url: String, runtime: Arc, config: Arc, - logger: Arc, - ) -> Result<(), Error> { - match self { - Self::Stopped { pending_registered_txs, pending_registered_outputs } => { - let client = Arc::new(ElectrumRuntimeClient::new( - server_url.clone(), - runtime, - config, - logger, - )?); - - // Apply any pending `Filter` entries - for (txid, script_pubkey) in pending_registered_txs.drain(..) { - client.register_tx(&txid, &script_pubkey); - } - - for output in pending_registered_outputs.drain(..) { - client.register_output(output) - } - - *self = Self::Started(client); - }, - Self::Started(_) => { - debug_assert!(false, "We shouldn't call start if we're already started") - }, - } - Ok(()) - } - - pub(crate) fn stop(&mut self) { - *self = Self::new() - } - - pub(crate) fn client(&self) -> Option> { - match self { - Self::Started(client) => Some(Arc::clone(&client)), - Self::Stopped { .. } => None, +impl ElectrumChainSource { + pub(super) fn new( + server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, + fee_estimator: Arc, tx_broadcaster: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + server_url, + sync_config, + electrum_runtime_status, + onchain_wallet, + onchain_wallet_sync_status, + lightning_wallet_sync_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, } } - fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { - match self { - Self::Started(client) => client.register_tx(txid, script_pubkey), - Self::Stopped { pending_registered_txs, .. } => { - pending_registered_txs.push((*txid, script_pubkey.to_owned())) - }, - } + pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { + self.electrum_runtime_status.write().unwrap().start( + self.server_url.clone(), + Arc::clone(&runtime), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) } - fn register_output(&mut self, output: lightning::chain::WatchedOutput) { - match self { - Self::Started(client) => client.register_output(output), - Self::Stopped { pending_registered_outputs, .. } => { - pending_registered_outputs.push(output) - }, - } + pub(super) fn stop(&self) { + self.electrum_runtime_status.write().unwrap().stop(); } } @@ -183,20 +163,7 @@ pub(crate) struct ChainSource { enum ChainSourceKind { Esplora(EsploraChainSource), - Electrum { - server_url: String, - sync_config: ElectrumSyncConfig, - electrum_runtime_status: RwLock, - onchain_wallet: Arc, - onchain_wallet_sync_status: Mutex, - lightning_wallet_sync_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, - }, + Electrum(ElectrumChainSource), Bitcoind { api_client: Arc, header_cache: tokio::sync::Mutex, @@ -241,23 +208,18 @@ impl ChainSource { kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); - let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let kind = ChainSourceKind::Electrum { + let electrum_chain_source = ElectrumChainSource::new( server_url, sync_config, - electrum_runtime_status, onchain_wallet, - onchain_wallet_sync_status, - lightning_wallet_sync_status, fee_estimator, tx_broadcaster, kv_store, config, - logger: Arc::clone(&logger), + Arc::clone(&logger), node_metrics, - }; + ); + let kind = ChainSourceKind::Electrum(electrum_chain_source); Self { kind, logger } } @@ -331,19 +293,8 @@ impl ChainSource { pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { match &self.kind { - ChainSourceKind::Electrum { - server_url, - electrum_runtime_status, - config, - logger, - .. - } => { - electrum_runtime_status.write().unwrap().start( - server_url.clone(), - Arc::clone(&runtime), - Arc::clone(&config), - Arc::clone(&logger), - )?; + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.start(runtime)? }, _ => { // Nothing to do for other chain sources. @@ -354,9 +305,7 @@ impl ChainSource { pub(crate) fn stop(&self) { match &self.kind { - ChainSourceKind::Electrum { electrum_runtime_status, .. } => { - electrum_runtime_status.write().unwrap().stop(); - }, + ChainSourceKind::Electrum(electrum_chain_source) => electrum_chain_source.stop(), _ => { // Nothing to do for other chain sources. }, @@ -406,15 +355,17 @@ impl ChainSource { return; } }, - ChainSourceKind::Electrum { sync_config, logger, .. } => { - if let Some(background_sync_config) = sync_config.background_sync_config.as_ref() { + ChainSourceKind::Electrum(electrum_chain_source) => { + if let Some(background_sync_config) = + electrum_chain_source.sync_config.background_sync_config.as_ref() + { self.start_tx_based_sync_loop( stop_sync_receiver, channel_manager, chain_monitor, output_sweeper, background_sync_config, - Arc::clone(&logger), + Arc::clone(&self.logger), ) .await } else { @@ -655,6 +606,90 @@ impl ChainSource { } } +impl ElectrumChainSource { + pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the onchain wallet" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + let apply_wallet_update = + |update_res: Result, now: Instant| match update_res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => Err(e), + }; + + let cached_txs = self.onchain_wallet.get_cached_txs(); + + let res = if incremental_sync { + let incremental_sync_request = self.onchain_wallet.get_incremental_sync_request(); + let incremental_sync_fut = electrum_client + .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); + + let now = Instant::now(); + let update_res = incremental_sync_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let full_scan_fut = + electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); + let now = Instant::now(); + let update_res = full_scan_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + }; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } +} + impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -663,97 +698,8 @@ impl ChainSource { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.sync_onchain_wallet().await }, - ChainSourceKind::Electrum { - electrum_runtime_status, - onchain_wallet, - onchain_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let electrum_client: Arc = if let Some(client) = - electrum_runtime_status.read().unwrap().client().as_ref() - { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the onchain wallet" - ); - return Err(Error::FeerateEstimationUpdateFailed); - }; - let receiver_res = { - let mut status_lock = onchain_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - - let apply_wallet_update = - |update_res: Result, now: Instant| match update_res { - Ok(update) => match onchain_wallet.apply_update(update) { - Ok(()) => { - log_info!( - logger, - "{} of on-chain wallet finished in {}ms.", - if incremental_sync { "Incremental sync" } else { "Sync" }, - now.elapsed().as_millis() - ); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_onchain_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } - Ok(()) - }, - Err(e) => Err(e), - }, - Err(e) => Err(e), - }; - - let cached_txs = onchain_wallet.get_cached_txs(); - - let res = if incremental_sync { - let incremental_sync_request = onchain_wallet.get_incremental_sync_request(); - let incremental_sync_fut = electrum_client - .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); - - let now = Instant::now(); - let update_res = incremental_sync_fut.await.map(|u| u.into()); - apply_wallet_update(update_res, now) - } else { - let full_scan_request = onchain_wallet.get_full_scan_request(); - let full_scan_fut = - electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); - let now = Instant::now(); - let update_res = full_scan_fut.await.map(|u| u.into()); - apply_wallet_update(update_res, now) - }; - - onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.sync_onchain_wallet().await }, ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via @@ -764,6 +710,74 @@ impl ChainSource { } } +impl ElectrumChainSource { + pub(crate) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the lightning wallet" + ); + return Err(Error::TxSyncFailed); + }; + + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + sync_cman as Arc, + sync_cmon as Arc, + sync_sweeper as Arc, + ]; + + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::TxSyncFailed + })?; + } + + let res = electrum_client.sync_confirmables(confirmables).await; + + if let Ok(_) = res { + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + } + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } +} + impl ChainSource { // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -777,76 +791,10 @@ impl ChainSource { .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) .await }, - ChainSourceKind::Electrum { - electrum_runtime_status, - lightning_wallet_sync_status, - kv_store, - logger, - node_metrics, - .. - } => { - let electrum_client: Arc = if let Some(client) = - electrum_runtime_status.read().unwrap().client().as_ref() - { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the lightning wallet" - ); - return Err(Error::TxSyncFailed); - }; - - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - sync_cman as Arc, - sync_cmon as Arc, - sync_sweeper as Arc, - ]; - - let receiver_res = { - let mut status_lock = lightning_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(logger, "Failed to receive wallet sync result: {:?}", e); - Error::TxSyncFailed - })?; - } - - let res = electrum_client.sync_confirmables(confirmables).await; - - if let Ok(_) = res { - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } - - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&kv_store), - Arc::clone(&logger), - Arc::clone(&node_metrics), - )?; - } - - lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source + .sync_lightning_wallet(channel_manager, chain_monitor, output_sweeper) + .await }, ChainSourceKind::Bitcoind { .. } => { // In BitcoindRpc mode we sync lightning and onchain wallet in one go via @@ -1013,56 +961,52 @@ impl ChainSource { } } -impl ChainSource { +impl ElectrumChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - match &self.kind { - ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.update_fee_rate_estimates().await - }, - ChainSourceKind::Electrum { - electrum_runtime_status, - fee_estimator, - kv_store, - logger, - node_metrics, - .. - } => { - let electrum_client: Arc = if let Some(client) = - electrum_runtime_status.read().unwrap().client().as_ref() - { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before updating fees" - ); - return Err(Error::FeerateEstimationUpdateFailed); - }; + let electrum_client: Arc = if let Some(client) = + self.electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before updating fees"); + return Err(Error::FeerateEstimationUpdateFailed); + }; - let now = Instant::now(); + let now = Instant::now(); - let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; - fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); - log_info!( - logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; - } + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } - Ok(()) + Ok(()) + } +} + +impl ChainSource { + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.update_fee_rate_estimates().await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.update_fee_rate_estimates().await }, ChainSourceKind::Bitcoind { api_client, @@ -1197,31 +1141,33 @@ impl ChainSource { } } +impl ElectrumChainSource { + pub(crate) async fn process_broadcast_queue(&self) { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before broadcasting"); + return; + }; + + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in next_package { + electrum_client.broadcast(tx).await; + } + } + } +} + impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.process_broadcast_queue().await }, - ChainSourceKind::Electrum { electrum_runtime_status, tx_broadcaster, .. } => { - let electrum_client: Arc = if let Some(client) = - electrum_runtime_status.read().unwrap().client().as_ref() - { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before broadcasting" - ); - return; - }; - - let mut receiver = tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in next_package { - electrum_client.broadcast(tx).await; - } - } + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.process_broadcast_queue().await }, ChainSourceKind::Bitcoind { api_client, tx_broadcaster, logger, .. } => { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 @@ -1281,25 +1227,34 @@ impl ChainSource { } } +impl Filter for ElectrumChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) + } + fn register_output(&self, output: WatchedOutput) { + self.electrum_runtime_status.write().unwrap().register_output(output) + } +} + impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.register_tx(txid, script_pubkey) }, - ChainSourceKind::Electrum { electrum_runtime_status, .. } => { - electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.register_tx(txid, script_pubkey) }, ChainSourceKind::Bitcoind { .. } => (), } } - fn register_output(&self, output: lightning::chain::WatchedOutput) { + fn register_output(&self, output: WatchedOutput) { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.register_output(output) }, - ChainSourceKind::Electrum { electrum_runtime_status, .. } => { - electrum_runtime_status.write().unwrap().register_output(output) + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.register_output(output) }, ChainSourceKind::Bitcoind { .. } => (), } From 40d6440218bad490176e5f5685df4b4380d45589 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Jul 2025 09:44:33 +0200 Subject: [PATCH 099/177] Move `ElectrumChainSource` type to `chain::electrum` module --- src/chain/electrum.rs | 305 +++++++++++++++++++++++++++++++++++++++--- src/chain/mod.rs | 282 +------------------------------------- 2 files changed, 291 insertions(+), 296 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 844e461879..44a637cc39 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -5,16 +5,21 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; + use crate::config::{ - Config, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, - LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, + Config, ElectrumSyncConfig, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, + FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, }; use crate::error::Error; use crate::fee_estimator::{ apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, - ConfirmationTarget, + ConfirmationTarget, OnchainFeeEstimator, }; +use crate::io::utils::write_node_metrics; use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::NodeMetrics; use lightning::chain::{Confirm, Filter, WatchedOutput}; use lightning::util::ser::Writeable; @@ -25,6 +30,7 @@ use bdk_chain::bdk_core::spk_client::FullScanResponse as BdkFullScanResponse; use bdk_chain::bdk_core::spk_client::SyncRequest as BdkSyncRequest; use bdk_chain::bdk_core::spk_client::SyncResponse as BdkSyncResponse; use bdk_wallet::KeychainKind as BdkKeyChainKind; +use bdk_wallet::Update as BdkUpdate; use bdk_electrum::BdkElectrumClient; @@ -35,14 +41,279 @@ use electrum_client::{Batch, ElectrumApi}; use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; use std::collections::HashMap; -use std::sync::Arc; -use std::time::{Duration, Instant}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 10; -pub(super) enum ElectrumRuntimeStatus { +pub(super) struct ElectrumChainSource { + server_url: String, + pub(super) sync_config: ElectrumSyncConfig, + electrum_runtime_status: RwLock, + onchain_wallet: Arc, + onchain_wallet_sync_status: Mutex, + lightning_wallet_sync_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl ElectrumChainSource { + pub(super) fn new( + server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, + fee_estimator: Arc, tx_broadcaster: Arc, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); + let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); + Self { + server_url, + sync_config, + electrum_runtime_status, + onchain_wallet, + onchain_wallet_sync_status, + lightning_wallet_sync_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { + self.electrum_runtime_status.write().unwrap().start( + self.server_url.clone(), + Arc::clone(&runtime), + Arc::clone(&self.config), + Arc::clone(&self.logger), + ) + } + + pub(super) fn stop(&self) { + self.electrum_runtime_status.write().unwrap().stop(); + } + + pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the onchain wallet" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; + let receiver_res = { + let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + let apply_wallet_update = + |update_res: Result, now: Instant| match update_res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => Err(e), + }; + + let cached_txs = self.onchain_wallet.get_cached_txs(); + + let res = if incremental_sync { + let incremental_sync_request = self.onchain_wallet.get_incremental_sync_request(); + let incremental_sync_fut = electrum_client + .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); + + let now = Instant::now(); + let update_res = incremental_sync_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let full_scan_fut = + electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); + let now = Instant::now(); + let update_res = full_scan_fut.await.map(|u| u.into()); + apply_wallet_update(update_res, now) + }; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + pub(crate) async fn sync_lightning_wallet( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the lightning wallet" + ); + return Err(Error::TxSyncFailed); + }; + + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + sync_cman as Arc, + sync_cmon as Arc, + sync_sweeper as Arc, + ]; + + let receiver_res = { + let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); + Error::TxSyncFailed + })?; + } + + let res = electrum_client.sync_confirmables(confirmables).await; + + if let Ok(_) = res { + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + } + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + let electrum_client: Arc = if let Some(client) = + self.electrum_runtime_status.read().unwrap().client().as_ref() + { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before updating fees"); + return Err(Error::FeerateEstimationUpdateFailed); + }; + + let now = Instant::now(); + + let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; + self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); + + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_queue(&self) { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!(false, "We should have started the chain source before broadcasting"); + return; + }; + + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in next_package { + electrum_client.broadcast(tx).await; + } + } + } +} + +impl Filter for ElectrumChainSource { + fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { + self.electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) + } + fn register_output(&self, output: lightning::chain::WatchedOutput) { + self.electrum_runtime_status.write().unwrap().register_output(output) + } +} + +enum ElectrumRuntimeStatus { Started(Arc), Stopped { pending_registered_txs: Vec<(Txid, ScriptBuf)>, @@ -51,7 +322,7 @@ pub(super) enum ElectrumRuntimeStatus { } impl ElectrumRuntimeStatus { - pub(super) fn new() -> Self { + fn new() -> Self { let pending_registered_txs = Vec::new(); let pending_registered_outputs = Vec::new(); Self::Stopped { pending_registered_txs, pending_registered_outputs } @@ -92,14 +363,14 @@ impl ElectrumRuntimeStatus { *self = Self::new() } - pub(super) fn client(&self) -> Option> { + fn client(&self) -> Option> { match self { Self::Started(client) => Some(Arc::clone(&client)), Self::Stopped { .. } => None, } } - pub(super) fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { + fn register_tx(&mut self, txid: &Txid, script_pubkey: &Script) { match self { Self::Started(client) => client.register_tx(txid, script_pubkey), Self::Stopped { pending_registered_txs, .. } => { @@ -108,7 +379,7 @@ impl ElectrumRuntimeStatus { } } - pub(super) fn register_output(&mut self, output: WatchedOutput) { + fn register_output(&mut self, output: lightning::chain::WatchedOutput) { match self { Self::Started(client) => client.register_output(output), Self::Stopped { pending_registered_outputs, .. } => { @@ -118,7 +389,7 @@ impl ElectrumRuntimeStatus { } } -pub(super) struct ElectrumRuntimeClient { +struct ElectrumRuntimeClient { electrum_client: Arc, bdk_electrum_client: Arc>, tx_sync: Arc>>, @@ -128,7 +399,7 @@ pub(super) struct ElectrumRuntimeClient { } impl ElectrumRuntimeClient { - pub(super) fn new( + fn new( server_url: String, runtime: Arc, config: Arc, logger: Arc, ) -> Result { @@ -158,7 +429,7 @@ impl ElectrumRuntimeClient { Ok(Self { electrum_client, bdk_electrum_client, tx_sync, runtime, config, logger }) } - pub(super) async fn sync_confirmables( + async fn sync_confirmables( &self, confirmables: Vec>, ) -> Result<(), Error> { let now = Instant::now(); @@ -192,7 +463,7 @@ impl ElectrumRuntimeClient { Ok(res) } - pub(super) async fn get_full_scan_wallet_update( + async fn get_full_scan_wallet_update( &self, request: BdkFullScanRequest, cached_txs: impl IntoIterator>>, ) -> Result, Error> { @@ -226,7 +497,7 @@ impl ElectrumRuntimeClient { }) } - pub(super) async fn get_incremental_sync_wallet_update( + async fn get_incremental_sync_wallet_update( &self, request: BdkSyncRequest<(BdkKeyChainKind, u32)>, cached_txs: impl IntoIterator>>, ) -> Result { @@ -255,7 +526,7 @@ impl ElectrumRuntimeClient { }) } - pub(super) async fn broadcast(&self, tx: Transaction) { + async fn broadcast(&self, tx: Transaction) { let electrum_client = Arc::clone(&self.electrum_client); let txid = tx.compute_txid(); @@ -297,7 +568,7 @@ impl ElectrumRuntimeClient { } } - pub(super) async fn get_fee_rate_cache_update( + async fn get_fee_rate_cache_update( &self, ) -> Result, Error> { let electrum_client = Arc::clone(&self.electrum_client); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 9119751733..0453101986 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -9,11 +9,10 @@ mod bitcoind; mod electrum; mod esplora; -use electrum::{ElectrumRuntimeClient, ElectrumRuntimeStatus}; - use crate::chain::bitcoind::{ BitcoindClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, }; +use crate::chain::electrum::ElectrumChainSource; use crate::chain::esplora::EsploraChainSource; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, @@ -30,7 +29,7 @@ use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, use crate::{Error, NodeMetrics}; use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::{Confirm, Filter, Listen, WatchedOutput}; +use lightning::chain::{Filter, Listen}; use lightning::util::ser::Writeable; use lightning_block_sync::gossip::UtxoSource; @@ -38,8 +37,6 @@ use lightning_block_sync::init::{synchronize_listeners, validate_best_block_head use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; -use bdk_wallet::Update as BdkUpdate; - use bitcoin::{FeeRate, Network, Script, Txid}; use std::collections::HashMap; @@ -101,61 +98,6 @@ impl WalletSyncStatus { } } -pub(super) struct ElectrumChainSource { - server_url: String, - pub(super) sync_config: ElectrumSyncConfig, - electrum_runtime_status: RwLock, - onchain_wallet: Arc, - onchain_wallet_sync_status: Mutex, - lightning_wallet_sync_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, -} - -impl ElectrumChainSource { - pub(super) fn new( - server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, - fee_estimator: Arc, tx_broadcaster: Arc, - kv_store: Arc, config: Arc, logger: Arc, - node_metrics: Arc>, - ) -> Self { - let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); - let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); - Self { - server_url, - sync_config, - electrum_runtime_status, - onchain_wallet, - onchain_wallet_sync_status, - lightning_wallet_sync_status, - fee_estimator, - tx_broadcaster, - kv_store, - config, - logger: Arc::clone(&logger), - node_metrics, - } - } - - pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { - self.electrum_runtime_status.write().unwrap().start( - self.server_url.clone(), - Arc::clone(&runtime), - Arc::clone(&self.config), - Arc::clone(&self.logger), - ) - } - - pub(super) fn stop(&self) { - self.electrum_runtime_status.write().unwrap().stop(); - } -} - pub(crate) struct ChainSource { kind: ChainSourceKind, logger: Arc, @@ -606,90 +548,6 @@ impl ChainSource { } } -impl ElectrumChainSource { - pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - let electrum_client: Arc = - if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the onchain wallet" - ); - return Err(Error::FeerateEstimationUpdateFailed); - }; - let receiver_res = { - let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - - let apply_wallet_update = - |update_res: Result, now: Instant| match update_res { - Ok(update) => match self.onchain_wallet.apply_update(update) { - Ok(()) => { - log_info!( - self.logger, - "{} of on-chain wallet finished in {}ms.", - if incremental_sync { "Incremental sync" } else { "Sync" }, - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_onchain_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - Ok(()) - }, - Err(e) => Err(e), - }, - Err(e) => Err(e), - }; - - let cached_txs = self.onchain_wallet.get_cached_txs(); - - let res = if incremental_sync { - let incremental_sync_request = self.onchain_wallet.get_incremental_sync_request(); - let incremental_sync_fut = electrum_client - .get_incremental_sync_wallet_update(incremental_sync_request, cached_txs); - - let now = Instant::now(); - let update_res = incremental_sync_fut.await.map(|u| u.into()); - apply_wallet_update(update_res, now) - } else { - let full_scan_request = self.onchain_wallet.get_full_scan_request(); - let full_scan_fut = - electrum_client.get_full_scan_wallet_update(full_scan_request, cached_txs); - let now = Instant::now(); - let update_res = full_scan_fut.await.map(|u| u.into()); - apply_wallet_update(update_res, now) - }; - - self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res - } -} - impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -710,74 +568,6 @@ impl ChainSource { } } -impl ElectrumChainSource { - pub(crate) async fn sync_lightning_wallet( - &self, channel_manager: Arc, chain_monitor: Arc, - output_sweeper: Arc, - ) -> Result<(), Error> { - let electrum_client: Arc = - if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the lightning wallet" - ); - return Err(Error::TxSyncFailed); - }; - - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - sync_cman as Arc, - sync_cmon as Arc, - sync_sweeper as Arc, - ]; - - let receiver_res = { - let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - if let Some(mut sync_receiver) = receiver_res { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet sync result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet sync result: {:?}", e); - Error::TxSyncFailed - })?; - } - - let res = electrum_client.sync_confirmables(confirmables).await; - - if let Ok(_) = res { - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - Arc::clone(&self.node_metrics), - )?; - } - - self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res - } -} - impl ChainSource { // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) @@ -961,44 +751,6 @@ impl ChainSource { } } -impl ElectrumChainSource { - pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - let electrum_client: Arc = if let Some(client) = - self.electrum_runtime_status.read().unwrap().client().as_ref() - { - Arc::clone(client) - } else { - debug_assert!(false, "We should have started the chain source before updating fees"); - return Err(Error::FeerateEstimationUpdateFailed); - }; - - let now = Instant::now(); - - let new_fee_rate_cache = electrum_client.get_fee_rate_cache_update().await?; - self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache); - - log_info!( - self.logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - Ok(()) - } -} - impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { @@ -1141,25 +893,6 @@ impl ChainSource { } } -impl ElectrumChainSource { - pub(crate) async fn process_broadcast_queue(&self) { - let electrum_client: Arc = - if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { - Arc::clone(client) - } else { - debug_assert!(false, "We should have started the chain source before broadcasting"); - return; - }; - - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in next_package { - electrum_client.broadcast(tx).await; - } - } - } -} - impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { @@ -1227,15 +960,6 @@ impl ChainSource { } } -impl Filter for ElectrumChainSource { - fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { - self.electrum_runtime_status.write().unwrap().register_tx(txid, script_pubkey) - } - fn register_output(&self, output: WatchedOutput) { - self.electrum_runtime_status.write().unwrap().register_output(output) - } -} - impl Filter for ChainSource { fn register_tx(&self, txid: &Txid, script_pubkey: &Script) { match &self.kind { @@ -1248,7 +972,7 @@ impl Filter for ChainSource { ChainSourceKind::Bitcoind { .. } => (), } } - fn register_output(&self, output: WatchedOutput) { + fn register_output(&self, output: lightning::chain::WatchedOutput) { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { esplora_chain_source.register_output(output) From 4541a0e45d1f662e524b80995c46d543801961a6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 24 Jul 2025 16:29:36 +0200 Subject: [PATCH 100/177] Move Bitcoind sync logic to a `BitcoindChainSource` type We refactor our `ChainSource` logic and move out the Bitcoind code into a new object. --- src/chain/bitcoind.rs | 9 +- src/chain/mod.rs | 1081 +++++++++++++++++++++-------------------- 2 files changed, 567 insertions(+), 523 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 98e77cac7c..52dad77416 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -7,10 +7,8 @@ use crate::types::{ChainMonitor, ChannelManager, Sweeper, Wallet}; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; use lightning::chain::Listen; + use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::poll::ValidatedBlockHeader; @@ -19,9 +17,12 @@ use lightning_block_sync::rpc::{RpcClient, RpcError}; use lightning_block_sync::{ AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache, }; - use serde::Serialize; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; + use std::collections::{HashMap, VecDeque}; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 0453101986..338fd0d309 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -43,8 +43,6 @@ use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; -const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; - pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, @@ -98,6 +96,250 @@ impl WalletSyncStatus { } } +const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; + +pub(super) struct BitcoindChainSource { + api_client: Arc, + header_cache: tokio::sync::Mutex, + latest_chain_tip: RwLock>, + onchain_wallet: Arc, + wallet_polling_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl BitcoindChainSource { + pub(crate) fn new_rpc( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rpc( + rpc_host.clone(), + rpc_port.clone(), + rpc_user.clone(), + rpc_password.clone(), + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + Self { + api_client, + header_cache, + latest_chain_tip, + onchain_wallet, + wallet_polling_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(crate) fn new_rest( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + rest_client_config: BitcoindRestClientConfig, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rest( + rest_client_config.rest_host, + rest_client_config.rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + + Self { + api_client, + header_cache, + latest_chain_tip, + wallet_polling_status, + onchain_wallet, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(super) fn as_utxo_source(&self) -> Arc { + self.api_client.utxo_source() + } + + pub(super) async fn continuously_sync_wallets( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) { + // First register for the wallet polling status to make sure `Node::sync_wallets` calls + // wait on the result before proceeding. + { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + if status_lock.register_or_subscribe_pending_sync().is_some() { + debug_assert!(false, "Sync already in progress. This should never happen."); + } + } + + log_info!( + self.logger, + "Starting initial synchronization of chain listeners. This might take a while..", + ); + + let mut backoff = CHAIN_POLLING_INTERVAL_SECS; + const MAX_BACKOFF_SECS: u64 = 300; + + loop { + let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; + let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; + let onchain_wallet_best_block_hash = + self.onchain_wallet.current_best_block().block_hash; + + let mut chain_listeners = vec![ + ( + onchain_wallet_best_block_hash, + &*self.onchain_wallet as &(dyn Listen + Send + Sync), + ), + (channel_manager_best_block_hash, &*channel_manager as &(dyn Listen + Send + Sync)), + (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), + ]; + + // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s + // before giving them to `ChainMonitor` it the first place. However, this isn't + // trivial as we load them on initialization (in the `Builder`) and only gain + // network access during `start`. For now, we just make sure we get the worst known + // block hash and sychronize them via `ChainMonitor`. + if let Some(worst_channel_monitor_block_hash) = chain_monitor + .list_monitors() + .iter() + .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) + .map(|m| m.current_best_block()) + .min_by_key(|b| b.height) + .map(|b| b.block_hash) + { + chain_listeners.push(( + worst_channel_monitor_block_hash, + &*chain_monitor as &(dyn Listen + Send + Sync), + )); + } + + let mut locked_header_cache = self.header_cache.lock().await; + let now = SystemTime::now(); + match synchronize_listeners( + self.api_client.as_ref(), + self.config.network, + &mut *locked_header_cache, + chain_listeners.clone(), + ) + .await + { + Ok(chain_tip) => { + { + log_info!( + self.logger, + "Finished synchronizing listeners in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(chain_tip); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to persist node metrics: {}", e); + }); + } + break; + }, + + Err(e) => { + log_error!(self.logger, "Failed to synchronize chain listeners: {:?}", e); + if e.kind() == BlockSourceErrorKind::Transient { + log_info!( + self.logger, + "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + backoff + ); + tokio::time::sleep(Duration::from_secs(backoff)).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); + } else { + log_error!( + self.logger, + "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + MAX_BACKOFF_SECS + ); + tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; + } + }, + } + } + + // Now propagate the initial result to unblock waiting subscribers. + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); + + let mut chain_polling_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + chain_polling_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let mut fee_rate_update_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + log_info!(self.logger, "Starting continuous polling for chain updates."); + + // Start the polling loop. + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + self.logger, + "Stopping polling for new chain data.", + ); + return; + } + _ = chain_polling_interval.tick() => { + let _ = self.poll_and_update_listeners( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper) + ).await; + } + _ = fee_rate_update_interval.tick() => { + let _ = self.update_fee_rate_estimates().await; + } + } + } + } +} + pub(crate) struct ChainSource { kind: ChainSourceKind, logger: Arc, @@ -106,19 +348,7 @@ pub(crate) struct ChainSource { enum ChainSourceKind { Esplora(EsploraChainSource), Electrum(ElectrumChainSource), - Bitcoind { - api_client: Arc, - header_cache: tokio::sync::Mutex, - latest_chain_tip: RwLock>, - onchain_wallet: Arc, - wallet_polling_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, - }, + Bitcoind(BitcoindChainSource), } impl ChainSource { @@ -171,29 +401,20 @@ impl ChainSource { tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - let api_client = Arc::new(BitcoindClient::new_rpc( - rpc_host.clone(), - rpc_port.clone(), - rpc_user.clone(), - rpc_password.clone(), - )); - - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); - let latest_chain_tip = RwLock::new(None); - let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - let kind = ChainSourceKind::Bitcoind { - api_client, - header_cache, - latest_chain_tip, + let bitcoind_chain_source = BitcoindChainSource::new_rpc( + rpc_host, + rpc_port, + rpc_user, + rpc_password, onchain_wallet, - wallet_polling_status, fee_estimator, tx_broadcaster, kv_store, config, - logger: Arc::clone(&logger), + Arc::clone(&logger), node_metrics, - }; + ); + let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); Self { kind, logger } } @@ -204,32 +425,21 @@ impl ChainSource { rest_client_config: BitcoindRestClientConfig, logger: Arc, node_metrics: Arc>, ) -> Self { - let api_client = Arc::new(BitcoindClient::new_rest( - rest_client_config.rest_host, - rest_client_config.rest_port, + let bitcoind_chain_source = BitcoindChainSource::new_rest( rpc_host, rpc_port, rpc_user, rpc_password, - )); - - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); - let latest_chain_tip = RwLock::new(None); - let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - - let kind = ChainSourceKind::Bitcoind { - api_client, - header_cache, - latest_chain_tip, - wallet_polling_status, onchain_wallet, fee_estimator, tx_broadcaster, kv_store, config, - logger: Arc::clone(&logger), + rest_client_config, + Arc::clone(&logger), node_metrics, - }; + ); + let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); Self { kind, logger } } @@ -256,7 +466,9 @@ impl ChainSource { pub(crate) fn as_utxo_source(&self) -> Option> { match &self.kind { - ChainSourceKind::Bitcoind { api_client, .. } => Some(api_client.utxo_source()), + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + Some(bitcoind_chain_source.as_utxo_source()) + }, _ => None, } } @@ -270,7 +482,7 @@ impl ChainSource { } pub(crate) async fn continuously_sync_wallets( - &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + &self, stop_sync_receiver: tokio::sync::watch::Receiver<()>, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) { @@ -319,171 +531,15 @@ impl ChainSource { return; } }, - ChainSourceKind::Bitcoind { - api_client, - header_cache, - latest_chain_tip, - onchain_wallet, - wallet_polling_status, - kv_store, - config, - logger, - node_metrics, - .. - } => { - // First register for the wallet polling status to make sure `Node::sync_wallets` calls - // wait on the result before proceeding. - { - let mut status_lock = wallet_polling_status.lock().unwrap(); - if status_lock.register_or_subscribe_pending_sync().is_some() { - debug_assert!(false, "Sync already in progress. This should never happen."); - } - } - - log_info!( - logger, - "Starting initial synchronization of chain listeners. This might take a while..", - ); - - let mut backoff = CHAIN_POLLING_INTERVAL_SECS; - const MAX_BACKOFF_SECS: u64 = 300; - - loop { - let channel_manager_best_block_hash = - channel_manager.current_best_block().block_hash; - let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; - let onchain_wallet_best_block_hash = - onchain_wallet.current_best_block().block_hash; - - let mut chain_listeners = vec![ - ( - onchain_wallet_best_block_hash, - &**onchain_wallet as &(dyn Listen + Send + Sync), - ), - ( - channel_manager_best_block_hash, - &*channel_manager as &(dyn Listen + Send + Sync), - ), - (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), - ]; - - // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s - // before giving them to `ChainMonitor` it the first place. However, this isn't - // trivial as we load them on initialization (in the `Builder`) and only gain - // network access during `start`. For now, we just make sure we get the worst known - // block hash and sychronize them via `ChainMonitor`. - if let Some(worst_channel_monitor_block_hash) = chain_monitor - .list_monitors() - .iter() - .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) - .map(|m| m.current_best_block()) - .min_by_key(|b| b.height) - .map(|b| b.block_hash) - { - chain_listeners.push(( - worst_channel_monitor_block_hash, - &*chain_monitor as &(dyn Listen + Send + Sync), - )); - } - - let mut locked_header_cache = header_cache.lock().await; - let now = SystemTime::now(); - match synchronize_listeners( - api_client.as_ref(), - config.network, - &mut *locked_header_cache, - chain_listeners.clone(), + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source + .continuously_sync_wallets( + stop_sync_receiver, + channel_manager, + chain_monitor, + output_sweeper, ) .await - { - Ok(chain_tip) => { - { - log_info!( - logger, - "Finished synchronizing listeners in {}ms", - now.elapsed().unwrap().as_millis() - ); - *latest_chain_tip.write().unwrap() = Some(chain_tip); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - ) - .unwrap_or_else(|e| { - log_error!(logger, "Failed to persist node metrics: {}", e); - }); - } - break; - }, - - Err(e) => { - log_error!(logger, "Failed to synchronize chain listeners: {:?}", e); - if e.kind() == BlockSourceErrorKind::Transient { - log_info!( - logger, - "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", - e, - backoff - ); - tokio::time::sleep(Duration::from_secs(backoff)).await; - backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); - } else { - log_error!( - logger, - "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", - e, - MAX_BACKOFF_SECS - ); - tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; - } - }, - } - } - - // Now propagate the initial result to unblock waiting subscribers. - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); - - let mut chain_polling_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - chain_polling_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let mut fee_rate_update_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval - .set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - log_info!(logger, "Starting continuous polling for chain updates."); - - // Start the polling loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - logger, - "Stopping polling for new chain data.", - ); - return; - } - _ = chain_polling_interval.tick() => { - let _ = self.poll_and_update_listeners(Arc::clone(&channel_manager), Arc::clone(&chain_monitor), Arc::clone(&output_sweeper)).await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - } - } }, } } @@ -595,6 +651,128 @@ impl ChainSource { } } +impl BitcoindChainSource { + pub(super) async fn poll_and_update_listeners( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet polling result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let latest_chain_tip_opt = self.latest_chain_tip.read().unwrap().clone(); + let chain_tip = if let Some(tip) = latest_chain_tip_opt { + tip + } else { + match validate_best_block_header(self.api_client.as_ref()).await { + Ok(tip) => { + *self.latest_chain_tip.write().unwrap() = Some(tip); + tip + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + }; + + let mut locked_header_cache = self.header_cache.lock().await; + let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network); + let chain_listener = ChainListener { + onchain_wallet: Arc::clone(&self.onchain_wallet), + channel_manager: Arc::clone(&channel_manager), + chain_monitor, + output_sweeper, + }; + let mut spv_client = + SpvClient::new(chain_tip, chain_poller, &mut *locked_header_cache, &chain_listener); + + let now = SystemTime::now(); + match spv_client.poll_best_tip().await { + Ok((ChainTip::Better(tip), true)) => { + log_trace!( + self.logger, + "Finished polling best tip in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(tip); + }, + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let cur_height = channel_manager.current_best_block().height; + + let now = SystemTime::now(); + let unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); + match self.api_client.get_updated_mempool_transactions(cur_height, unconfirmed_txids).await + { + Ok((unconfirmed_txs, evicted_txids)) => { + log_trace!( + self.logger, + "Finished polling mempool of size {} and {} evicted transactions in {}ms", + unconfirmed_txs.len(), + evicted_txids.len(), + now.elapsed().unwrap().as_millis() + ); + self.onchain_wallet + .apply_mempool_txs(unconfirmed_txs, evicted_txids) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to apply mempool transactions: {:?}", e); + }); + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for mempool transactions: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + + let write_res = write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + ); + match write_res { + Ok(()) => (), + Err(e) => { + log_error!(self.logger, "Failed to persist node metrics: {}", e); + let res = Err(Error::PersistenceFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let res = Ok(()); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + res + } +} + impl ChainSource { pub(crate) async fn poll_and_update_listeners( &self, channel_manager: Arc, chain_monitor: Arc, @@ -611,143 +789,132 @@ impl ChainSource { // `sync_onchain_wallet` and `sync_lightning_wallet`. So nothing to do here. unreachable!("Listeners will be synced via transction-based syncing") }, - ChainSourceKind::Bitcoind { - api_client, - header_cache, - latest_chain_tip, - onchain_wallet, - wallet_polling_status, - kv_store, - config, - logger, - node_metrics, - .. - } => { - let receiver_res = { - let mut status_lock = wallet_polling_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - - if let Some(mut sync_receiver) = receiver_res { - log_info!(logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); - log_error!(logger, "Failed to receive wallet polling result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let latest_chain_tip_opt = latest_chain_tip.read().unwrap().clone(); - let chain_tip = if let Some(tip) = latest_chain_tip_opt { - tip - } else { - match validate_best_block_header(api_client.as_ref()).await { - Ok(tip) => { - *latest_chain_tip.write().unwrap() = Some(tip); - tip - }, - Err(e) => { - log_error!(logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status - .lock() - .unwrap() - .propagate_result_to_subscribers(res); - return res; - }, - } - }; - - let mut locked_header_cache = header_cache.lock().await; - let chain_poller = ChainPoller::new(Arc::clone(&api_client), config.network); - let chain_listener = ChainListener { - onchain_wallet: Arc::clone(&onchain_wallet), - channel_manager: Arc::clone(&channel_manager), - chain_monitor, - output_sweeper, - }; - let mut spv_client = SpvClient::new( - chain_tip, - chain_poller, - &mut *locked_header_cache, - &chain_listener, - ); + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source + .poll_and_update_listeners(channel_manager, chain_monitor, output_sweeper) + .await + }, + } + } +} - let now = SystemTime::now(); - match spv_client.poll_best_tip().await { - Ok((ChainTip::Better(tip), true)) => { - log_trace!( - logger, - "Finished polling best tip in {}ms", - now.elapsed().unwrap().as_millis() - ); - *latest_chain_tip.write().unwrap() = Some(tip); - }, - Ok(_) => {}, - Err(e) => { - log_error!(logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } +impl BitcoindChainSource { + pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + macro_rules! get_fee_rate_update { + ($estimation_fut: expr) => {{ + let update_res = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + $estimation_fut, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })?; + update_res + }}; + } + let confirmation_targets = get_all_conf_targets(); + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + let now = Instant::now(); + for target in confirmation_targets { + let fee_rate_update_res = match target { + ConfirmationTarget::Lightning( + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, + ) => { + let estimation_fut = self.api_client.get_mempool_minimum_fee_rate(); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::UrgentOnChainSweep) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + _ => { + // Otherwise, we default to economical block-target estimate. + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Economical; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + }; + + let fee_rate = match (fee_rate_update_res, self.config.network) { + (Ok(rate), _) => rate, + (Err(e), Network::Bitcoin) => { + // Strictly fail on mainnet. + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + return Err(Error::FeerateEstimationUpdateFailed); + }, + (Err(e), n) if n == Network::Regtest || n == Network::Signet => { + // On regtest/signet we just fall back to the usual 1 sat/vb == 250 + // sat/kwu default. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", + e, + ); + FeeRate::from_sat_per_kwu(250) + }, + (Err(e), _) => { + // On testnet `estimatesmartfee` can be unreliable so we just skip in + // case of a failure, which will have us falling back to defaults. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", + e, + ); + return Ok(()); + }, + }; - let cur_height = channel_manager.current_best_block().height; + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - let now = SystemTime::now(); - let unconfirmed_txids = onchain_wallet.get_unconfirmed_txids(); - match api_client - .get_updated_mempool_transactions(cur_height, unconfirmed_txids) - .await - { - Ok((unconfirmed_txs, evicted_txids)) => { - log_trace!( - logger, - "Finished polling mempool of size {} and {} evicted transactions in {}ms", - unconfirmed_txs.len(), - evicted_txids.len(), - now.elapsed().unwrap().as_millis() - ); - onchain_wallet - .apply_mempool_txs(unconfirmed_txs, evicted_txids) - .unwrap_or_else(|e| { - log_error!(logger, "Failed to apply mempool transactions: {:?}", e); - }); - }, - Err(e) => { - log_error!(logger, "Failed to poll for mempool transactions: {:?}", e); - let res = Err(Error::TxSyncFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } + new_fee_rate_cache.insert(target, adjusted_fee_rate); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } - let write_res = write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - ); - match write_res { - Ok(()) => (), - Err(e) => { - log_error!(logger, "Failed to persist node metrics: {}", e); - let res = Err(Error::PersistenceFailed); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } + if self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { + // We only log if the values changed, as it might be very spammy otherwise. + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + } - let res = Ok(()); - wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - res - }, + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; } + + Ok(()) } } @@ -760,135 +927,62 @@ impl ChainSource { ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.update_fee_rate_estimates().await }, - ChainSourceKind::Bitcoind { - api_client, - fee_estimator, - config, - kv_store, - logger, - node_metrics, - .. - } => { - macro_rules! get_fee_rate_update { - ($estimation_fut: expr) => {{ - let update_res = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - $estimation_fut, - ) - .await - .map_err(|e| { - log_error!(logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })?; - update_res - }}; - } - let confirmation_targets = get_all_conf_targets(); - - let mut new_fee_rate_cache = HashMap::with_capacity(10); - let now = Instant::now(); - for target in confirmation_targets { - let fee_rate_update_res = match target { - ConfirmationTarget::Lightning( - LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, - ) => { - let estimation_fut = api_client.get_mempool_minimum_fee_rate(); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning( - LdkConfirmationTarget::MaximumFeeEstimate, - ) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = - api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning( - LdkConfirmationTarget::UrgentOnChainSweep, - ) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = - api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - _ => { - // Otherwise, we default to economical block-target estimate. - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Economical; - let estimation_fut = - api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - }; - - let fee_rate = match (fee_rate_update_res, config.network) { - (Ok(rate), _) => rate, - (Err(e), Network::Bitcoin) => { - // Strictly fail on mainnet. - log_error!(logger, "Failed to retrieve fee rate estimates: {}", e); - return Err(Error::FeerateEstimationUpdateFailed); + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.update_fee_rate_estimates().await + }, + } + } +} + +impl BitcoindChainSource { + pub(crate) async fn process_broadcast_queue(&self) { + // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 + // features, we should eventually switch to use `submitpackage` via the + // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual + // transactions. + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in &next_package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, - (Err(e), n) if n == Network::Regtest || n == Network::Signet => { - // On regtest/signet we just fall back to the usual 1 sat/vb == 250 - // sat/kwu default. + Err(e) => { log_error!( - logger, - "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", - e, + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e ); - FeeRate::from_sat_per_kwu(250) - }, - (Err(e), _) => { - // On testnet `estimatesmartfee` can be unreliable so we just skip in - // case of a failure, which will have us falling back to defaults. - log_error!( - logger, - "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", - e, + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) ); - return Ok(()); }, - }; - - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - - new_fee_rate_cache.insert(target, adjusted_fee_rate); - - log_trace!( - logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } - - if fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { - // We only log if the values changed, as it might be very spammy otherwise. - log_info!( - logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - } - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&kv_store), - Arc::clone(&logger), - )?; + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, } - - Ok(()) - }, + } } } } @@ -902,59 +996,8 @@ impl ChainSource { ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.process_broadcast_queue().await }, - ChainSourceKind::Bitcoind { api_client, tx_broadcaster, logger, .. } => { - // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 - // features, we should eventually switch to use `submitpackage` via the - // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual - // transactions. - let mut receiver = tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - api_client.broadcast_transaction(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!( - logger, - "Successfully broadcast transaction {}", - txid - ); - }, - Err(e) => { - log_error!( - logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - Err(e) => { - log_error!( - logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } - } - } + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.process_broadcast_queue().await }, } } From a7e54d84ca596249bc31c6106e206e2a85b215be Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Jul 2025 10:04:05 +0200 Subject: [PATCH 101/177] Move `BitcoindChainSource` type to `chain::bitcoind` module --- src/chain/bitcoind.rs | 559 ++++++++++++++++++++++++++++++++++++++++- src/chain/mod.rs | 566 +----------------------------------------- 2 files changed, 563 insertions(+), 562 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 52dad77416..b87ee13ed7 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -5,27 +5,578 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{ChainMonitor, ChannelManager, Sweeper, Wallet}; +use super::WalletSyncStatus; +use crate::config::{ + BitcoindRestClientConfig, Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + ConfirmationTarget, OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; + +use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; use lightning::chain::Listen; +use lightning::util::ser::Writeable; use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; -use lightning_block_sync::poll::ValidatedBlockHeader; +use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; +use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::rest::RestClient; use lightning_block_sync::rpc::{RpcClient, RpcError}; use lightning_block_sync::{ AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache, }; +use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; + use serde::Serialize; use base64::prelude::BASE64_STANDARD; use base64::Engine; -use bitcoin::{BlockHash, FeeRate, Transaction, Txid}; +use bitcoin::{BlockHash, FeeRate, Network, Transaction, Txid}; use std::collections::{HashMap, VecDeque}; use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::Arc; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; + +pub(super) struct BitcoindChainSource { + api_client: Arc, + header_cache: tokio::sync::Mutex, + latest_chain_tip: RwLock>, + onchain_wallet: Arc, + wallet_polling_status: Mutex, + fee_estimator: Arc, + tx_broadcaster: Arc, + kv_store: Arc, + config: Arc, + logger: Arc, + node_metrics: Arc>, +} + +impl BitcoindChainSource { + pub(crate) fn new_rpc( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rpc( + rpc_host.clone(), + rpc_port.clone(), + rpc_user.clone(), + rpc_password.clone(), + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + Self { + api_client, + header_cache, + latest_chain_tip, + onchain_wallet, + wallet_polling_status, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(crate) fn new_rest( + rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, + onchain_wallet: Arc, fee_estimator: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, + rest_client_config: BitcoindRestClientConfig, logger: Arc, + node_metrics: Arc>, + ) -> Self { + let api_client = Arc::new(BitcoindClient::new_rest( + rest_client_config.rest_host, + rest_client_config.rest_port, + rpc_host, + rpc_port, + rpc_user, + rpc_password, + )); + + let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); + let latest_chain_tip = RwLock::new(None); + let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); + + Self { + api_client, + header_cache, + latest_chain_tip, + wallet_polling_status, + onchain_wallet, + fee_estimator, + tx_broadcaster, + kv_store, + config, + logger: Arc::clone(&logger), + node_metrics, + } + } + + pub(super) fn as_utxo_source(&self) -> Arc { + self.api_client.utxo_source() + } + + pub(super) async fn continuously_sync_wallets( + &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, + channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) { + // First register for the wallet polling status to make sure `Node::sync_wallets` calls + // wait on the result before proceeding. + { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + if status_lock.register_or_subscribe_pending_sync().is_some() { + debug_assert!(false, "Sync already in progress. This should never happen."); + } + } + + log_info!( + self.logger, + "Starting initial synchronization of chain listeners. This might take a while..", + ); + + let mut backoff = CHAIN_POLLING_INTERVAL_SECS; + const MAX_BACKOFF_SECS: u64 = 300; + + loop { + let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; + let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; + let onchain_wallet_best_block_hash = + self.onchain_wallet.current_best_block().block_hash; + + let mut chain_listeners = vec![ + ( + onchain_wallet_best_block_hash, + &*self.onchain_wallet as &(dyn Listen + Send + Sync), + ), + (channel_manager_best_block_hash, &*channel_manager as &(dyn Listen + Send + Sync)), + (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), + ]; + + // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s + // before giving them to `ChainMonitor` it the first place. However, this isn't + // trivial as we load them on initialization (in the `Builder`) and only gain + // network access during `start`. For now, we just make sure we get the worst known + // block hash and sychronize them via `ChainMonitor`. + if let Some(worst_channel_monitor_block_hash) = chain_monitor + .list_monitors() + .iter() + .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) + .map(|m| m.current_best_block()) + .min_by_key(|b| b.height) + .map(|b| b.block_hash) + { + chain_listeners.push(( + worst_channel_monitor_block_hash, + &*chain_monitor as &(dyn Listen + Send + Sync), + )); + } + + let mut locked_header_cache = self.header_cache.lock().await; + let now = SystemTime::now(); + match synchronize_listeners( + self.api_client.as_ref(), + self.config.network, + &mut *locked_header_cache, + chain_listeners.clone(), + ) + .await + { + Ok(chain_tip) => { + { + log_info!( + self.logger, + "Finished synchronizing listeners in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(chain_tip); + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + ) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to persist node metrics: {}", e); + }); + } + break; + }, + + Err(e) => { + log_error!(self.logger, "Failed to synchronize chain listeners: {:?}", e); + if e.kind() == BlockSourceErrorKind::Transient { + log_info!( + self.logger, + "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + backoff + ); + tokio::time::sleep(Duration::from_secs(backoff)).await; + backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); + } else { + log_error!( + self.logger, + "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", + e, + MAX_BACKOFF_SECS + ); + tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; + } + }, + } + } + + // Now propagate the initial result to unblock waiting subscribers. + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); + + let mut chain_polling_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + chain_polling_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + let mut fee_rate_update_interval = + tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); + // When starting up, we just blocked on updating, so skip the first tick. + fee_rate_update_interval.reset(); + fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + log_info!(self.logger, "Starting continuous polling for chain updates."); + + // Start the polling loop. + loop { + tokio::select! { + _ = stop_sync_receiver.changed() => { + log_trace!( + self.logger, + "Stopping polling for new chain data.", + ); + return; + } + _ = chain_polling_interval.tick() => { + let _ = self.poll_and_update_listeners( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&output_sweeper) + ).await; + } + _ = fee_rate_update_interval.tick() => { + let _ = self.update_fee_rate_estimates().await; + } + } + } + } + + pub(super) async fn poll_and_update_listeners( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let receiver_res = { + let mut status_lock = self.wallet_polling_status.lock().unwrap(); + status_lock.register_or_subscribe_pending_sync() + }; + + if let Some(mut sync_receiver) = receiver_res { + log_info!(self.logger, "Sync in progress, skipping."); + return sync_receiver.recv().await.map_err(|e| { + debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); + log_error!(self.logger, "Failed to receive wallet polling result: {:?}", e); + Error::WalletOperationFailed + })?; + } + + let latest_chain_tip_opt = self.latest_chain_tip.read().unwrap().clone(); + let chain_tip = if let Some(tip) = latest_chain_tip_opt { + tip + } else { + match validate_best_block_header(self.api_client.as_ref()).await { + Ok(tip) => { + *self.latest_chain_tip.write().unwrap() = Some(tip); + tip + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + }; + + let mut locked_header_cache = self.header_cache.lock().await; + let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network); + let chain_listener = ChainListener { + onchain_wallet: Arc::clone(&self.onchain_wallet), + channel_manager: Arc::clone(&channel_manager), + chain_monitor, + output_sweeper, + }; + let mut spv_client = + SpvClient::new(chain_tip, chain_poller, &mut *locked_header_cache, &chain_listener); + + let now = SystemTime::now(); + match spv_client.poll_best_tip().await { + Ok((ChainTip::Better(tip), true)) => { + log_trace!( + self.logger, + "Finished polling best tip in {}ms", + now.elapsed().unwrap().as_millis() + ); + *self.latest_chain_tip.write().unwrap() = Some(tip); + }, + Ok(_) => {}, + Err(e) => { + log_error!(self.logger, "Failed to poll for chain data: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let cur_height = channel_manager.current_best_block().height; + + let now = SystemTime::now(); + let unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); + match self.api_client.get_updated_mempool_transactions(cur_height, unconfirmed_txids).await + { + Ok((unconfirmed_txs, evicted_txids)) => { + log_trace!( + self.logger, + "Finished polling mempool of size {} and {} evicted transactions in {}ms", + unconfirmed_txs.len(), + evicted_txids.len(), + now.elapsed().unwrap().as_millis() + ); + self.onchain_wallet + .apply_mempool_txs(unconfirmed_txs, evicted_txids) + .unwrap_or_else(|e| { + log_error!(self.logger, "Failed to apply mempool transactions: {:?}", e); + }); + }, + Err(e) => { + log_error!(self.logger, "Failed to poll for mempool transactions: {:?}", e); + let res = Err(Error::TxSyncFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + + let write_res = write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + ); + match write_res { + Ok(()) => (), + Err(e) => { + log_error!(self.logger, "Failed to persist node metrics: {}", e); + let res = Err(Error::PersistenceFailed); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + return res; + }, + } + + let res = Ok(()); + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + res + } + + pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { + macro_rules! get_fee_rate_update { + ($estimation_fut: expr) => {{ + let update_res = tokio::time::timeout( + Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), + $estimation_fut, + ) + .await + .map_err(|e| { + log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); + Error::FeerateEstimationUpdateTimeout + })?; + update_res + }}; + } + let confirmation_targets = get_all_conf_targets(); + + let mut new_fee_rate_cache = HashMap::with_capacity(10); + let now = Instant::now(); + for target in confirmation_targets { + let fee_rate_update_res = match target { + ConfirmationTarget::Lightning( + LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, + ) => { + let estimation_fut = self.api_client.get_mempool_minimum_fee_rate(); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::UrgentOnChainSweep) => { + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Conservative; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + _ => { + // Otherwise, we default to economical block-target estimate. + let num_blocks = get_num_block_defaults_for_target(target); + let estimation_mode = FeeRateEstimationMode::Economical; + let estimation_fut = + self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); + get_fee_rate_update!(estimation_fut) + }, + }; + + let fee_rate = match (fee_rate_update_res, self.config.network) { + (Ok(rate), _) => rate, + (Err(e), Network::Bitcoin) => { + // Strictly fail on mainnet. + log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); + return Err(Error::FeerateEstimationUpdateFailed); + }, + (Err(e), n) if n == Network::Regtest || n == Network::Signet => { + // On regtest/signet we just fall back to the usual 1 sat/vb == 250 + // sat/kwu default. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", + e, + ); + FeeRate::from_sat_per_kwu(250) + }, + (Err(e), _) => { + // On testnet `estimatesmartfee` can be unreliable so we just skip in + // case of a failure, which will have us falling back to defaults. + log_error!( + self.logger, + "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", + e, + ); + return Ok(()); + }, + }; + + // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that + // require some post-estimation adjustments to the fee rates, which we do here. + let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); + + new_fee_rate_cache.insert(target, adjusted_fee_rate); + + log_trace!( + self.logger, + "Fee rate estimation updated for {:?}: {} sats/kwu", + target, + adjusted_fee_rate.to_sat_per_kwu(), + ); + } + + if self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { + // We only log if the values changed, as it might be very spammy otherwise. + log_info!( + self.logger, + "Fee rate cache update finished in {}ms.", + now.elapsed().as_millis() + ); + } + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + )?; + } + + Ok(()) + } + + pub(crate) async fn process_broadcast_queue(&self) { + // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 + // features, we should eventually switch to use `submitpackage` via the + // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual + // transactions. + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + for tx in &next_package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + } + } + } + } +} pub enum BitcoindClient { Rpc { diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 338fd0d309..d756301f39 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -9,39 +9,28 @@ mod bitcoind; mod electrum; mod esplora; -use crate::chain::bitcoind::{ - BitcoindClient, BoundedHeaderCache, ChainListener, FeeRateEstimationMode, -}; +use crate::chain::bitcoind::BitcoindChainSource; use crate::chain::electrum::ElectrumChainSource; use crate::chain::esplora::EsploraChainSource; use crate::config::{ BackgroundSyncConfig, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, - FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, - TX_BROADCAST_TIMEOUT_SECS, WALLET_SYNC_INTERVAL_MINIMUM_SECS, -}; -use crate::fee_estimator::{ - apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, - ConfirmationTarget, OnchainFeeEstimator, + RESOLVED_CHANNEL_MONITOR_ARCHIVAL_INTERVAL, WALLET_SYNC_INTERVAL_MINIMUM_SECS, }; +use crate::fee_estimator::OnchainFeeEstimator; use crate::io::utils::write_node_metrics; -use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::logger::{log_info, log_trace, LdkLogger, Logger}; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; -use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::{Filter, Listen}; -use lightning::util::ser::Writeable; +use lightning::chain::Filter; use lightning_block_sync::gossip::UtxoSource; -use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; -use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; -use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; -use bitcoin::{FeeRate, Network, Script, Txid}; +use bitcoin::{Script, Txid}; use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use std::sync::{Arc, RwLock}; +use std::time::Duration; pub(crate) enum WalletSyncStatus { Completed, @@ -96,250 +85,6 @@ impl WalletSyncStatus { } } -const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; - -pub(super) struct BitcoindChainSource { - api_client: Arc, - header_cache: tokio::sync::Mutex, - latest_chain_tip: RwLock>, - onchain_wallet: Arc, - wallet_polling_status: Mutex, - fee_estimator: Arc, - tx_broadcaster: Arc, - kv_store: Arc, - config: Arc, - logger: Arc, - node_metrics: Arc>, -} - -impl BitcoindChainSource { - pub(crate) fn new_rpc( - rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, - onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, - ) -> Self { - let api_client = Arc::new(BitcoindClient::new_rpc( - rpc_host.clone(), - rpc_port.clone(), - rpc_user.clone(), - rpc_password.clone(), - )); - - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); - let latest_chain_tip = RwLock::new(None); - let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - Self { - api_client, - header_cache, - latest_chain_tip, - onchain_wallet, - wallet_polling_status, - fee_estimator, - tx_broadcaster, - kv_store, - config, - logger: Arc::clone(&logger), - node_metrics, - } - } - - pub(crate) fn new_rest( - rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, - onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - rest_client_config: BitcoindRestClientConfig, logger: Arc, - node_metrics: Arc>, - ) -> Self { - let api_client = Arc::new(BitcoindClient::new_rest( - rest_client_config.rest_host, - rest_client_config.rest_port, - rpc_host, - rpc_port, - rpc_user, - rpc_password, - )); - - let header_cache = tokio::sync::Mutex::new(BoundedHeaderCache::new()); - let latest_chain_tip = RwLock::new(None); - let wallet_polling_status = Mutex::new(WalletSyncStatus::Completed); - - Self { - api_client, - header_cache, - latest_chain_tip, - wallet_polling_status, - onchain_wallet, - fee_estimator, - tx_broadcaster, - kv_store, - config, - logger: Arc::clone(&logger), - node_metrics, - } - } - - pub(super) fn as_utxo_source(&self) -> Arc { - self.api_client.utxo_source() - } - - pub(super) async fn continuously_sync_wallets( - &self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>, - channel_manager: Arc, chain_monitor: Arc, - output_sweeper: Arc, - ) { - // First register for the wallet polling status to make sure `Node::sync_wallets` calls - // wait on the result before proceeding. - { - let mut status_lock = self.wallet_polling_status.lock().unwrap(); - if status_lock.register_or_subscribe_pending_sync().is_some() { - debug_assert!(false, "Sync already in progress. This should never happen."); - } - } - - log_info!( - self.logger, - "Starting initial synchronization of chain listeners. This might take a while..", - ); - - let mut backoff = CHAIN_POLLING_INTERVAL_SECS; - const MAX_BACKOFF_SECS: u64 = 300; - - loop { - let channel_manager_best_block_hash = channel_manager.current_best_block().block_hash; - let sweeper_best_block_hash = output_sweeper.current_best_block().block_hash; - let onchain_wallet_best_block_hash = - self.onchain_wallet.current_best_block().block_hash; - - let mut chain_listeners = vec![ - ( - onchain_wallet_best_block_hash, - &*self.onchain_wallet as &(dyn Listen + Send + Sync), - ), - (channel_manager_best_block_hash, &*channel_manager as &(dyn Listen + Send + Sync)), - (sweeper_best_block_hash, &*output_sweeper as &(dyn Listen + Send + Sync)), - ]; - - // TODO: Eventually we might want to see if we can synchronize `ChannelMonitor`s - // before giving them to `ChainMonitor` it the first place. However, this isn't - // trivial as we load them on initialization (in the `Builder`) and only gain - // network access during `start`. For now, we just make sure we get the worst known - // block hash and sychronize them via `ChainMonitor`. - if let Some(worst_channel_monitor_block_hash) = chain_monitor - .list_monitors() - .iter() - .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) - .map(|m| m.current_best_block()) - .min_by_key(|b| b.height) - .map(|b| b.block_hash) - { - chain_listeners.push(( - worst_channel_monitor_block_hash, - &*chain_monitor as &(dyn Listen + Send + Sync), - )); - } - - let mut locked_header_cache = self.header_cache.lock().await; - let now = SystemTime::now(); - match synchronize_listeners( - self.api_client.as_ref(), - self.config.network, - &mut *locked_header_cache, - chain_listeners.clone(), - ) - .await - { - Ok(chain_tip) => { - { - log_info!( - self.logger, - "Finished synchronizing listeners in {}ms", - now.elapsed().unwrap().as_millis() - ); - *self.latest_chain_tip.write().unwrap() = Some(chain_tip); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - ) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to persist node metrics: {}", e); - }); - } - break; - }, - - Err(e) => { - log_error!(self.logger, "Failed to synchronize chain listeners: {:?}", e); - if e.kind() == BlockSourceErrorKind::Transient { - log_info!( - self.logger, - "Transient error syncing chain listeners: {:?}. Retrying in {} seconds.", - e, - backoff - ); - tokio::time::sleep(Duration::from_secs(backoff)).await; - backoff = std::cmp::min(backoff * 2, MAX_BACKOFF_SECS); - } else { - log_error!( - self.logger, - "Persistent error syncing chain listeners: {:?}. Retrying in {} seconds.", - e, - MAX_BACKOFF_SECS - ); - tokio::time::sleep(Duration::from_secs(MAX_BACKOFF_SECS)).await; - } - }, - } - } - - // Now propagate the initial result to unblock waiting subscribers. - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(Ok(())); - - let mut chain_polling_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - chain_polling_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - let mut fee_rate_update_interval = - tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS)); - // When starting up, we just blocked on updating, so skip the first tick. - fee_rate_update_interval.reset(); - fee_rate_update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - - log_info!(self.logger, "Starting continuous polling for chain updates."); - - // Start the polling loop. - loop { - tokio::select! { - _ = stop_sync_receiver.changed() => { - log_trace!( - self.logger, - "Stopping polling for new chain data.", - ); - return; - } - _ = chain_polling_interval.tick() => { - let _ = self.poll_and_update_listeners( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), - Arc::clone(&output_sweeper) - ).await; - } - _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; - } - } - } - } -} - pub(crate) struct ChainSource { kind: ChainSourceKind, logger: Arc, @@ -651,128 +396,6 @@ impl ChainSource { } } -impl BitcoindChainSource { - pub(super) async fn poll_and_update_listeners( - &self, channel_manager: Arc, chain_monitor: Arc, - output_sweeper: Arc, - ) -> Result<(), Error> { - let receiver_res = { - let mut status_lock = self.wallet_polling_status.lock().unwrap(); - status_lock.register_or_subscribe_pending_sync() - }; - - if let Some(mut sync_receiver) = receiver_res { - log_info!(self.logger, "Sync in progress, skipping."); - return sync_receiver.recv().await.map_err(|e| { - debug_assert!(false, "Failed to receive wallet polling result: {:?}", e); - log_error!(self.logger, "Failed to receive wallet polling result: {:?}", e); - Error::WalletOperationFailed - })?; - } - - let latest_chain_tip_opt = self.latest_chain_tip.read().unwrap().clone(); - let chain_tip = if let Some(tip) = latest_chain_tip_opt { - tip - } else { - match validate_best_block_header(self.api_client.as_ref()).await { - Ok(tip) => { - *self.latest_chain_tip.write().unwrap() = Some(tip); - tip - }, - Err(e) => { - log_error!(self.logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - }; - - let mut locked_header_cache = self.header_cache.lock().await; - let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network); - let chain_listener = ChainListener { - onchain_wallet: Arc::clone(&self.onchain_wallet), - channel_manager: Arc::clone(&channel_manager), - chain_monitor, - output_sweeper, - }; - let mut spv_client = - SpvClient::new(chain_tip, chain_poller, &mut *locked_header_cache, &chain_listener); - - let now = SystemTime::now(); - match spv_client.poll_best_tip().await { - Ok((ChainTip::Better(tip), true)) => { - log_trace!( - self.logger, - "Finished polling best tip in {}ms", - now.elapsed().unwrap().as_millis() - ); - *self.latest_chain_tip.write().unwrap() = Some(tip); - }, - Ok(_) => {}, - Err(e) => { - log_error!(self.logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let cur_height = channel_manager.current_best_block().height; - - let now = SystemTime::now(); - let unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); - match self.api_client.get_updated_mempool_transactions(cur_height, unconfirmed_txids).await - { - Ok((unconfirmed_txs, evicted_txids)) => { - log_trace!( - self.logger, - "Finished polling mempool of size {} and {} evicted transactions in {}ms", - unconfirmed_txs.len(), - evicted_txids.len(), - now.elapsed().unwrap().as_millis() - ); - self.onchain_wallet - .apply_mempool_txs(unconfirmed_txs, evicted_txids) - .unwrap_or_else(|e| { - log_error!(self.logger, "Failed to apply mempool transactions: {:?}", e); - }); - }, - Err(e) => { - log_error!(self.logger, "Failed to poll for mempool transactions: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - - let write_res = write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - ); - match write_res { - Ok(()) => (), - Err(e) => { - log_error!(self.logger, "Failed to persist node metrics: {}", e); - let res = Err(Error::PersistenceFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } - - let res = Ok(()); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - res - } -} - impl ChainSource { pub(crate) async fn poll_and_update_listeners( &self, channel_manager: Arc, chain_monitor: Arc, @@ -798,126 +421,6 @@ impl ChainSource { } } -impl BitcoindChainSource { - pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { - macro_rules! get_fee_rate_update { - ($estimation_fut: expr) => {{ - let update_res = tokio::time::timeout( - Duration::from_secs(FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS), - $estimation_fut, - ) - .await - .map_err(|e| { - log_error!(self.logger, "Updating fee rate estimates timed out: {}", e); - Error::FeerateEstimationUpdateTimeout - })?; - update_res - }}; - } - let confirmation_targets = get_all_conf_targets(); - - let mut new_fee_rate_cache = HashMap::with_capacity(10); - let now = Instant::now(); - for target in confirmation_targets { - let fee_rate_update_res = match target { - ConfirmationTarget::Lightning( - LdkConfirmationTarget::MinAllowedAnchorChannelRemoteFee, - ) => { - let estimation_fut = self.api_client.get_mempool_minimum_fee_rate(); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = - self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - ConfirmationTarget::Lightning(LdkConfirmationTarget::UrgentOnChainSweep) => { - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Conservative; - let estimation_fut = - self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - _ => { - // Otherwise, we default to economical block-target estimate. - let num_blocks = get_num_block_defaults_for_target(target); - let estimation_mode = FeeRateEstimationMode::Economical; - let estimation_fut = - self.api_client.get_fee_estimate_for_target(num_blocks, estimation_mode); - get_fee_rate_update!(estimation_fut) - }, - }; - - let fee_rate = match (fee_rate_update_res, self.config.network) { - (Ok(rate), _) => rate, - (Err(e), Network::Bitcoin) => { - // Strictly fail on mainnet. - log_error!(self.logger, "Failed to retrieve fee rate estimates: {}", e); - return Err(Error::FeerateEstimationUpdateFailed); - }, - (Err(e), n) if n == Network::Regtest || n == Network::Signet => { - // On regtest/signet we just fall back to the usual 1 sat/vb == 250 - // sat/kwu default. - log_error!( - self.logger, - "Failed to retrieve fee rate estimates: {}. Falling back to default of 1 sat/vb.", - e, - ); - FeeRate::from_sat_per_kwu(250) - }, - (Err(e), _) => { - // On testnet `estimatesmartfee` can be unreliable so we just skip in - // case of a failure, which will have us falling back to defaults. - log_error!( - self.logger, - "Failed to retrieve fee rate estimates: {}. Falling back to defaults.", - e, - ); - return Ok(()); - }, - }; - - // LDK 0.0.118 introduced changes to the `ConfirmationTarget` semantics that - // require some post-estimation adjustments to the fee rates, which we do here. - let adjusted_fee_rate = apply_post_estimation_adjustments(target, fee_rate); - - new_fee_rate_cache.insert(target, adjusted_fee_rate); - - log_trace!( - self.logger, - "Fee rate estimation updated for {:?}: {} sats/kwu", - target, - adjusted_fee_rate.to_sat_per_kwu(), - ); - } - - if self.fee_estimator.set_fee_rate_cache(new_fee_rate_cache) { - // We only log if the values changed, as it might be very spammy otherwise. - log_info!( - self.logger, - "Fee rate cache update finished in {}ms.", - now.elapsed().as_millis() - ); - } - - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_fee_rate_cache_update_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - Ok(()) - } -} - impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { @@ -934,59 +437,6 @@ impl ChainSource { } } -impl BitcoindChainSource { - pub(crate) async fn process_broadcast_queue(&self) { - // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 - // features, we should eventually switch to use `submitpackage` via the - // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual - // transactions. - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - self.api_client.broadcast_transaction(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } - } - } - } -} - impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { From 7cb1d34e09d7ab8ca050528ace1363713ed0d914 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 29 Jul 2025 10:06:18 +0200 Subject: [PATCH 102/177] Drop intermittent `impl` blocks on `ChainSource` .. now that we don't need them anymore for review, we drop the extra `impl` blocks again. --- src/chain/mod.rs | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index d756301f39..91cce1fe3d 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -347,9 +347,7 @@ impl ChainSource { } } } -} -impl ChainSource { // Synchronize the onchain wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { @@ -367,9 +365,7 @@ impl ChainSource { }, } } -} -impl ChainSource { // Synchronize the Lightning wallet via transaction-based protocols (i.e., Esplora, Electrum, // etc.) pub(crate) async fn sync_lightning_wallet( @@ -394,9 +390,7 @@ impl ChainSource { }, } } -} -impl ChainSource { pub(crate) async fn poll_and_update_listeners( &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, @@ -419,9 +413,7 @@ impl ChainSource { }, } } -} -impl ChainSource { pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { @@ -435,9 +427,7 @@ impl ChainSource { }, } } -} -impl ChainSource { pub(crate) async fn process_broadcast_queue(&self) { match &self.kind { ChainSourceKind::Esplora(esplora_chain_source) => { From fb34a2703278e0cc316f940b2e5916c8dbd09538 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 14:26:27 +0200 Subject: [PATCH 103/177] Move inner code to `sync_lightning_wallet_inner` for Esplora Previously, we might have overlooked some cases that would exit the method and bubble up an error via `?` instead of propagating to all subscribers. Here, we split out the code to an inner method to ensure we always propagate. --- src/chain/esplora.rs | 115 +++++++++++++++++++++++-------------------- 1 file changed, 61 insertions(+), 54 deletions(-) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 3a911394ce..a93276018c 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -212,15 +212,6 @@ impl EsploraChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - &*sync_cman as &(dyn Confirm + Sync + Send), - &*sync_cmon as &(dyn Confirm + Sync + Send), - &*sync_sweeper as &(dyn Confirm + Sync + Send), - ]; - let receiver_res = { let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); status_lock.register_or_subscribe_pending_sync() @@ -233,58 +224,74 @@ impl EsploraChainSource { Error::WalletOperationFailed })?; } - let res = { - let timeout_fut = tokio::time::timeout( - Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), - self.tx_sync.sync(confirmables), - ); - let now = Instant::now(); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_info!( - self.logger, - "Sync of Lightning wallet finished in {}ms.", - now.elapsed().as_millis() - ); - let unix_time_secs_opt = - SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_lightning_wallet_sync_timestamp = - unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger), - )?; - } - - periodically_archive_fully_resolved_monitors( - Arc::clone(&channel_manager), - Arc::clone(&chain_monitor), + let res = + self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await; + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_lightning_wallet_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + &*sync_cman as &(dyn Confirm + Sync + Send), + &*sync_cmon as &(dyn Confirm + Sync + Send), + &*sync_sweeper as &(dyn Confirm + Sync + Send), + ]; + + let timeout_fut = tokio::time::timeout( + Duration::from_secs(LDK_WALLET_SYNC_TIMEOUT_SECS), + self.tx_sync.sync(confirmables), + ); + let now = Instant::now(); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_info!( + self.logger, + "Sync of Lightning wallet finished in {}ms.", + now.elapsed().as_millis() + ); + + let unix_time_secs_opt = + SystemTime::now().duration_since(UNIX_EPOCH).ok().map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_lightning_wallet_sync_timestamp = + unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, Arc::clone(&self.kv_store), Arc::clone(&self.logger), - Arc::clone(&self.node_metrics), )?; - Ok(()) - }, - Err(e) => { - log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); - Err(e.into()) - }, + } + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + Arc::clone(&chain_monitor), + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; + Ok(()) }, Err(e) => { - log_error!(self.logger, "Lightning wallet sync timed out: {}", e); - Err(Error::TxSyncTimeout) + log_error!(self.logger, "Sync of Lightning wallet failed: {}", e); + Err(e.into()) }, - } - }; - - self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - - res + }, + Err(e) => { + log_error!(self.logger, "Lightning wallet sync timed out: {}", e); + Err(Error::TxSyncTimeout) + }, + } } pub(crate) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { From c1157a340ebac5bd53f7f262decfbf6529d976a3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 14:30:54 +0200 Subject: [PATCH 104/177] Move inner code to `sync_onchain_wallet_inner` for Esplora Previously, we might have overlooked some cases that would exit the method and bubble up an error via `?` instead of propagating to all subscribers. Here, we split out the code to an inner method to ensure we always propagate. --- src/chain/esplora.rs | 182 ++++++++++++++++++++++--------------------- 1 file changed, 92 insertions(+), 90 deletions(-) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index a93276018c..5932426b73 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -112,102 +112,104 @@ impl EsploraChainSource { })?; } - let res = { - // If this is our first sync, do a full scan with the configured gap limit. - // Otherwise just do an incremental sync. - let incremental_sync = - self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); - - macro_rules! get_and_apply_wallet_update { - ($sync_future: expr) => {{ - let now = Instant::now(); - match $sync_future.await { - Ok(res) => match res { - Ok(update) => match self.onchain_wallet.apply_update(update) { - Ok(()) => { - log_info!( - self.logger, - "{} of on-chain wallet finished in {}ms.", - if incremental_sync { "Incremental sync" } else { "Sync" }, - now.elapsed().as_millis() - ); - let unix_time_secs_opt = SystemTime::now() - .duration_since(UNIX_EPOCH) - .ok() - .map(|d| d.as_secs()); - { - let mut locked_node_metrics = self.node_metrics.write().unwrap(); - locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - write_node_metrics( - &*locked_node_metrics, - Arc::clone(&self.kv_store), - Arc::clone(&self.logger) - )?; - } - Ok(()) - }, - Err(e) => Err(e), - }, - Err(e) => match *e { - esplora_client::Error::Reqwest(he) => { - log_error!( - self.logger, - "{} of on-chain wallet failed due to HTTP connection error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - he - ); - Err(Error::WalletOperationFailed) - }, - _ => { - log_error!( - self.logger, - "{} of on-chain wallet failed due to Esplora error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationFailed) - }, - }, - }, - Err(e) => { - log_error!( - self.logger, - "{} of on-chain wallet timed out: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - e - ); - Err(Error::WalletOperationTimeout) - }, - } - }} - } - - if incremental_sync { - let sync_request = self.onchain_wallet.get_incremental_sync_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } else { - let full_scan_request = self.onchain_wallet.get_full_scan_request(); - let wallet_sync_timeout_fut = tokio::time::timeout( - Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), - self.esplora_client.full_scan( - full_scan_request, - BDK_CLIENT_STOP_GAP, - BDK_CLIENT_CONCURRENCY, - ), - ); - get_and_apply_wallet_update!(wallet_sync_timeout_fut) - } - }; + let res = self.sync_onchain_wallet_inner().await; self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); res } + async fn sync_onchain_wallet_inner(&self) -> Result<(), Error> { + // If this is our first sync, do a full scan with the configured gap limit. + // Otherwise just do an incremental sync. + let incremental_sync = + self.node_metrics.read().unwrap().latest_onchain_wallet_sync_timestamp.is_some(); + + macro_rules! get_and_apply_wallet_update { + ($sync_future: expr) => {{ + let now = Instant::now(); + match $sync_future.await { + Ok(res) => match res { + Ok(update) => match self.onchain_wallet.apply_update(update) { + Ok(()) => { + log_info!( + self.logger, + "{} of on-chain wallet finished in {}ms.", + if incremental_sync { "Incremental sync" } else { "Sync" }, + now.elapsed().as_millis() + ); + let unix_time_secs_opt = SystemTime::now() + .duration_since(UNIX_EPOCH) + .ok() + .map(|d| d.as_secs()); + { + let mut locked_node_metrics = self.node_metrics.write().unwrap(); + locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; + write_node_metrics( + &*locked_node_metrics, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger) + )?; + } + Ok(()) + }, + Err(e) => Err(e), + }, + Err(e) => match *e { + esplora_client::Error::Reqwest(he) => { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP connection error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + he + ); + Err(Error::WalletOperationFailed) + }, + _ => { + log_error!( + self.logger, + "{} of on-chain wallet failed due to Esplora error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationFailed) + }, + }, + }, + Err(e) => { + log_error!( + self.logger, + "{} of on-chain wallet timed out: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + e + ); + Err(Error::WalletOperationTimeout) + }, + } + }} + } + + if incremental_sync { + let sync_request = self.onchain_wallet.get_incremental_sync_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.sync(sync_request, BDK_CLIENT_CONCURRENCY), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } else { + let full_scan_request = self.onchain_wallet.get_full_scan_request(); + let wallet_sync_timeout_fut = tokio::time::timeout( + Duration::from_secs(BDK_WALLET_SYNC_TIMEOUT_SECS), + self.esplora_client.full_scan( + full_scan_request, + BDK_CLIENT_STOP_GAP, + BDK_CLIENT_CONCURRENCY, + ), + ); + get_and_apply_wallet_update!(wallet_sync_timeout_fut) + } + } + pub(super) async fn sync_lightning_wallet( &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, From c7655456185d35d3c840c9ff61fd61532427cc16 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 14:41:06 +0200 Subject: [PATCH 105/177] Move inner code to `sync_lightning_wallet_inner` for Electrum Previously, we might have overlooked some cases that would exit the method and bubble up an error via `?` instead of propagating to all subscribers. Here, we split out the code to an inner method to ensure we always propagate. --- src/chain/electrum.rs | 54 +++++++++++++++++++++++++------------------ 1 file changed, 32 insertions(+), 22 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 44a637cc39..a287ad41a6 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -188,26 +188,6 @@ impl ElectrumChainSource { &self, channel_manager: Arc, chain_monitor: Arc, output_sweeper: Arc, ) -> Result<(), Error> { - let electrum_client: Arc = - if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the lightning wallet" - ); - return Err(Error::TxSyncFailed); - }; - - let sync_cman = Arc::clone(&channel_manager); - let sync_cmon = Arc::clone(&chain_monitor); - let sync_sweeper = Arc::clone(&output_sweeper); - let confirmables = vec![ - sync_cman as Arc, - sync_cmon as Arc, - sync_sweeper as Arc, - ]; - let receiver_res = { let mut status_lock = self.lightning_wallet_sync_status.lock().unwrap(); status_lock.register_or_subscribe_pending_sync() @@ -221,6 +201,38 @@ impl ElectrumChainSource { })?; } + let res = + self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await; + + self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_lightning_wallet_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { + let sync_cman = Arc::clone(&channel_manager); + let sync_cmon = Arc::clone(&chain_monitor); + let sync_sweeper = Arc::clone(&output_sweeper); + let confirmables = vec![ + sync_cman as Arc, + sync_cmon as Arc, + sync_sweeper as Arc, + ]; + + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the lightning wallet" + ); + return Err(Error::TxSyncFailed); + }; + let res = electrum_client.sync_confirmables(confirmables).await; if let Ok(_) = res { @@ -245,8 +257,6 @@ impl ElectrumChainSource { )?; } - self.lightning_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - res } From 128fdfd09388632046ab61d07565204b711b5a46 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 14:43:26 +0200 Subject: [PATCH 106/177] Move inner code to `sync_onchain_wallet_inner` for Electrum Previously, we might have overlooked some cases that would exit the method and bubble up an error via `?` instead of propagating to all subscribers. Here, we split out the code to an inner method to ensure we always propagate. --- src/chain/electrum.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index a287ad41a6..6193c67b3f 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -103,16 +103,6 @@ impl ElectrumChainSource { } pub(crate) async fn sync_onchain_wallet(&self) -> Result<(), Error> { - let electrum_client: Arc = - if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { - Arc::clone(client) - } else { - debug_assert!( - false, - "We should have started the chain source before syncing the onchain wallet" - ); - return Err(Error::FeerateEstimationUpdateFailed); - }; let receiver_res = { let mut status_lock = self.onchain_wallet_sync_status.lock().unwrap(); status_lock.register_or_subscribe_pending_sync() @@ -126,6 +116,24 @@ impl ElectrumChainSource { })?; } + let res = self.sync_onchain_wallet_inner().await; + + self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn sync_onchain_wallet_inner(&self) -> Result<(), Error> { + let electrum_client: Arc = + if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { + Arc::clone(client) + } else { + debug_assert!( + false, + "We should have started the chain source before syncing the onchain wallet" + ); + return Err(Error::FeerateEstimationUpdateFailed); + }; // If this is our first sync, do a full scan with the configured gap limit. // Otherwise just do an incremental sync. let incremental_sync = @@ -179,8 +187,6 @@ impl ElectrumChainSource { apply_wallet_update(update_res, now) }; - self.onchain_wallet_sync_status.lock().unwrap().propagate_result_to_subscribers(res); - res } From 9444bc4d31d42ac1e315985dc0c611aac230091f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 14:46:56 +0200 Subject: [PATCH 107/177] Move inner code to `poll_and_update_listeners_inner` Previously, we might have overlooked some cases that would exit the method and bubble up an error via `?` instead of propagating to all subscribers. Here, we split out the code to an inner method to ensure we always propagate. --- src/chain/bitcoind.rs | 42 +++++++++++++++++++----------------------- 1 file changed, 19 insertions(+), 23 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index b87ee13ed7..5fd4bdd43e 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -306,6 +306,19 @@ impl BitcoindChainSource { })?; } + let res = self + .poll_and_update_listeners_inner(channel_manager, chain_monitor, output_sweeper) + .await; + + self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); + + res + } + + async fn poll_and_update_listeners_inner( + &self, channel_manager: Arc, chain_monitor: Arc, + output_sweeper: Arc, + ) -> Result<(), Error> { let latest_chain_tip_opt = self.latest_chain_tip.read().unwrap().clone(); let chain_tip = if let Some(tip) = latest_chain_tip_opt { tip @@ -317,9 +330,7 @@ impl BitcoindChainSource { }, Err(e) => { log_error!(self.logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; + return Err(Error::TxSyncFailed); }, } }; @@ -348,9 +359,7 @@ impl BitcoindChainSource { Ok(_) => {}, Err(e) => { log_error!(self.logger, "Failed to poll for chain data: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; + return Err(Error::TxSyncFailed); }, } @@ -376,9 +385,7 @@ impl BitcoindChainSource { }, Err(e) => { log_error!(self.logger, "Failed to poll for mempool transactions: {:?}", e); - let res = Err(Error::TxSyncFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; + return Err(Error::TxSyncFailed); }, } @@ -388,24 +395,13 @@ impl BitcoindChainSource { locked_node_metrics.latest_lightning_wallet_sync_timestamp = unix_time_secs_opt; locked_node_metrics.latest_onchain_wallet_sync_timestamp = unix_time_secs_opt; - let write_res = write_node_metrics( + write_node_metrics( &*locked_node_metrics, Arc::clone(&self.kv_store), Arc::clone(&self.logger), - ); - match write_res { - Ok(()) => (), - Err(e) => { - log_error!(self.logger, "Failed to persist node metrics: {}", e); - let res = Err(Error::PersistenceFailed); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - return res; - }, - } + )?; - let res = Ok(()); - self.wallet_polling_status.lock().unwrap().propagate_result_to_subscribers(res); - res + Ok(()) } pub(super) async fn update_fee_rate_estimates(&self) -> Result<(), Error> { From a6349a47e037f96cbc2589324e02954fcd0fba49 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 15:01:58 +0200 Subject: [PATCH 108/177] Call `periodically_archive_fully_resolved_monitors` for Bitcoind Which we previously overlooked --- src/chain/bitcoind.rs | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 5fd4bdd43e..fc5f7048fc 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -5,7 +5,7 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::WalletSyncStatus; +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ BitcoindRestClientConfig, Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, @@ -340,7 +340,7 @@ impl BitcoindChainSource { let chain_listener = ChainListener { onchain_wallet: Arc::clone(&self.onchain_wallet), channel_manager: Arc::clone(&channel_manager), - chain_monitor, + chain_monitor: Arc::clone(&chain_monitor), output_sweeper, }; let mut spv_client = @@ -355,6 +355,14 @@ impl BitcoindChainSource { now.elapsed().unwrap().as_millis() ); *self.latest_chain_tip.write().unwrap() = Some(tip); + + periodically_archive_fully_resolved_monitors( + Arc::clone(&channel_manager), + chain_monitor, + Arc::clone(&self.kv_store), + Arc::clone(&self.logger), + Arc::clone(&self.node_metrics), + )?; }, Ok(_) => {}, Err(e) => { From be2bc0782cfcef658dc751a96c85af717e3d491c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 7 Aug 2025 14:05:00 +0200 Subject: [PATCH 109/177] Bump `electrum-client` to v0.24.0 We bump the `electrum-client` dependency to the recently-introduced version v0.24.0. --- Cargo.toml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index dcb7d022ff..da04c4f4a6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,7 +82,7 @@ esplora-client = { version = "0.12", default-features = false, features = ["toki # `lightning-transaction-sync` APIs. We should drop it as part of the upgrade # to LDK 0.2. esplora-client_0_11 = { package = "esplora-client", version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } -electrum-client = { version = "0.23.1", default-features = true } +electrum-client = { version = "0.24.0", default-features = true } libc = "0.2" uniffi = { version = "0.28.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } @@ -103,11 +103,11 @@ proptest = "1.0.0" regex = "1.5.6" [target.'cfg(not(no_download))'.dev-dependencies] -electrsd = { version = "0.34.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } +electrsd = { version = "0.35.0", default-features = false, features = ["legacy", "esplora_a33e97e1", "corepc-node_27_2"] } [target.'cfg(no_download)'.dev-dependencies] -electrsd = { version = "0.34.0", default-features = false, features = ["legacy"] } -corepc-node = { version = "0.7.0", default-features = false, features = ["27_2"] } +electrsd = { version = "0.35.0", default-features = false, features = ["legacy"] } +corepc-node = { version = "0.8.0", default-features = false, features = ["27_2"] } [target.'cfg(cln_test)'.dev-dependencies] clightningrpc = { version = "0.3.0-beta.8", default-features = false } From 3d356910790923b5f54dfc6d4f52cea87619a08a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 7 Aug 2025 14:18:02 +0200 Subject: [PATCH 110/177] Fix lifetime elision warnings introduced by rustc 1.89 The just-released rustc 1.89 added a new `mismatched-lifetime-syntaxes` lint which had two warnings pop up. We fix these here. --- src/chain/bitcoind.rs | 2 +- src/tx_broadcaster.rs | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index fc5f7048fc..d7d3254605 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -1155,7 +1155,7 @@ impl BlockSource for BitcoindClient { } } - fn get_best_block(&self) -> AsyncBlockSourceResult<(bitcoin::BlockHash, Option)> { + fn get_best_block(&self) -> AsyncBlockSourceResult<'_, (bitcoin::BlockHash, Option)> { match self { BitcoindClient::Rpc { rpc_client, .. } => { Box::pin(async move { rpc_client.get_best_block().await }) diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 09189b1371..4d9397a610 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -36,7 +36,9 @@ where Self { queue_sender, queue_receiver: Mutex::new(queue_receiver), logger } } - pub(crate) async fn get_broadcast_queue(&self) -> MutexGuard>> { + pub(crate) async fn get_broadcast_queue( + &self, + ) -> MutexGuard<'_, mpsc::Receiver>> { self.queue_receiver.lock().await } } From 1af27ac46363657a05d03d27087bcbd725845f46 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 11 Aug 2025 10:33:55 +0200 Subject: [PATCH 111/177] Bump MSRV to rustc 1.85 We generally align our MSRV with Debian's stable channel. Debian 13 'Trixie' was just released, shipping rustc 1.85. We therefore bump our MSRV on the `main` branch here. --- .github/workflows/rust.yml | 11 +++-------- README.md | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 1c4e6ed15e..aff6109089 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -18,7 +18,7 @@ jobs: toolchain: [ stable, beta, - 1.75.0, # Our MSRV + 1.85.0, # Our MSRV ] include: - toolchain: stable @@ -29,7 +29,7 @@ jobs: platform: macos-latest - toolchain: stable platform: windows-latest - - toolchain: 1.75.0 + - toolchain: 1.85.0 msrv: true runs-on: ${{ matrix.platform }} steps: @@ -42,11 +42,6 @@ jobs: - name: Check formatting on Rust ${{ matrix.toolchain }} if: matrix.check-fmt run: rustup component add rustfmt && cargo fmt --all -- --check - - name: Pin packages to allow for MSRV - if: matrix.msrv - run: | - cargo update -p home --precise "0.5.9" --verbose # home v0.5.11 requires rustc 1.81 or newer - cargo update -p idna_adapter --precise "1.1.0" --verbose # idna_adapter 1.2 switched to ICU4X, requiring 1.81 and newer - name: Set RUSTFLAGS to deny warnings if: "matrix.toolchain == 'stable'" run: echo "RUSTFLAGS=-D warnings" >> "$GITHUB_ENV" @@ -79,7 +74,7 @@ jobs: if: matrix.build-uniffi run: cargo build --features uniffi --verbose --color always - name: Build documentation on Rust ${{ matrix.toolchain }} - if: "matrix.platform != 'windows-latest' || matrix.toolchain != '1.75.0'" + if: "matrix.platform != 'windows-latest' || matrix.toolchain != '1.85.0'" run: | cargo doc --release --verbose --color always cargo doc --document-private-items --verbose --color always diff --git a/README.md b/README.md index ed35d86408..d11c5fc8e3 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ LDK Node currently comes with a decidedly opinionated set of design choices: LDK Node itself is written in [Rust][rust] and may therefore be natively added as a library dependency to any `std` Rust program. However, beyond its Rust API it also offers language bindings for [Swift][swift], [Kotlin][kotlin], and [Python][python] based on the [UniFFI](https://github.com/mozilla/uniffi-rs/). Moreover, [Flutter bindings][flutter_bindings] are also available. ## MSRV -The Minimum Supported Rust Version (MSRV) is currently 1.75.0. +The Minimum Supported Rust Version (MSRV) is currently 1.85.0. [api_docs]: https://docs.rs/ldk-node/*/ldk_node/ [api_docs_node]: https://docs.rs/ldk-node/*/ldk_node/struct.Node.html From 90cde611afb078314901b3b466eb80776a2450d4 Mon Sep 17 00:00:00 2001 From: aagbotemi Date: Sat, 31 May 2025 12:42:17 +0100 Subject: [PATCH 112/177] feat: add ability to set payment preimage for spontaneous payments --- bindings/ldk_node.udl | 4 ++ src/payment/spontaneous.rs | 26 ++++++++++-- tests/integration_tests_rust.rs | 72 ++++++++++++++++++++++++++++++++- 3 files changed, 96 insertions(+), 6 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 3c240b43c3..26480ca4b5 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -213,6 +213,10 @@ interface SpontaneousPayment { [Throws=NodeError] PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters, sequence custom_tlvs); [Throws=NodeError] + PaymentId send_with_preimage(u64 amount_msat, PublicKey node_id, PaymentPreimage preimage, SendingParameters? sending_parameters); + [Throws=NodeError] + PaymentId send_with_preimage_and_custom_tlvs(u64 amount_msat, PublicKey node_id, sequence custom_tlvs, PaymentPreimage preimage, SendingParameters? sending_parameters); + [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 1508b6cd84..a7e7876d75 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -57,7 +57,7 @@ impl SpontaneousPayment { pub fn send( &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, None) + self.send_inner(amount_msat, node_id, sending_parameters, None, None) } /// Send a spontaneous payment including a list of custom TLVs. @@ -65,19 +65,37 @@ impl SpontaneousPayment { &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, custom_tlvs: Vec, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs)) + self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs), None) + } + + /// Send a spontaneous payment with custom preimage + pub fn send_with_preimage( + &self, amount_msat: u64, node_id: PublicKey, preimage: PaymentPreimage, + sending_parameters: Option, + ) -> Result { + self.send_inner(amount_msat, node_id, sending_parameters, None, Some(preimage)) + } + + /// Send a spontaneous payment with custom preimage including a list of custom TLVs. + pub fn send_with_preimage_and_custom_tlvs( + &self, amount_msat: u64, node_id: PublicKey, custom_tlvs: Vec, + preimage: PaymentPreimage, sending_parameters: Option, + ) -> Result { + self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs), Some(preimage)) } fn send_inner( &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Option>, + custom_tlvs: Option>, preimage: Option, ) -> Result { let rt_lock = self.runtime.read().unwrap(); if rt_lock.is_none() { return Err(Error::NotRunning); } - let payment_preimage = PaymentPreimage(self.keys_manager.get_secure_random_bytes()); + let payment_preimage = preimage + .unwrap_or_else(|| PaymentPreimage(self.keys_manager.get_secure_random_bytes())); + let payment_hash = PaymentHash::from(payment_preimage); let payment_id = PaymentId(payment_hash.0); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fbd95ef502..57742e09ef 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -19,8 +19,8 @@ use common::{ use ldk_node::config::EsploraSyncConfig; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ - ConfirmationStatus, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, - SendingParameters, + ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, + QrPaymentResult, SendingParameters, }; use ldk_node::{Builder, Event, NodeError}; @@ -29,8 +29,10 @@ use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use lightning_types::payment::PaymentPreimage; use bitcoin::address::NetworkUnchecked; +use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; use bitcoin::Address; use bitcoin::Amount; @@ -1389,3 +1391,69 @@ fn facade_logging() { validate_log_entry(entry); } } + +#[test] +fn spontaneous_send_with_custom_preimage() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, true, false); + + let address_a = node_a.onchain_payment().new_address().unwrap(); + let premine_sat = 1_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_a], + Amount::from_sat(premine_sat), + ); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + open_channel(&node_a, &node_b, 500_000, true, &electrsd); + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + node_a.sync_wallets().unwrap(); + node_b.sync_wallets().unwrap(); + expect_channel_ready_event!(node_a, node_b.node_id()); + expect_channel_ready_event!(node_b, node_a.node_id()); + + let seed = b"test_payment_preimage"; + let bytes: Sha256Hash = Sha256Hash::hash(seed); + let custom_bytes = bytes.to_byte_array(); + let custom_preimage = PaymentPreimage(custom_bytes); + + let amount_msat = 100_000; + let payment_id = node_a + .spontaneous_payment() + .send_with_preimage(amount_msat, node_b.node_id(), custom_preimage, None) + .unwrap(); + + // check payment status and verify stored preimage + expect_payment_successful_event!(node_a, Some(payment_id), None); + let details: PaymentDetails = + node_a.list_payments_with_filter(|p| p.id == payment_id).first().unwrap().clone(); + assert_eq!(details.status, PaymentStatus::Succeeded); + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected a spontaneous PaymentKind with a preimage"); + } + + // Verify receiver side (node_b) + expect_payment_received_event!(node_b, amount_msat); + let receiver_payments: Vec = node_b.list_payments_with_filter(|p| { + p.direction == PaymentDirection::Inbound + && matches!(p.kind, PaymentKind::Spontaneous { .. }) + }); + + assert_eq!(receiver_payments.len(), 1); + let receiver_details = &receiver_payments[0]; + assert_eq!(receiver_details.status, PaymentStatus::Succeeded); + assert_eq!(receiver_details.amount_msat, Some(amount_msat)); + assert_eq!(receiver_details.direction, PaymentDirection::Inbound); + + // Verify receiver also has the same preimage + if let PaymentKind::Spontaneous { preimage: Some(pi), .. } = &receiver_details.kind { + assert_eq!(pi.0, custom_bytes); + } else { + panic!("Expected receiver to have spontaneous PaymentKind with preimage"); + } +} From 1d06c7aa1c2c966d89bde3991c86e70c0941ac43 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 1 Aug 2025 14:16:24 +0200 Subject: [PATCH 113/177] Ensure we always startup with a `rustls` `CryptoProvider` The `rustls` library recently introduced this weird behavior where they expect users to, apart from configuring the respective feature, also explictly call `CryptoProvider::install_default`. Otherwise they'd simply panic at runtime whenever the first network call requiring TLS would be made. While we already made a change upstream at `rust-electrum-client`, we also make sure here that we definitely, always, absolutley are sure that we have a `CryptoProvider` set on startup. --- Cargo.toml | 1 + src/builder.rs | 23 ++++++++++++++++++++++- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index da04c4f4a6..96a9eea534 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -67,6 +67,7 @@ bdk_electrum = { version = "0.23.0", default-features = false, features = ["use- bdk_wallet = { version = "2.0.0", default-features = false, features = ["std", "keys-bip39"]} reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } +rustls = { version = "0.23", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"] } bitcoin = "0.32.4" bip39 = "2.0.0" diff --git a/src/builder.rs b/src/builder.rs index 85ec70d18a..1e9c731e33 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -75,7 +75,7 @@ use std::fmt; use std::fs; use std::path::PathBuf; use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex, RwLock}; +use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; @@ -1051,6 +1051,8 @@ fn build_with_store_internal( liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], logger: Arc, kv_store: Arc, ) -> Result { + optionally_install_rustls_cryptoprovider(); + if let Err(err) = may_announce_channel(&config) { if config.announcement_addresses.is_some() { log_error!(logger, "Announcement addresses were set but some required configuration options for node announcement are missing: {}", err); @@ -1663,6 +1665,25 @@ fn build_with_store_internal( }) } +fn optionally_install_rustls_cryptoprovider() { + // Acquire a global Mutex, ensuring that only one process at a time install the provider. This + // is mostly required for running tests concurrently. + static INIT_CRYPTO: Once = Once::new(); + + INIT_CRYPTO.call_once(|| { + // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now. + if rustls::crypto::CryptoProvider::get_default().is_none() { + let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + } + + // Refuse to startup without TLS support. Better to catch it now than even later at runtime. + assert!( + rustls::crypto::CryptoProvider::get_default().is_some(), + "We need to have a CryptoProvider" + ); + }); +} + /// Sets up the node logger. fn setup_logger( log_writer_config: &Option, config: &Config, From 3ff489068f6e24c362980e1ea208f4937dbe4ab9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 14 Aug 2025 14:45:08 +0200 Subject: [PATCH 114/177] Add test that drops the node in an async context .. as tokio tends to panic if dropping a runtime in an async context and we're not super careful. Here, we add some test coverage for this edge case in Rust tests. --- tests/integration_tests_rust.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 57742e09ef..ad38674294 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1457,3 +1457,14 @@ fn spontaneous_send_with_custom_preimage() { panic!("Expected receiver to have spontaneous PaymentKind with preimage"); } } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn drop_in_async_context() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let seed_bytes = vec![42u8; 64]; + + let config = random_config(true); + let node = setup_node(&chain_source, config, Some(seed_bytes)); + node.stop().unwrap(); +} From b7c07043e6cc1b57ac9019f3efbd1e8f3201f818 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 14 Aug 2025 13:35:39 +0200 Subject: [PATCH 115/177] Have `ChainSource` hold `tx_broadcaster` reference Previously, individual chain sources would hold references to the `Broadcaster` to acquire the broadcast queue. Here, we move this to `ChainSource`, which allows us to handle the queue in a single place, while the individual chain sources will deal with the actual packages only. --- src/chain/bitcoind.rs | 76 +++++++++++------------- src/chain/electrum.rs | 18 ++---- src/chain/esplora.rs | 131 ++++++++++++++++++++---------------------- src/chain/mod.rs | 36 ++++++------ 4 files changed, 119 insertions(+), 142 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index d7d3254605..a120f82538 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -16,7 +16,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::write_node_metrics; use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; @@ -54,7 +54,6 @@ pub(super) struct BitcoindChainSource { onchain_wallet: Arc, wallet_polling_status: Mutex, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, @@ -65,8 +64,8 @@ impl BitcoindChainSource { pub(crate) fn new_rpc( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, ) -> Self { let api_client = Arc::new(BitcoindClient::new_rpc( rpc_host.clone(), @@ -85,7 +84,6 @@ impl BitcoindChainSource { onchain_wallet, wallet_polling_status, fee_estimator, - tx_broadcaster, kv_store, config, logger: Arc::clone(&logger), @@ -96,9 +94,8 @@ impl BitcoindChainSource { pub(crate) fn new_rest( rpc_host: String, rpc_port: u16, rpc_user: String, rpc_password: String, onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - rest_client_config: BitcoindRestClientConfig, logger: Arc, - node_metrics: Arc>, + kv_store: Arc, config: Arc, rest_client_config: BitcoindRestClientConfig, + logger: Arc, node_metrics: Arc>, ) -> Self { let api_client = Arc::new(BitcoindClient::new_rest( rest_client_config.rest_host, @@ -120,7 +117,6 @@ impl BitcoindChainSource { wallet_polling_status, onchain_wallet, fee_estimator, - tx_broadcaster, kv_store, config, logger: Arc::clone(&logger), @@ -530,53 +526,45 @@ impl BitcoindChainSource { Ok(()) } - pub(crate) async fn process_broadcast_queue(&self) { + pub(crate) async fn process_broadcast_package(&self, package: Vec) { // While it's a bit unclear when we'd be able to lean on Bitcoin Core >v28 // features, we should eventually switch to use `submitpackage` via the // `rust-bitcoind-json-rpc` crate rather than just broadcasting individual // transactions. - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - self.api_client.broadcast_transaction(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(id) => { - debug_assert_eq!(id, txid); - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); - }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, + for tx in &package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.api_client.broadcast_transaction(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(id) => { + debug_assert_eq!(id, txid); + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); }, Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); + log_error!(self.logger, "Failed to broadcast transaction {}: {}", txid, e); log_trace!( self.logger, "Failed broadcast transaction bytes: {}", log_bytes!(tx.encode()) ); }, - } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, } } } diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 6193c67b3f..abbb758dde 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -18,7 +18,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::write_node_metrics; use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::NodeMetrics; use lightning::chain::{Confirm, Filter, WatchedOutput}; @@ -56,7 +56,6 @@ pub(super) struct ElectrumChainSource { onchain_wallet_sync_status: Mutex, lightning_wallet_sync_status: Mutex, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, @@ -66,9 +65,8 @@ pub(super) struct ElectrumChainSource { impl ElectrumChainSource { pub(super) fn new( server_url: String, sync_config: ElectrumSyncConfig, onchain_wallet: Arc, - fee_estimator: Arc, tx_broadcaster: Arc, - kv_store: Arc, config: Arc, logger: Arc, - node_metrics: Arc>, + fee_estimator: Arc, kv_store: Arc, config: Arc, + logger: Arc, node_metrics: Arc>, ) -> Self { let electrum_runtime_status = RwLock::new(ElectrumRuntimeStatus::new()); let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); @@ -81,7 +79,6 @@ impl ElectrumChainSource { onchain_wallet_sync_status, lightning_wallet_sync_status, fee_estimator, - tx_broadcaster, kv_store, config, logger: Arc::clone(&logger), @@ -302,7 +299,7 @@ impl ElectrumChainSource { Ok(()) } - pub(crate) async fn process_broadcast_queue(&self) { + pub(crate) async fn process_broadcast_package(&self, package: Vec) { let electrum_client: Arc = if let Some(client) = self.electrum_runtime_status.read().unwrap().client().as_ref() { Arc::clone(client) @@ -311,11 +308,8 @@ impl ElectrumChainSource { return; }; - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in next_package { - electrum_client.broadcast(tx).await; - } + for tx in package { + electrum_client.broadcast(tx).await; } } } diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 5932426b73..a8806a4137 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -18,7 +18,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::write_node_metrics; use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; use lightning::chain::{Confirm, Filter, WatchedOutput}; @@ -30,7 +30,7 @@ use bdk_esplora::EsploraAsyncExt; use esplora_client::AsyncClient as EsploraAsyncClient; -use bitcoin::{FeeRate, Network, Script, Txid}; +use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; use std::collections::HashMap; use std::sync::{Arc, Mutex, RwLock}; @@ -44,7 +44,6 @@ pub(super) struct EsploraChainSource { tx_sync: Arc>>, lightning_wallet_sync_status: Mutex, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: Arc, @@ -55,8 +54,8 @@ impl EsploraChainSource { pub(crate) fn new( server_url: String, headers: HashMap, sync_config: EsploraSyncConfig, onchain_wallet: Arc, fee_estimator: Arc, - tx_broadcaster: Arc, kv_store: Arc, config: Arc, - logger: Arc, node_metrics: Arc>, + kv_store: Arc, config: Arc, logger: Arc, + node_metrics: Arc>, ) -> Self { // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. @@ -90,7 +89,6 @@ impl EsploraChainSource { tx_sync, lightning_wallet_sync_status, fee_estimator, - tx_broadcaster, kv_store, config, logger, @@ -372,76 +370,73 @@ impl EsploraChainSource { Ok(()) } - pub(crate) async fn process_broadcast_queue(&self) { - let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - for tx in &next_package { - let txid = tx.compute_txid(); - let timeout_fut = tokio::time::timeout( - Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), - self.esplora_client.broadcast(tx), - ); - match timeout_fut.await { - Ok(res) => match res { - Ok(()) => { - log_trace!(self.logger, "Successfully broadcast transaction {}", txid); - }, - Err(e) => match e { - esplora_client::Error::HttpResponse { status, message } => { - if status == 400 { - // Log 400 at lesser level, as this often just means bitcoind already knows the - // transaction. - // FIXME: We can further differentiate here based on the error - // message which will be available with rust-esplora-client 0.7 and - // later. - log_trace!( - self.logger, - "Failed to broadcast due to HTTP connection error: {}", - message - ); - } else { - log_error!( - self.logger, - "Failed to broadcast due to HTTP connection error: {} - {}", - status, - message - ); - } + pub(crate) async fn process_broadcast_package(&self, package: Vec) { + for tx in &package { + let txid = tx.compute_txid(); + let timeout_fut = tokio::time::timeout( + Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), + self.esplora_client.broadcast(tx), + ); + match timeout_fut.await { + Ok(res) => match res { + Ok(()) => { + log_trace!(self.logger, "Successfully broadcast transaction {}", txid); + }, + Err(e) => match e { + esplora_client::Error::HttpResponse { status, message } => { + if status == 400 { + // Log 400 at lesser level, as this often just means bitcoind already knows the + // transaction. + // FIXME: We can further differentiate here based on the error + // message which will be available with rust-esplora-client 0.7 and + // later. log_trace!( self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) + "Failed to broadcast due to HTTP connection error: {}", + message ); - }, - _ => { + } else { log_error!( self.logger, - "Failed to broadcast transaction {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) + "Failed to broadcast due to HTTP connection error: {} - {}", + status, + message ); - }, + } + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, + _ => { + log_error!( + self.logger, + "Failed to broadcast transaction {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); }, }, - Err(e) => { - log_error!( - self.logger, - "Failed to broadcast transaction due to timeout {}: {}", - txid, - e - ); - log_trace!( - self.logger, - "Failed broadcast transaction bytes: {}", - log_bytes!(tx.encode()) - ); - }, - } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to broadcast transaction due to timeout {}: {}", + txid, + e + ); + log_trace!( + self.logger, + "Failed broadcast transaction bytes: {}", + log_bytes!(tx.encode()) + ); + }, } } } diff --git a/src/chain/mod.rs b/src/chain/mod.rs index 91cce1fe3d..db0573d3de 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -87,6 +87,7 @@ impl WalletSyncStatus { pub(crate) struct ChainSource { kind: ChainSourceKind, + tx_broadcaster: Arc, logger: Arc, } @@ -109,14 +110,13 @@ impl ChainSource { sync_config, onchain_wallet, fee_estimator, - tx_broadcaster, kv_store, config, Arc::clone(&logger), node_metrics, ); let kind = ChainSourceKind::Esplora(esplora_chain_source); - Self { kind, logger } + Self { kind, tx_broadcaster, logger } } pub(crate) fn new_electrum( @@ -130,14 +130,13 @@ impl ChainSource { sync_config, onchain_wallet, fee_estimator, - tx_broadcaster, kv_store, config, Arc::clone(&logger), node_metrics, ); let kind = ChainSourceKind::Electrum(electrum_chain_source); - Self { kind, logger } + Self { kind, tx_broadcaster, logger } } pub(crate) fn new_bitcoind_rpc( @@ -153,14 +152,13 @@ impl ChainSource { rpc_password, onchain_wallet, fee_estimator, - tx_broadcaster, kv_store, config, Arc::clone(&logger), node_metrics, ); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); - Self { kind, logger } + Self { kind, tx_broadcaster, logger } } pub(crate) fn new_bitcoind_rest( @@ -177,7 +175,6 @@ impl ChainSource { rpc_password, onchain_wallet, fee_estimator, - tx_broadcaster, kv_store, config, rest_client_config, @@ -185,7 +182,7 @@ impl ChainSource { node_metrics, ); let kind = ChainSourceKind::Bitcoind(bitcoind_chain_source); - Self { kind, logger } + Self { kind, tx_broadcaster, logger } } pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { @@ -429,16 +426,19 @@ impl ChainSource { } pub(crate) async fn process_broadcast_queue(&self) { - match &self.kind { - ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.process_broadcast_queue().await - }, - ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.process_broadcast_queue().await - }, - ChainSourceKind::Bitcoind(bitcoind_chain_source) => { - bitcoind_chain_source.process_broadcast_queue().await - }, + let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; + while let Some(next_package) = receiver.recv().await { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.process_broadcast_package(next_package).await + }, + } } } } From 6b9d899e22b9a444f4809cc431b4f41efbef4fd3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 14 Aug 2025 13:42:43 +0200 Subject: [PATCH 116/177] Move continuous tx broadcast processing loop to `ChainSource` Rather than looping in the `spawn` method directly, we move the loop to a refactored `continuously_process_broadcast_queue` method on `ChainSource`, which also allows us to react on the stop signal if we're polling `recv`. --- src/chain/mod.rs | 40 +++++++++++++++++++++++++++------------- src/lib.rs | 21 ++------------------- 2 files changed, 29 insertions(+), 32 deletions(-) diff --git a/src/chain/mod.rs b/src/chain/mod.rs index db0573d3de..a4ab2c76b5 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -18,7 +18,7 @@ use crate::config::{ }; use crate::fee_estimator::OnchainFeeEstimator; use crate::io::utils::write_node_metrics; -use crate::logger::{log_info, log_trace, LdkLogger, Logger}; +use crate::logger::{log_debug, log_info, log_trace, LdkLogger, Logger}; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; @@ -425,19 +425,33 @@ impl ChainSource { } } - pub(crate) async fn process_broadcast_queue(&self) { + pub(crate) async fn continuously_process_broadcast_queue( + &self, mut stop_tx_bcast_receiver: tokio::sync::watch::Receiver<()>, + ) { let mut receiver = self.tx_broadcaster.get_broadcast_queue().await; - while let Some(next_package) = receiver.recv().await { - match &self.kind { - ChainSourceKind::Esplora(esplora_chain_source) => { - esplora_chain_source.process_broadcast_package(next_package).await - }, - ChainSourceKind::Electrum(electrum_chain_source) => { - electrum_chain_source.process_broadcast_package(next_package).await - }, - ChainSourceKind::Bitcoind(bitcoind_chain_source) => { - bitcoind_chain_source.process_broadcast_package(next_package).await - }, + loop { + let tx_bcast_logger = Arc::clone(&self.logger); + tokio::select! { + _ = stop_tx_bcast_receiver.changed() => { + log_debug!( + tx_bcast_logger, + "Stopping broadcasting transactions.", + ); + return; + } + Some(next_package) = receiver.recv() => { + match &self.kind { + ChainSourceKind::Esplora(esplora_chain_source) => { + esplora_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Electrum(electrum_chain_source) => { + electrum_chain_source.process_broadcast_package(next_package).await + }, + ChainSourceKind::Bitcoind(bitcoind_chain_source) => { + bitcoind_chain_source.process_broadcast_package(next_package).await + }, + } + } } } } diff --git a/src/lib.rs b/src/lib.rs index 89a17ab03d..4ebc30b9bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -498,27 +498,10 @@ impl Node { }); } - let mut stop_tx_bcast = self.stop_sender.subscribe(); + let stop_tx_bcast = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); - let tx_bcast_logger = Arc::clone(&self.logger); runtime.spawn(async move { - // Every second we try to clear our broadcasting queue. - let mut interval = tokio::time::interval(Duration::from_secs(1)); - interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); - loop { - tokio::select! { - _ = stop_tx_bcast.changed() => { - log_debug!( - tx_bcast_logger, - "Stopping broadcasting transactions.", - ); - return; - } - _ = interval.tick() => { - chain_source.process_broadcast_queue().await; - } - } - } + chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await }); let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( From 17a45dd18bcca217e32ef9c98483e4f4fed91f34 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 14 Aug 2025 14:23:59 +0200 Subject: [PATCH 117/177] Wait on all background tasks to finish (or abort) Previously, we'd only wait for the background processor tasks to successfully finish. It turned out that this could lead to races when the other background tasks took too long to shutdown. Here, we attempt to wait on all background tasks shutting down for a bit, before moving on. --- src/builder.rs | 4 ++ src/config.rs | 3 ++ src/lib.rs | 143 +++++++++++++++++++++++++++++++++++++------------ 3 files changed, 117 insertions(+), 33 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 85ec70d18a..5ead3783b8 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1632,11 +1632,15 @@ fn build_with_store_internal( let (stop_sender, _) = tokio::sync::watch::channel(()); let background_processor_task = Mutex::new(None); + let background_tasks = Mutex::new(None); + let cancellable_background_tasks = Mutex::new(None); Ok(Node { runtime, stop_sender, background_processor_task, + background_tasks, + cancellable_background_tasks, config, wallet, chain_source, diff --git a/src/config.rs b/src/config.rs index a5048e64fe..02df8bbc76 100644 --- a/src/config.rs +++ b/src/config.rs @@ -79,6 +79,9 @@ pub(crate) const LDK_WALLET_SYNC_TIMEOUT_SECS: u64 = 10; // The timeout after which we give up waiting on LDK's event handler to exit on shutdown. pub(crate) const LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS: u64 = 30; +// The timeout after which we give up waiting on a background task to exit on shutdown. +pub(crate) const BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS: u64 = 5; + // The timeout after which we abort a fee rate cache update operation. pub(crate) const FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS: u64 = 5; diff --git a/src/lib.rs b/src/lib.rs index 4ebc30b9bf..a3cce07521 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,8 +127,8 @@ pub use builder::NodeBuilder as Builder; use chain::ChainSource; use config::{ default_user_config, may_announce_channel, ChannelConfig, Config, - LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, - RGS_SYNC_INTERVAL, + BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; @@ -179,6 +179,8 @@ pub struct Node { runtime: Arc>>>, stop_sender: tokio::sync::watch::Sender<()>, background_processor_task: Mutex>>, + background_tasks: Mutex>>, + cancellable_background_tasks: Mutex>>, config: Arc, wallet: Arc, chain_source: Arc, @@ -232,6 +234,10 @@ impl Node { return Err(Error::AlreadyRunning); } + let mut background_tasks = tokio::task::JoinSet::new(); + let mut cancellable_background_tasks = tokio::task::JoinSet::new(); + let runtime_handle = runtime.handle(); + log_info!( self.logger, "Starting up LDK Node with node ID {} on network: {}", @@ -258,11 +264,19 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - runtime.spawn(async move { - chain_source - .continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper) - .await; - }); + background_tasks.spawn_on( + async move { + chain_source + .continuously_sync_wallets( + stop_sync_receiver, + sync_cman, + sync_cmon, + sync_sweeper, + ) + .await; + }, + runtime_handle, + ); if self.gossip_source.is_rgs() { let gossip_source = Arc::clone(&self.gossip_source); @@ -270,7 +284,7 @@ impl Node { let gossip_sync_logger = Arc::clone(&self.logger); let gossip_node_metrics = Arc::clone(&self.node_metrics); let mut stop_gossip_sync = self.stop_sender.subscribe(); - runtime.spawn(async move { + cancellable_background_tasks.spawn_on(async move { let mut interval = tokio::time::interval(RGS_SYNC_INTERVAL); loop { tokio::select! { @@ -311,7 +325,7 @@ impl Node { } } } - }); + }, runtime_handle); } if let Some(listening_addresses) = &self.config.listening_addresses { @@ -337,7 +351,7 @@ impl Node { bind_addrs.extend(resolved_address); } - runtime.spawn(async move { + cancellable_background_tasks.spawn_on(async move { { let listener = tokio::net::TcpListener::bind(&*bind_addrs).await @@ -356,7 +370,7 @@ impl Node { _ = stop_listen.changed() => { log_debug!( listening_logger, - "Stopping listening to inbound connections.", + "Stopping listening to inbound connections." ); break; } @@ -375,7 +389,7 @@ impl Node { } listening_indicator.store(false, Ordering::Release); - }); + }, runtime_handle); } // Regularly reconnect to persisted peers. @@ -384,7 +398,7 @@ impl Node { let connect_logger = Arc::clone(&self.logger); let connect_peer_store = Arc::clone(&self.peer_store); let mut stop_connect = self.stop_sender.subscribe(); - runtime.spawn(async move { + cancellable_background_tasks.spawn_on(async move { let mut interval = tokio::time::interval(PEER_RECONNECTION_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { @@ -392,7 +406,7 @@ impl Node { _ = stop_connect.changed() => { log_debug!( connect_logger, - "Stopping reconnecting known peers.", + "Stopping reconnecting known peers." ); return; } @@ -412,7 +426,7 @@ impl Node { } } } - }); + }, runtime_handle); // Regularly broadcast node announcements. let bcast_cm = Arc::clone(&self.channel_manager); @@ -424,7 +438,7 @@ impl Node { let mut stop_bcast = self.stop_sender.subscribe(); let node_alias = self.config.node_alias.clone(); if may_announce_channel(&self.config).is_ok() { - runtime.spawn(async move { + cancellable_background_tasks.spawn_on(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. #[cfg(not(test))] let mut interval = tokio::time::interval(Duration::from_secs(30)); @@ -495,14 +509,15 @@ impl Node { } } } - }); + }, runtime_handle); } let stop_tx_bcast = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); - runtime.spawn(async move { - chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await - }); + cancellable_background_tasks.spawn_on( + async move { chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await }, + runtime_handle, + ); let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( Arc::clone(&self.tx_broadcaster), @@ -587,24 +602,33 @@ impl Node { let mut stop_liquidity_handler = self.stop_sender.subscribe(); let liquidity_handler = Arc::clone(&liquidity_source); let liquidity_logger = Arc::clone(&self.logger); - runtime.spawn(async move { - loop { - tokio::select! { - _ = stop_liquidity_handler.changed() => { - log_debug!( - liquidity_logger, - "Stopping processing liquidity events.", - ); - return; + background_tasks.spawn_on( + async move { + loop { + tokio::select! { + _ = stop_liquidity_handler.changed() => { + log_debug!( + liquidity_logger, + "Stopping processing liquidity events.", + ); + return; + } + _ = liquidity_handler.handle_next_event() => {} } - _ = liquidity_handler.handle_next_event() => {} } - } - }); + }, + runtime_handle, + ); } *runtime_lock = Some(runtime); + debug_assert!(self.background_tasks.lock().unwrap().is_none()); + *self.background_tasks.lock().unwrap() = Some(background_tasks); + + debug_assert!(self.cancellable_background_tasks.lock().unwrap().is_none()); + *self.cancellable_background_tasks.lock().unwrap() = Some(cancellable_background_tasks); + log_info!(self.logger, "Startup complete."); Ok(()) } @@ -635,6 +659,17 @@ impl Node { }, } + // Cancel cancellable background tasks + if let Some(mut tasks) = self.cancellable_background_tasks.lock().unwrap().take() { + let runtime_2 = Arc::clone(&runtime); + tasks.abort_all(); + tokio::task::block_in_place(move || { + runtime_2.block_on(async { while let Some(_) = tasks.join_next().await {} }) + }); + } else { + debug_assert!(false, "Expected some cancellable background tasks"); + }; + // Disconnect all peers. self.peer_manager.disconnect_all_peers(); log_debug!(self.logger, "Disconnected all network peers."); @@ -643,6 +678,46 @@ impl Node { self.chain_source.stop(); log_debug!(self.logger, "Stopped chain sources."); + // Wait until non-cancellable background tasks (mod LDK's background processor) are done. + let runtime_3 = Arc::clone(&runtime); + if let Some(mut tasks) = self.background_tasks.lock().unwrap().take() { + tokio::task::block_in_place(move || { + runtime_3.block_on(async { + loop { + let timeout_fut = tokio::time::timeout( + Duration::from_secs(BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS), + tasks.join_next_with_id(), + ); + match timeout_fut.await { + Ok(Some(Ok((id, _)))) => { + log_trace!(self.logger, "Stopped background task with id {}", id); + }, + Ok(Some(Err(e))) => { + tasks.abort_all(); + log_trace!(self.logger, "Stopping background task failed: {}", e); + break; + }, + Ok(None) => { + log_debug!(self.logger, "Stopped all background tasks"); + break; + }, + Err(e) => { + tasks.abort_all(); + log_error!( + self.logger, + "Stopping background task timed out: {}", + e + ); + break; + }, + } + } + }) + }); + } else { + debug_assert!(false, "Expected some background tasks"); + }; + // Wait until background processing stopped, at least until a timeout is reached. if let Some(background_processor_task) = self.background_processor_task.lock().unwrap().take() @@ -676,7 +751,9 @@ impl Node { log_error!(self.logger, "Stopping event handling timed out: {}", e); }, } - } + } else { + debug_assert!(false, "Expected a background processing task"); + }; #[cfg(tokio_unstable)] { From f9d65df9e6663617f07305cc728aa3ab63346d08 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 14 Aug 2025 18:38:57 +0200 Subject: [PATCH 118/177] Update CHANGELOG for v0.6.2 --- CHANGELOG.md | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe613a07ba..05813b6215 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,24 @@ +# 0.6.2 - Aug. 14, 2025 +This patch release fixes a panic that could have been hit when syncing to a +TLS-enabled Electrum server, as well as some minor issues when shutting down +the node. + +## Bug Fixes and Improvements +- If not set by the user, we now install a default `CryptoProvider` for the + `rustls` TLS library. This fixes an issue that would have the node panic + whenever they first try to access an Electrum server behind an `ssl://` + address. (#600) +- We improved robustness of the shutdown procedure. In particular, we now + wait for more background tasks to finish processing before shutting down + LDK background processing. Previously some tasks were kept running which + could have lead to race conditions. (#613) + +In total, this release features 12 files changed, 198 insertions, 92 +deletions in 13 commits from 2 authors in alphabetical order: + +- Elias Rohrer +- moisesPomilio + # 0.6.1 - Jun. 19, 2025 This patch release fixes minor issues with the recently-exposed `Bolt11Invoice` type in bindings. From 80a5abcef8f45c61eaa2ef3a172ae48704068fa3 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 15 Aug 2025 11:05:44 +0200 Subject: [PATCH 119/177] Update Swift files for v0.6.2 --- Package.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Package.swift b/Package.swift index 78a38f294c..00f3eeb845 100644 --- a/Package.swift +++ b/Package.swift @@ -3,8 +3,8 @@ import PackageDescription -let tag = "v0.6.1" -let checksum = "73f53b615d5bfdf76f2e7233bde17a2a62631292ce506763a7150344230859c8" +let tag = "v0.6.2" +let checksum = "dee28eb2bc019eeb61cc28ca5c19fdada465a6eb2b5169d2dbaa369f0c63ba03" let url = "https://github.com/lightningdevkit/ldk-node/releases/download/\(tag)/LDKNodeFFI.xcframework.zip" let package = Package( From 6e938fefe393f36eebefbab66af649faa5a1c38d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 15 Aug 2025 11:08:48 +0200 Subject: [PATCH 120/177] Drop nexus publishing plugin Maven Central recently deprecated the Sonatype-style publishing, which means the nexus publishing gradle plugin we used didn't work anymore. As Maven Central has yet to release a replacement plugin for gradle, we simply drop nexus publishing support here and manually upload the archives in the meantime, which is simple enough. We also drop the publishing CI jobs that originally aimed to automate publishing to Maven Central, which we however never came around to use since we didn't want to fully trust Github CI with publishing binaries for us. --- .github/workflows/publish-android.yml | 43 ---------- .github/workflows/publish-jvm.yml | 86 ------------------- .../kotlin/ldk-node-android/build.gradle.kts | 20 +---- bindings/kotlin/ldk-node-jvm/build.gradle.kts | 21 +---- 4 files changed, 2 insertions(+), 168 deletions(-) delete mode 100644 .github/workflows/publish-android.yml delete mode 100644 .github/workflows/publish-jvm.yml diff --git a/.github/workflows/publish-android.yml b/.github/workflows/publish-android.yml deleted file mode 100644 index b6b24ac90e..0000000000 --- a/.github/workflows/publish-android.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: Publish ldk-node-android to Maven Central -on: [workflow_dispatch] - -jobs: - build: - runs-on: ubuntu-20.04 - steps: - - name: "Check out PR branch" - uses: actions/checkout@v2 - - - name: "Cache" - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: "Set up JDK" - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: "Install Rust Android targets" - run: rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi - - - name: "Build ldk-node-android library" - run: | - export PATH=$PATH:$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/linux-x86_64/bin - ./scripts/uniffi_bindgen_generate_kotlin_android.sh - - - name: "Publish to Maven Local and Maven Central" - env: - ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} - ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} - run: | - cd bindings/kotlin/ldk-node-android - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/.github/workflows/publish-jvm.yml b/.github/workflows/publish-jvm.yml deleted file mode 100644 index 0ae40e0a11..0000000000 --- a/.github/workflows/publish-jvm.yml +++ /dev/null @@ -1,86 +0,0 @@ -name: Publish ldk-node-jvm to Maven Central -on: [workflow_dispatch] - -jobs: - build-jvm-macOS-M1-native-lib: - name: "Create M1 and x86_64 JVM native binaries" - runs-on: macos-12 - steps: - - name: "Checkout publishing branch" - uses: actions/checkout@v2 - - - name: Cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: Set up JDK - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: Install aarch64 Rust target - run: rustup target add aarch64-apple-darwin - - - name: Build ldk-node-jvm library - run: | - ./scripts/uniffi_bindgen_generate_kotlin.sh - - # build aarch64 + x86_64 native libraries and upload - - name: Upload macOS native libraries for reuse in publishing job - uses: actions/upload-artifact@v3 - with: - # name: no name is required because we upload the entire directory - # the default name "artifact" will be used - path: /Users/runner/work/ldk-node/ldk-node/bindings/kotlin/ldk-node-jvm/lib/src/main/resources/ - - build-jvm-full-library: - name: "Create full ldk-node-jvm library" - needs: [build-jvm-macOS-M1-native-lib] - runs-on: ubuntu-20.04 - steps: - - name: "Check out PR branch" - uses: actions/checkout@v2 - - - name: "Cache" - uses: actions/cache@v2 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - ./target - key: ${{ runner.os }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }} - - - name: "Set up JDK" - uses: actions/setup-java@v2 - with: - distribution: temurin - java-version: 11 - - - name: "Build ldk-node-jvm library" - run: | - ./scripts/uniffi_bindgen_generate_kotlin.sh - - - name: Download macOS native libraries from previous job - uses: actions/download-artifact@v4.1.7 - id: download - with: - # download the artifact created in the prior job (named "artifact") - name: artifact - path: ./bindings/kotlin/ldk-node-jvm/lib/src/main/resources/ - - - name: "Publish to Maven Local and Maven Central" - env: - ORG_GRADLE_PROJECT_signingKeyId: ${{ secrets.PGP_KEY_ID }} - ORG_GRADLE_PROJECT_signingKey: ${{ secrets.PGP_SECRET_KEY }} - ORG_GRADLE_PROJECT_signingPassword: ${{ secrets.PGP_PASSPHRASE }} - ORG_GRADLE_PROJECT_ossrhUsername: ${{ secrets.NEXUS_USERNAME }} - ORG_GRADLE_PROJECT_ossrhPassword: ${{ secrets.NEXUS_PASSWORD }} - run: | - cd bindings/kotlin/ldk-node-jvm - ./gradlew publishToSonatype closeAndReleaseSonatypeStagingRepository diff --git a/bindings/kotlin/ldk-node-android/build.gradle.kts b/bindings/kotlin/ldk-node-android/build.gradle.kts index ab7262dd75..bb38991d37 100644 --- a/bindings/kotlin/ldk-node-android/build.gradle.kts +++ b/bindings/kotlin/ldk-node-android/build.gradle.kts @@ -1,6 +1,7 @@ buildscript { repositories { google() + mavenCentral() } dependencies { classpath("com.android.tools.build:gradle:7.1.2") @@ -8,29 +9,10 @@ buildscript { } plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } // library version is defined in gradle.properties val libraryVersion: String by project -// These properties are required here so that the nexus publish-plugin -// finds a staging profile with the correct group (group is otherwise set as "") -// and knows whether to publish to a SNAPSHOT repository or not -// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin group = "org.lightningdevkit" version = libraryVersion - -nexusPublishing { - repositories { - create("sonatype") { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - - val ossrhUsername: String? by project - val ossrhPassword: String? by project - username.set(ossrhUsername) - password.set(ossrhPassword) - } - } -} \ No newline at end of file diff --git a/bindings/kotlin/ldk-node-jvm/build.gradle.kts b/bindings/kotlin/ldk-node-jvm/build.gradle.kts index ab7262dd75..faf316ef06 100644 --- a/bindings/kotlin/ldk-node-jvm/build.gradle.kts +++ b/bindings/kotlin/ldk-node-jvm/build.gradle.kts @@ -1,36 +1,17 @@ buildscript { repositories { google() + mavenCentral() } dependencies { - classpath("com.android.tools.build:gradle:7.1.2") } } plugins { - id("io.github.gradle-nexus.publish-plugin") version "1.1.0" } // library version is defined in gradle.properties val libraryVersion: String by project -// These properties are required here so that the nexus publish-plugin -// finds a staging profile with the correct group (group is otherwise set as "") -// and knows whether to publish to a SNAPSHOT repository or not -// https://github.com/gradle-nexus/publish-plugin#applying-the-plugin group = "org.lightningdevkit" version = libraryVersion - -nexusPublishing { - repositories { - create("sonatype") { - nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/")) - snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/")) - - val ossrhUsername: String? by project - val ossrhPassword: String? by project - username.set(ossrhUsername) - password.set(ossrhPassword) - } - } -} \ No newline at end of file From 89bdacb59171a103c46d47c56818df2d492f5ede Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 15 Aug 2025 11:12:09 +0200 Subject: [PATCH 121/177] Add simpler helper script to generate MD5 and SHA checksum files .. which we use before manually publishing to Maven Central. --- scripts/generate_checksum_files.sh | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 scripts/generate_checksum_files.sh diff --git a/scripts/generate_checksum_files.sh b/scripts/generate_checksum_files.sh new file mode 100644 index 0000000000..bbfa41a9a2 --- /dev/null +++ b/scripts/generate_checksum_files.sh @@ -0,0 +1,5 @@ +#!/bin/bash +md5sum $1 | cut -d ' ' -f 1 > $1.md5 +sha1sum $1 | cut -d ' ' -f 1 > $1.sha1 +sha256sum $1 | cut -d ' ' -f 1 > $1.sha256 +sha512sum $1 | cut -d ' ' -f 1 > $1.sha512 From e57927a914b3872606e6167cb3e19cbfb6a59208 Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Fri, 15 Aug 2025 09:59:49 -0300 Subject: [PATCH 122/177] Add reorg property test and refactor funding logic - Introduced a property test to verify reorg handling during both channel opening and closing (normal and force close). - Added `invalidate_block` helper to roll back the chain and regenerate blocks to the previous height. --- tests/common/mod.rs | 38 +++++++-- tests/reorg_test.rs | 193 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 226 insertions(+), 5 deletions(-) create mode 100644 tests/reorg_test.rs diff --git a/tests/common/mod.rs b/tests/common/mod.rs index daed864754..ab66f0fdd6 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -40,7 +40,9 @@ use electrum_client::ElectrumApi; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; +use serde_json::{json, Value}; +use std::collections::HashMap; use std::env; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -395,6 +397,21 @@ pub(crate) fn generate_blocks_and_wait( println!("\n"); } +pub(crate) fn invalidate_blocks(bitcoind: &BitcoindClient, num_blocks: usize) { + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let cur_height = blockchain_info.blocks as usize; + let target_height = cur_height - num_blocks + 1; + let block_hash = bitcoind + .get_block_hash(target_height as u64) + .expect("failed to get block hash") + .block_hash() + .expect("block hash should be present"); + bitcoind.invalidate_block(block_hash).expect("failed to invalidate block"); + let blockchain_info = bitcoind.get_blockchain_info().expect("failed to get blockchain info"); + let new_cur_height = blockchain_info.blocks as usize; + assert!(new_cur_height + num_blocks == cur_height); +} + pub(crate) fn wait_for_block(electrs: &E, min_height: usize) { let mut header = match electrs.block_headers_subscribe() { Ok(header) => header, @@ -474,18 +491,27 @@ pub(crate) fn premine_and_distribute_funds( let _ = bitcoind.load_wallet("ldk_node_test"); generate_blocks_and_wait(bitcoind, electrs, 101); - for addr in addrs { - let txid = bitcoind.send_to_address(&addr, amount).unwrap().0.parse().unwrap(); - wait_for_tx(electrs, txid); - } + let amounts: HashMap = + addrs.iter().map(|addr| (addr.to_string(), amount.to_btc())).collect(); + + let empty_account = json!(""); + let amounts_json = json!(amounts); + let txid = bitcoind + .call::("sendmany", &[empty_account, amounts_json]) + .unwrap() + .as_str() + .unwrap() + .parse() + .unwrap(); + wait_for_tx(electrs, txid); generate_blocks_and_wait(bitcoind, electrs, 1); } pub fn open_channel( node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, should_announce: bool, electrsd: &ElectrsD, -) { +) -> OutPoint { if should_announce { node_a .open_announced_channel( @@ -513,6 +539,8 @@ pub fn open_channel( let funding_txo_b = expect_channel_pending_event!(node_b, node_a.node_id()); assert_eq!(funding_txo_a, funding_txo_b); wait_for_tx(&electrsd.client, funding_txo_a.txid); + + funding_txo_a } pub(crate) fn do_channel_full_cycle( diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs new file mode 100644 index 0000000000..707b67e888 --- /dev/null +++ b/tests/reorg_test.rs @@ -0,0 +1,193 @@ +mod common; +use bitcoin::Amount; +use ldk_node::payment::{PaymentDirection, PaymentKind}; +use ldk_node::{Event, LightningBalance, PendingSweepBalance}; +use proptest::{prelude::prop, proptest}; +use std::collections::HashMap; + +use crate::common::{ + expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, + premine_and_distribute_funds, random_config, setup_bitcoind_and_electrsd, setup_node, + wait_for_outpoint_spend, TestChainSource, +}; + +proptest! { + #![proptest_config(proptest::test_runner::Config::with_cases(5))] + #[test] + fn reorg_test(reorg_depth in 1..=6usize, force_close in prop::bool::ANY) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + + let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); + let chain_source_electrsd = TestChainSource::Electrum(&electrsd); + let chain_source_esplora = TestChainSource::Esplora(&electrsd); + + macro_rules! config_node { + ($chain_source: expr, $anchor_channels: expr) => {{ + let config_a = random_config($anchor_channels); + let node = setup_node(&$chain_source, config_a, None); + node + }}; + } + let anchor_channels = true; + let nodes = vec![ + config_node!(chain_source_electrsd, anchor_channels), + config_node!(chain_source_bitcoind, anchor_channels), + config_node!(chain_source_esplora, anchor_channels), + ]; + + let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); + macro_rules! reorg { + ($reorg_depth: expr) => {{ + invalidate_blocks(bitcoind, $reorg_depth); + generate_blocks_and_wait(bitcoind, electrs, $reorg_depth); + }}; + } + + let amount_sat = 2_100_000; + let addr_nodes = + nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); + premine_and_distribute_funds(bitcoind, electrs, addr_nodes, Amount::from_sat(amount_sat)); + + macro_rules! sync_wallets { + () => { + nodes.iter().for_each(|node| node.sync_wallets().unwrap()) + }; + } + sync_wallets!(); + nodes.iter().for_each(|node| { + assert_eq!(node.list_balances().spendable_onchain_balance_sats, amount_sat); + assert_eq!(node.list_balances().total_onchain_balance_sats, amount_sat); + }); + + + let mut nodes_funding_tx = HashMap::new(); + let funding_amount_sat = 2_000_000; + for (node, next_node) in nodes.iter().zip(nodes.iter().cycle().skip(1)) { + let funding_txo = open_channel(node, next_node, funding_amount_sat, true, &electrsd); + nodes_funding_tx.insert(node.node_id(), funding_txo); + } + + generate_blocks_and_wait(bitcoind, electrs, 6); + sync_wallets!(); + + reorg!(reorg_depth); + sync_wallets!(); + + macro_rules! collect_channel_ready_events { + ($node:expr, $expected:expr) => {{ + let mut user_channels = HashMap::new(); + for _ in 0..$expected { + match $node.wait_next_event() { + Event::ChannelReady { user_channel_id, counterparty_node_id, .. } => { + $node.event_handled().unwrap(); + user_channels.insert(counterparty_node_id, user_channel_id); + }, + other => panic!("Unexpected event: {:?}", other), + } + } + user_channels + }}; + } + + let mut node_channels_id = HashMap::new(); + for (i, node) in nodes.iter().enumerate() { + assert_eq!( + node + .list_payments_with_filter(|p| p.direction == PaymentDirection::Outbound + && matches!(p.kind, PaymentKind::Onchain { .. })) + .len(), + 1 + ); + + let user_channels = collect_channel_ready_events!(node, 2); + let next_node = nodes.get((i + 1) % nodes.len()).unwrap(); + let prev_node = nodes.get((i + nodes.len() - 1) % nodes.len()).unwrap(); + + assert!(user_channels.get(&Some(next_node.node_id())) != None); + assert!(user_channels.get(&Some(prev_node.node_id())) != None); + + let user_channel_id = + user_channels.get(&Some(next_node.node_id())).expect("Missing user channel for node"); + node_channels_id.insert(node.node_id(), *user_channel_id); + } + + + for (node, next_node) in nodes.iter().zip(nodes.iter().cycle().skip(1)) { + let user_channel_id = node_channels_id.get(&node.node_id()).expect("user channel id not exist"); + let funding = nodes_funding_tx.get(&node.node_id()).expect("Funding tx not exist"); + + if force_close { + node.force_close_channel(&user_channel_id, next_node.node_id(), None).unwrap(); + } else { + node.close_channel(&user_channel_id, next_node.node_id()).unwrap(); + } + + expect_event!(node, ChannelClosed); + expect_event!(next_node, ChannelClosed); + + wait_for_outpoint_spend(electrs, *funding); + } + + reorg!(reorg_depth); + sync_wallets!(); + + generate_blocks_and_wait(bitcoind, electrs, 1); + sync_wallets!(); + + if force_close { + nodes.iter().for_each(|node| { + node.sync_wallets().unwrap(); + // If there is no more balance, there is nothing to process here. + if node.list_balances().lightning_balances.len() < 1 { + return; + } + match node.list_balances().lightning_balances[0] { + LightningBalance::ClaimableAwaitingConfirmations { + confirmation_height, + .. + } => { + let cur_height = node.status().current_best_block.height; + let blocks_to_go = confirmation_height - cur_height; + generate_blocks_and_wait(bitcoind, electrs, blocks_to_go as usize); + node.sync_wallets().unwrap(); + }, + _ => panic!("Unexpected balance state for node_hub!"), + } + + assert!(node.list_balances().lightning_balances.len() < 2); + assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); + match node.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::BroadcastAwaitingConfirmation { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + + generate_blocks_and_wait(&bitcoind, electrs, 1); + node.sync_wallets().unwrap(); + assert!(node.list_balances().lightning_balances.len() < 2); + assert!(node.list_balances().pending_balances_from_channel_closures.len() > 0); + match node.list_balances().pending_balances_from_channel_closures[0] { + PendingSweepBalance::AwaitingThresholdConfirmations { .. } => {}, + _ => panic!("Unexpected balance state!"), + } + }); + } + + generate_blocks_and_wait(bitcoind, electrs, 6); + sync_wallets!(); + + reorg!(reorg_depth); + sync_wallets!(); + + let fee_sat = 7000; + // Check balance after close channel + nodes.iter().for_each(|node| { + assert!(node.list_balances().spendable_onchain_balance_sats > amount_sat - fee_sat); + assert!(node.list_balances().spendable_onchain_balance_sats < amount_sat); + + assert_eq!(node.list_balances().total_anchor_channels_reserve_sats, 0); + assert!(node.list_balances().lightning_balances.is_empty()); + + assert_eq!(node.next_event(), None); + }); + } +} From 48790021ae3c6315e1d3bf36e3a457e151cb4105 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 19 May 2025 16:27:28 +0200 Subject: [PATCH 123/177] Introduce `Runtime` object allowng to detect outer runtime context Instead of holding an `Arc>>` and dealing with stuff like `tokio::task::block_in_place` at all callsites, we introduce a `Runtime` object that takes care of the state transitions, and allows to detect and reuse an outer runtime context. We also adjust the `with_runtime` API to take a `tokio::runtime::Handle` rather than an `Arc`. --- bindings/ldk_node.udl | 7 +- src/builder.rs | 44 +++++++++++- src/chain/electrum.rs | 11 ++- src/chain/mod.rs | 3 +- src/event.rs | 69 +++++++++---------- src/gossip.rs | 33 +++------ src/lib.rs | 133 +++++++++++++++---------------------- src/liquidity.rs | 53 ++++++--------- src/payment/bolt11.rs | 87 ++++++++++++------------ src/payment/bolt12.rs | 24 ++++--- src/payment/onchain.rs | 14 ++-- src/payment/spontaneous.rs | 14 ++-- src/runtime.rs | 72 ++++++++++++++++++++ 13 files changed, 308 insertions(+), 256 deletions(-) create mode 100644 src/runtime.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 26480ca4b5..f3560ec09c 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -64,7 +64,7 @@ dictionary LogRecord { [Trait, WithForeign] interface LogWriter { - void log(LogRecord record); + void log(LogRecord record); }; interface Builder { @@ -161,8 +161,8 @@ interface Node { [Enum] interface Bolt11InvoiceDescription { - Hash(string hash); - Direct(string description); + Hash(string hash); + Direct(string description); }; interface Bolt11Payment { @@ -335,6 +335,7 @@ enum BuildError { "InvalidListeningAddresses", "InvalidAnnouncementAddresses", "InvalidNodeAlias", + "RuntimeSetupFailed", "ReadFailed", "WriteFailed", "StoragePathAccessFailed", diff --git a/src/builder.rs b/src/builder.rs index 1152f18c3b..729cefe1bc 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -28,6 +28,7 @@ use crate::liquidity::{ use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::peer_store::PeerStore; +use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; use crate::types::{ ChainMonitor, ChannelManager, DynStore, GossipSync, Graph, KeysManager, MessageRouter, @@ -168,6 +169,8 @@ pub enum BuildError { InvalidAnnouncementAddresses, /// The provided alias is invalid. InvalidNodeAlias, + /// An attempt to setup a runtime has failed. + RuntimeSetupFailed, /// We failed to read data from the [`KVStore`]. /// /// [`KVStore`]: lightning::util::persist::KVStore @@ -205,6 +208,7 @@ impl fmt::Display for BuildError { Self::InvalidAnnouncementAddresses => { write!(f, "Given announcement addresses are invalid.") }, + Self::RuntimeSetupFailed => write!(f, "Failed to setup a runtime."), Self::ReadFailed => write!(f, "Failed to read from store."), Self::WriteFailed => write!(f, "Failed to write to store."), Self::StoragePathAccessFailed => write!(f, "Failed to access the given storage path."), @@ -236,6 +240,7 @@ pub struct NodeBuilder { gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, + runtime_handle: Option, } impl NodeBuilder { @@ -252,6 +257,7 @@ impl NodeBuilder { let gossip_source_config = None; let liquidity_source_config = None; let log_writer_config = None; + let runtime_handle = None; Self { config, entropy_source_config, @@ -259,9 +265,20 @@ impl NodeBuilder { gossip_source_config, liquidity_source_config, log_writer_config, + runtime_handle, } } + /// Configures the [`Node`] instance to (re-)use a specific `tokio` runtime. + /// + /// If not provided, the node will spawn its own runtime or reuse any outer runtime context it + /// can detect. + #[cfg_attr(feature = "uniffi", allow(dead_code))] + pub fn set_runtime(&mut self, runtime_handle: tokio::runtime::Handle) -> &mut Self { + self.runtime_handle = Some(runtime_handle); + self + } + /// Configures the [`Node`] instance to source its wallet entropy from a seed file on disk. /// /// If the given file does not exist a new random seed file will be generated and @@ -650,6 +667,15 @@ impl NodeBuilder { ) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; + let runtime = if let Some(handle) = self.runtime_handle.as_ref() { + Arc::new(Runtime::with_handle(handle.clone())) + } else { + Arc::new(Runtime::new().map_err(|e| { + log_error!(logger, "Failed to setup tokio runtime: {}", e); + BuildError::RuntimeSetupFailed + })?) + }; + let seed_bytes = seed_bytes_from_config( &self.config, self.entropy_source_config.as_ref(), @@ -678,6 +704,7 @@ impl NodeBuilder { self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), seed_bytes, + runtime, logger, Arc::new(vss_store), ) @@ -687,6 +714,15 @@ impl NodeBuilder { pub fn build_with_store(&self, kv_store: Arc) -> Result { let logger = setup_logger(&self.log_writer_config, &self.config)?; + let runtime = if let Some(handle) = self.runtime_handle.as_ref() { + Arc::new(Runtime::with_handle(handle.clone())) + } else { + Arc::new(Runtime::new().map_err(|e| { + log_error!(logger, "Failed to setup tokio runtime: {}", e); + BuildError::RuntimeSetupFailed + })?) + }; + let seed_bytes = seed_bytes_from_config( &self.config, self.entropy_source_config.as_ref(), @@ -700,6 +736,7 @@ impl NodeBuilder { self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), seed_bytes, + runtime, logger, kv_store, ) @@ -1049,7 +1086,7 @@ fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - logger: Arc, kv_store: Arc, + runtime: Arc, logger: Arc, kv_store: Arc, ) -> Result { optionally_install_rustls_cryptoprovider(); @@ -1241,8 +1278,6 @@ fn build_with_store_internal( }, }; - let runtime = Arc::new(RwLock::new(None)); - // Initialize the ChainMonitor let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( Some(Arc::clone(&chain_source)), @@ -1637,6 +1672,8 @@ fn build_with_store_internal( let background_tasks = Mutex::new(None); let cancellable_background_tasks = Mutex::new(None); + let is_running = Arc::new(RwLock::new(false)); + Ok(Node { runtime, stop_sender, @@ -1664,6 +1701,7 @@ fn build_with_store_internal( scorer, peer_store, payment_store, + is_running, is_listening, node_metrics, }) diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index abbb758dde..b6d37409ba 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -18,6 +18,7 @@ use crate::fee_estimator::{ }; use crate::io::utils::write_node_metrics; use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::NodeMetrics; @@ -86,7 +87,7 @@ impl ElectrumChainSource { } } - pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { + pub(super) fn start(&self, runtime: Arc) -> Result<(), Error> { self.electrum_runtime_status.write().unwrap().start( self.server_url.clone(), Arc::clone(&runtime), @@ -339,7 +340,7 @@ impl ElectrumRuntimeStatus { } pub(super) fn start( - &mut self, server_url: String, runtime: Arc, config: Arc, + &mut self, server_url: String, runtime: Arc, config: Arc, logger: Arc, ) -> Result<(), Error> { match self { @@ -403,15 +404,14 @@ struct ElectrumRuntimeClient { electrum_client: Arc, bdk_electrum_client: Arc>, tx_sync: Arc>>, - runtime: Arc, + runtime: Arc, config: Arc, logger: Arc, } impl ElectrumRuntimeClient { fn new( - server_url: String, runtime: Arc, config: Arc, - logger: Arc, + server_url: String, runtime: Arc, config: Arc, logger: Arc, ) -> Result { let electrum_config = ElectrumConfigBuilder::new() .retry(ELECTRUM_CLIENT_NUM_RETRIES) @@ -544,7 +544,6 @@ impl ElectrumRuntimeClient { let spawn_fut = self.runtime.spawn_blocking(move || electrum_client.transaction_broadcast(&tx)); - let timeout_fut = tokio::time::timeout(Duration::from_secs(TX_BROADCAST_TIMEOUT_SECS), spawn_fut); diff --git a/src/chain/mod.rs b/src/chain/mod.rs index a4ab2c76b5..f3a29e9843 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -19,6 +19,7 @@ use crate::config::{ use crate::fee_estimator::OnchainFeeEstimator; use crate::io::utils::write_node_metrics; use crate::logger::{log_debug, log_info, log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; @@ -185,7 +186,7 @@ impl ChainSource { Self { kind, tx_broadcaster, logger } } - pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { + pub(crate) fn start(&self, runtime: Arc) -> Result<(), Error> { match &self.kind { ChainSourceKind::Electrum(electrum_chain_source) => { electrum_chain_source.start(runtime)? diff --git a/src/event.rs b/src/event.rs index 22848bec1c..ae81f50e96 100644 --- a/src/event.rs +++ b/src/event.rs @@ -29,6 +29,8 @@ use crate::io::{ }; use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +use crate::runtime::Runtime; + use lightning::events::bump_transaction::BumpTransactionEvent; use lightning::events::{ClosureReason, PaymentPurpose, ReplayEvent}; use lightning::events::{Event as LdkEvent, PaymentFailureReason}; @@ -53,7 +55,7 @@ use core::future::Future; use core::task::{Poll, Waker}; use std::collections::VecDeque; use std::ops::Deref; -use std::sync::{Arc, Condvar, Mutex, RwLock}; +use std::sync::{Arc, Condvar, Mutex}; use std::time::Duration; /// An event emitted by [`Node`], which should be handled by the user. @@ -451,7 +453,7 @@ where liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>, - runtime: Arc>>>, + runtime: Arc, logger: L, config: Arc, } @@ -466,8 +468,8 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc, peer_store: Arc>, - runtime: Arc>>>, logger: L, config: Arc, + payment_store: Arc, peer_store: Arc>, runtime: Arc, + logger: L, config: Arc, ) -> Self { Self { event_queue, @@ -1049,17 +1051,14 @@ where let forwarding_channel_manager = self.channel_manager.clone(); let min = time_forwardable.as_millis() as u64; - let runtime_lock = self.runtime.read().unwrap(); - debug_assert!(runtime_lock.is_some()); + let future = async move { + let millis_to_sleep = thread_rng().gen_range(min..min * 5) as u64; + tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; - if let Some(runtime) = runtime_lock.as_ref() { - runtime.spawn(async move { - let millis_to_sleep = thread_rng().gen_range(min..min * 5) as u64; - tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; + forwarding_channel_manager.process_pending_htlc_forwards(); + }; - forwarding_channel_manager.process_pending_htlc_forwards(); - }); - } + self.runtime.spawn(future); }, LdkEvent::SpendableOutputs { outputs, channel_id } => { match self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None) { @@ -1421,31 +1420,27 @@ where debug_assert!(false, "We currently don't handle BOLT12 invoices manually, so this event should never be emitted."); }, LdkEvent::ConnectionNeeded { node_id, addresses } => { - let runtime_lock = self.runtime.read().unwrap(); - debug_assert!(runtime_lock.is_some()); - - if let Some(runtime) = runtime_lock.as_ref() { - let spawn_logger = self.logger.clone(); - let spawn_cm = Arc::clone(&self.connection_manager); - runtime.spawn(async move { - for addr in &addresses { - match spawn_cm.connect_peer_if_necessary(node_id, addr.clone()).await { - Ok(()) => { - return; - }, - Err(e) => { - log_error!( - spawn_logger, - "Failed to establish connection to peer {}@{}: {}", - node_id, - addr, - e - ); - }, - } + let spawn_logger = self.logger.clone(); + let spawn_cm = Arc::clone(&self.connection_manager); + let future = async move { + for addr in &addresses { + match spawn_cm.connect_peer_if_necessary(node_id, addr.clone()).await { + Ok(()) => { + return; + }, + Err(e) => { + log_error!( + spawn_logger, + "Failed to establish connection to peer {}@{}: {}", + node_id, + addr, + e + ); + }, } - }); - } + } + }; + self.runtime.spawn(future); }, LdkEvent::BumpTransaction(bte) => { match bte { diff --git a/src/gossip.rs b/src/gossip.rs index a8a6e3831f..1185f07182 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -7,7 +7,8 @@ use crate::chain::ChainSource; use crate::config::RGS_SYNC_TIMEOUT_SECS; -use crate::logger::{log_error, log_trace, LdkLogger, Logger}; +use crate::logger::{log_trace, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{GossipSync, Graph, P2PGossipSync, PeerManager, RapidGossipSync, UtxoLookup}; use crate::Error; @@ -15,13 +16,12 @@ use lightning_block_sync::gossip::{FutureSpawner, GossipVerifier}; use std::future::Future; use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::{Arc, RwLock}; +use std::sync::Arc; use std::time::Duration; pub(crate) enum GossipSource { P2PNetwork { gossip_sync: Arc, - logger: Arc, }, RapidGossipSync { gossip_sync: Arc, @@ -38,7 +38,7 @@ impl GossipSource { None::>, Arc::clone(&logger), )); - Self::P2PNetwork { gossip_sync, logger } + Self::P2PNetwork { gossip_sync } } pub fn new_rgs( @@ -63,12 +63,12 @@ impl GossipSource { pub(crate) fn set_gossip_verifier( &self, chain_source: Arc, peer_manager: Arc, - runtime: Arc>>>, + runtime: Arc, ) { match self { - Self::P2PNetwork { gossip_sync, logger } => { + Self::P2PNetwork { gossip_sync } => { if let Some(utxo_source) = chain_source.as_utxo_source() { - let spawner = RuntimeSpawner::new(Arc::clone(&runtime), Arc::clone(&logger)); + let spawner = RuntimeSpawner::new(Arc::clone(&runtime)); let gossip_verifier = Arc::new(GossipVerifier::new( utxo_source, spawner, @@ -133,28 +133,17 @@ impl GossipSource { } pub(crate) struct RuntimeSpawner { - runtime: Arc>>>, - logger: Arc, + runtime: Arc, } impl RuntimeSpawner { - pub(crate) fn new( - runtime: Arc>>>, logger: Arc, - ) -> Self { - Self { runtime, logger } + pub(crate) fn new(runtime: Arc) -> Self { + Self { runtime } } } impl FutureSpawner for RuntimeSpawner { fn spawn + Send + 'static>(&self, future: T) { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { - log_error!(self.logger, "Tried spawing a future while the runtime wasn't available. This should never happen."); - debug_assert!(false, "Tried spawing a future while the runtime wasn't available. This should never happen."); - return; - } - - let runtime = rt_lock.as_ref().unwrap(); - runtime.spawn(future); + self.runtime.spawn(future); } } diff --git a/src/lib.rs b/src/lib.rs index a3cce07521..cc5e383a1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,7 @@ pub mod logger; mod message_handler; pub mod payment; mod peer_store; +mod runtime; mod sweep; mod tx_broadcaster; mod types; @@ -105,6 +106,7 @@ pub use lightning; pub use lightning_invoice; pub use lightning_liquidity; pub use lightning_types; +pub use tokio; pub use vss_client; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; @@ -141,6 +143,7 @@ use payment::{ UnifiedQrPayment, }; use peer_store::{PeerInfo, PeerStore}; +use runtime::Runtime; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, @@ -176,7 +179,7 @@ uniffi::include_scaffolding!("ldk_node"); /// /// Needs to be initialized and instantiated through [`Builder::build`]. pub struct Node { - runtime: Arc>>>, + runtime: Arc, stop_sender: tokio::sync::watch::Sender<()>, background_processor_task: Mutex>>, background_tasks: Mutex>>, @@ -202,6 +205,7 @@ pub struct Node { scorer: Arc>, peer_store: Arc>>, payment_store: Arc, + is_running: Arc>, is_listening: Arc, node_metrics: Arc>, } @@ -210,33 +214,21 @@ impl Node { /// Starts the necessary background tasks, such as handling events coming from user input, /// LDK/BDK, and the peer-to-peer network. /// - /// After this returns, the [`Node`] instance can be controlled via the provided API methods in - /// a thread-safe manner. - pub fn start(&self) -> Result<(), Error> { - let runtime = - Arc::new(tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap()); - self.start_with_runtime(runtime) - } - - /// Starts the necessary background tasks (such as handling events coming from user input, - /// LDK/BDK, and the peer-to-peer network) on the the given `runtime`. - /// - /// This allows to have LDK Node reuse an outer pre-existing runtime, e.g., to avoid stacking Tokio - /// runtime contexts. + /// This will try to auto-detect an outer pre-existing runtime, e.g., to avoid stacking Tokio + /// runtime contexts. Note we require the outer runtime to be of the `multithreaded` flavor. /// /// After this returns, the [`Node`] instance can be controlled via the provided API methods in /// a thread-safe manner. - pub fn start_with_runtime(&self, runtime: Arc) -> Result<(), Error> { + pub fn start(&self) -> Result<(), Error> { // Acquire a run lock and hold it until we're setup. - let mut runtime_lock = self.runtime.write().unwrap(); - if runtime_lock.is_some() { - // We're already running. + let mut is_running_lock = self.is_running.write().unwrap(); + if *is_running_lock { return Err(Error::AlreadyRunning); } let mut background_tasks = tokio::task::JoinSet::new(); let mut cancellable_background_tasks = tokio::task::JoinSet::new(); - let runtime_handle = runtime.handle(); + let runtime_handle = self.runtime.handle(); log_info!( self.logger, @@ -246,17 +238,14 @@ impl Node { ); // Start up any runtime-dependant chain sources (e.g. Electrum) - self.chain_source.start(Arc::clone(&runtime)).map_err(|e| { + self.chain_source.start(Arc::clone(&self.runtime)).map_err(|e| { log_error!(self.logger, "Failed to start chain syncing: {}", e); e })?; // Block to ensure we update our fee rate cache once on startup let chain_source = Arc::clone(&self.chain_source); - let runtime_ref = &runtime; - tokio::task::block_in_place(move || { - runtime_ref.block_on(async move { chain_source.update_fee_rate_estimates().await }) - })?; + self.runtime.block_on(async move { chain_source.update_fee_rate_estimates().await })?; // Spawn background task continuously syncing onchain, lightning, and fee rate cache. let stop_sync_receiver = self.stop_sender.subscribe(); @@ -574,7 +563,7 @@ impl Node { }) }; - let handle = runtime.spawn(async move { + let handle = self.runtime.spawn(async move { process_events_async( background_persister, |e| background_event_handler.handle_event(e), @@ -621,8 +610,6 @@ impl Node { ); } - *runtime_lock = Some(runtime); - debug_assert!(self.background_tasks.lock().unwrap().is_none()); *self.background_tasks.lock().unwrap() = Some(background_tasks); @@ -630,6 +617,7 @@ impl Node { *self.cancellable_background_tasks.lock().unwrap() = Some(cancellable_background_tasks); log_info!(self.logger, "Startup complete."); + *is_running_lock = true; Ok(()) } @@ -637,9 +625,10 @@ impl Node { /// /// After this returns most API methods will return [`Error::NotRunning`]. pub fn stop(&self) -> Result<(), Error> { - let runtime = self.runtime.write().unwrap().take().ok_or(Error::NotRunning)?; - #[cfg(tokio_unstable)] - let metrics_runtime = Arc::clone(&runtime); + let mut is_running_lock = self.is_running.write().unwrap(); + if !*is_running_lock { + return Err(Error::NotRunning); + } log_info!(self.logger, "Shutting down LDK Node with node ID {}...", self.node_id()); @@ -661,10 +650,10 @@ impl Node { // Cancel cancellable background tasks if let Some(mut tasks) = self.cancellable_background_tasks.lock().unwrap().take() { - let runtime_2 = Arc::clone(&runtime); + let runtime_handle = self.runtime.handle(); tasks.abort_all(); tokio::task::block_in_place(move || { - runtime_2.block_on(async { while let Some(_) = tasks.join_next().await {} }) + runtime_handle.block_on(async { while let Some(_) = tasks.join_next().await {} }) }); } else { debug_assert!(false, "Expected some cancellable background tasks"); @@ -679,10 +668,10 @@ impl Node { log_debug!(self.logger, "Stopped chain sources."); // Wait until non-cancellable background tasks (mod LDK's background processor) are done. - let runtime_3 = Arc::clone(&runtime); + let runtime_handle = self.runtime.handle(); if let Some(mut tasks) = self.background_tasks.lock().unwrap().take() { tokio::task::block_in_place(move || { - runtime_3.block_on(async { + runtime_handle.block_on(async { loop { let timeout_fut = tokio::time::timeout( Duration::from_secs(BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS), @@ -724,7 +713,7 @@ impl Node { { let abort_handle = background_processor_task.abort_handle(); let timeout_res = tokio::task::block_in_place(move || { - runtime.block_on(async { + self.runtime.block_on(async { tokio::time::timeout( Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), background_processor_task, @@ -757,20 +746,22 @@ impl Node { #[cfg(tokio_unstable)] { + let runtime_handle = self.runtime.handle(); log_trace!( self.logger, "Active runtime tasks left prior to shutdown: {}", - metrics_runtime.metrics().active_tasks_count() + runtime_handle.metrics().active_tasks_count() ); } log_info!(self.logger, "Shutdown complete."); + *is_running_lock = false; Ok(()) } /// Returns the status of the [`Node`]. pub fn status(&self) -> NodeStatus { - let is_running = self.runtime.read().unwrap().is_some(); + let is_running = *self.is_running.read().unwrap(); let is_listening = self.is_listening.load(Ordering::Acquire); let current_best_block = self.channel_manager.current_best_block().into(); let locked_node_metrics = self.node_metrics.read().unwrap(); @@ -891,6 +882,7 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -908,6 +900,7 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -918,9 +911,9 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn bolt12_payment(&self) -> Bolt12Payment { Bolt12Payment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -931,9 +924,9 @@ impl Node { #[cfg(feature = "uniffi")] pub fn bolt12_payment(&self) -> Arc { Arc::new(Bolt12Payment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -942,11 +935,11 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn spontaneous_payment(&self) -> SpontaneousPayment { SpontaneousPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -955,11 +948,11 @@ impl Node { #[cfg(feature = "uniffi")] pub fn spontaneous_payment(&self) -> Arc { Arc::new(SpontaneousPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.channel_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.payment_store), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -968,10 +961,10 @@ impl Node { #[cfg(not(feature = "uniffi"))] pub fn onchain_payment(&self) -> OnchainPayment { OnchainPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.wallet), Arc::clone(&self.channel_manager), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), ) } @@ -980,10 +973,10 @@ impl Node { #[cfg(feature = "uniffi")] pub fn onchain_payment(&self) -> Arc { Arc::new(OnchainPayment::new( - Arc::clone(&self.runtime), Arc::clone(&self.wallet), Arc::clone(&self.channel_manager), Arc::clone(&self.config), + Arc::clone(&self.is_running), Arc::clone(&self.logger), )) } @@ -1061,11 +1054,9 @@ impl Node { pub fn connect( &self, node_id: PublicKey, address: SocketAddress, persist: bool, ) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let runtime = rt_lock.as_ref().unwrap(); let peer_info = PeerInfo { node_id, address }; @@ -1075,10 +1066,8 @@ impl Node { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to peer {}@{}. ", peer_info.node_id, peer_info.address); @@ -1095,8 +1084,7 @@ impl Node { /// Will also remove the peer from the peer store, i.e., after this has been called we won't /// try to reconnect on restart. pub fn disconnect(&self, counterparty_node_id: PublicKey) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -1118,11 +1106,9 @@ impl Node { push_to_counterparty_msat: Option, channel_config: Option, announce_for_forwarding: bool, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } - let runtime = rt_lock.as_ref().unwrap(); let peer_info = PeerInfo { node_id, address }; @@ -1146,10 +1132,8 @@ impl Node { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; // Fail if we have less than the channel value + anchor reserve available (if applicable). @@ -1298,8 +1282,7 @@ impl Node { /// /// [`EsploraSyncConfig::background_sync_config`]: crate::config::EsploraSyncConfig::background_sync_config pub fn sync_wallets(&self) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -1307,24 +1290,16 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - tokio::task::block_in_place(move || { - tokio::runtime::Builder::new_multi_thread().enable_all().build().unwrap().block_on( - async move { - if chain_source.is_transaction_based() { - chain_source.update_fee_rate_estimates().await?; - chain_source - .sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper) - .await?; - chain_source.sync_onchain_wallet().await?; - } else { - chain_source.update_fee_rate_estimates().await?; - chain_source - .poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper) - .await?; - } - Ok(()) - }, - ) + self.runtime.block_on(async move { + if chain_source.is_transaction_based() { + chain_source.update_fee_rate_estimates().await?; + chain_source.sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper).await?; + chain_source.sync_onchain_wallet().await?; + } else { + chain_source.update_fee_rate_estimates().await?; + chain_source.poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper).await?; + } + Ok(()) }) } diff --git a/src/liquidity.rs b/src/liquidity.rs index a4516edd07..9b103ee821 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -10,6 +10,7 @@ use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::runtime::Runtime; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; @@ -1388,7 +1389,7 @@ pub(crate) struct LSPS2BuyResponse { /// [`Bolt11Payment::receive_via_jit_channel`]: crate::payment::Bolt11Payment::receive_via_jit_channel #[derive(Clone)] pub struct LSPS1Liquidity { - runtime: Arc>>>, + runtime: Arc, wallet: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, @@ -1397,7 +1398,7 @@ pub struct LSPS1Liquidity { impl LSPS1Liquidity { pub(crate) fn new( - runtime: Arc>>>, wallet: Arc, + runtime: Arc, wallet: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, logger: Arc, ) -> Self { @@ -1418,19 +1419,14 @@ impl LSPS1Liquidity { let (lsp_node_id, lsp_address) = liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); - let con_node_id = lsp_node_id; let con_addr = lsp_address.clone(); let con_cm = Arc::clone(&self.connection_manager); // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to LSP {}@{}. ", lsp_node_id, lsp_address); @@ -1438,18 +1434,16 @@ impl LSPS1Liquidity { let refund_address = self.wallet.get_new_address()?; let liquidity_source = Arc::clone(&liquidity_source); - let response = tokio::task::block_in_place(move || { - runtime.block_on(async move { - liquidity_source - .lsps1_request_channel( - lsp_balance_sat, - client_balance_sat, - channel_expiry_blocks, - announce_channel, - refund_address, - ) - .await - }) + let response = self.runtime.block_on(async move { + liquidity_source + .lsps1_request_channel( + lsp_balance_sat, + client_balance_sat, + channel_expiry_blocks, + announce_channel, + refund_address, + ) + .await })?; Ok(response) @@ -1463,27 +1457,20 @@ impl LSPS1Liquidity { let (lsp_node_id, lsp_address) = liquidity_source.get_lsps1_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); - let con_node_id = lsp_node_id; let con_addr = lsp_address.clone(); let con_cm = Arc::clone(&self.connection_manager); // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; let liquidity_source = Arc::clone(&liquidity_source); - let response = tokio::task::block_in_place(move || { - runtime - .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await }) - })?; - + let response = self + .runtime + .block_on(async move { liquidity_source.lsps1_check_order_status(order_id).await })?; Ok(response) } } diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 817a428bdf..389c818c83 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -22,6 +22,7 @@ use crate::payment::store::{ }; use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; +use crate::runtime::Runtime; use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::bolt11_payment; @@ -57,24 +58,24 @@ type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription; /// [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md /// [`Node::bolt11_payment`]: crate::Node::bolt11_payment pub struct Bolt11Payment { - runtime: Arc>>>, + runtime: Arc, channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, config: Arc, + is_running: Arc>, logger: Arc, } impl Bolt11Payment { pub(crate) fn new( - runtime: Arc>>>, - channel_manager: Arc, + runtime: Arc, channel_manager: Arc, connection_manager: Arc>>, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>>, - config: Arc, logger: Arc, + config: Arc, is_running: Arc>, logger: Arc, ) -> Self { Self { runtime, @@ -84,6 +85,7 @@ impl Bolt11Payment { payment_store, peer_store, config, + is_running, logger, } } @@ -95,12 +97,12 @@ impl Bolt11Payment { pub fn send( &self, invoice: &Bolt11Invoice, sending_parameters: Option, ) -> Result { - let invoice = maybe_deref(invoice); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let invoice = maybe_deref(invoice); + let (payment_hash, recipient_onion, mut route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); Error::InvalidInvoice @@ -204,12 +206,12 @@ impl Bolt11Payment { &self, invoice: &Bolt11Invoice, amount_msat: u64, sending_parameters: Option, ) -> Result { - let invoice = maybe_deref(invoice); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let invoice = maybe_deref(invoice); + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( @@ -619,9 +621,6 @@ impl Bolt11Payment { let (node_id, address) = liquidity_source.get_lsps2_lsp_details().ok_or(Error::LiquiditySourceUnavailable)?; - let rt_lock = self.runtime.read().unwrap(); - let runtime = rt_lock.as_ref().unwrap(); - let peer_info = PeerInfo { node_id, address }; let con_node_id = peer_info.node_id; @@ -630,39 +629,35 @@ impl Bolt11Payment { // We need to use our main runtime here as a local runtime might not be around to poll // connection futures going forward. - tokio::task::block_in_place(move || { - runtime.block_on(async move { - con_cm.connect_peer_if_necessary(con_node_id, con_addr).await - }) + self.runtime.block_on(async move { + con_cm.connect_peer_if_necessary(con_node_id, con_addr).await })?; log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address); let liquidity_source = Arc::clone(&liquidity_source); let (invoice, lsp_total_opening_fee, lsp_prop_opening_fee) = - tokio::task::block_in_place(move || { - runtime.block_on(async move { - if let Some(amount_msat) = amount_msat { - liquidity_source - .lsps2_receive_to_jit_channel( - amount_msat, - description, - expiry_secs, - max_total_lsp_fee_limit_msat, - ) - .await - .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) - } else { - liquidity_source - .lsps2_receive_variable_amount_to_jit_channel( - description, - expiry_secs, - max_proportional_lsp_fee_limit_ppm_msat, - ) - .await - .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) - } - }) + self.runtime.block_on(async move { + if let Some(amount_msat) = amount_msat { + liquidity_source + .lsps2_receive_to_jit_channel( + amount_msat, + description, + expiry_secs, + max_total_lsp_fee_limit_msat, + ) + .await + .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) + } else { + liquidity_source + .lsps2_receive_variable_amount_to_jit_channel( + description, + expiry_secs, + max_proportional_lsp_fee_limit_ppm_msat, + ) + .await + .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) + } })?; // Register payment in payment store. @@ -712,12 +707,12 @@ impl Bolt11Payment { /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { - let invoice = maybe_deref(invoice); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let invoice = maybe_deref(invoice); + let (_payment_hash, _recipient_onion, route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); Error::InvalidInvoice @@ -745,12 +740,12 @@ impl Bolt11Payment { pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, ) -> Result<(), Error> { - let invoice = maybe_deref(invoice); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let invoice = maybe_deref(invoice); + let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index b9efa3241d..8e10b9f4f7 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -49,19 +49,18 @@ type Refund = Arc; /// [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md /// [`Node::bolt12_payment`]: crate::Node::bolt12_payment pub struct Bolt12Payment { - runtime: Arc>>>, channel_manager: Arc, payment_store: Arc, + is_running: Arc>, logger: Arc, } impl Bolt12Payment { pub(crate) fn new( - runtime: Arc>>>, channel_manager: Arc, payment_store: Arc, - logger: Arc, + is_running: Arc>, logger: Arc, ) -> Self { - Self { runtime, channel_manager, payment_store, logger } + Self { channel_manager, payment_store, is_running, logger } } /// Send a payment given an offer. @@ -73,11 +72,12 @@ impl Bolt12Payment { pub fn send( &self, offer: &Offer, quantity: Option, payer_note: Option, ) -> Result { - let offer = maybe_deref(offer); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + + let offer = maybe_deref(offer); + let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); @@ -175,12 +175,12 @@ impl Bolt12Payment { pub fn send_using_amount( &self, offer: &Offer, amount_msat: u64, quantity: Option, payer_note: Option, ) -> Result { - let offer = maybe_deref(offer); - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } + let offer = maybe_deref(offer); + let mut random_bytes = [0u8; 32]; rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); @@ -346,6 +346,10 @@ impl Bolt12Payment { /// [`Refund`]: lightning::offers::refund::Refund /// [`Bolt12Invoice`]: lightning::offers::invoice::Bolt12Invoice pub fn request_refund_payment(&self, refund: &Refund) -> Result { + if !*self.is_running.read().unwrap() { + return Err(Error::NotRunning); + } + let refund = maybe_deref(refund); let invoice = self.channel_manager.request_refund_payment(&refund).map_err(|e| { log_error!(self.logger, "Failed to request refund payment: {:?}", e); diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 046d66c693..2614e55cec 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -41,19 +41,19 @@ macro_rules! maybe_map_fee_rate_opt { /// /// [`Node::onchain_payment`]: crate::Node::onchain_payment pub struct OnchainPayment { - runtime: Arc>>>, wallet: Arc, channel_manager: Arc, config: Arc, + is_running: Arc>, logger: Arc, } impl OnchainPayment { pub(crate) fn new( - runtime: Arc>>>, wallet: Arc, - channel_manager: Arc, config: Arc, logger: Arc, + wallet: Arc, channel_manager: Arc, config: Arc, + is_running: Arc>, logger: Arc, ) -> Self { - Self { runtime, wallet, channel_manager, config, logger } + Self { wallet, channel_manager, config, is_running, logger } } /// Retrieve a new on-chain/funding address. @@ -75,8 +75,7 @@ impl OnchainPayment { pub fn send_to_address( &self, address: &bitcoin::Address, amount_sats: u64, fee_rate: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -106,8 +105,7 @@ impl OnchainPayment { pub fn send_all_to_address( &self, address: &bitcoin::Address, retain_reserves: bool, fee_rate: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index a7e7876d75..3e48fd090f 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -33,21 +33,21 @@ const LDK_DEFAULT_FINAL_CLTV_EXPIRY_DELTA: u32 = 144; /// /// [`Node::spontaneous_payment`]: crate::Node::spontaneous_payment pub struct SpontaneousPayment { - runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, payment_store: Arc, config: Arc, + is_running: Arc>, logger: Arc, } impl SpontaneousPayment { pub(crate) fn new( - runtime: Arc>>>, channel_manager: Arc, keys_manager: Arc, - payment_store: Arc, config: Arc, logger: Arc, + payment_store: Arc, config: Arc, is_running: Arc>, + logger: Arc, ) -> Self { - Self { runtime, channel_manager, keys_manager, payment_store, config, logger } + Self { channel_manager, keys_manager, payment_store, config, is_running, logger } } /// Send a spontaneous aka. "keysend", payment. @@ -88,8 +88,7 @@ impl SpontaneousPayment { &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, custom_tlvs: Option>, preimage: Option, ) -> Result { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } @@ -198,8 +197,7 @@ impl SpontaneousPayment { /// /// [`Bolt11Payment::send_probes`]: crate::payment::Bolt11Payment pub fn send_probes(&self, amount_msat: u64, node_id: PublicKey) -> Result<(), Error> { - let rt_lock = self.runtime.read().unwrap(); - if rt_lock.is_none() { + if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } diff --git a/src/runtime.rs b/src/runtime.rs new file mode 100644 index 0000000000..4c1241165f --- /dev/null +++ b/src/runtime.rs @@ -0,0 +1,72 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +use tokio::task::JoinHandle; + +use std::future::Future; + +pub(crate) struct Runtime { + mode: RuntimeMode, +} + +impl Runtime { + pub fn new() -> Result { + let mode = match tokio::runtime::Handle::try_current() { + Ok(handle) => RuntimeMode::Handle(handle), + Err(_) => { + let rt = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + RuntimeMode::Owned(rt) + }, + }; + Ok(Self { mode }) + } + + pub fn with_handle(handle: tokio::runtime::Handle) -> Self { + let mode = RuntimeMode::Handle(handle); + Self { mode } + } + + pub fn spawn(&self, future: F) -> JoinHandle + where + F: Future + Send + 'static, + F::Output: Send + 'static, + { + let handle = self.handle(); + handle.spawn(future) + } + + pub fn spawn_blocking(&self, func: F) -> JoinHandle + where + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, + { + let handle = self.handle(); + handle.spawn_blocking(func) + } + + pub fn block_on(&self, future: F) -> F::Output { + // While we generally decided not to overthink via which call graph users would enter our + // runtime context, we'd still try to reuse whatever current context would be present + // during `block_on`, as this is the context `block_in_place` would operate on. So we try + // to detect the outer context here, and otherwise use whatever was set during + // initialization. + let handle = tokio::runtime::Handle::try_current().unwrap_or(self.handle().clone()); + tokio::task::block_in_place(move || handle.block_on(future)) + } + + pub fn handle(&self) -> &tokio::runtime::Handle { + match &self.mode { + RuntimeMode::Owned(rt) => rt.handle(), + RuntimeMode::Handle(handle) => handle, + } + } +} + +enum RuntimeMode { + Owned(tokio::runtime::Runtime), + Handle(tokio::runtime::Handle), +} From 12bd254c02ff8565fde276fd073a7b288eb88be0 Mon Sep 17 00:00:00 2001 From: Andrei Date: Mon, 18 Aug 2025 00:00:00 +0000 Subject: [PATCH 124/177] Add corresponding `_for_hash` methods to receive via jit channel Add corresponding `receive_via_jit_channel_for_hash()` and `receive_variable_amount_via_jit_channel_for_hash()` methods that accept a custom payment hash from the user. These methods allow implementing swap-in functionality with a JIT channel (i.e., swapping an on-chain payment to Lightning by opening a new channel). --- bindings/ldk_node.udl | 4 ++ src/event.rs | 3 +- src/liquidity.rs | 46 ++++++++++---- src/payment/bolt11.rs | 98 +++++++++++++++++++++++++++- tests/integration_tests_rust.rs | 109 ++++++++++++++++++++++++++++++-- 5 files changed, 238 insertions(+), 22 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index f3560ec09c..076d7fc9b1 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -189,7 +189,11 @@ interface Bolt11Payment { [Throws=NodeError] Bolt11Invoice receive_via_jit_channel(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat); [Throws=NodeError] + Bolt11Invoice receive_via_jit_channel_for_hash(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_lsp_fee_limit_msat, PaymentHash payment_hash); + [Throws=NodeError] Bolt11Invoice receive_variable_amount_via_jit_channel([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat); + [Throws=NodeError] + Bolt11Invoice receive_variable_amount_via_jit_channel_for_hash([ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_proportional_lsp_fee_limit_ppm_msat, PaymentHash payment_hash); }; interface Bolt12Payment { diff --git a/src/event.rs b/src/event.rs index ae81f50e96..883177d67f 100644 --- a/src/event.rs +++ b/src/event.rs @@ -685,7 +685,8 @@ where // the payment has been registered via `_for_hash` variants and needs to be manually claimed via // user interaction. match info.kind { - PaymentKind::Bolt11 { preimage, .. } => { + PaymentKind::Bolt11 { preimage, .. } + | PaymentKind::Bolt11Jit { preimage, .. } => { if purpose.preimage().is_none() { debug_assert!( preimage.is_none(), diff --git a/src/liquidity.rs b/src/liquidity.rs index 9b103ee821..6ee8066c19 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -988,7 +988,7 @@ where pub(crate) async fn lsps2_receive_to_jit_channel( &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_total_lsp_fee_limit_msat: Option, + max_total_lsp_fee_limit_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -1040,6 +1040,7 @@ where Some(amount_msat), description, expiry_secs, + payment_hash, )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); @@ -1048,7 +1049,7 @@ where pub(crate) async fn lsps2_receive_variable_amount_to_jit_channel( &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, - max_proportional_lsp_fee_limit_ppm_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result<(Bolt11Invoice, u64), Error> { let fee_response = self.lsps2_request_opening_fee_params().await?; @@ -1082,8 +1083,13 @@ where ); let buy_response = self.lsps2_send_buy_request(None, min_opening_params).await?; - let invoice = - self.lsps2_create_jit_invoice(buy_response, None, description, expiry_secs)?; + let invoice = self.lsps2_create_jit_invoice( + buy_response, + None, + description, + expiry_secs, + payment_hash, + )?; log_info!(self.logger, "JIT-channel invoice created: {}", invoice); Ok((invoice, min_prop_fee_ppm_msat)) @@ -1166,18 +1172,36 @@ where fn lsps2_create_jit_invoice( &self, buy_response: LSPS2BuyResponse, amount_msat: Option, description: &Bolt11InvoiceDescription, expiry_secs: u32, + payment_hash: Option, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; - let (payment_hash, payment_secret) = self - .channel_manager - .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) - .map_err(|e| { - log_error!(self.logger, "Failed to register inbound payment: {:?}", e); - Error::InvoiceCreationFailed - })?; + let (payment_hash, payment_secret) = match payment_hash { + Some(payment_hash) => { + let payment_secret = self + .channel_manager + .create_inbound_payment_for_hash( + payment_hash, + None, + expiry_secs, + Some(min_final_cltv_expiry_delta), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?; + (payment_hash, payment_secret) + }, + None => self + .channel_manager + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .map_err(|e| { + log_error!(self.logger, "Failed to register inbound payment: {:?}", e); + Error::InvoiceCreationFailed + })?, + }; let route_hint = RouteHint(vec![RouteHintHop { src_node_id: lsps2_client.lsp_node_id, diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 389c818c83..92d7fc9485 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -362,8 +362,17 @@ impl Bolt11Payment { } if let Some(details) = self.payment_store.get(&payment_id) { - if let Some(expected_amount_msat) = details.amount_msat { - if claimable_amount_msat < expected_amount_msat { + // For payments requested via `receive*_via_jit_channel_for_hash()` + // `skimmed_fee_msat` held by LSP must be taken into account. + let skimmed_fee_msat = match details.kind { + PaymentKind::Bolt11Jit { + counterparty_skimmed_fee_msat: Some(skimmed_fee_msat), + .. + } => skimmed_fee_msat, + _ => 0, + }; + if let Some(invoice_amount_msat) = details.amount_msat { + if claimable_amount_msat < invoice_amount_msat - skimmed_fee_msat { log_error!( self.logger, "Failed to manually claim payment {} as the claimable amount is less than expected", @@ -580,6 +589,46 @@ impl Bolt11Payment { expiry_secs, max_total_lsp_fee_limit_msat, None, + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a payment of the amount given and + /// receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the + /// channel to us. We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_via_jit_channel_for_hash( + &self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_total_lsp_fee_limit_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + Some(amount_msat), + &description, + expiry_secs, + max_total_lsp_fee_limit_msat, + None, + Some(payment_hash), )?; Ok(maybe_wrap(invoice)) } @@ -606,6 +655,47 @@ impl Bolt11Payment { expiry_secs, None, max_proportional_lsp_fee_limit_ppm_msat, + None, + )?; + Ok(maybe_wrap(invoice)) + } + + /// Returns a payable invoice that can be used to request a variable amount payment (also known + /// as "zero-amount" invoice) and receive it via a newly created just-in-time (JIT) channel. + /// + /// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel + /// to us, supplying just-in-time inbound liquidity. + /// + /// If set, `max_proportional_lsp_fee_limit_ppm_msat` will limit how much proportional fee, in + /// parts-per-million millisatoshis, we allow the LSP to take for opening the channel to us. + /// We'll use its cheapest offer otherwise. + /// + /// We will register the given payment hash and emit a [`PaymentClaimable`] event once + /// the inbound payment arrives. The check that [`counterparty_skimmed_fee_msat`] is within the limits + /// is performed *before* emitting the event. + /// + /// **Note:** users *MUST* handle this event and claim the payment manually via + /// [`claim_for_hash`] as soon as they have obtained access to the preimage of the given + /// payment hash. If they're unable to obtain the preimage, they *MUST* immediately fail the payment via + /// [`fail_for_hash`]. + /// + /// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md + /// [`PaymentClaimable`]: crate::Event::PaymentClaimable + /// [`claim_for_hash`]: Self::claim_for_hash + /// [`fail_for_hash`]: Self::fail_for_hash + /// [`counterparty_skimmed_fee_msat`]: crate::payment::PaymentKind::Bolt11Jit::counterparty_skimmed_fee_msat + pub fn receive_variable_amount_via_jit_channel_for_hash( + &self, description: &Bolt11InvoiceDescription, expiry_secs: u32, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: PaymentHash, + ) -> Result { + let description = maybe_try_convert_enum(description)?; + let invoice = self.receive_via_jit_channel_inner( + None, + &description, + expiry_secs, + None, + max_proportional_lsp_fee_limit_ppm_msat, + Some(payment_hash), )?; Ok(maybe_wrap(invoice)) } @@ -613,7 +703,7 @@ impl Bolt11Payment { fn receive_via_jit_channel_inner( &self, amount_msat: Option, description: &LdkBolt11InvoiceDescription, expiry_secs: u32, max_total_lsp_fee_limit_msat: Option, - max_proportional_lsp_fee_limit_ppm_msat: Option, + max_proportional_lsp_fee_limit_ppm_msat: Option, payment_hash: Option, ) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -645,6 +735,7 @@ impl Bolt11Payment { description, expiry_secs, max_total_lsp_fee_limit_msat, + payment_hash, ) .await .map(|(invoice, total_fee)| (invoice, Some(total_fee), None)) @@ -654,6 +745,7 @@ impl Bolt11Payment { description, expiry_secs, max_proportional_lsp_fee_limit_ppm_msat, + payment_hash, ) .await .map(|(invoice, prop_fee)| (invoice, None, Some(prop_fee))) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index ad38674294..9fea3094f4 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -9,7 +9,8 @@ mod common; use common::{ do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, - expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, + generate_blocks_and_wait, logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, premine_and_distribute_funds, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, @@ -29,7 +30,7 @@ use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::util::persist::KVStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use lightning_types::payment::PaymentPreimage; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::sha256::Hash as Sha256Hash; @@ -1334,6 +1335,7 @@ fn lsps2_client_service_integration() { let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); expect_channel_pending_event!(service_node, client_node.node_id()); expect_channel_ready_event!(service_node, client_node.node_id()); + expect_event!(service_node, PaymentForwarded); expect_channel_pending_event!(client_node, service_node.node_id()); expect_channel_ready_event!(client_node, service_node.node_id()); @@ -1359,19 +1361,112 @@ fn lsps2_client_service_integration() { println!("Generating regular invoice!"); let invoice_description = - Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()); + Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap()).into(); let amount_msat = 5_000_000; - let invoice = client_node - .bolt11_payment() - .receive(amount_msat, &invoice_description.into(), 1024) - .unwrap(); + let invoice = + client_node.bolt11_payment().receive(amount_msat, &invoice_description, 1024).unwrap(); // Have the payer_node pay the invoice, to check regular forwards service_node -> client_node // are working as expected. println!("Paying regular invoice!"); let payment_id = payer_node.bolt11_payment().send(&invoice, None).unwrap(); expect_payment_successful_event!(payer_node, Some(payment_id), None); + expect_event!(service_node, PaymentForwarded); expect_payment_received_event!(client_node, amount_msat); + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and claim_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 200_000_000; + let manual_preimage = PaymentPreimage([42u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + let claimable_amount_msat = expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Claiming payment!"); + client_node + .bolt11_payment() + .claim_for_hash(manual_payment_hash, claimable_amount_msat, manual_preimage) + .unwrap(); + + expect_event!(service_node, PaymentForwarded); + expect_payment_successful_event!(payer_node, Some(payment_id), None); + let client_payment_id = + expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap(); + let client_payment = client_node.payment(&client_payment_id).unwrap(); + match client_payment.kind { + PaymentKind::Bolt11Jit { counterparty_skimmed_fee_msat, .. } => { + assert_eq!(counterparty_skimmed_fee_msat, Some(service_fee_msat)); + }, + _ => panic!("Unexpected payment kind"), + } + + //////////////////////////////////////////////////////////////////////////// + // receive_via_jit_channel_for_hash and fail_for_hash + //////////////////////////////////////////////////////////////////////////// + println!("Generating JIT invoice!"); + // Increase the amount to make sure it does not fit into the existing channels. + let jit_amount_msat = 400_000_000; + let manual_preimage = PaymentPreimage([43u8; 32]); + let manual_payment_hash: PaymentHash = manual_preimage.into(); + let jit_invoice = client_node + .bolt11_payment() + .receive_via_jit_channel_for_hash( + jit_amount_msat, + &invoice_description, + 1024, + None, + manual_payment_hash, + ) + .unwrap(); + + // Have the payer_node pay the invoice, therby triggering channel open service_node -> client_node. + println!("Paying JIT invoice!"); + let payment_id = payer_node.bolt11_payment().send(&jit_invoice, None).unwrap(); + expect_channel_pending_event!(service_node, client_node.node_id()); + expect_channel_ready_event!(service_node, client_node.node_id()); + expect_channel_pending_event!(client_node, service_node.node_id()); + expect_channel_ready_event!(client_node, service_node.node_id()); + + let service_fee_msat = (jit_amount_msat * channel_opening_fee_ppm as u64) / 1_000_000; + let expected_received_amount_msat = jit_amount_msat - service_fee_msat; + expect_payment_claimable_event!( + client_node, + payment_id, + manual_payment_hash, + expected_received_amount_msat + ); + println!("Failing payment!"); + client_node.bolt11_payment().fail_for_hash(manual_payment_hash).unwrap(); + + expect_event!(payer_node, PaymentFailed); + assert_eq!(client_node.payment(&payment_id).unwrap().status, PaymentStatus::Failed); } #[test] From 3fe4f2f6b086f88737b4a5534faf1879183592bc Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:37:38 -0300 Subject: [PATCH 125/177] Fix RPC Txid handling and mempool eviction - Replace `bitcoin::consensus::encode::deserialize_hex()` with `hex_str.parse::()` when parsing Txids from RPC, and `serialize_hex()` with `txid.to_string()` when sending to RPC, ensuring proper handling of Bitcoin Core's reversed-byte hexadecimal format. - Fix mempool eviction logic: transactions that are no longer in the mempool are now correctly removed from wallet consideration, preventing stale pending transactions from inflating unconfirmed balances. - Refactor `unconfirmed_txids` to `bdk_unconfirmed_txids` to make it easier to identify what are unconfirmed bdk transactions --- src/chain/bitcoind.rs | 32 ++++++++++++++++++-------------- 1 file changed, 18 insertions(+), 14 deletions(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index a120f82538..6ea9f271e1 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -370,8 +370,11 @@ impl BitcoindChainSource { let cur_height = channel_manager.current_best_block().height; let now = SystemTime::now(); - let unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); - match self.api_client.get_updated_mempool_transactions(cur_height, unconfirmed_txids).await + let bdk_unconfirmed_txids = self.onchain_wallet.get_unconfirmed_txids(); + match self + .api_client + .get_updated_mempool_transactions(cur_height, bdk_unconfirmed_txids) + .await { Ok((unconfirmed_txs, evicted_txids)) => { log_trace!( @@ -754,7 +757,7 @@ impl BitcoindClient { async fn get_raw_transaction_rpc( rpc_client: Arc, txid: &Txid, ) -> std::io::Result> { - let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); + let txid_hex = txid.to_string(); let txid_json = serde_json::json!(txid_hex); match rpc_client .call_method::("getrawtransaction", &[txid_json]) @@ -792,7 +795,7 @@ impl BitcoindClient { async fn get_raw_transaction_rest( rest_client: Arc, txid: &Txid, ) -> std::io::Result> { - let txid_hex = bitcoin::consensus::encode::serialize_hex(txid); + let txid_hex = txid.to_string(); let tx_path = format!("tx/{}.json", txid_hex); match rest_client .request_resource::(&tx_path) @@ -889,7 +892,7 @@ impl BitcoindClient { async fn get_mempool_entry_inner( client: Arc, txid: Txid, ) -> std::io::Result> { - let txid_hex = bitcoin::consensus::encode::serialize_hex(&txid); + let txid_hex = txid.to_string(); let txid_json = serde_json::json!(txid_hex); match client.call_method::("getmempoolentry", &[txid_json]).await { @@ -964,11 +967,12 @@ impl BitcoindClient { /// - mempool transactions, alongside their first-seen unix timestamps. /// - transactions that have been evicted from the mempool, alongside the last time they were seen absent. pub(crate) async fn get_updated_mempool_transactions( - &self, best_processed_height: u32, unconfirmed_txids: Vec, + &self, best_processed_height: u32, bdk_unconfirmed_txids: Vec, ) -> std::io::Result<(Vec<(Transaction, u64)>, Vec<(Txid, u64)>)> { let mempool_txs = self.get_mempool_transactions_and_timestamp_at_height(best_processed_height).await?; - let evicted_txids = self.get_evicted_mempool_txids_and_timestamp(unconfirmed_txids).await?; + let evicted_txids = + self.get_evicted_mempool_txids_and_timestamp(bdk_unconfirmed_txids).await?; Ok((mempool_txs, evicted_txids)) } @@ -1078,14 +1082,14 @@ impl BitcoindClient { // To this end, we first update our local mempool_entries_cache and then return all unconfirmed // wallet `Txid`s that don't appear in the mempool still. async fn get_evicted_mempool_txids_and_timestamp( - &self, unconfirmed_txids: Vec, + &self, bdk_unconfirmed_txids: Vec, ) -> std::io::Result> { match self { BitcoindClient::Rpc { latest_mempool_timestamp, mempool_entries_cache, .. } => { Self::get_evicted_mempool_txids_and_timestamp_inner( latest_mempool_timestamp, mempool_entries_cache, - unconfirmed_txids, + bdk_unconfirmed_txids, ) .await }, @@ -1093,7 +1097,7 @@ impl BitcoindClient { Self::get_evicted_mempool_txids_and_timestamp_inner( latest_mempool_timestamp, mempool_entries_cache, - unconfirmed_txids, + bdk_unconfirmed_txids, ) .await }, @@ -1103,13 +1107,13 @@ impl BitcoindClient { async fn get_evicted_mempool_txids_and_timestamp_inner( latest_mempool_timestamp: &AtomicU64, mempool_entries_cache: &tokio::sync::Mutex>, - unconfirmed_txids: Vec, + bdk_unconfirmed_txids: Vec, ) -> std::io::Result> { let latest_mempool_timestamp = latest_mempool_timestamp.load(Ordering::Relaxed); let mempool_entries_cache = mempool_entries_cache.lock().await; - let evicted_txids = unconfirmed_txids + let evicted_txids = bdk_unconfirmed_txids .into_iter() - .filter(|txid| mempool_entries_cache.contains_key(txid)) + .filter(|txid| !mempool_entries_cache.contains_key(txid)) .map(|txid| (txid, latest_mempool_timestamp)) .collect(); Ok(evicted_txids) @@ -1236,7 +1240,7 @@ impl TryInto for JsonResponse { for hex in res { let txid = if let Some(hex_str) = hex.as_str() { - match bitcoin::consensus::encode::deserialize_hex(hex_str) { + match hex_str.parse::() { Ok(txid) => txid, Err(_) => { return Err(std::io::Error::new( From 5fe06c842cccae824e094640053b9a3a86fcd38c Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Thu, 14 Aug 2025 16:40:47 -0300 Subject: [PATCH 126/177] Add property-based tests for RPC JSON responses - Validate roundtrip serialization/deserialization for Txids, transactions, mempool entries, and fee responses. - Ensure Txid parsing/serialization matches Bitcoin Core RPC expectations. --- src/chain/bitcoind.rs | 161 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 161 insertions(+) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 6ea9f271e1..c282a6141d 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -1411,3 +1411,164 @@ impl std::fmt::Display for HttpError { write!(f, "status_code: {}, contents: {}", self.status_code, contents) } } + +#[cfg(test)] +mod tests { + use bitcoin::hashes::Hash; + use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness}; + use lightning_block_sync::http::JsonResponse; + use proptest::{arbitrary::any, collection::vec, prop_assert_eq, prop_compose, proptest}; + use serde_json::json; + + use crate::chain::bitcoind::{ + FeeResponse, GetMempoolEntryResponse, GetRawMempoolResponse, GetRawTransactionResponse, + MempoolMinFeeResponse, + }; + + prop_compose! { + fn arbitrary_witness()( + witness_elements in vec(vec(any::(), 0..100), 0..20) + ) -> Witness { + let mut witness = Witness::new(); + for element in witness_elements { + witness.push(element); + } + witness + } + } + + prop_compose! { + fn arbitrary_txin()( + outpoint_hash in any::<[u8; 32]>(), + outpoint_vout in any::(), + script_bytes in vec(any::(), 0..100), + witness in arbitrary_witness(), + sequence in any::() + ) -> TxIn { + TxIn { + previous_output: OutPoint { + txid: Txid::from_byte_array(outpoint_hash), + vout: outpoint_vout, + }, + script_sig: ScriptBuf::from_bytes(script_bytes), + sequence: bitcoin::Sequence::from_consensus(sequence), + witness, + } + } + } + + prop_compose! { + fn arbitrary_txout()( + value in 0u64..21_000_000_00_000_000u64, + script_bytes in vec(any::(), 0..100) + ) -> TxOut { + TxOut { + value: bitcoin::Amount::from_sat(value), + script_pubkey: ScriptBuf::from_bytes(script_bytes), + } + } + } + + prop_compose! { + fn arbitrary_transaction()( + version in any::(), + inputs in vec(arbitrary_txin(), 1..20), + outputs in vec(arbitrary_txout(), 1..20), + lock_time in any::() + ) -> Transaction { + Transaction { + version: bitcoin::transaction::Version(version), + input: inputs, + output: outputs, + lock_time: bitcoin::absolute::LockTime::from_consensus(lock_time), + } + } + } + + proptest! { + #![proptest_config(proptest::test_runner::Config::with_cases(20))] + + #[test] + fn prop_get_raw_mempool_response_roundtrip(txids in vec(any::<[u8;32]>(), 0..10)) { + let txid_vec: Vec = txids.into_iter().map(Txid::from_byte_array).collect(); + let original = GetRawMempoolResponse(txid_vec.clone()); + + let json_vec: Vec = txid_vec.iter().map(|t| t.to_string()).collect(); + let json_val = serde_json::Value::Array(json_vec.iter().map(|s| json!(s)).collect()); + + let resp = JsonResponse(json_val); + let decoded: GetRawMempoolResponse = resp.try_into().unwrap(); + + prop_assert_eq!(original.0.len(), decoded.0.len()); + + prop_assert_eq!(original.0, decoded.0); + } + + #[test] + fn prop_get_mempool_entry_response_roundtrip( + time in any::(), + height in any::() + ) { + let json_val = json!({ + "time": time, + "height": height + }); + + let resp = JsonResponse(json_val); + let decoded: GetMempoolEntryResponse = resp.try_into().unwrap(); + + prop_assert_eq!(decoded.time, time); + prop_assert_eq!(decoded.height, height); + } + + #[test] + fn prop_get_raw_transaction_response_roundtrip(tx in arbitrary_transaction()) { + let hex = bitcoin::consensus::encode::serialize_hex(&tx); + let json_val = serde_json::Value::String(hex.clone()); + + let resp = JsonResponse(json_val); + let decoded: GetRawTransactionResponse = resp.try_into().unwrap(); + + prop_assert_eq!(decoded.0.compute_txid(), tx.compute_txid()); + prop_assert_eq!(decoded.0.compute_wtxid(), tx.compute_wtxid()); + + prop_assert_eq!(decoded.0, tx); + } + + #[test] + fn prop_fee_response_roundtrip(fee_rate in any::()) { + let fee_rate = fee_rate.abs(); + let json_val = json!({ + "feerate": fee_rate, + "errors": serde_json::Value::Null + }); + + let resp = JsonResponse(json_val); + let decoded: FeeResponse = resp.try_into().unwrap(); + + let expected = { + let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + prop_assert_eq!(decoded.0, expected); + } + + #[test] + fn prop_mempool_min_fee_response_roundtrip(fee_rate in any::()) { + let fee_rate = fee_rate.abs(); + let json_val = json!({ + "mempoolminfee": fee_rate + }); + + let resp = JsonResponse(json_val); + let decoded: MempoolMinFeeResponse = resp.try_into().unwrap(); + + let expected = { + let fee_rate_sat_per_kwu = (fee_rate * 25_000_000.0).round() as u64; + FeeRate::from_sat_per_kwu(fee_rate_sat_per_kwu) + }; + prop_assert_eq!(decoded.0, expected); + } + + } +} From 4922e8da5b0622bb89e35c86fd6b143a1f0cecd2 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 18 Aug 2025 15:41:39 +0200 Subject: [PATCH 127/177] Refactor move background task fields to `Runtime` Previously we a) added a new internal `Runtime` API that cleans up our internal logic and b) added tracking for spawned background tasks to be able to await/abort them on shutdown. Here we move the tracking into the `Runtime` object, which will allow us to easily extend the tracking to *any* spawned tasks in the next step. --- src/builder.rs | 15 ++-- src/lib.rs | 186 ++++++++++--------------------------------------- src/runtime.rs | 158 +++++++++++++++++++++++++++++++++++++++-- 3 files changed, 192 insertions(+), 167 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 729cefe1bc..7f15cced69 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -668,9 +668,9 @@ impl NodeBuilder { let logger = setup_logger(&self.log_writer_config, &self.config)?; let runtime = if let Some(handle) = self.runtime_handle.as_ref() { - Arc::new(Runtime::with_handle(handle.clone())) + Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger))) } else { - Arc::new(Runtime::new().map_err(|e| { + Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| { log_error!(logger, "Failed to setup tokio runtime: {}", e); BuildError::RuntimeSetupFailed })?) @@ -715,9 +715,9 @@ impl NodeBuilder { let logger = setup_logger(&self.log_writer_config, &self.config)?; let runtime = if let Some(handle) = self.runtime_handle.as_ref() { - Arc::new(Runtime::with_handle(handle.clone())) + Arc::new(Runtime::with_handle(handle.clone(), Arc::clone(&logger))) } else { - Arc::new(Runtime::new().map_err(|e| { + Arc::new(Runtime::new(Arc::clone(&logger)).map_err(|e| { log_error!(logger, "Failed to setup tokio runtime: {}", e); BuildError::RuntimeSetupFailed })?) @@ -1668,18 +1668,11 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); - let background_processor_task = Mutex::new(None); - let background_tasks = Mutex::new(None); - let cancellable_background_tasks = Mutex::new(None); - let is_running = Arc::new(RwLock::new(false)); Ok(Node { runtime, stop_sender, - background_processor_task, - background_tasks, - cancellable_background_tasks, config, wallet, chain_source, diff --git a/src/lib.rs b/src/lib.rs index cc5e383a1f..1604d1b461 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -128,9 +128,8 @@ pub use builder::NodeBuilder as Builder; use chain::ChainSource; use config::{ - default_user_config, may_announce_channel, ChannelConfig, Config, - BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, - NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, + PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; @@ -181,9 +180,6 @@ uniffi::include_scaffolding!("ldk_node"); pub struct Node { runtime: Arc, stop_sender: tokio::sync::watch::Sender<()>, - background_processor_task: Mutex>>, - background_tasks: Mutex>>, - cancellable_background_tasks: Mutex>>, config: Arc, wallet: Arc, chain_source: Arc, @@ -226,10 +222,6 @@ impl Node { return Err(Error::AlreadyRunning); } - let mut background_tasks = tokio::task::JoinSet::new(); - let mut cancellable_background_tasks = tokio::task::JoinSet::new(); - let runtime_handle = self.runtime.handle(); - log_info!( self.logger, "Starting up LDK Node with node ID {} on network: {}", @@ -253,19 +245,11 @@ impl Node { let sync_cman = Arc::clone(&self.channel_manager); let sync_cmon = Arc::clone(&self.chain_monitor); let sync_sweeper = Arc::clone(&self.output_sweeper); - background_tasks.spawn_on( - async move { - chain_source - .continuously_sync_wallets( - stop_sync_receiver, - sync_cman, - sync_cmon, - sync_sweeper, - ) - .await; - }, - runtime_handle, - ); + self.runtime.spawn_background_task(async move { + chain_source + .continuously_sync_wallets(stop_sync_receiver, sync_cman, sync_cmon, sync_sweeper) + .await; + }); if self.gossip_source.is_rgs() { let gossip_source = Arc::clone(&self.gossip_source); @@ -273,7 +257,7 @@ impl Node { let gossip_sync_logger = Arc::clone(&self.logger); let gossip_node_metrics = Arc::clone(&self.node_metrics); let mut stop_gossip_sync = self.stop_sender.subscribe(); - cancellable_background_tasks.spawn_on(async move { + self.runtime.spawn_cancellable_background_task(async move { let mut interval = tokio::time::interval(RGS_SYNC_INTERVAL); loop { tokio::select! { @@ -314,7 +298,7 @@ impl Node { } } } - }, runtime_handle); + }); } if let Some(listening_addresses) = &self.config.listening_addresses { @@ -340,7 +324,7 @@ impl Node { bind_addrs.extend(resolved_address); } - cancellable_background_tasks.spawn_on(async move { + self.runtime.spawn_cancellable_background_task(async move { { let listener = tokio::net::TcpListener::bind(&*bind_addrs).await @@ -378,7 +362,7 @@ impl Node { } listening_indicator.store(false, Ordering::Release); - }, runtime_handle); + }); } // Regularly reconnect to persisted peers. @@ -387,7 +371,7 @@ impl Node { let connect_logger = Arc::clone(&self.logger); let connect_peer_store = Arc::clone(&self.peer_store); let mut stop_connect = self.stop_sender.subscribe(); - cancellable_background_tasks.spawn_on(async move { + self.runtime.spawn_cancellable_background_task(async move { let mut interval = tokio::time::interval(PEER_RECONNECTION_INTERVAL); interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); loop { @@ -415,7 +399,7 @@ impl Node { } } } - }, runtime_handle); + }); // Regularly broadcast node announcements. let bcast_cm = Arc::clone(&self.channel_manager); @@ -427,7 +411,7 @@ impl Node { let mut stop_bcast = self.stop_sender.subscribe(); let node_alias = self.config.node_alias.clone(); if may_announce_channel(&self.config).is_ok() { - cancellable_background_tasks.spawn_on(async move { + self.runtime.spawn_cancellable_background_task(async move { // We check every 30 secs whether our last broadcast is NODE_ANN_BCAST_INTERVAL away. #[cfg(not(test))] let mut interval = tokio::time::interval(Duration::from_secs(30)); @@ -498,15 +482,14 @@ impl Node { } } } - }, runtime_handle); + }); } let stop_tx_bcast = self.stop_sender.subscribe(); let chain_source = Arc::clone(&self.chain_source); - cancellable_background_tasks.spawn_on( - async move { chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await }, - runtime_handle, - ); + self.runtime.spawn_cancellable_background_task(async move { + chain_source.continuously_process_broadcast_queue(stop_tx_bcast).await + }); let bump_tx_event_handler = Arc::new(BumpTransactionEventHandler::new( Arc::clone(&self.tx_broadcaster), @@ -563,7 +546,7 @@ impl Node { }) }; - let handle = self.runtime.spawn(async move { + self.runtime.spawn_background_processor_task(async move { process_events_async( background_persister, |e| background_event_handler.handle_event(e), @@ -584,38 +567,27 @@ impl Node { panic!("Failed to process events"); }); }); - debug_assert!(self.background_processor_task.lock().unwrap().is_none()); - *self.background_processor_task.lock().unwrap() = Some(handle); if let Some(liquidity_source) = self.liquidity_source.as_ref() { let mut stop_liquidity_handler = self.stop_sender.subscribe(); let liquidity_handler = Arc::clone(&liquidity_source); let liquidity_logger = Arc::clone(&self.logger); - background_tasks.spawn_on( - async move { - loop { - tokio::select! { - _ = stop_liquidity_handler.changed() => { - log_debug!( - liquidity_logger, - "Stopping processing liquidity events.", - ); - return; - } - _ = liquidity_handler.handle_next_event() => {} + self.runtime.spawn_background_task(async move { + loop { + tokio::select! { + _ = stop_liquidity_handler.changed() => { + log_debug!( + liquidity_logger, + "Stopping processing liquidity events.", + ); + return; } + _ = liquidity_handler.handle_next_event() => {} } - }, - runtime_handle, - ); + } + }); } - debug_assert!(self.background_tasks.lock().unwrap().is_none()); - *self.background_tasks.lock().unwrap() = Some(background_tasks); - - debug_assert!(self.cancellable_background_tasks.lock().unwrap().is_none()); - *self.cancellable_background_tasks.lock().unwrap() = Some(cancellable_background_tasks); - log_info!(self.logger, "Startup complete."); *is_running_lock = true; Ok(()) @@ -649,15 +621,7 @@ impl Node { } // Cancel cancellable background tasks - if let Some(mut tasks) = self.cancellable_background_tasks.lock().unwrap().take() { - let runtime_handle = self.runtime.handle(); - tasks.abort_all(); - tokio::task::block_in_place(move || { - runtime_handle.block_on(async { while let Some(_) = tasks.join_next().await {} }) - }); - } else { - debug_assert!(false, "Expected some cancellable background tasks"); - }; + self.runtime.abort_cancellable_background_tasks(); // Disconnect all peers. self.peer_manager.disconnect_all_peers(); @@ -668,91 +632,13 @@ impl Node { log_debug!(self.logger, "Stopped chain sources."); // Wait until non-cancellable background tasks (mod LDK's background processor) are done. - let runtime_handle = self.runtime.handle(); - if let Some(mut tasks) = self.background_tasks.lock().unwrap().take() { - tokio::task::block_in_place(move || { - runtime_handle.block_on(async { - loop { - let timeout_fut = tokio::time::timeout( - Duration::from_secs(BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS), - tasks.join_next_with_id(), - ); - match timeout_fut.await { - Ok(Some(Ok((id, _)))) => { - log_trace!(self.logger, "Stopped background task with id {}", id); - }, - Ok(Some(Err(e))) => { - tasks.abort_all(); - log_trace!(self.logger, "Stopping background task failed: {}", e); - break; - }, - Ok(None) => { - log_debug!(self.logger, "Stopped all background tasks"); - break; - }, - Err(e) => { - tasks.abort_all(); - log_error!( - self.logger, - "Stopping background task timed out: {}", - e - ); - break; - }, - } - } - }) - }); - } else { - debug_assert!(false, "Expected some background tasks"); - }; + self.runtime.wait_on_background_tasks(); - // Wait until background processing stopped, at least until a timeout is reached. - if let Some(background_processor_task) = - self.background_processor_task.lock().unwrap().take() - { - let abort_handle = background_processor_task.abort_handle(); - let timeout_res = tokio::task::block_in_place(move || { - self.runtime.block_on(async { - tokio::time::timeout( - Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), - background_processor_task, - ) - .await - }) - }); - - match timeout_res { - Ok(stop_res) => match stop_res { - Ok(()) => log_debug!(self.logger, "Stopped background processing of events."), - Err(e) => { - abort_handle.abort(); - log_error!( - self.logger, - "Stopping event handling failed. This should never happen: {}", - e - ); - panic!("Stopping event handling failed. This should never happen."); - }, - }, - Err(e) => { - abort_handle.abort(); - log_error!(self.logger, "Stopping event handling timed out: {}", e); - }, - } - } else { - debug_assert!(false, "Expected a background processing task"); - }; + // Finally, wait until background processing stopped, at least until a timeout is reached. + self.runtime.wait_on_background_processor_task(); #[cfg(tokio_unstable)] - { - let runtime_handle = self.runtime.handle(); - log_trace!( - self.logger, - "Active runtime tasks left prior to shutdown: {}", - runtime_handle.metrics().active_tasks_count() - ); - } + self.runtime.log_metrics(); log_info!(self.logger, "Shutdown complete."); *is_running_lock = false; diff --git a/src/runtime.rs b/src/runtime.rs index 4c1241165f..0bd3941c79 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -5,16 +5,27 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use tokio::task::JoinHandle; +use crate::config::{ + BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, +}; +use crate::logger::{log_debug, log_error, log_trace, LdkLogger, Logger}; + +use tokio::task::{JoinHandle, JoinSet}; use std::future::Future; +use std::sync::{Arc, Mutex}; +use std::time::Duration; pub(crate) struct Runtime { mode: RuntimeMode, + background_tasks: Mutex>, + cancellable_background_tasks: Mutex>, + background_processor_task: Mutex>>, + logger: Arc, } impl Runtime { - pub fn new() -> Result { + pub fn new(logger: Arc) -> Result { let mode = match tokio::runtime::Handle::try_current() { Ok(handle) => RuntimeMode::Handle(handle), Err(_) => { @@ -22,12 +33,32 @@ impl Runtime { RuntimeMode::Owned(rt) }, }; - Ok(Self { mode }) + let background_tasks = Mutex::new(JoinSet::new()); + let cancellable_background_tasks = Mutex::new(JoinSet::new()); + let background_processor_task = Mutex::new(None); + + Ok(Self { + mode, + background_tasks, + cancellable_background_tasks, + background_processor_task, + logger, + }) } - pub fn with_handle(handle: tokio::runtime::Handle) -> Self { + pub fn with_handle(handle: tokio::runtime::Handle, logger: Arc) -> Self { let mode = RuntimeMode::Handle(handle); - Self { mode } + let background_tasks = Mutex::new(JoinSet::new()); + let cancellable_background_tasks = Mutex::new(JoinSet::new()); + let background_processor_task = Mutex::new(None); + + Self { + mode, + background_tasks, + cancellable_background_tasks, + background_processor_task, + logger, + } } pub fn spawn(&self, future: F) -> JoinHandle @@ -39,6 +70,36 @@ impl Runtime { handle.spawn(future) } + pub fn spawn_background_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut background_tasks = self.background_tasks.lock().unwrap(); + let runtime_handle = self.handle(); + background_tasks.spawn_on(future, runtime_handle); + } + + pub fn spawn_cancellable_background_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut cancellable_background_tasks = self.cancellable_background_tasks.lock().unwrap(); + let runtime_handle = self.handle(); + cancellable_background_tasks.spawn_on(future, runtime_handle); + } + + pub fn spawn_background_processor_task(&self, future: F) + where + F: Future + Send + 'static, + { + let mut background_processor_task = self.background_processor_task.lock().unwrap(); + debug_assert!(background_processor_task.is_none(), "Expected no background processor_task"); + + let runtime_handle = self.handle(); + let handle = runtime_handle.spawn(future); + *background_processor_task = Some(handle); + } + pub fn spawn_blocking(&self, func: F) -> JoinHandle where F: FnOnce() -> R + Send + 'static, @@ -58,7 +119,92 @@ impl Runtime { tokio::task::block_in_place(move || handle.block_on(future)) } - pub fn handle(&self) -> &tokio::runtime::Handle { + pub fn abort_cancellable_background_tasks(&self) { + let mut tasks = core::mem::take(&mut *self.cancellable_background_tasks.lock().unwrap()); + debug_assert!(tasks.len() > 0, "Expected some cancellable background_tasks"); + tasks.abort_all(); + self.block_on(async { while let Some(_) = tasks.join_next().await {} }) + } + + pub fn wait_on_background_tasks(&self) { + let mut tasks = core::mem::take(&mut *self.background_tasks.lock().unwrap()); + debug_assert!(tasks.len() > 0, "Expected some background_tasks"); + self.block_on(async { + loop { + let timeout_fut = tokio::time::timeout( + Duration::from_secs(BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS), + tasks.join_next_with_id(), + ); + match timeout_fut.await { + Ok(Some(Ok((id, _)))) => { + log_trace!(self.logger, "Stopped background task with id {}", id); + }, + Ok(Some(Err(e))) => { + tasks.abort_all(); + log_trace!(self.logger, "Stopping background task failed: {}", e); + break; + }, + Ok(None) => { + log_debug!(self.logger, "Stopped all background tasks"); + break; + }, + Err(e) => { + tasks.abort_all(); + log_error!(self.logger, "Stopping background task timed out: {}", e); + break; + }, + } + } + }) + } + + pub fn wait_on_background_processor_task(&self) { + if let Some(background_processor_task) = + self.background_processor_task.lock().unwrap().take() + { + let abort_handle = background_processor_task.abort_handle(); + let timeout_res = self.block_on(async { + tokio::time::timeout( + Duration::from_secs(LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS), + background_processor_task, + ) + .await + }); + + match timeout_res { + Ok(stop_res) => match stop_res { + Ok(()) => log_debug!(self.logger, "Stopped background processing of events."), + Err(e) => { + abort_handle.abort(); + log_error!( + self.logger, + "Stopping event handling failed. This should never happen: {}", + e + ); + panic!("Stopping event handling failed. This should never happen."); + }, + }, + Err(e) => { + abort_handle.abort(); + log_error!(self.logger, "Stopping event handling timed out: {}", e); + }, + } + } else { + debug_assert!(false, "Expected a background processing task"); + }; + } + + #[cfg(tokio_unstable)] + pub fn log_metrics(&self) { + let runtime_handle = self.handle(); + log_trace!( + self.logger, + "Active runtime tasks left prior to shutdown: {}", + runtime_handle.metrics().active_tasks_count() + ); + } + + fn handle(&self) -> &tokio::runtime::Handle { match &self.mode { RuntimeMode::Owned(rt) => rt.handle(), RuntimeMode::Handle(handle) => handle, From 90a4fe1b7d33e7a61fb074e5e45e0e2d087f1456 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 19 Aug 2025 13:30:19 +0200 Subject: [PATCH 128/177] Drop `Runtime::spawn` in favor of `spawn_cancellable_background_task` We now drop the generic `spawn` from our internal `Runtime` API, ensuring we'd always have to either use `spawn_cancellable_background_task` or `spawn_background_task`. --- src/event.rs | 4 ++-- src/gossip.rs | 2 +- src/runtime.rs | 9 --------- 3 files changed, 3 insertions(+), 12 deletions(-) diff --git a/src/event.rs b/src/event.rs index 883177d67f..ff94d51d17 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1059,7 +1059,7 @@ where forwarding_channel_manager.process_pending_htlc_forwards(); }; - self.runtime.spawn(future); + self.runtime.spawn_cancellable_background_task(future); }, LdkEvent::SpendableOutputs { outputs, channel_id } => { match self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None) { @@ -1441,7 +1441,7 @@ where } } }; - self.runtime.spawn(future); + self.runtime.spawn_cancellable_background_task(future); }, LdkEvent::BumpTransaction(bte) => { match bte { diff --git a/src/gossip.rs b/src/gossip.rs index 1185f07182..258f9f7364 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -144,6 +144,6 @@ impl RuntimeSpawner { impl FutureSpawner for RuntimeSpawner { fn spawn + Send + 'static>(&self, future: T) { - self.runtime.spawn(future); + self.runtime.spawn_cancellable_background_task(future); } } diff --git a/src/runtime.rs b/src/runtime.rs index 0bd3941c79..b30790a041 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -61,15 +61,6 @@ impl Runtime { } } - pub fn spawn(&self, future: F) -> JoinHandle - where - F: Future + Send + 'static, - F::Output: Send + 'static, - { - let handle = self.handle(); - handle.spawn(future) - } - pub fn spawn_background_task(&self, future: F) where F: Future + Send + 'static, From f3aab90cf93c5fdd15e5a05715b91b56ffe66bfd Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 19 Aug 2025 10:32:51 -0300 Subject: [PATCH 129/177] Add RBF integration tests with multi-node setup - Test mempool-only RBF handling and balance adjustments. - Test RBF transactions confirmed in block, ensuring stale unconfirmed txs are removed. - Introduce `distribute_funds_unconfirmed` for creating unconfirmed outputs. --- tests/common/mod.rs | 90 +++++++++++++++++-- tests/integration_tests_rust.rs | 148 ++++++++++++++++++++++++++++++-- 2 files changed, 226 insertions(+), 12 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index ab66f0fdd6..780e9bbf4f 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -30,8 +30,10 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage}; use lightning_persister::fs_store::FilesystemStore; use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; -use bitcoin::{Address, Amount, Network, OutPoint, Txid}; +use bitcoin::hashes::{hex::FromHex, Hash}; +use bitcoin::{ + Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness, +}; use electrsd::corepc_node::Client as BitcoindClient; use electrsd::corepc_node::Node as BitcoinD; @@ -42,7 +44,7 @@ use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde_json::{json, Value}; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::env; use std::path::PathBuf; use std::sync::{Arc, RwLock}; @@ -487,12 +489,25 @@ where pub(crate) fn premine_and_distribute_funds( bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, ) { + premine_blocks(bitcoind, electrs); + + distribute_funds_unconfirmed(bitcoind, electrs, addrs, amount); + generate_blocks_and_wait(bitcoind, electrs, 1); +} + +pub(crate) fn premine_blocks(bitcoind: &BitcoindClient, electrs: &E) { let _ = bitcoind.create_wallet("ldk_node_test"); let _ = bitcoind.load_wallet("ldk_node_test"); generate_blocks_and_wait(bitcoind, electrs, 101); +} - let amounts: HashMap = - addrs.iter().map(|addr| (addr.to_string(), amount.to_btc())).collect(); +pub(crate) fn distribute_funds_unconfirmed( + bitcoind: &BitcoindClient, electrs: &E, addrs: Vec
, amount: Amount, +) -> Txid { + let mut amounts = HashMap::::new(); + for addr in &addrs { + amounts.insert(addr.to_string(), amount.to_btc()); + } let empty_account = json!(""); let amounts_json = json!(amounts); @@ -505,7 +520,70 @@ pub(crate) fn premine_and_distribute_funds( .unwrap(); wait_for_tx(electrs, txid); - generate_blocks_and_wait(bitcoind, electrs, 1); + + txid +} + +pub(crate) fn prepare_rbf( + electrs: &E, txid: Txid, scripts_buf: &HashSet, +) -> (Transaction, usize) { + let tx = electrs.transaction_get(&txid).unwrap(); + + let fee_output_index = tx + .output + .iter() + .position(|output| !scripts_buf.contains(&output.script_pubkey)) + .expect("No output available for fee bumping"); + + (tx, fee_output_index) +} + +pub(crate) fn bump_fee_and_broadcast( + bitcoind: &BitcoindClient, electrs: &E, mut tx: Transaction, fee_output_index: usize, + is_insert_block: bool, +) -> Transaction { + let mut bump_fee_amount_sat = tx.vsize() as u64; + let attempts = 5; + + for _ in 0..attempts { + let fee_output = &mut tx.output[fee_output_index]; + let new_fee_value = fee_output.value.to_sat().saturating_sub(bump_fee_amount_sat); + if new_fee_value < 546 { + panic!("Warning: Fee output approaching dust limit ({} sats)", new_fee_value); + } + fee_output.value = Amount::from_sat(new_fee_value); + + for input in &mut tx.input { + input.sequence = Sequence::ENABLE_RBF_NO_LOCKTIME; + input.script_sig = ScriptBuf::new(); + input.witness = Witness::new(); + } + + let signed_result = bitcoind.sign_raw_transaction_with_wallet(&tx).unwrap(); + assert!(signed_result.complete, "Failed to sign RBF transaction"); + + let tx_bytes = Vec::::from_hex(&signed_result.hex).unwrap(); + tx = bitcoin::consensus::encode::deserialize::(&tx_bytes).unwrap(); + + match bitcoind.send_raw_transaction(&tx) { + Ok(res) => { + if is_insert_block { + generate_blocks_and_wait(bitcoind, electrs, 1); + } + let new_txid: Txid = res.0.parse().unwrap(); + wait_for_tx(electrs, new_txid); + return tx; + }, + Err(_) => { + bump_fee_amount_sat += bump_fee_amount_sat * 5; + if tx.output[fee_output_index].value.to_sat() < bump_fee_amount_sat { + panic!("Insufficient funds to increase fee"); + } + }, + } + } + + panic!("Failed to bump fee after {} attempts", attempts); } pub fn open_channel( diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 9fea3094f4..0932116ef2 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -8,13 +8,14 @@ mod common; use common::{ - do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, + bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, + expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, logging::{init_log_logger, validate_log_entry, TestLogWriter}, - open_channel, premine_and_distribute_funds, random_config, random_listening_addresses, - setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_two_nodes, wait_for_tx, - TestChainSource, TestSyncStore, + open_channel, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, + random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, + setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; @@ -35,10 +36,10 @@ use lightning_types::payment::{PaymentHash, PaymentPreimage}; use bitcoin::address::NetworkUnchecked; use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::hashes::Hash; -use bitcoin::Address; -use bitcoin::Amount; +use bitcoin::{Address, Amount, ScriptBuf}; use log::LevelFilter; +use std::collections::HashSet; use std::str::FromStr; use std::sync::Arc; @@ -670,6 +671,141 @@ fn onchain_wallet_recovery() { ); } +#[test] +fn test_rbf_via_mempool() { + run_rbf_test(false); +} + +#[test] +fn test_rbf_via_direct_block_insertion() { + run_rbf_test(true); +} + +// `is_insert_block`: +// - `true`: transaction is mined immediately (no mempool), testing confirmed-Tx handling. +// - `false`: transaction stays in mempool until confirmation, testing unconfirmed-Tx handling. +fn run_rbf_test(is_insert_block: bool) { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source_bitcoind = TestChainSource::BitcoindRpcSync(&bitcoind); + let chain_source_electrsd = TestChainSource::Electrum(&electrsd); + let chain_source_esplora = TestChainSource::Esplora(&electrsd); + + macro_rules! config_node { + ($chain_source: expr, $anchor_channels: expr) => {{ + let config_a = random_config($anchor_channels); + let node = setup_node(&$chain_source, config_a, None); + node + }}; + } + let anchor_channels = false; + let nodes = vec![ + config_node!(chain_source_electrsd, anchor_channels), + config_node!(chain_source_bitcoind, anchor_channels), + config_node!(chain_source_esplora, anchor_channels), + ]; + + let (bitcoind, electrs) = (&bitcoind.client, &electrsd.client); + premine_blocks(bitcoind, electrs); + + // Helpers declaration before starting the test + let all_addrs = + nodes.iter().map(|node| node.onchain_payment().new_address().unwrap()).collect::>(); + let amount_sat = 2_100_000; + let mut txid; + macro_rules! distribute_funds_all_nodes { + () => { + txid = distribute_funds_unconfirmed( + bitcoind, + electrs, + all_addrs.clone(), + Amount::from_sat(amount_sat), + ); + }; + } + macro_rules! validate_balances { + ($expected_balance_sat: expr, $is_spendable: expr) => { + let spend_balance = if $is_spendable { $expected_balance_sat } else { 0 }; + for node in &nodes { + node.sync_wallets().unwrap(); + let balances = node.list_balances(); + assert_eq!(balances.spendable_onchain_balance_sats, spend_balance); + assert_eq!(balances.total_onchain_balance_sats, $expected_balance_sat); + } + }; + } + + let scripts_buf: HashSet = + all_addrs.iter().map(|addr| addr.script_pubkey()).collect(); + let mut tx; + let mut fee_output_index; + + // Modify the output to the nodes + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + let new_addr = bitcoind.new_address().unwrap(); + output.script_pubkey = new_addr.script_pubkey(); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + validate_balances!(0, is_insert_block); + + // Not modifying the output scripts, but still bumping the fee. + distribute_funds_all_nodes!(); + validate_balances!(amount_sat, false); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + validate_balances!(amount_sat, is_insert_block); + + let mut final_amount_sat = amount_sat * 2; + let value_sat = 21_000; + + // Increase the value of the nodes' outputs + distribute_funds_all_nodes!(); + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() + value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + final_amount_sat += value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + // Decreases the value of the nodes' outputs + distribute_funds_all_nodes!(); + final_amount_sat += amount_sat; + (tx, fee_output_index) = prepare_rbf(electrs, txid, &scripts_buf); + tx.output.iter_mut().for_each(|output| { + if scripts_buf.contains(&output.script_pubkey) { + output.value = Amount::from_sat(output.value.to_sat() - value_sat); + } + }); + bump_fee_and_broadcast(bitcoind, electrs, tx, fee_output_index, is_insert_block); + final_amount_sat -= value_sat; + validate_balances!(final_amount_sat, is_insert_block); + + if !is_insert_block { + generate_blocks_and_wait(bitcoind, electrs, 1); + validate_balances!(final_amount_sat, true); + } + + // Check if it is possible to send all funds from the node + let mut txids = Vec::new(); + let addr = bitcoind.new_address().unwrap(); + nodes.iter().for_each(|node| { + let txid = node.onchain_payment().send_all_to_address(&addr, true, None).unwrap(); + txids.push(txid); + }); + txids.iter().for_each(|txid| { + wait_for_tx(electrs, *txid); + }); + generate_blocks_and_wait(bitcoind, electrs, 6); + validate_balances!(0, true); +} + #[test] fn sign_verify_msg() { let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From b7550709fc191853d05658a05529b1073d71d4fb Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 20 Aug 2025 09:14:20 +0200 Subject: [PATCH 130/177] Introduce separate stop signal for background processor Previously, we'd use the same stop signal for the background processor as for all other background tasks which could result in the BP getting stopped while other tasks are still produced changes that needed to be processed before shutdown. Here we introduce a separate stop signal for LDK's background processor, ensuring we first shutdown everything else and disconnect all peers before finally sending the BP shutdown signal and awaiting its shutdown. --- src/builder.rs | 2 ++ src/lib.rs | 32 +++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 7f15cced69..0ef3434d62 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1668,11 +1668,13 @@ fn build_with_store_internal( }; let (stop_sender, _) = tokio::sync::watch::channel(()); + let (background_processor_stop_sender, _) = tokio::sync::watch::channel(()); let is_running = Arc::new(RwLock::new(false)); Ok(Node { runtime, stop_sender, + background_processor_stop_sender, config, wallet, chain_source, diff --git a/src/lib.rs b/src/lib.rs index 1604d1b461..e77a2f8a68 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -180,6 +180,7 @@ uniffi::include_scaffolding!("ldk_node"); pub struct Node { runtime: Arc, stop_sender: tokio::sync::watch::Sender<()>, + background_processor_stop_sender: tokio::sync::watch::Sender<()>, config: Arc, wallet: Arc, chain_source: Arc, @@ -525,7 +526,7 @@ impl Node { let background_logger = Arc::clone(&self.logger); let background_error_logger = Arc::clone(&self.logger); let background_scorer = Arc::clone(&self.scorer); - let stop_bp = self.stop_sender.subscribe(); + let stop_bp = self.background_processor_stop_sender.subscribe(); let sleeper_logger = Arc::clone(&self.logger); let sleeper = move |d| { let mut stop = stop_bp.clone(); @@ -607,18 +608,20 @@ impl Node { // Stop any runtime-dependant chain sources. self.chain_source.stop(); - // Stop the runtime. - match self.stop_sender.send(()) { - Ok(_) => log_trace!(self.logger, "Sent shutdown signal to background tasks."), - Err(e) => { + // Stop background tasks. + self.stop_sender + .send(()) + .map(|_| { + log_trace!(self.logger, "Sent shutdown signal to background tasks."); + }) + .unwrap_or_else(|e| { log_error!( self.logger, "Failed to send shutdown signal. This should never happen: {}", e ); debug_assert!(false); - }, - } + }); // Cancel cancellable background tasks self.runtime.abort_cancellable_background_tasks(); @@ -634,6 +637,21 @@ impl Node { // Wait until non-cancellable background tasks (mod LDK's background processor) are done. self.runtime.wait_on_background_tasks(); + // Stop the background processor. + self.background_processor_stop_sender + .send(()) + .map(|_| { + log_trace!(self.logger, "Sent shutdown signal to background processor."); + }) + .unwrap_or_else(|e| { + log_error!( + self.logger, + "Failed to send shutdown signal. This should never happen: {}", + e + ); + debug_assert!(false); + }); + // Finally, wait until background processing stopped, at least until a timeout is reached. self.runtime.wait_on_background_processor_task(); From c428c4ca8a16cbf0ed377560b7f0bdbff4b1742c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 20 Aug 2025 09:17:43 +0200 Subject: [PATCH 131/177] Remove duplicate call to `chain_source.stop()` This was introduced during a rebase. Here we simply drop the redundant call. --- src/lib.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index e77a2f8a68..da86fce73d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -605,9 +605,6 @@ impl Node { log_info!(self.logger, "Shutting down LDK Node with node ID {}...", self.node_id()); - // Stop any runtime-dependant chain sources. - self.chain_source.stop(); - // Stop background tasks. self.stop_sender .send(()) @@ -630,13 +627,13 @@ impl Node { self.peer_manager.disconnect_all_peers(); log_debug!(self.logger, "Disconnected all network peers."); + // Wait until non-cancellable background tasks (mod LDK's background processor) are done. + self.runtime.wait_on_background_tasks(); + // Stop any runtime-dependant chain sources. self.chain_source.stop(); log_debug!(self.logger, "Stopped chain sources."); - // Wait until non-cancellable background tasks (mod LDK's background processor) are done. - self.runtime.wait_on_background_tasks(); - // Stop the background processor. self.background_processor_stop_sender .send(()) From ab3d78d1ecd05a755c836915284e5ca60c65692a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 22 May 2025 11:17:24 +0200 Subject: [PATCH 132/177] Use `crate::runtime::Runtime` for `VssStore` .. which now gives us cleaner reuse/handling of outer runtime contexts, cleanup on `Drop`, etc. --- src/builder.rs | 5 +-- src/io/vss_store.rs | 107 +++++++++++++++++++++++++------------------- 2 files changed, 61 insertions(+), 51 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 7f15cced69..02ad77ea25 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -694,10 +694,7 @@ impl NodeBuilder { 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 VssStore: {}", e); - BuildError::KVStoreSetupFailed - })?; + VssStore::new(vss_url, store_id, vss_seed_bytes, header_provider, Arc::clone(&runtime)); build_with_store_internal( config, self.chain_data_source_config.as_ref(), diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 296eaabe32..e2cfc3c7b0 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -6,6 +6,8 @@ // accordance with one or both of these licenses. use crate::io::utils::check_namespace_key_validity; +use crate::runtime::Runtime; + use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use lightning::io::{self, Error, ErrorKind}; use lightning::util::persist::KVStore; @@ -15,7 +17,6 @@ use rand::RngCore; use std::panic::RefUnwindSafe; use std::sync::Arc; use std::time::Duration; -use tokio::runtime::Runtime; use vss_client::client::VssClient; use vss_client::error::VssError; use vss_client::headers::VssHeaderProvider; @@ -41,7 +42,7 @@ type CustomRetryPolicy = FilteredRetryPolicy< pub struct VssStore { client: VssClient, store_id: String, - runtime: Runtime, + runtime: Arc, storable_builder: StorableBuilder, key_obfuscator: KeyObfuscator, } @@ -49,9 +50,8 @@ pub struct VssStore { impl VssStore { pub(crate) fn new( base_url: String, store_id: String, vss_seed: [u8; 32], - header_provider: Arc, - ) -> io::Result { - let runtime = tokio::runtime::Builder::new_multi_thread().enable_all().build()?; + header_provider: Arc, runtime: Arc, + ) -> Self { let (data_encryption_key, obfuscation_master_key) = derive_data_encryption_and_obfuscation_keys(&vss_seed); let key_obfuscator = KeyObfuscator::new(obfuscation_master_key); @@ -70,7 +70,7 @@ impl VssStore { }) as _); let client = VssClient::new_with_headers(base_url, retry_policy, header_provider); - Ok(Self { client, store_id, runtime, storable_builder, key_obfuscator }) + Self { client, store_id, runtime, storable_builder, key_obfuscator } } fn build_key( @@ -136,19 +136,16 @@ impl KVStore for VssStore { store_id: self.store_id.clone(), key: self.build_key(primary_namespace, secondary_namespace, key)?, }; - - let resp = - tokio::task::block_in_place(|| self.runtime.block_on(self.client.get_object(&request))) - .map_err(|e| { - let msg = format!( - "Failed to read from key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - match e { - VssError::NoSuchKeyError(..) => Error::new(ErrorKind::NotFound, msg), - _ => Error::new(ErrorKind::Other, msg), - } - })?; + let resp = self.runtime.block_on(self.client.get_object(&request)).map_err(|e| { + let msg = format!( + "Failed to read from key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + match e { + VssError::NoSuchKeyError(..) => Error::new(ErrorKind::NotFound, msg), + _ => Error::new(ErrorKind::Other, msg), + } + })?; // unwrap safety: resp.value must be always present for a non-erroneous VSS response, otherwise // it is an API-violation which is converted to [`VssError::InternalServerError`] in [`VssClient`] let storable = Storable::decode(&resp.value.unwrap().value[..]).map_err(|e| { @@ -179,14 +176,13 @@ impl KVStore for VssStore { delete_items: vec![], }; - tokio::task::block_in_place(|| self.runtime.block_on(self.client.put_object(&request))) - .map_err(|e| { - let msg = format!( - "Failed to write to key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - Error::new(ErrorKind::Other, msg) - })?; + self.runtime.block_on(self.client.put_object(&request)).map_err(|e| { + let msg = format!( + "Failed to write to key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + Error::new(ErrorKind::Other, msg) + })?; Ok(()) } @@ -204,30 +200,29 @@ impl KVStore for VssStore { }), }; - tokio::task::block_in_place(|| self.runtime.block_on(self.client.delete_object(&request))) - .map_err(|e| { - let msg = format!( - "Failed to delete key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - Error::new(ErrorKind::Other, msg) - })?; + self.runtime.block_on(self.client.delete_object(&request)).map_err(|e| { + let msg = format!( + "Failed to delete key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + Error::new(ErrorKind::Other, msg) + })?; Ok(()) } fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; - let keys = tokio::task::block_in_place(|| { - self.runtime.block_on(self.list_all_keys(primary_namespace, secondary_namespace)) - }) - .map_err(|e| { - let msg = format!( - "Failed to retrieve keys in namespace: {}/{} : {}", - primary_namespace, secondary_namespace, e - ); - Error::new(ErrorKind::Other, msg) - })?; + let keys = self + .runtime + .block_on(self.list_all_keys(primary_namespace, secondary_namespace)) + .map_err(|e| { + let msg = format!( + "Failed to retrieve keys in namespace: {}/{} : {}", + primary_namespace, secondary_namespace, e + ); + Error::new(ErrorKind::Other, msg) + })?; Ok(keys) } @@ -266,10 +261,27 @@ mod tests { use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng, RngCore}; use std::collections::HashMap; + use tokio::runtime; use vss_client::headers::FixedHeaders; #[test] - fn read_write_remove_list_persist() { + fn vss_read_write_remove_list_persist() { + let runtime = Arc::new(Runtime::new().unwrap()); + let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); + let mut rng = thread_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 vss_store = + VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider, runtime).unwrap(); + + do_read_write_remove_list_persist(&vss_store); + } + + #[tokio::test(flavor = "multi_thread", worker_threads = 1)] + async fn vss_read_write_remove_list_persist_in_runtime_context() { + let runtime = Arc::new(Runtime::new().unwrap()); let vss_base_url = std::env::var("TEST_VSS_BASE_URL").unwrap(); let mut rng = thread_rng(); let rand_store_id: String = (0..7).map(|_| rng.sample(Alphanumeric) as char).collect(); @@ -277,8 +289,9 @@ mod tests { rng.fill_bytes(&mut vss_seed); let header_provider = Arc::new(FixedHeaders::new(HashMap::new())); let vss_store = - VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider).unwrap(); + VssStore::new(vss_base_url, rand_store_id, vss_seed, header_provider, runtime).unwrap(); do_read_write_remove_list_persist(&vss_store); + drop(vss_store) } } From 24e6947be9a4b66d85a0bb8125ee7863678484e4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 21 Aug 2025 11:53:15 +0200 Subject: [PATCH 133/177] Require 16kb pages sizes for Android builds Google announced that starting Nov 2025 all apps in the Play Store will be required to be compatible with 16KB page sizes (cf. https://developer.android.com/guide/practices/page-sizes). Here we update our linker flags to ensure the generated shared libraries meet that requirement. --- scripts/uniffi_bindgen_generate_kotlin_android.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/scripts/uniffi_bindgen_generate_kotlin_android.sh b/scripts/uniffi_bindgen_generate_kotlin_android.sh index 142e5f75d9..c87db59242 100755 --- a/scripts/uniffi_bindgen_generate_kotlin_android.sh +++ b/scripts/uniffi_bindgen_generate_kotlin_android.sh @@ -35,9 +35,9 @@ case "$OSTYPE" in PATH="$ANDROID_NDK_ROOT/toolchains/llvm/prebuilt/$LLVM_ARCH_PATH/bin:$PATH" rustup target add x86_64-linux-android aarch64-linux-android armv7-linux-androideabi -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target x86_64-linux-android || exit 1 -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --features uniffi --target armv7-linux-androideabi || exit 1 -CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target aarch64-linux-android || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER="x86_64-linux-android21-clang" CC="x86_64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target x86_64-linux-android || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="armv7a-linux-androideabi21-clang" CC="armv7a-linux-androideabi21-clang" cargo build --profile release-smaller --features uniffi --target armv7-linux-androideabi || exit 1 +RUSTFLAGS="-C link-args=-Wl,-z,max-page-size=16384,-z,common-page-size=16384" CFLAGS="-D__ANDROID_MIN_SDK_VERSION__=21" AR=llvm-ar CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="aarch64-linux-android21-clang" CC="aarch64-linux-android21-clang" cargo build --profile release-smaller --features uniffi --target aarch64-linux-android || exit 1 $UNIFFI_BINDGEN_BIN generate bindings/ldk_node.udl --language kotlin --config uniffi-android.toml -o "$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/kotlin || exit 1 JNI_LIB_DIR="$BINDINGS_DIR"/"$PROJECT_DIR"/lib/src/main/jniLibs/ From 02208abdd6eb33e8f59cfada192ef007c24389e1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 2 Sep 2025 17:01:43 +0200 Subject: [PATCH 134/177] Relax `MaximumFeeEstimate` confirmation target estimation `MaximumFeeEstimate` is mostly used for protection against fee-inflation attacks. As users were previously impacted by this limit being too restrictive (read: too low), we bump it here a bit to give them some leeway. --- src/fee_estimator.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/fee_estimator.rs b/src/fee_estimator.rs index f000245aa9..f8ddcd5fd2 100644 --- a/src/fee_estimator.rs +++ b/src/fee_estimator.rs @@ -150,6 +150,17 @@ pub(crate) fn apply_post_estimation_adjustments( .max(FEERATE_FLOOR_SATS_PER_KW as u64); FeeRate::from_sat_per_kwu(slightly_less_than_background) }, + ConfirmationTarget::Lightning(LdkConfirmationTarget::MaximumFeeEstimate) => { + // MaximumFeeEstimate is mostly used for protection against fee-inflation attacks. As + // users were previously impacted by this limit being too restrictive (read: too low), + // we bump it here a bit to give them some leeway. + let slightly_bump = estimated_rate + .to_sat_per_kwu() + .saturating_mul(11) + .saturating_div(10) + .saturating_add(2500); + FeeRate::from_sat_per_kwu(slightly_bump) + }, _ => estimated_rate, } } From 66b1a883f48d70faecd946cc522d803c21b811d7 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 31 Jul 2025 08:54:54 +0200 Subject: [PATCH 135/177] Stop migrating spendable outputs from pre-v0.3 format As part of the version v0.3 release we switched to the upstreamed `OutputSweeper` which slightly changed our serialization format, having us run a migration step on startup for backwards compatibility ever since. Here we drop the migration code running on startup, for simplicity's sake, but also because it's going to be async going forward and we currently don't have a runtime available on startup (which might change soon, but still). As the v0.3 release now well over a year ago, it's very unlikely that there are any v0.2 (or even v0.3) users left. If there are any affected users left, they'll first have to upgrade to any version pre-v0.7, startup, and then upgrade to v0.7 or later. --- src/balance.rs | 13 +++++-- src/builder.rs | 16 +------- src/io/mod.rs | 5 --- src/io/utils.rs | 100 +----------------------------------------------- src/lib.rs | 1 - src/sweep.rs | 47 ----------------------- 6 files changed, 12 insertions(+), 170 deletions(-) delete mode 100644 src/sweep.rs diff --git a/src/balance.rs b/src/balance.rs index b5e2f5eb71..d0ebc310b0 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -5,17 +5,16 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::sweep::value_from_descriptor; - use lightning::chain::channelmonitor::Balance as LdkBalance; use lightning::chain::channelmonitor::BalanceSource; use lightning::ln::types::ChannelId; +use lightning::sign::SpendableOutputDescriptor; use lightning::util::sweep::{OutputSpendStatus, TrackedSpendableOutput}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; use bitcoin::secp256k1::PublicKey; -use bitcoin::{BlockHash, Txid}; +use bitcoin::{Amount, BlockHash, Txid}; /// Details of the known available balances returned by [`Node::list_balances`]. /// @@ -385,3 +384,11 @@ impl PendingSweepBalance { } } } + +fn value_from_descriptor(descriptor: &SpendableOutputDescriptor) -> Amount { + match &descriptor { + SpendableOutputDescriptor::StaticOutput { output, .. } => output.value, + SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value, + SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value, + } +} diff --git a/src/builder.rs b/src/builder.rs index e160d1f6eb..289c2954c1 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -25,7 +25,7 @@ use crate::io::{ use crate::liquidity::{ LSPS1ClientConfig, LSPS2ClientConfig, LSPS2ServiceConfig, LiquiditySourceBuilder, }; -use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger}; +use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; use crate::peer_store::PeerStore; use crate::runtime::Runtime; @@ -1627,20 +1627,6 @@ fn build_with_store_internal( }, }; - match io::utils::migrate_deprecated_spendable_outputs( - Arc::clone(&output_sweeper), - Arc::clone(&kv_store), - Arc::clone(&logger), - ) { - Ok(()) => { - log_info!(logger, "Successfully migrated OutputSweeper data."); - }, - Err(e) => { - log_error!(logger, "Failed to migrate OutputSweeper data: {}", e); - return Err(BuildError::ReadFailed); - }, - } - let event_queue = match io::utils::read_event_queue(Arc::clone(&kv_store), Arc::clone(&logger)) { Ok(event_queue) => Arc::new(event_queue), diff --git a/src/io/mod.rs b/src/io/mod.rs index 3192dbb863..7a52a5c984 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -27,11 +27,6 @@ pub(crate) const PEER_INFO_PERSISTENCE_KEY: &str = "peers"; pub(crate) const PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = "payments"; pub(crate) const PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; -/// The spendable output information used to persisted under this prefix until LDK Node v0.3.0. -pub(crate) const DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE: &str = - "spendable_outputs"; -pub(crate) const DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE: &str = ""; - /// The node metrics will be persisted under this key. pub(crate) const NODE_METRICS_PRIMARY_NAMESPACE: &str = ""; pub(crate) const NODE_METRICS_SECONDARY_NAMESPACE: &str = ""; diff --git a/src/io/utils.rs b/src/io/utils.rs index b5537ed7d0..06a1017ba1 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -15,7 +15,6 @@ use crate::io::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::peer_store::PeerStore; -use crate::sweep::DeprecatedSpendableOutputInfo; use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; @@ -33,7 +32,7 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; use lightning::util::string::PrintableString; -use lightning::util::sweep::{OutputSpendStatus, OutputSweeper}; +use lightning::util::sweep::OutputSweeper; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; @@ -258,103 +257,6 @@ pub(crate) fn read_output_sweeper( }) } -/// Read previously persisted spendable output information from the store and migrate to the -/// upstreamed `OutputSweeper`. -/// -/// We first iterate all `DeprecatedSpendableOutputInfo`s and have them tracked by the new -/// `OutputSweeper`. In order to be certain the initial output spends will happen in a single -/// transaction (and safe on-chain fees), we batch them to happen at current height plus two -/// blocks. Lastly, we remove the previously persisted data once we checked they are tracked and -/// awaiting their initial spend at the correct height. -/// -/// Note that this migration will be run in the `Builder`, i.e., at the time when the migration is -/// happening no background sync is ongoing, so we shouldn't have a risk of interleaving block -/// connections during the migration. -pub(crate) fn migrate_deprecated_spendable_outputs( - sweeper: Arc, kv_store: Arc, logger: L, -) -> Result<(), std::io::Error> -where - L::Target: LdkLogger, -{ - let best_block = sweeper.current_best_block(); - - for stored_key in kv_store.list( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - )? { - let mut reader = Cursor::new(kv_store.read( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &stored_key, - )?); - let output = DeprecatedSpendableOutputInfo::read(&mut reader).map_err(|e| { - log_error!(logger, "Failed to deserialize SpendableOutputInfo: {}", e); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to deserialize SpendableOutputInfo", - ) - })?; - let descriptors = vec![output.descriptor.clone()]; - let spend_delay = Some(best_block.height + 2); - sweeper - .track_spendable_outputs(descriptors, output.channel_id, true, spend_delay) - .map_err(|_| { - log_error!(logger, "Failed to track spendable outputs. Aborting migration, will retry in the future."); - std::io::Error::new( - std::io::ErrorKind::InvalidData, - "Failed to track spendable outputs. Aborting migration, will retry in the future.", - ) - })?; - - if let Some(tracked_spendable_output) = - sweeper.tracked_spendable_outputs().iter().find(|o| o.descriptor == output.descriptor) - { - match tracked_spendable_output.status { - OutputSpendStatus::PendingInitialBroadcast { delayed_until_height } => { - if delayed_until_height == spend_delay { - kv_store.remove( - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - DEPRECATED_SPENDABLE_OUTPUT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - &stored_key, - false, - )?; - } else { - debug_assert!(false, "Unexpected status in OutputSweeper migration."); - log_error!(logger, "Unexpected status in OutputSweeper migration."); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - } - }, - _ => { - debug_assert!(false, "Unexpected status in OutputSweeper migration."); - log_error!(logger, "Unexpected status in OutputSweeper migration."); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - }, - } - } else { - debug_assert!( - false, - "OutputSweeper failed to track and persist outputs during migration." - ); - log_error!( - logger, - "OutputSweeper failed to track and persist outputs during migration." - ); - return Err(std::io::Error::new( - std::io::ErrorKind::Other, - "Failed to migrate OutputSweeper state.", - )); - } - } - - Ok(()) -} - pub(crate) fn read_node_metrics( kv_store: Arc, logger: L, ) -> Result diff --git a/src/lib.rs b/src/lib.rs index da86fce73d..9035d53618 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -95,7 +95,6 @@ mod message_handler; pub mod payment; mod peer_store; mod runtime; -mod sweep; mod tx_broadcaster; mod types; mod wallet; diff --git a/src/sweep.rs b/src/sweep.rs deleted file mode 100644 index ba10869b8a..0000000000 --- a/src/sweep.rs +++ /dev/null @@ -1,47 +0,0 @@ -// This file is Copyright its original authors, visible in version control history. -// -// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in -// accordance with one or both of these licenses. - -//! The output sweeper used to live here before we upstreamed it to `rust-lightning` and migrated -//! to the upstreamed version with LDK Node v0.3.0 (May 2024). We should drop this module entirely -//! once sufficient time has passed for us to be confident any users completed the migration. - -use lightning::impl_writeable_tlv_based; -use lightning::ln::types::ChannelId; -use lightning::sign::SpendableOutputDescriptor; - -use bitcoin::{Amount, BlockHash, Transaction}; - -#[derive(Clone, Debug, PartialEq, Eq)] -pub(crate) struct DeprecatedSpendableOutputInfo { - pub(crate) id: [u8; 32], - pub(crate) descriptor: SpendableOutputDescriptor, - pub(crate) channel_id: Option, - pub(crate) first_broadcast_hash: Option, - pub(crate) latest_broadcast_height: Option, - pub(crate) latest_spending_tx: Option, - pub(crate) confirmation_height: Option, - pub(crate) confirmation_hash: Option, -} - -impl_writeable_tlv_based!(DeprecatedSpendableOutputInfo, { - (0, id, required), - (2, descriptor, required), - (4, channel_id, option), - (6, first_broadcast_hash, option), - (8, latest_broadcast_height, option), - (10, latest_spending_tx, option), - (12, confirmation_height, option), - (14, confirmation_hash, option), -}); - -pub(crate) fn value_from_descriptor(descriptor: &SpendableOutputDescriptor) -> Amount { - match &descriptor { - SpendableOutputDescriptor::StaticOutput { output, .. } => output.value, - SpendableOutputDescriptor::DelayedPaymentOutput(output) => output.output.value, - SpendableOutputDescriptor::StaticPaymentOutput(output) => output.output.value, - } -} From 02c2663715041a6c04e0627f7095d981505d37af Mon Sep 17 00:00:00 2001 From: moisesPomilio <93723302+moisesPompilio@users.noreply.github.com> Date: Tue, 9 Sep 2025 10:24:53 -0300 Subject: [PATCH 136/177] Check last_best_block_hash before updating fee_rate to avoid unnecessary updates Close #520 --- src/chain/bitcoind.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index c282a6141d..a808f62947 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -261,6 +261,7 @@ impl BitcoindChainSource { log_info!(self.logger, "Starting continuous polling for chain updates."); // Start the polling loop. + let mut last_best_block_hash = None; loop { tokio::select! { _ = stop_sync_receiver.changed() => { @@ -278,7 +279,12 @@ impl BitcoindChainSource { ).await; } _ = fee_rate_update_interval.tick() => { - let _ = self.update_fee_rate_estimates().await; + if last_best_block_hash != Some(channel_manager.current_best_block().block_hash) { + let update_res = self.update_fee_rate_estimates().await; + if update_res.is_ok() { + last_best_block_hash = Some(channel_manager.current_best_block().block_hash); + } + } } } } From ed12e65591813d22da5be51b0af8d92e8e7ba5cd Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 6 Feb 2025 16:14:34 +0100 Subject: [PATCH 137/177] Bump to LDK main (4e32d852) We bump our LDK dependency to 0.2-devel, up to commit `4e32d85249359d8ef8ece97d89848e40154363ab`. --- Cargo.toml | 51 +++-- bindings/ldk_node.udl | 81 ++++---- src/builder.rs | 49 +++-- src/chain/bitcoind.rs | 12 +- src/chain/electrum.rs | 9 +- src/chain/esplora.rs | 15 +- src/config.rs | 12 +- src/data_store.rs | 22 +-- src/event.rs | 82 +++++--- src/ffi/types.rs | 94 +++++++-- src/io/sqlite_store/migrations.rs | 2 +- src/io/sqlite_store/mod.rs | 11 +- src/io/test_utils.rs | 24 +-- src/io/utils.rs | 9 +- src/io/vss_store.rs | 10 +- src/lib.rs | 65 +++---- src/liquidity.rs | 154 +++++---------- src/message_handler.rs | 3 +- src/payment/bolt11.rs | 189 +++++++++--------- src/payment/bolt12.rs | 55 +++--- src/payment/mod.rs | 84 -------- src/payment/spontaneous.rs | 58 +++--- src/payment/store.rs | 2 +- src/peer_store.rs | 2 +- src/types.rs | 29 +-- src/wallet/mod.rs | 311 ++++++++++++++---------------- tests/common/mod.rs | 14 +- tests/integration_tests_rust.rs | 19 +- 28 files changed, 690 insertions(+), 778 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 96a9eea534..aaaa55f39b 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,38 +28,53 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -lightning = { version = "0.1.0", features = ["std"] } -lightning-types = { version = "0.2.0" } -lightning-invoice = { version = "0.33.0", features = ["std"] } -lightning-net-tokio = { version = "0.1.0" } -lightning-persister = { version = "0.1.0" } -lightning-background-processor = { version = "0.1.0", features = ["futures"] } -lightning-rapid-gossip-sync = { version = "0.1.0" } -lightning-block-sync = { version = "0.1.0", features = ["rpc-client", "rest-client", "tokio"] } -lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } -lightning-liquidity = { version = "0.1.0", features = ["std"] } +#lightning = { version = "0.1.0", features = ["std"] } +#lightning-types = { version = "0.2.0" } +#lightning-invoice = { version = "0.33.0", features = ["std"] } +#lightning-net-tokio = { version = "0.1.0" } +#lightning-persister = { version = "0.1.0" } +#lightning-background-processor = { version = "0.1.0" } +#lightning-rapid-gossip-sync = { version = "0.1.0" } +#lightning-block-sync = { version = "0.1.0", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } +#lightning-liquidity = { version = "0.1.0", features = ["std"] } +#lightning-macros = { version = "0.1.0" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } #lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["futures"] } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rpc-client", "tokio"] } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rest-client", "rpc-client", "tokio"] } #lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum", "time"] } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } + +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["esplora-async-https", "electrum", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } #lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } #lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } #lightning-persister = { path = "../rust-lightning/lightning-persister" } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor", features = ["futures"] } +#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rpc-client", "tokio"] } +#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } #lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } +#lightning-macros = { path = "../rust-lightning/lightning-macros" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -78,11 +93,6 @@ rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] } - -# FIXME: This was introduced to decouple the `bdk_esplora` and -# `lightning-transaction-sync` APIs. We should drop it as part of the upgrade -# to LDK 0.2. -esplora-client_0_11 = { package = "esplora-client", version = "0.11", default-features = false, features = ["tokio", "async-https-rustls"] } electrum-client = { version = "0.24.0", default-features = true } libc = "0.2" uniffi = { version = "0.28.3", features = ["build"], optional = true } @@ -97,8 +107,9 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -lightning = { version = "0.1.0", features = ["std", "_test_utils"] } +#lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 076d7fc9b1..b9bab61e8a 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -12,7 +12,7 @@ dictionary Config { sequence trusted_peers_0conf; u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; - SendingParameters? sending_parameters; + RouteParametersConfig? route_parameters; }; dictionary AnchorChannelsConfig { @@ -167,13 +167,13 @@ interface Bolt11InvoiceDescription { interface Bolt11Payment { [Throws=NodeError] - PaymentId send([ByRef]Bolt11Invoice invoice, SendingParameters? sending_parameters); + PaymentId send([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, SendingParameters? sending_parameters); + PaymentId send_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes([ByRef]Bolt11Invoice invoice); + void send_probes([ByRef]Bolt11Invoice invoice, RouteParametersConfig? route_parameters); [Throws=NodeError] - void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat); + void send_probes_using_amount([ByRef]Bolt11Invoice invoice, u64 amount_msat, RouteParametersConfig? route_parameters); [Throws=NodeError] void claim_for_hash(PaymentHash payment_hash, u64 claimable_amount_msat, PaymentPreimage preimage); [Throws=NodeError] @@ -213,13 +213,13 @@ interface Bolt12Payment { interface SpontaneousPayment { [Throws=NodeError] - PaymentId send(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters); + PaymentId send(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, SendingParameters? sending_parameters, sequence custom_tlvs); + PaymentId send_with_custom_tlvs(u64 amount_msat, PublicKey node_id, RouteParametersConfig? route_parameters, sequence custom_tlvs); [Throws=NodeError] - PaymentId send_with_preimage(u64 amount_msat, PublicKey node_id, PaymentPreimage preimage, SendingParameters? sending_parameters); + PaymentId send_with_preimage(u64 amount_msat, PublicKey node_id, PaymentPreimage preimage, RouteParametersConfig? route_parameters); [Throws=NodeError] - PaymentId send_with_preimage_and_custom_tlvs(u64 amount_msat, PublicKey node_id, sequence custom_tlvs, PaymentPreimage preimage, SendingParameters? sending_parameters); + PaymentId send_with_preimage_and_custom_tlvs(u64 amount_msat, PublicKey node_id, sequence custom_tlvs, PaymentPreimage preimage, RouteParametersConfig? route_parameters); [Throws=NodeError] void send_probes(u64 amount_msat, PublicKey node_id); }; @@ -254,7 +254,7 @@ interface LSPS1Liquidity { [Throws=NodeError] LSPS1OrderStatus request_channel(u64 lsp_balance_sat, u64 client_balance_sat, u32 channel_expiry_blocks, boolean announce_channel); [Throws=NodeError] - LSPS1OrderStatus check_order_status(OrderId order_id); + LSPS1OrderStatus check_order_status(LSPS1OrderId order_id); }; [Error] @@ -392,7 +392,7 @@ enum PaymentFailureReason { [Enum] interface ClosureReason { CounterpartyForceClosed(UntrustedString peer_msg); - HolderForceClosed(boolean? broadcasted_latest_txn); + HolderForceClosed(boolean? broadcasted_latest_txn, string message); LegacyCooperativeClosure(); CounterpartyInitiatedCooperativeClosure(); LocallyInitiatedCooperativeClosure(); @@ -402,8 +402,9 @@ interface ClosureReason { DisconnectedPeer(); OutdatedChannelManager(); CounterpartyCoopClosedUnfundedChannel(); + LocallyCoopClosedUnfundedChannel(); FundingBatchClosure(); - HTLCsTimedOut(); + HTLCsTimedOut( PaymentHash? payment_hash ); PeerFeerateTooLow(u32 peer_feerate_sat_per_kw, u32 required_feerate_sat_per_kw); }; @@ -456,11 +457,11 @@ dictionary PaymentDetails { u64 latest_update_timestamp; }; -dictionary SendingParameters { - MaxTotalRoutingFeeLimit? max_total_routing_fee_msat; - u32? max_total_cltv_expiry_delta; - u8? max_path_count; - u8? max_channel_saturation_power_of_half; +dictionary RouteParametersConfig { + u64? max_total_routing_fee_msat; + u32 max_total_cltv_expiry_delta; + u8 max_path_count; + u8 max_channel_saturation_power_of_half; }; dictionary CustomTlvRecord { @@ -469,13 +470,13 @@ dictionary CustomTlvRecord { }; dictionary LSPS1OrderStatus { - OrderId order_id; - OrderParameters order_params; - PaymentInfo payment_options; - ChannelOrderInfo? channel_state; + LSPS1OrderId order_id; + LSPS1OrderParams order_params; + LSPS1PaymentInfo payment_options; + LSPS1ChannelInfo? channel_state; }; -dictionary OrderParameters { +dictionary LSPS1OrderParams { u64 lsp_balance_sat; u64 client_balance_sat; u16 required_channel_confirmations; @@ -485,22 +486,22 @@ dictionary OrderParameters { boolean announce_channel; }; -dictionary PaymentInfo { - Bolt11PaymentInfo? bolt11; - OnchainPaymentInfo? onchain; +dictionary LSPS1PaymentInfo { + LSPS1Bolt11PaymentInfo? bolt11; + LSPS1OnchainPaymentInfo? onchain; }; -dictionary Bolt11PaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1Bolt11PaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Bolt11Invoice invoice; }; -dictionary OnchainPaymentInfo { - PaymentState state; - DateTime expires_at; +dictionary LSPS1OnchainPaymentInfo { + LSPS1PaymentState state; + LSPSDateTime expires_at; u64 fee_total_sat; u64 order_total_sat; Address address; @@ -509,24 +510,18 @@ dictionary OnchainPaymentInfo { Address? refund_onchain_address; }; -dictionary ChannelOrderInfo { - DateTime funded_at; +dictionary LSPS1ChannelInfo { + LSPSDateTime funded_at; OutPoint funding_outpoint; - DateTime expires_at; + LSPSDateTime expires_at; }; -enum PaymentState { +enum LSPS1PaymentState { "ExpectPayment", "Paid", "Refunded", }; -[Enum] -interface MaxTotalRoutingFeeLimit { - None (); - Some ( u64 amount_msat ); -}; - [NonExhaustive] enum Network { "Bitcoin", @@ -861,7 +856,7 @@ typedef string UntrustedString; typedef string NodeAlias; [Custom] -typedef string OrderId; +typedef string LSPS1OrderId; [Custom] -typedef string DateTime; +typedef string LSPSDateTime; diff --git a/src/builder.rs b/src/builder.rs index 289c2954c1..094c21e726 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -48,7 +48,7 @@ use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters, }; -use lightning::sign::EntropySource; +use lightning::sign::{EntropySource, NodeSigner}; use lightning::util::persist::{ read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, @@ -173,17 +173,17 @@ pub enum BuildError { RuntimeSetupFailed, /// We failed to read data from the [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync ReadFailed, /// We failed to write data to the [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync WriteFailed, /// We failed to access the given `storage_dir_path`. StoragePathAccessFailed, /// We failed to setup our [`KVStore`]. /// - /// [`KVStore`]: lightning::util::persist::KVStore + /// [`KVStore`]: lightning::util::persist::KVStoreSync KVStoreSetupFailed, /// We failed to setup the onchain wallet. WalletSetupFailed, @@ -1275,15 +1275,6 @@ fn build_with_store_internal( }, }; - // Initialize the ChainMonitor - let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( - Some(Arc::clone(&chain_source)), - Arc::clone(&tx_broadcaster), - Arc::clone(&logger), - Arc::clone(&fee_estimator), - Arc::clone(&kv_store), - )); - // Initialize the KeysManager let cur_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH).map_err(|e| { log_error!(logger, "Failed to get current time: {}", e); @@ -1299,6 +1290,19 @@ fn build_with_store_internal( Arc::clone(&logger), )); + let peer_storage_key = keys_manager.get_peer_storage_key(); + + // Initialize the ChainMonitor + let chain_monitor: Arc = Arc::new(chainmonitor::ChainMonitor::new( + Some(Arc::clone(&chain_source)), + Arc::clone(&tx_broadcaster), + Arc::clone(&logger), + Arc::clone(&fee_estimator), + Arc::clone(&kv_store), + Arc::clone(&keys_manager), + peer_storage_key, + )); + // Initialize the network graph, scorer, and router let network_graph = match io::utils::read_network_graph(Arc::clone(&kv_store), Arc::clone(&logger)) { @@ -1359,17 +1363,6 @@ fn build_with_store_internal( }; let mut user_config = default_user_config(&config); - if liquidity_source_config.and_then(|lsc| lsc.lsps2_client.as_ref()).is_some() { - // Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll - // check that they don't take too much before claiming. - user_config.channel_config.accept_underpaying_htlcs = true; - - // FIXME: When we're an LSPS2 client, set maximum allowed inbound HTLC value in flight - // to 100%. We should eventually be able to set this on a per-channel basis, but for - // now we just bump the default for all channels. - user_config.channel_handshake_config.max_inbound_htlc_value_in_flight_percent_of_channel = - 100; - } if liquidity_source_config.and_then(|lsc| lsc.lsps2_service.as_ref()).is_some() { // If we act as an LSPS2 service, we need to to be able to intercept HTLCs and forward the @@ -1447,8 +1440,8 @@ fn build_with_store_internal( // Give ChannelMonitors to ChainMonitor for (_blockhash, channel_monitor) in channel_monitors.into_iter() { - let funding_outpoint = channel_monitor.get_funding_txo().0; - chain_monitor.watch_channel(funding_outpoint, channel_monitor).map_err(|e| { + let channel_id = channel_monitor.channel_id(); + chain_monitor.watch_channel(channel_id, channel_monitor).map_err(|e| { log_error!(logger, "Failed to watch channel monitor: {:?}", e); BuildError::InvalidChannelMonitor })?; @@ -1560,6 +1553,7 @@ fn build_with_store_internal( as Arc, onion_message_handler: Arc::clone(&onion_messenger), custom_message_handler, + send_only_message_handler: Arc::clone(&chain_monitor), }, GossipSync::Rapid(_) => MessageHandler { chan_handler: Arc::clone(&channel_manager), @@ -1567,6 +1561,7 @@ fn build_with_store_internal( as Arc, onion_message_handler: Arc::clone(&onion_messenger), custom_message_handler, + send_only_message_handler: Arc::clone(&chain_monitor), }, GossipSync::None => { unreachable!("We must always have a gossip sync!"); @@ -1611,7 +1606,7 @@ fn build_with_store_internal( Ok(output_sweeper) => Arc::new(output_sweeper), Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - Arc::new(OutputSweeper::new( + Arc::new(OutputSweeper::new_with_kv_store_sync( channel_manager.current_best_block(), Arc::clone(&tx_broadcaster), Arc::clone(&fee_estimator), diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index c282a6141d..7157e5a4fd 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -173,7 +173,7 @@ impl BitcoindChainSource { if let Some(worst_channel_monitor_block_hash) = chain_monitor .list_monitors() .iter() - .flat_map(|(txo, _)| chain_monitor.get_monitor(*txo)) + .flat_map(|channel_id| chain_monitor.get_monitor(*channel_id)) .map(|m| m.current_best_block()) .min_by_key(|b| b.height) .map(|b| b.block_hash) @@ -1381,11 +1381,11 @@ impl Listen for ChainListener { self.output_sweeper.block_connected(block, height); } - fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { - self.onchain_wallet.block_disconnected(header, height); - self.channel_manager.block_disconnected(header, height); - self.chain_monitor.block_disconnected(header, height); - self.output_sweeper.block_disconnected(header, height); + fn blocks_disconnected(&self, fork_point_block: lightning::chain::BestBlock) { + self.onchain_wallet.blocks_disconnected(fork_point_block); + self.channel_manager.blocks_disconnected(fork_point_block); + self.chain_monitor.blocks_disconnected(fork_point_block); + self.output_sweeper.blocks_disconnected(fork_point_block); } } diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index b6d37409ba..40d929ce7f 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -402,7 +402,7 @@ impl ElectrumRuntimeStatus { struct ElectrumRuntimeClient { electrum_client: Arc, - bdk_electrum_client: Arc>, + bdk_electrum_client: Arc>>, tx_sync: Arc>>, runtime: Arc, config: Arc, @@ -424,12 +424,7 @@ impl ElectrumRuntimeClient { Error::ConnectionFailed })?, ); - let electrum_client_2 = - ElectrumClient::from_config(&server_url, electrum_config).map_err(|e| { - log_error!(logger, "Failed to connect to electrum server: {}", e); - Error::ConnectionFailed - })?; - let bdk_electrum_client = Arc::new(BdkElectrumClient::new(electrum_client_2)); + let bdk_electrum_client = Arc::new(BdkElectrumClient::new(Arc::clone(&electrum_client))); let tx_sync = Arc::new( ElectrumSyncClient::new(server_url.clone(), Arc::clone(&logger)).map_err(|e| { log_error!(logger, "Failed to connect to electrum server: {}", e); diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index a8806a4137..8e9a4dbd44 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -57,19 +57,6 @@ impl EsploraChainSource { kv_store: Arc, config: Arc, logger: Arc, node_metrics: Arc>, ) -> Self { - // FIXME / TODO: We introduced this to make `bdk_esplora` work separately without updating - // `lightning-transaction-sync`. We should revert this as part of of the upgrade to LDK 0.2. - let mut client_builder_0_11 = esplora_client_0_11::Builder::new(&server_url); - client_builder_0_11 = client_builder_0_11.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); - - for (header_name, header_value) in &headers { - client_builder_0_11 = client_builder_0_11.header(header_name, header_value); - } - - let esplora_client_0_11 = client_builder_0_11.build_async().unwrap(); - let tx_sync = - Arc::new(EsploraSyncClient::from_client(esplora_client_0_11, Arc::clone(&logger))); - let mut client_builder = esplora_client::Builder::new(&server_url); client_builder = client_builder.timeout(DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS); @@ -78,6 +65,8 @@ impl EsploraChainSource { } let esplora_client = client_builder.build_async().unwrap(); + let tx_sync = + Arc::new(EsploraSyncClient::from_client(esplora_client.clone(), Arc::clone(&logger))); let onchain_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); let lightning_wallet_sync_status = Mutex::new(WalletSyncStatus::Completed); diff --git a/src/config.rs b/src/config.rs index 02df8bbc76..84f62d220a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -8,10 +8,10 @@ //! Objects for configuring the node. use crate::logger::LogLevel; -use crate::payment::SendingParameters; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; +use lightning::routing::router::RouteParametersConfig; use lightning::util::config::ChannelConfig as LdkChannelConfig; use lightning::util::config::MaxDustHTLCExposure as LdkMaxDustHTLCExposure; use lightning::util::config::UserConfig; @@ -114,9 +114,9 @@ pub const WALLET_KEYS_SEED_LEN: usize = 64; /// | `probing_liquidity_limit_multiplier` | 3 | /// | `log_level` | Debug | /// | `anchor_channels_config` | Some(..) | -/// | `sending_parameters` | None | +/// | `route_parameters` | None | /// -/// See [`AnchorChannelsConfig`] and [`SendingParameters`] for more information regarding their +/// See [`AnchorChannelsConfig`] and [`RouteParametersConfig`] for more information regarding their /// respective default values. /// /// [`Node`]: crate::Node @@ -173,12 +173,12 @@ pub struct Config { pub anchor_channels_config: Option, /// Configuration options for payment routing and pathfinding. /// - /// Setting the `SendingParameters` provides flexibility to customize how payments are routed, + /// Setting the [`RouteParametersConfig`] provides flexibility to customize how payments are routed, /// including setting limits on routing fees, CLTV expiry, and channel utilization. /// /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. - pub sending_parameters: Option, + pub route_parameters: Option, } impl Default for Config { @@ -191,7 +191,7 @@ impl Default for Config { trusted_peers_0conf: Vec::new(), probing_liquidity_limit_multiplier: DEFAULT_PROBING_LIQUIDITY_LIMIT_MULTIPLIER, anchor_channels_config: Some(AnchorChannelsConfig::default()), - sending_parameters: None, + route_parameters: None, node_alias: None, } } diff --git a/src/data_store.rs b/src/data_store.rs index 78e3e78701..45802c272b 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -143,18 +143,18 @@ where let store_key = object.id().encode_to_hex_str(); let data = object.encode(); self.kv_store - .write(&self.primary_namespace, &self.secondary_namespace, &store_key, &data) + .write(&self.primary_namespace, &self.secondary_namespace, &store_key, data) .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - &self.primary_namespace, - &self.secondary_namespace, - store_key, - e - ); - Error::PersistenceFailed - })?; + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; Ok(()) } } diff --git a/src/event.rs b/src/event.rs index ff94d51d17..bad1b84ab2 100644 --- a/src/event.rs +++ b/src/event.rs @@ -38,6 +38,9 @@ use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; use lightning::routing::gossip::NodeId; +use lightning::util::config::{ + ChannelConfigOverrides, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, +}; use lightning::util::errors::APIError; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; @@ -56,7 +59,6 @@ use core::task::{Poll, Waker}; use std::collections::VecDeque; use std::ops::Deref; use std::sync::{Arc, Condvar, Mutex}; -use std::time::Duration; /// An event emitted by [`Node`], which should be handled by the user. /// @@ -358,7 +360,7 @@ where EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_KEY, - &data, + data, ) .map_err(|e| { log_error!( @@ -544,7 +546,7 @@ where Err(err) => { log_error!(self.logger, "Failed to create funding transaction: {}", err); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Failed to create funding transaction".to_string(), @@ -565,13 +567,10 @@ where payment_hash, purpose, amount_msat, - receiver_node_id: _, - via_channel_id: _, - via_user_channel_id: _, claim_deadline, onion_fields, counterparty_skimmed_fee_msat, - payment_id: _, + .. } => { let payment_id = PaymentId(payment_hash.0); if let Some(info) = self.payment_store.get(&payment_id) { @@ -1043,26 +1042,17 @@ where LdkEvent::PaymentPathFailed { .. } => {}, LdkEvent::ProbeSuccessful { .. } => {}, LdkEvent::ProbeFailed { .. } => {}, - LdkEvent::HTLCHandlingFailed { failed_next_destination, .. } => { + LdkEvent::HTLCHandlingFailed { failure_type, .. } => { if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_htlc_handling_failed(failed_next_destination); + liquidity_source.handle_htlc_handling_failed(failure_type); } }, - LdkEvent::PendingHTLCsForwardable { time_forwardable } => { - let forwarding_channel_manager = self.channel_manager.clone(); - let min = time_forwardable.as_millis() as u64; - - let future = async move { - let millis_to_sleep = thread_rng().gen_range(min..min * 5) as u64; - tokio::time::sleep(Duration::from_millis(millis_to_sleep)).await; - - forwarding_channel_manager.process_pending_htlc_forwards(); - }; - - self.runtime.spawn_cancellable_background_task(future); - }, LdkEvent::SpendableOutputs { outputs, channel_id } => { - match self.output_sweeper.track_spendable_outputs(outputs, channel_id, true, None) { + match self + .output_sweeper + .track_spendable_outputs(outputs, channel_id, true, None) + .await + { Ok(_) => return Ok(()), Err(_) => { log_error!(self.logger, "Failed to track spendable outputs"); @@ -1084,7 +1074,7 @@ where log_error!(self.logger, "Rejecting inbound announced channel from peer {} due to missing configuration: {}", counterparty_node_id, err); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Channel request rejected".to_string(), @@ -1128,7 +1118,7 @@ where required_amount_sats, ); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Channel request rejected".to_string(), @@ -1145,7 +1135,7 @@ where counterparty_node_id, ); self.channel_manager - .force_close_without_broadcasting_txn( + .force_close_broadcasting_latest_txn( &temporary_channel_id, &counterparty_node_id, "Channel request rejected".to_string(), @@ -1157,19 +1147,46 @@ where } } - let user_channel_id: u128 = rand::thread_rng().gen::(); + let user_channel_id: u128 = thread_rng().gen::(); let allow_0conf = self.config.trusted_peers_0conf.contains(&counterparty_node_id); + let mut channel_override_config = None; + if let Some((lsp_node_id, _)) = self + .liquidity_source + .as_ref() + .and_then(|ls| ls.as_ref().get_lsps2_lsp_details()) + { + if lsp_node_id == counterparty_node_id { + // When we're an LSPS2 client, allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll + // check that they don't take too much before claiming. + // + // We also set maximum allowed inbound HTLC value in flight + // to 100%. We should eventually be able to set this on a per-channel basis, but for + // now we just bump the default for all channels. + channel_override_config = Some(ChannelConfigOverrides { + handshake_overrides: Some(ChannelHandshakeConfigUpdate { + max_inbound_htlc_value_in_flight_percent_of_channel: Some(100), + ..Default::default() + }), + update_overrides: Some(ChannelConfigUpdate { + accept_underpaying_htlcs: Some(true), + ..Default::default() + }), + }); + } + } let res = if allow_0conf { self.channel_manager.accept_inbound_channel_from_trusted_peer_0conf( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) } else { self.channel_manager.accept_inbound_channel( &temporary_channel_id, &counterparty_node_id, user_channel_id, + channel_override_config, ) }; @@ -1469,7 +1486,7 @@ where BumpTransactionEvent::HTLCResolution { .. } => {}, } - self.bump_tx_event_handler.handle_event(&bte); + self.bump_tx_event_handler.handle_event(&bte).await; }, LdkEvent::OnionMessageIntercepted { .. } => { debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); @@ -1477,6 +1494,15 @@ where LdkEvent::OnionMessagePeerConnected { .. } => { debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); }, + LdkEvent::PersistStaticInvoice { .. } => { + debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted."); + }, + LdkEvent::StaticInvoiceRequested { .. } => { + debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted."); + }, + LdkEvent::FundingTransactionReadyForSigning { .. } => { + debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted."); + }, } Ok(()) } diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 984e4da8fb..02d3217870 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -15,26 +15,29 @@ pub use crate::config::{ EsploraSyncConfig, MaxDustHTLCExposure, }; pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; -pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig, OnchainPaymentInfo, PaymentInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; pub use crate::logger::{LogLevel, LogRecord, LogWriter}; pub use crate::payment::store::{ ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, }; -pub use crate::payment::{MaxTotalRoutingFeeLimit, QrPaymentResult, SendingParameters}; +pub use crate::payment::QrPaymentResult; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; pub use lightning::ln::types::ChannelId; pub use lightning::offers::offer::OfferId; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; -pub use lightning::util::string::UntrustedString; +pub use lightning::routing::router::RouteParametersConfig; +pub use lightning_types::string::UntrustedString; pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; -pub use lightning_liquidity::lsps1::msgs::ChannelInfo as ChannelOrderInfo; -pub use lightning_liquidity::lsps1::msgs::{OrderId, OrderParameters, PaymentState}; +pub use lightning_liquidity::lsps0::ser::LSPSDateTime; +pub use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, +}; pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; @@ -42,8 +45,6 @@ pub use bip39::Mnemonic; pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; -pub type DateTime = chrono::DateTime; - use crate::UniffiCustomTypeConverter; use crate::builder::sanitize_alias; @@ -125,9 +126,8 @@ impl From for OfferAmount { fn from(ldk_amount: LdkAmount) -> Self { match ldk_amount { LdkAmount::Bitcoin { amount_msats } => OfferAmount::Bitcoin { amount_msats }, - LdkAmount::Currency { iso4217_code, amount } => OfferAmount::Currency { - iso4217_code: iso4217_code.iter().map(|&b| b as char).collect(), - amount, + LdkAmount::Currency { iso4217_code, amount } => { + OfferAmount::Currency { iso4217_code: iso4217_code.as_str().to_owned(), amount } }, } } @@ -1066,13 +1066,71 @@ impl std::fmt::Display for Bolt11Invoice { } } +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1PaymentInfo { + /// A Lightning payment using BOLT 11. + pub bolt11: Option, + /// An onchain payment. + pub onchain: Option, +} + +#[cfg(feature = "uniffi")] +impl From for LSPS1PaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo) -> Self { + LSPS1PaymentInfo { + bolt11: value.bolt11.map(|b| b.into()), + onchain: value.onchain.map(|o| o.into()), + } + } +} + +/// An onchain payment. +#[cfg(feature = "uniffi")] +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct LSPS1OnchainPaymentInfo { + /// Indicates the current state of the payment. + pub state: lightning_liquidity::lsps1::msgs::LSPS1PaymentState, + /// The datetime when the payment option expires. + pub expires_at: LSPSDateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + pub order_total_sat: u64, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + pub address: bitcoin::Address, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + pub min_fee_for_0conf: Arc, + /// The address where the LSP will send the funds if the order fails. + pub refund_onchain_address: Option, +} + +#[cfg(feature = "uniffi")] +impl From for LSPS1OnchainPaymentInfo { + fn from(value: lightning_liquidity::lsps1::msgs::LSPS1OnchainPaymentInfo) -> Self { + Self { + state: value.state, + expires_at: value.expires_at, + fee_total_sat: value.fee_total_sat, + order_total_sat: value.order_total_sat, + address: value.address, + min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, + min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), + refund_onchain_address: value.refund_onchain_address, + } + } +} /// A Lightning payment using BOLT 11. #[derive(Clone, Debug, PartialEq, Eq)] -pub struct Bolt11PaymentInfo { +pub struct LSPS1Bolt11PaymentInfo { /// Indicates the current state of the payment. - pub state: PaymentState, + pub state: LSPS1PaymentState, /// The datetime when the payment option expires. - pub expires_at: chrono::DateTime, + pub expires_at: LSPSDateTime, /// The total fee the LSP will charge to open this channel in satoshi. pub fee_total_sat: u64, /// The amount the client needs to pay to have the requested channel openend. @@ -1081,8 +1139,8 @@ pub struct Bolt11PaymentInfo { pub invoice: Arc, } -impl From for Bolt11PaymentInfo { - fn from(info: lightning_liquidity::lsps1::msgs::Bolt11PaymentInfo) -> Self { +impl From for LSPS1Bolt11PaymentInfo { + fn from(info: lightning_liquidity::lsps1::msgs::LSPS1Bolt11PaymentInfo) -> Self { Self { state: info.state, expires_at: info.expires_at, @@ -1093,7 +1151,7 @@ impl From for Bolt11Payment } } -impl UniffiCustomTypeConverter for OrderId { +impl UniffiCustomTypeConverter for LSPS1OrderId { type Builtin = String; fn into_custom(val: Self::Builtin) -> uniffi::Result { @@ -1105,11 +1163,11 @@ impl UniffiCustomTypeConverter for OrderId { } } -impl UniffiCustomTypeConverter for DateTime { +impl UniffiCustomTypeConverter for LSPSDateTime { type Builtin = String; fn into_custom(val: Self::Builtin) -> uniffi::Result { - Ok(DateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) + Ok(LSPSDateTime::from_str(&val).map_err(|_| Error::InvalidDateTime)?) } fn from_custom(obj: Self) -> Self::Builtin { diff --git a/src/io/sqlite_store/migrations.rs b/src/io/sqlite_store/migrations.rs index 0486b8a4f9..15e60bcc2b 100644 --- a/src/io/sqlite_store/migrations.rs +++ b/src/io/sqlite_store/migrations.rs @@ -78,7 +78,7 @@ mod tests { use crate::io::sqlite_store::SqliteStore; use crate::io::test_utils::{do_read_write_remove_list_persist, random_storage_path}; - use lightning::util::persist::KVStore; + use lightning::util::persist::KVStoreSync; use rusqlite::{named_params, Connection}; diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index b72db5a2ba..4006ab2cc4 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -9,8 +9,9 @@ use crate::io::utils::check_namespace_key_validity; use lightning::io; -use lightning::util::persist::KVStore; -use lightning::util::string::PrintableString; +use lightning::util::persist::KVStoreSync; + +use lightning_types::string::PrintableString; use rusqlite::{named_params, Connection}; @@ -34,7 +35,7 @@ pub const DEFAULT_KV_TABLE_NAME: &str = "ldk_data"; // The current SQLite `user_version`, which we can use if we'd ever need to do a schema migration. const SCHEMA_USER_VERSION: u16 = 2; -/// A [`KVStore`] implementation that writes to and reads from an [SQLite] database. +/// A [`KVStoreSync`] implementation that writes to and reads from an [SQLite] database. /// /// [SQLite]: https://sqlite.org pub struct SqliteStore { @@ -129,7 +130,7 @@ impl SqliteStore { } } -impl KVStore for SqliteStore { +impl KVStoreSync for SqliteStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { @@ -179,7 +180,7 @@ impl KVStore for SqliteStore { } fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index df806779e8..244dd9cdc5 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -9,7 +9,7 @@ use lightning::ln::functional_test_utils::{ connect_block, create_announced_chan_between_nodes, create_chanmon_cfgs, create_dummy_block, create_network, create_node_cfgs, create_node_chanmgrs, send_payment, }; -use lightning::util::persist::{read_channel_monitors, KVStore, KVSTORE_NAMESPACE_KEY_MAX_LEN}; +use lightning::util::persist::{read_channel_monitors, KVStoreSync, KVSTORE_NAMESPACE_KEY_MAX_LEN}; use lightning::events::ClosureReason; use lightning::util::test_utils; @@ -29,23 +29,24 @@ pub(crate) fn random_storage_path() -> PathBuf { temp_path } -pub(crate) fn do_read_write_remove_list_persist(kv_store: &K) { - let data = [42u8; 32]; +pub(crate) fn do_read_write_remove_list_persist(kv_store: &K) { + let data = vec![42u8; 32]; let primary_namespace = "testspace"; let secondary_namespace = "testsubspace"; let key = "testkey"; // Test the basic KVStore operations. - kv_store.write(primary_namespace, secondary_namespace, key, &data).unwrap(); + kv_store.write(primary_namespace, secondary_namespace, key, data.clone()).unwrap(); // Test empty primary/secondary namespaces are allowed, but not empty primary namespace and non-empty // secondary primary_namespace, and not empty key. - kv_store.write("", "", key, &data).unwrap(); - let res = std::panic::catch_unwind(|| kv_store.write("", secondary_namespace, key, &data)); + kv_store.write("", "", key, data.clone()).unwrap(); + let res = + std::panic::catch_unwind(|| kv_store.write("", secondary_namespace, key, data.clone())); assert!(res.is_err()); let res = std::panic::catch_unwind(|| { - kv_store.write(primary_namespace, secondary_namespace, "", &data) + kv_store.write(primary_namespace, secondary_namespace, "", data.clone()) }); assert!(res.is_err()); @@ -63,7 +64,7 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s // Ensure we have no issue operating with primary_namespace/secondary_namespace/key being KVSTORE_NAMESPACE_KEY_MAX_LEN let max_chars: String = std::iter::repeat('A').take(KVSTORE_NAMESPACE_KEY_MAX_LEN).collect(); - kv_store.write(&max_chars, &max_chars, &max_chars, &data).unwrap(); + kv_store.write(&max_chars, &max_chars, &max_chars, data.clone()).unwrap(); let listed_keys = kv_store.list(&max_chars, &max_chars).unwrap(); assert_eq!(listed_keys.len(), 1); @@ -80,7 +81,7 @@ pub(crate) fn do_read_write_remove_list_persist(kv_s // Integration-test the given KVStore implementation. Test relaying a few payments and check that // the persisted data is updated the appropriate number of times. -pub(crate) fn do_test_store(store_0: &K, store_1: &K) { +pub(crate) fn do_test_store(store_0: &K, store_1: &K) { let chanmon_cfgs = create_chanmon_cfgs(2); let mut node_cfgs = create_node_cfgs(2, &chanmon_cfgs); let chain_mon_0 = test_utils::TestChainMonitor::new( @@ -145,18 +146,19 @@ pub(crate) fn do_test_store(store_0: &K, store_1: &K) { // Force close because cooperative close doesn't result in any persisted // updates. + let message = "Channel force-closed".to_owned(); nodes[0] .node .force_close_broadcasting_latest_txn( &nodes[0].node.list_channels()[0].channel_id, &nodes[1].node.get_our_node_id(), - "whoops".to_string(), + message.clone(), ) .unwrap(); check_closed_event!( nodes[0], 1, - ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true) }, + ClosureReason::HolderForceClosed { broadcasted_latest_txn: Some(true), message }, [nodes[1].node.get_our_node_id()], 100000 ); diff --git a/src/io/utils.rs b/src/io/utils.rs index 06a1017ba1..51e7be5054 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -31,9 +31,10 @@ use lightning::util::persist::{ SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; -use lightning::util::string::PrintableString; use lightning::util::sweep::OutputSweeper; +use lightning_types::string::PrintableString; + use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; @@ -251,7 +252,7 @@ pub(crate) fn read_output_sweeper( kv_store, logger.clone(), ); - OutputSweeper::read(&mut reader, args).map_err(|e| { + OutputSweeper::read_with_kv_store_sync(&mut reader, args).map_err(|e| { log_error!(logger, "Failed to deserialize OutputSweeper: {}", e); std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to deserialize OutputSweeper") }) @@ -286,7 +287,7 @@ where NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, NODE_METRICS_KEY, - &data, + data, ) .map_err(|e| { log_error!( @@ -441,7 +442,7 @@ macro_rules! impl_read_write_change_set_type { L::Target: LdkLogger, { let data = ChangeSetSerWrapper(value).encode(); - kv_store.write($primary_namespace, $secondary_namespace, $key, &data).map_err(|e| { + kv_store.write($primary_namespace, $secondary_namespace, $key, data).map_err(|e| { log_error!( logger, "Writing data to key {}/{}/{} failed due to: {}", diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index e2cfc3c7b0..87f966a9ba 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -10,7 +10,7 @@ use crate::runtime::Runtime; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use lightning::io::{self, Error, ErrorKind}; -use lightning::util::persist::KVStore; +use lightning::util::persist::KVStoreSync; use prost::Message; use rand::RngCore; #[cfg(test)] @@ -38,7 +38,7 @@ type CustomRetryPolicy = FilteredRetryPolicy< Box bool + 'static + Send + Sync>, >; -/// A [`KVStore`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. +/// A [`KVStoreSync`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. pub struct VssStore { client: VssClient, store_id: String, @@ -127,7 +127,7 @@ impl VssStore { } } -impl KVStore for VssStore { +impl KVStoreSync for VssStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { @@ -160,11 +160,11 @@ impl KVStore for VssStore { } fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; let version = -1; - let storable = self.storable_builder.build(buf.to_vec(), version); + let storable = self.storable_builder.build(buf, version); let request = PutObjectRequest { store_id: self.store_id.clone(), global_version: None, diff --git a/src/lib.rs b/src/lib.rs index 9035d53618..160762dd2c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -158,7 +158,7 @@ use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; -use lightning_background_processor::process_events_async; +use lightning_background_processor::process_events_async_with_kv_store_sync; use bitcoin::secp256k1::PublicKey; @@ -521,6 +521,9 @@ impl Node { let background_chan_man = Arc::clone(&self.channel_manager); let background_gossip_sync = self.gossip_source.as_gossip_sync(); let background_peer_man = Arc::clone(&self.peer_manager); + let background_liquidity_man_opt = + self.liquidity_source.as_ref().map(|ls| ls.liquidity_manager()); + let background_sweeper = Arc::clone(&self.output_sweeper); let background_onion_messenger = Arc::clone(&self.onion_messenger); let background_logger = Arc::clone(&self.logger); let background_error_logger = Arc::clone(&self.logger); @@ -547,7 +550,7 @@ impl Node { }; self.runtime.spawn_background_processor_task(async move { - process_events_async( + process_events_async_with_kv_store_sync( background_persister, |e| background_event_handler.handle_event(e), background_chain_mon, @@ -555,6 +558,8 @@ impl Node { Some(background_onion_messenger), background_gossip_sync, background_peer_man, + background_liquidity_man_opt, + Some(background_sweeper), background_logger, Some(background_scorer), sleeper, @@ -1193,12 +1198,17 @@ impl Node { self.runtime.block_on(async move { if chain_source.is_transaction_based() { chain_source.update_fee_rate_estimates().await?; - chain_source.sync_lightning_wallet(sync_cman, sync_cmon, sync_sweeper).await?; + chain_source + .sync_lightning_wallet(sync_cman, sync_cmon, Arc::clone(&sync_sweeper)) + .await?; chain_source.sync_onchain_wallet().await?; } else { chain_source.update_fee_rate_estimates().await?; - chain_source.poll_and_update_listeners(sync_cman, sync_cmon, sync_sweeper).await?; + chain_source + .poll_and_update_listeners(sync_cman, sync_cmon, Arc::clone(&sync_sweeper)) + .await?; } + let _ = sync_sweeper.regenerate_and_broadcast_spend_if_necessary().await; Ok(()) }) } @@ -1247,35 +1257,16 @@ impl Node { open_channels.iter().find(|c| c.user_channel_id == user_channel_id.0) { if force { - if self.config.anchor_channels_config.as_ref().map_or(false, |acc| { - acc.trusted_peers_no_reserve.contains(&counterparty_node_id) - }) { - self.channel_manager - .force_close_without_broadcasting_txn( - &channel_details.channel_id, - &counterparty_node_id, - force_close_reason.unwrap_or_default(), - ) - .map_err(|e| { - log_error!( - self.logger, - "Failed to force-close channel to trusted peer: {:?}", - e - ); - Error::ChannelClosingFailed - })?; - } else { - self.channel_manager - .force_close_broadcasting_latest_txn( - &channel_details.channel_id, - &counterparty_node_id, - force_close_reason.unwrap_or_default(), - ) - .map_err(|e| { - log_error!(self.logger, "Failed to force-close channel: {:?}", e); - Error::ChannelClosingFailed - })?; - } + self.channel_manager + .force_close_broadcasting_latest_txn( + &channel_details.channel_id, + &counterparty_node_id, + force_close_reason.unwrap_or_default(), + ) + .map_err(|e| { + log_error!(self.logger, "Failed to force-close channel: {:?}", e); + Error::ChannelClosingFailed + })?; } else { self.channel_manager .close_channel(&channel_details.channel_id, &counterparty_node_id) @@ -1340,12 +1331,10 @@ impl Node { let mut total_lightning_balance_sats = 0; let mut lightning_balances = Vec::new(); - for (funding_txo, channel_id) in self.chain_monitor.list_monitors() { - match self.chain_monitor.get_monitor(funding_txo) { + for channel_id in self.chain_monitor.list_monitors() { + match self.chain_monitor.get_monitor(channel_id) { Ok(monitor) => { - // unwrap safety: `get_counterparty_node_id` will always be `Some` after 0.0.110 and - // LDK Node 0.1 depended on 0.0.115 already. - let counterparty_node_id = monitor.get_counterparty_node_id().unwrap(); + let counterparty_node_id = monitor.get_counterparty_node_id(); for ldk_balance in monitor.get_claimable_balances() { total_lightning_balance_sats += ldk_balance.claimable_amount_satoshis(); lightning_balances.push(LightningBalance::from_ldk_balance( diff --git a/src/liquidity.rs b/src/liquidity.rs index 6ee8066c19..5d0bf5afec 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -14,7 +14,7 @@ use crate::runtime::Runtime; use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; -use lightning::events::HTLCDestination; +use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; use lightning::ln::types::ChannelId; @@ -22,14 +22,16 @@ use lightning::routing::router::{RouteHint, RouteHintHop}; use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; -use lightning_liquidity::events::Event; -use lightning_liquidity::lsps0::ser::RequestId; +use lightning_liquidity::events::LiquidityEvent; +use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; use lightning_liquidity::lsps1::event::LSPS1ClientEvent; -use lightning_liquidity::lsps1::msgs::{ChannelInfo, LSPS1Options, OrderId, OrderParameters}; +use lightning_liquidity::lsps1::msgs::{ + LSPS1ChannelInfo, LSPS1Options, LSPS1OrderId, LSPS1OrderParams, +}; use lightning_liquidity::lsps2::client::LSPS2ClientConfig as LdkLSPS2ClientConfig; use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; -use lightning_liquidity::lsps2::msgs::{OpeningFeeParams, RawOpeningFeeParams}; +use lightning_liquidity::lsps2::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFeeParams}; use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; @@ -41,7 +43,7 @@ use bitcoin::secp256k1::{PublicKey, Secp256k1}; use tokio::sync::oneshot; -use chrono::{DateTime, Utc}; +use chrono::Utc; use rand::Rng; @@ -62,10 +64,10 @@ struct LSPS1Client { token: Option, ldk_client_config: LdkLSPS1ClientConfig, pending_opening_params_requests: - Mutex>>, - pending_create_order_requests: Mutex>>, + Mutex>>, + pending_create_order_requests: Mutex>>, pending_check_order_status_requests: - Mutex>>, + Mutex>>, } #[derive(Debug, Clone)] @@ -80,8 +82,8 @@ struct LSPS2Client { lsp_address: SocketAddress, token: Option, ldk_client_config: LdkLSPS2ClientConfig, - pending_fee_requests: Mutex>>, - pending_buy_requests: Mutex>>, + pending_fee_requests: Mutex>>, + pending_buy_requests: Mutex>>, } #[derive(Debug, Clone)] @@ -221,16 +223,22 @@ where pub(crate) fn build(self) -> LiquiditySource { let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { let lsps2_service_config = Some(s.ldk_service_config.clone()); + let lsps5_service_config = None; let advertise_service = s.service_config.advertise_service; - LiquidityServiceConfig { lsps2_service_config, advertise_service } + LiquidityServiceConfig { lsps2_service_config, lsps5_service_config, advertise_service } }); let lsps1_client_config = self.lsps1_client.as_ref().map(|s| s.ldk_client_config.clone()); let lsps2_client_config = self.lsps2_client.as_ref().map(|s| s.ldk_client_config.clone()); - let liquidity_client_config = - Some(LiquidityClientConfig { lsps1_client_config, lsps2_client_config }); + let lsps5_client_config = None; + let liquidity_client_config = Some(LiquidityClientConfig { + lsps1_client_config, + lsps2_client_config, + lsps5_client_config, + }); let liquidity_manager = Arc::new(LiquidityManager::new( + Arc::clone(&self.keys_manager), Arc::clone(&self.keys_manager), Arc::clone(&self.channel_manager), Some(Arc::clone(&self.chain_source)), @@ -275,13 +283,11 @@ where L::Target: LdkLogger, { pub(crate) fn set_peer_manager(&self, peer_manager: Arc) { - *self.peer_manager.write().unwrap() = Some(Arc::clone(&peer_manager)); - let process_msgs_callback = move || peer_manager.process_events(); - self.liquidity_manager.set_process_msgs_callback(process_msgs_callback); + *self.peer_manager.write().unwrap() = Some(peer_manager); } - pub(crate) fn liquidity_manager(&self) -> &LiquidityManager { - self.liquidity_manager.as_ref() + pub(crate) fn liquidity_manager(&self) -> Arc { + Arc::clone(&self.liquidity_manager) } pub(crate) fn get_lsps1_lsp_details(&self) -> Option<(PublicKey, SocketAddress)> { @@ -294,7 +300,7 @@ where pub(crate) async fn handle_next_event(&self) { match self.liquidity_manager.next_event_async().await { - Event::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady { request_id, counterparty_node_id, supported_options, @@ -347,7 +353,7 @@ where ); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderCreated { request_id, counterparty_node_id, order_id, @@ -405,7 +411,7 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderCreated event!"); } }, - Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + LiquidityEvent::LSPS1Client(LSPS1ClientEvent::OrderStatus { request_id, counterparty_node_id, order_id, @@ -463,7 +469,7 @@ where log_error!(self.logger, "Received unexpected LSPS1Client::OrderStatus event!"); } }, - Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::GetInfo { request_id, counterparty_node_id, token, @@ -484,7 +490,7 @@ where if token != Some(required) { log_error!( self.logger, - "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", + "Rejecting LSPS2 request {:?} from counterparty {} as the client provided an invalid token.", request_id, counterparty_node_id ); @@ -502,10 +508,8 @@ where } } - let mut valid_until: DateTime = Utc::now(); - valid_until += LSPS2_GETINFO_REQUEST_EXPIRY; - - let opening_fee_params = RawOpeningFeeParams { + let valid_until = LSPSDateTime(Utc::now() + LSPS2_GETINFO_REQUEST_EXPIRY); + let opening_fee_params = LSPS2RawOpeningFeeParams { min_fee_msat: service_config.min_channel_opening_fee_msat, proportional: service_config.channel_opening_fee_ppm, valid_until, @@ -533,7 +537,7 @@ where return; } }, - Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::BuyRequest { request_id, counterparty_node_id, opening_fee_params: _, @@ -600,7 +604,7 @@ where return; } }, - Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + LiquidityEvent::LSPS2Service(LSPS2ServiceEvent::OpenChannel { their_network_key, amt_to_forward_msat, opening_fee_msat: _, @@ -674,7 +678,7 @@ where return; } - let mut config = *self.channel_manager.get_current_default_configuration(); + let mut config = self.channel_manager.get_current_config().clone(); // We set these LSP-specific values during Node building, here we're making sure it's actually set. debug_assert_eq!( @@ -714,7 +718,7 @@ where }, } }, - Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { request_id, counterparty_node_id, opening_fee_params_menu, @@ -764,7 +768,7 @@ where ); } }, - Event::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + LiquidityEvent::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { request_id, counterparty_node_id, intercept_scid, @@ -904,7 +908,7 @@ where return Err(Error::LiquidityRequestFailed); } - let order_params = OrderParameters { + let order_params = LSPS1OrderParams { lsp_balance_sat, client_balance_sat, required_channel_confirmations: lsp_limits.min_required_channel_confirmations, @@ -953,7 +957,7 @@ where } pub(crate) async fn lsps1_check_order_status( - &self, order_id: OrderId, + &self, order_id: LSPS1OrderId, ) -> Result { let lsps1_client = self.lsps1_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; let client_handler = self.liquidity_manager.lsps1_client_handler().ok_or_else(|| { @@ -1127,7 +1131,7 @@ where } async fn lsps2_send_buy_request( - &self, amount_msat: Option, opening_fee_params: OpeningFeeParams, + &self, amount_msat: Option, opening_fee_params: LSPS2OpeningFeeParams, ) -> Result { let lsps2_client = self.lsps2_client.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; @@ -1280,9 +1284,9 @@ where } } - pub(crate) fn handle_htlc_handling_failed(&self, failed_next_destination: HTLCDestination) { + pub(crate) fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.htlc_handling_failed(failed_next_destination) { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type) { log_error!( self.logger, "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", @@ -1316,82 +1320,24 @@ pub(crate) struct LSPS1OpeningParamsResponse { #[derive(Debug, Clone)] pub struct LSPS1OrderStatus { /// The id of the channel order. - pub order_id: OrderId, + pub order_id: LSPS1OrderId, /// The parameters of channel order. - pub order_params: OrderParameters, + pub order_params: LSPS1OrderParams, /// Contains details about how to pay for the order. - pub payment_options: PaymentInfo, + pub payment_options: LSPS1PaymentInfo, /// Contains information about the channel state. - pub channel_state: Option, + pub channel_state: Option, } #[cfg(not(feature = "uniffi"))] -type PaymentInfo = lightning_liquidity::lsps1::msgs::PaymentInfo; - -/// Details regarding how to pay for an order. -#[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct PaymentInfo { - /// A Lightning payment using BOLT 11. - pub bolt11: Option, - /// An onchain payment. - pub onchain: Option, -} - -#[cfg(feature = "uniffi")] -impl From for PaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::PaymentInfo) -> Self { - PaymentInfo { - bolt11: value.bolt11.map(|b| b.into()), - onchain: value.onchain.map(|o| o.into()), - } - } -} - -/// An onchain payment. -#[cfg(feature = "uniffi")] -#[derive(Clone, Debug, PartialEq, Eq)] -pub struct OnchainPaymentInfo { - /// Indicates the current state of the payment. - pub state: lightning_liquidity::lsps1::msgs::PaymentState, - /// The datetime when the payment option expires. - pub expires_at: chrono::DateTime, - /// The total fee the LSP will charge to open this channel in satoshi. - pub fee_total_sat: u64, - /// The amount the client needs to pay to have the requested channel openend. - pub order_total_sat: u64, - /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel - /// opened. - pub address: bitcoin::Address, - /// The minimum number of block confirmations that are required for the on-chain payment to be - /// considered confirmed. - pub min_onchain_payment_confirmations: Option, - /// The minimum fee rate for the on-chain payment in case the client wants the payment to be - /// confirmed without a confirmation. - pub min_fee_for_0conf: Arc, - /// The address where the LSP will send the funds if the order fails. - pub refund_onchain_address: Option, -} +type LSPS1PaymentInfo = lightning_liquidity::lsps1::msgs::LSPS1PaymentInfo; #[cfg(feature = "uniffi")] -impl From for OnchainPaymentInfo { - fn from(value: lightning_liquidity::lsps1::msgs::OnchainPaymentInfo) -> Self { - Self { - state: value.state, - expires_at: value.expires_at, - fee_total_sat: value.fee_total_sat, - order_total_sat: value.order_total_sat, - address: value.address, - min_onchain_payment_confirmations: value.min_onchain_payment_confirmations, - min_fee_for_0conf: Arc::new(value.min_fee_for_0conf), - refund_onchain_address: value.refund_onchain_address, - } - } -} +type LSPS1PaymentInfo = crate::ffi::LSPS1PaymentInfo; #[derive(Debug, Clone)] pub(crate) struct LSPS2FeeResponse { - opening_fee_params_menu: Vec, + opening_fee_params_menu: Vec, } #[derive(Debug, Clone)] @@ -1474,7 +1420,7 @@ impl LSPS1Liquidity { } /// Connects to the configured LSP and checks for the status of a previously-placed order. - pub fn check_order_status(&self, order_id: OrderId) -> Result { + pub fn check_order_status(&self, order_id: LSPS1OrderId) -> Result { let liquidity_source = self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?; diff --git a/src/message_handler.rs b/src/message_handler.rs index cebd1ea07c..25995a4815 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -10,6 +10,7 @@ use crate::liquidity::LiquiditySource; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; +use lightning::util::ser::LengthLimitedRead; use lightning_types::features::{InitFeatures, NodeFeatures}; @@ -47,7 +48,7 @@ where { type CustomMessage = RawLSPSMessage; - fn read( + fn read( &self, message_type: u16, buffer: &mut RD, ) -> Result, lightning::ln::msgs::DecodeError> { match self { diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 92d7fc9485..7dcb2817c6 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -20,16 +20,14 @@ use crate::payment::store::{ LSPFeeLimits, PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; -use crate::payment::SendingParameters; use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; use crate::types::{ChannelManager, PaymentStore}; -use lightning::ln::bolt11_payment; use lightning::ln::channelmanager::{ - Bolt11InvoiceParameters, PaymentId, RecipientOnionFields, Retry, RetryableSendFailure, + Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, }; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -92,22 +90,17 @@ impl Bolt11Payment { /// Send a payment given an invoice. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, invoice: &Bolt11Invoice, sending_parameters: Option, + &self, invoice: &Bolt11Invoice, route_parameters: Option, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } let invoice = maybe_deref(invoice); - - let (payment_hash, recipient_onion, mut route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead."); - Error::InvalidInvoice - })?; - + let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array()); let payment_id = PaymentId(invoice.payment_hash().to_byte_array()); if let Some(payment) = self.payment_store.get(&payment_id) { if payment.status == PaymentStatus::Pending @@ -118,29 +111,16 @@ impl Bolt11Payment { } } - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; - - let payment_secret = Some(*invoice.payment_secret()); + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_onion, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + None, + route_parameters, retry_strategy, ) { Ok(()) => { @@ -166,7 +146,13 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { + Err(Bolt11PaymentError::InvalidAmount) => { + log_error!(self.logger, + "Failed to send payment due to the given invoice being \"zero-amount\". Please use send_using_amount instead." + ); + return Err(Error::InvalidInvoice); + }, + Err(Bolt11PaymentError::SendingFailed(e)) => { log_error!(self.logger, "Failed to send payment: {:?}", e); match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), @@ -200,18 +186,17 @@ impl Bolt11Payment { /// This can be used to pay a so-called "zero-amount" invoice, i.e., an invoice that leaves the /// amount paid to be determined by the user. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, - sending_parameters: Option, + route_parameters: Option, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } let invoice = maybe_deref(invoice); - if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( @@ -232,46 +217,16 @@ impl Bolt11Payment { } } - let payment_secret = invoice.payment_secret(); - let expiry_time = invoice.duration_since_epoch().saturating_add(invoice.expiry_time()); - let mut payment_params = PaymentParameters::from_node_id( - invoice.recover_payee_pub_key(), - invoice.min_final_cltv_expiry_delta() as u32, - ) - .with_expiry_time(expiry_time.as_secs()) - .with_route_hints(invoice.route_hints()) - .map_err(|_| Error::InvalidInvoice)?; - if let Some(features) = invoice.features() { - payment_params = payment_params - .with_bolt11_features(features.clone()) - .map_err(|_| Error::InvalidInvoice)?; - } - let mut route_params = - RouteParameters::from_payment_params_and_value(payment_params, amount_msat); - - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; - + let route_parameters = + route_parameters.or(self.config.route_parameters).unwrap_or_default(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let recipient_fields = RecipientOnionFields::secret_only(*payment_secret); + let payment_secret = Some(*invoice.payment_secret()); - match self.channel_manager.send_payment( - payment_hash, - recipient_fields, + match self.channel_manager.pay_for_bolt11_invoice( + invoice, payment_id, - route_params, + Some(amount_msat), + route_parameters, retry_strategy, ) { Ok(()) => { @@ -286,7 +241,7 @@ impl Bolt11Payment { let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage: None, - secret: Some(*payment_secret), + secret: payment_secret, }; let payment = PaymentDetails::new( @@ -301,16 +256,22 @@ impl Bolt11Payment { Ok(payment_id) }, - Err(e) => { + Err(Bolt11PaymentError::InvalidAmount) => { + log_error!( + self.logger, + "Failed to send payment due to amount given being insufficient." + ); + return Err(Error::InvalidInvoice); + }, + Err(Bolt11PaymentError::SendingFailed(e)) => { log_error!(self.logger, "Failed to send payment: {:?}", e); - match e { RetryableSendFailure::DuplicatePayment => Err(Error::DuplicatePayment), _ => { let kind = PaymentKind::Bolt11 { hash: payment_hash, preimage: None, - secret: Some(*payment_secret), + secret: payment_secret, }; let payment = PaymentDetails::new( payment_id, @@ -320,8 +281,8 @@ impl Bolt11Payment { PaymentDirection::Outbound, PaymentStatus::Failed, ); - self.payment_store.insert(payment)?; + self.payment_store.insert(payment)?; Err(Error::PaymentSendingFailed) }, } @@ -798,18 +759,41 @@ impl Bolt11Payment { /// payment. To mitigate this issue, channels with available liquidity less than the required /// amount times [`Config::probing_liquidity_limit_multiplier`] won't be used to send /// pre-flight probes. - pub fn send_probes(&self, invoice: &Bolt11Invoice) -> Result<(), Error> { + /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + pub fn send_probes( + &self, invoice: &Bolt11Invoice, route_parameters: Option, + ) -> Result<(), Error> { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } let invoice = maybe_deref(invoice); + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); - let (_payment_hash, _recipient_onion, route_params) = bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { + let amount_msat = invoice.amount_milli_satoshis().ok_or_else(|| { log_error!(self.logger, "Failed to send probes due to the given invoice being \"zero-amount\". Please use send_probes_using_amount instead."); Error::InvalidInvoice })?; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } + let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); self.channel_manager @@ -828,36 +812,49 @@ impl Bolt11Payment { /// This can be used to send pre-flight probes for a so-called "zero-amount" invoice, i.e., an /// invoice that leaves the amount paid to be determined by the user. /// + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. + /// /// See [`Self::send_probes`] for more information. pub fn send_probes_using_amount( &self, invoice: &Bolt11Invoice, amount_msat: u64, + route_parameters: Option, ) -> Result<(), Error> { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); } let invoice = maybe_deref(invoice); + let payment_params = PaymentParameters::from_bolt11_invoice(invoice); - let (_payment_hash, _recipient_onion, route_params) = if let Some(invoice_amount_msat) = - invoice.amount_milli_satoshis() - { + if let Some(invoice_amount_msat) = invoice.amount_milli_satoshis() { if amount_msat < invoice_amount_msat { log_error!( self.logger, - "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", invoice_amount_msat, amount_msat); + "Failed to send probes as the given amount needs to be at least the invoice amount: required {}msat, gave {}msat.", + invoice_amount_msat, + amount_msat + ); return Err(Error::InvalidAmount); } + } - bolt11_payment::payment_parameters_from_invoice(&invoice).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being \"zero-amount\"."); - Error::InvalidInvoice - })? - } else { - bolt11_payment::payment_parameters_from_variable_amount_invoice(&invoice, amount_msat).map_err(|_| { - log_error!(self.logger, "Failed to send probes due to the given invoice unexpectedly being not \"zero-amount\"."); - Error::InvalidInvoice - })? - }; + let mut route_params = + RouteParameters::from_payment_params_and_value(payment_params, amount_msat); + + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let liquidity_limit_multiplier = Some(self.config.probing_liquidity_limit_multiplier); diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 8e10b9f4f7..4e968deb7a 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -19,7 +19,9 @@ use crate::types::{ChannelManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; -use lightning::util::string::UntrustedString; +use lightning::routing::router::RouteParametersConfig; + +use lightning_types::string::UntrustedString; use rand::RngCore; @@ -82,7 +84,7 @@ impl Bolt12Payment { rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -103,7 +105,7 @@ impl Bolt12Payment { payer_note.clone(), payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); @@ -185,7 +187,7 @@ impl Bolt12Payment { rand::thread_rng().fill_bytes(&mut random_bytes); let payment_id = PaymentId(random_bytes); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let offer_amount_msat = match offer.amount() { Some(Amount::Bitcoin { amount_msats }) => amount_msats, @@ -210,7 +212,7 @@ impl Bolt12Payment { payer_note.clone(), payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); @@ -273,17 +275,17 @@ impl Bolt12Payment { pub(crate) fn receive_inner( &self, amount_msat: u64, description: &str, expiry_secs: Option, quantity: Option, ) -> Result { - let absolute_expiry = expiry_secs.map(|secs| { - (SystemTime::now() + Duration::from_secs(secs as u64)) - .duration_since(UNIX_EPOCH) - .unwrap() - }); + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; - let offer_builder = - self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) + .duration_since(UNIX_EPOCH) + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } let mut offer = offer_builder.amount_msats(amount_msat).description(description.to_string()); @@ -319,17 +321,18 @@ impl Bolt12Payment { pub fn receive_variable_amount( &self, description: &str, expiry_secs: Option, ) -> Result { - let absolute_expiry = expiry_secs.map(|secs| { - (SystemTime::now() + Duration::from_secs(secs as u64)) + let mut offer_builder = self.channel_manager.create_offer_builder().map_err(|e| { + log_error!(self.logger, "Failed to create offer builder: {:?}", e); + Error::OfferCreationFailed + })?; + + if let Some(expiry_secs) = expiry_secs { + let absolute_expiry = (SystemTime::now() + Duration::from_secs(expiry_secs as u64)) .duration_since(UNIX_EPOCH) - .unwrap() - }); + .unwrap(); + offer_builder = offer_builder.absolute_expiry(absolute_expiry); + } - let offer_builder = - self.channel_manager.create_offer_builder(absolute_expiry).map_err(|e| { - log_error!(self.logger, "Failed to create offer builder: {:?}", e); - Error::OfferCreationFailed - })?; let offer = offer_builder.description(description.to_string()).build().map_err(|e| { log_error!(self.logger, "Failed to create offer: {:?}", e); Error::OfferCreationFailed @@ -396,7 +399,7 @@ impl Bolt12Payment { .duration_since(UNIX_EPOCH) .unwrap(); let retry_strategy = Retry::Timeout(LDK_PAYMENT_RETRY_TIMEOUT); - let max_total_routing_fee_msat = None; + let route_params_config = RouteParametersConfig::default(); let mut refund_builder = self .channel_manager @@ -405,7 +408,7 @@ impl Bolt12Payment { absolute_expiry, payment_id, retry_strategy, - max_total_routing_fee_msat, + route_params_config, ) .map_err(|e| { log_error!(self.logger, "Failed to create refund builder: {:?}", e); diff --git a/src/payment/mod.rs b/src/payment/mod.rs index b031e37fd5..54f7894dcd 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -22,87 +22,3 @@ pub use store::{ ConfirmationStatus, LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, }; pub use unified_qr::{QrPaymentResult, UnifiedQrPayment}; - -/// Represents information used to send a payment. -#[derive(Clone, Debug, PartialEq)] -pub struct SendingParameters { - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(not(feature = "uniffi"))] - pub max_total_routing_fee_msat: Option>, - /// The maximum total fees, in millisatoshi, that may accrue during route finding. - /// - /// This limit also applies to the total fees that may arise while retrying failed payment - /// paths. - /// - /// Note that values below a few sats may result in some paths being spuriously ignored. - #[cfg(feature = "uniffi")] - pub max_total_routing_fee_msat: Option, - /// The maximum total CLTV delta we accept for the route. - /// - /// Defaults to [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]. - /// - /// [`DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA`]: lightning::routing::router::DEFAULT_MAX_TOTAL_CLTV_EXPIRY_DELTA - pub max_total_cltv_expiry_delta: Option, - /// The maximum number of paths that may be used by (MPP) payments. - /// - /// Defaults to [`DEFAULT_MAX_PATH_COUNT`]. - /// - /// [`DEFAULT_MAX_PATH_COUNT`]: lightning::routing::router::DEFAULT_MAX_PATH_COUNT - pub max_path_count: Option, - /// Selects the maximum share of a channel's total capacity which will be sent over a channel, - /// as a power of 1/2. - /// - /// A higher value prefers to send the payment using more MPP parts whereas - /// a lower value prefers to send larger MPP parts, potentially saturating channels and - /// increasing failure probability for those paths. - /// - /// Note that this restriction will be relaxed during pathfinding after paths which meet this - /// restriction have been found. While paths which meet this criteria will be searched for, it - /// is ultimately up to the scorer to select them over other paths. - /// - /// Examples: - /// - /// | Value | Max Proportion of Channel Capacity Used | - /// |-------|-----------------------------------------| - /// | 0 | Up to 100% of the channel’s capacity | - /// | 1 | Up to 50% of the channel’s capacity | - /// | 2 | Up to 25% of the channel’s capacity | - /// | 3 | Up to 12.5% of the channel’s capacity | - /// - /// Default value: 2 - pub max_channel_saturation_power_of_half: Option, -} - -/// Represents the possible states of [`SendingParameters::max_total_routing_fee_msat`]. -// -// Required only in bindings as UniFFI can't expose `Option>`. -#[cfg(feature = "uniffi")] -#[derive(Copy, Clone, Debug, PartialEq, Eq)] -pub enum MaxTotalRoutingFeeLimit { - None, - Some { amount_msat: u64 }, -} - -#[cfg(feature = "uniffi")] -impl From for Option { - fn from(value: MaxTotalRoutingFeeLimit) -> Self { - match value { - MaxTotalRoutingFeeLimit::Some { amount_msat } => Some(amount_msat), - MaxTotalRoutingFeeLimit::None => None, - } - } -} - -#[cfg(feature = "uniffi")] -impl From> for MaxTotalRoutingFeeLimit { - fn from(value: Option) -> Self { - value.map_or(MaxTotalRoutingFeeLimit::None, |amount_msat| MaxTotalRoutingFeeLimit::Some { - amount_msat, - }) - } -} diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 3e48fd090f..181307a0f5 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -11,11 +11,10 @@ use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::payment::SendingParameters; use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; -use lightning::routing::router::{PaymentParameters, RouteParameters}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning::sign::EntropySource; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -52,41 +51,43 @@ impl SpontaneousPayment { /// Send a spontaneous aka. "keysend", payment. /// - /// If `sending_parameters` are provided they will override the default as well as the - /// node-wide parameters configured via [`Config::sending_parameters`] on a per-field basis. + /// If `route_parameters` are provided they will override the default as well as the + /// node-wide parameters configured via [`Config::route_parameters`] on a per-field basis. pub fn send( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, None, None) + self.send_inner(amount_msat, node_id, route_parameters, None, None) } /// Send a spontaneous payment including a list of custom TLVs. pub fn send_with_custom_tlvs( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Vec, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Vec, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs), None) + self.send_inner(amount_msat, node_id, route_parameters, Some(custom_tlvs), None) } /// Send a spontaneous payment with custom preimage pub fn send_with_preimage( &self, amount_msat: u64, node_id: PublicKey, preimage: PaymentPreimage, - sending_parameters: Option, + route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, None, Some(preimage)) + self.send_inner(amount_msat, node_id, route_parameters, None, Some(preimage)) } /// Send a spontaneous payment with custom preimage including a list of custom TLVs. pub fn send_with_preimage_and_custom_tlvs( &self, amount_msat: u64, node_id: PublicKey, custom_tlvs: Vec, - preimage: PaymentPreimage, sending_parameters: Option, + preimage: PaymentPreimage, route_parameters: Option, ) -> Result { - self.send_inner(amount_msat, node_id, sending_parameters, Some(custom_tlvs), Some(preimage)) + self.send_inner(amount_msat, node_id, route_parameters, Some(custom_tlvs), Some(preimage)) } fn send_inner( - &self, amount_msat: u64, node_id: PublicKey, sending_parameters: Option, - custom_tlvs: Option>, preimage: Option, + &self, amount_msat: u64, node_id: PublicKey, + route_parameters: Option, custom_tlvs: Option>, + preimage: Option, ) -> Result { if !*self.is_running.read().unwrap() { return Err(Error::NotRunning); @@ -112,20 +113,19 @@ impl SpontaneousPayment { amount_msat, ); - let override_params = - sending_parameters.as_ref().or(self.config.sending_parameters.as_ref()); - if let Some(override_params) = override_params { - override_params - .max_total_routing_fee_msat - .map(|f| route_params.max_total_routing_fee_msat = f.into()); - override_params - .max_total_cltv_expiry_delta - .map(|d| route_params.payment_params.max_total_cltv_expiry_delta = d); - override_params.max_path_count.map(|p| route_params.payment_params.max_path_count = p); - override_params - .max_channel_saturation_power_of_half - .map(|s| route_params.payment_params.max_channel_saturation_power_of_half = s); - }; + if let Some(RouteParametersConfig { + max_total_routing_fee_msat, + max_total_cltv_expiry_delta, + max_path_count, + max_channel_saturation_power_of_half, + }) = route_parameters.as_ref().or(self.config.route_parameters.as_ref()) + { + route_params.max_total_routing_fee_msat = *max_total_routing_fee_msat; + route_params.payment_params.max_total_cltv_expiry_delta = *max_total_cltv_expiry_delta; + route_params.payment_params.max_path_count = *max_path_count; + route_params.payment_params.max_channel_saturation_power_of_half = + *max_channel_saturation_power_of_half; + } let recipient_fields = match custom_tlvs { Some(tlvs) => RecipientOnionFields::spontaneous_empty() diff --git a/src/payment/store.rs b/src/payment/store.rs index 75b2b1b2aa..568394b488 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -9,13 +9,13 @@ use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; use lightning::util::ser::{Readable, Writeable}; -use lightning::util::string::UntrustedString; use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, impl_writeable_tlv_based_enum, write_tlv_fields, }; use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +use lightning_types::string::UntrustedString; use bitcoin::{BlockHash, Txid}; diff --git a/src/peer_store.rs b/src/peer_store.rs index 4d1c651576..cf3755d237 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -73,7 +73,7 @@ where PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PEER_INFO_PERSISTENCE_KEY, - &data, + data, ) .map_err(|e| { log_error!( diff --git a/src/types.rs b/src/types.rs index 3103ead3f3..b9bc1c317c 100644 --- a/src/types.rs +++ b/src/types.rs @@ -25,20 +25,23 @@ use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; use lightning::sign::InMemorySigner; -use lightning::util::persist::KVStore; +use lightning::util::persist::KVStoreSync; +use lightning::util::persist::KVStoreSyncWrapper; use lightning::util::ser::{Readable, Writeable, Writer}; -use lightning::util::sweep::OutputSweeper; +use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; use lightning_net_tokio::SocketDescriptor; +use lightning_liquidity::utils::time::DefaultTimeProvider; + use bitcoin::secp256k1::PublicKey; use bitcoin::OutPoint; use std::sync::{Arc, Mutex}; -pub(crate) type DynStore = dyn KVStore + Sync + Send; +pub(crate) type DynStore = dyn KVStoreSync + Sync + Send; pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, @@ -47,6 +50,7 @@ pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< Arc, Arc, Arc, + Arc, >; pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< @@ -57,10 +61,16 @@ pub(crate) type PeerManager = lightning::ln::peer_handler::PeerManager< Arc, Arc>>, Arc, + Arc, >; -pub(crate) type LiquidityManager = - lightning_liquidity::LiquidityManager, Arc, Arc>; +pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< + Arc, + Arc, + Arc, + Arc, + Arc, +>; pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< Arc, @@ -76,11 +86,8 @@ pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< pub(crate) type Broadcaster = crate::tx_broadcaster::TransactionBroadcaster>; -pub(crate) type Wallet = - crate::wallet::Wallet, Arc, Arc>; - -pub(crate) type KeysManager = - crate::wallet::WalletKeysManager, Arc, Arc>; +pub(crate) type Wallet = crate::wallet::Wallet; +pub(crate) type KeysManager = crate::wallet::WalletKeysManager; pub(crate) type Router = DefaultRouter< Arc, @@ -132,7 +139,7 @@ pub(crate) type Sweeper = OutputSweeper< Arc, Arc, Arc, - Arc, + KVStoreSyncWrapper>, Arc, Arc, >; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index fbac1d1b6d..c03353ef87 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -8,12 +8,12 @@ use persist::KVStoreWalletPersister; use crate::config::Config; -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::fee_estimator::{ConfirmationTarget, FeeEstimator}; +use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; use crate::payment::store::ConfirmationStatus; use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::PaymentStore; +use crate::types::{Broadcaster, PaymentStore}; use crate::Error; use lightning::chain::chaininterface::BroadcasterInterface; @@ -23,11 +23,11 @@ use lightning::chain::{BestBlock, Listen}; use lightning::events::bump_transaction::{Utxo, WalletSource}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::inbound_payment::ExpandedKey; -use lightning::ln::msgs::{DecodeError, UnsignedGossipMessage}; +use lightning::ln::msgs::UnsignedGossipMessage; use lightning::ln::script::ShutdownScript; use lightning::sign::{ ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, - Recipient, SignerProvider, SpendableOutputDescriptor, + PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, }; use lightning::util::message_signing; @@ -44,13 +44,14 @@ use bitcoin::key::XOnlyPublicKey; use bitcoin::psbt::Psbt; use bitcoin::secp256k1::ecdh::SharedSecret; use bitcoin::secp256k1::ecdsa::{RecoverableSignature, Signature}; -use bitcoin::secp256k1::{PublicKey, Scalar, Secp256k1, SecretKey, Signing}; +use bitcoin::secp256k1::{All, PublicKey, Scalar, Secp256k1, SecretKey}; use bitcoin::{ Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, WitnessVersion, }; -use std::ops::Deref; +use std::future::Future; +use std::pin::Pin; use std::str::FromStr; use std::sync::{Arc, Mutex}; @@ -63,32 +64,23 @@ pub(crate) enum OnchainSendAmount { pub(crate) mod persist; pub(crate) mod ser; -pub(crate) struct Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct Wallet { // A BDK on-chain wallet. inner: Mutex>, persister: Mutex, - broadcaster: B, - fee_estimator: E, + broadcaster: Arc, + fee_estimator: Arc, payment_store: Arc, config: Arc, - logger: L, + logger: Arc, } -impl Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl Wallet { pub(crate) fn new( wallet: bdk_wallet::PersistedWallet, - wallet_persister: KVStoreWalletPersister, broadcaster: B, fee_estimator: E, - payment_store: Arc, config: Arc, logger: L, + wallet_persister: KVStoreWalletPersister, broadcaster: Arc, + fee_estimator: Arc, payment_store: Arc, + config: Arc, logger: Arc, ) -> Self { let inner = Mutex::new(wallet); let persister = Mutex::new(wallet_persister); @@ -318,7 +310,7 @@ where #[cfg(debug_assertions)] if balance.confirmed != Amount::ZERO { debug_assert!( - self.list_confirmed_utxos().map_or(false, |v| !v.is_empty()), + self.list_confirmed_utxos_inner().map_or(false, |v| !v.is_empty()), "Confirmed amounts should always be available for Anchor spending" ); } @@ -568,80 +560,8 @@ where Ok(txid) } -} - -impl Listen for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn filtered_block_connected( - &self, _header: &bitcoin::block::Header, - _txdata: &lightning::chain::transaction::TransactionData, _height: u32, - ) { - debug_assert!(false, "Syncing filtered blocks is currently not supported"); - // As far as we can tell this would be a no-op anyways as we don't have to tell BDK about - // the header chain of intermediate blocks. According to the BDK team, it's sufficient to - // only connect full blocks starting from the last point of disagreement. - } - - fn block_connected(&self, block: &bitcoin::Block, height: u32) { - let mut locked_wallet = self.inner.lock().unwrap(); - - let pre_checkpoint = locked_wallet.latest_checkpoint(); - if pre_checkpoint.height() != height - 1 - || pre_checkpoint.hash() != block.header.prev_blockhash - { - log_debug!( - self.logger, - "Detected reorg while applying a connected block to on-chain wallet: new block with hash {} at height {}", - block.header.block_hash(), - height - ); - } - match locked_wallet.apply_block(block, height) { - Ok(()) => { - if let Err(e) = self.update_payment_store(&mut *locked_wallet) { - log_error!(self.logger, "Failed to update payment store: {}", e); - return; - } - }, - Err(e) => { - log_error!( - self.logger, - "Failed to apply connected block to on-chain wallet: {}", - e - ); - return; - }, - }; - - let mut locked_persister = self.persister.lock().unwrap(); - match locked_wallet.persist(&mut locked_persister) { - Ok(_) => (), - Err(e) => { - log_error!(self.logger, "Failed to persist on-chain wallet: {}", e); - return; - }, - }; - } - - fn block_disconnected(&self, _header: &bitcoin::block::Header, _height: u32) { - // This is a no-op as we don't have to tell BDK about disconnections. According to the BDK - // team, it's sufficient in case of a reorg to always connect blocks starting from the last - // point of disagreement. - } -} - -impl WalletSource for Wallet -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn list_confirmed_utxos(&self) -> Result, ()> { + fn list_confirmed_utxos_inner(&self) -> Result, ()> { let locked_wallet = self.inner.lock().unwrap(); let mut utxos = Vec::new(); let confirmed_txs: Vec = locked_wallet @@ -733,7 +653,7 @@ where Ok(utxos) } - fn get_change_script(&self) -> Result { + fn get_change_script_inner(&self) -> Result { let mut locked_wallet = self.inner.lock().unwrap(); let mut locked_persister = self.persister.lock().unwrap(); @@ -745,7 +665,7 @@ where Ok(address_info.address.script_pubkey()) } - fn sign_psbt(&self, mut psbt: Psbt) -> Result { + fn sign_psbt_inner(&self, mut psbt: Psbt) -> Result { let locked_wallet = self.inner.lock().unwrap(); // While BDK populates both `witness_utxo` and `non_witness_utxo` fields, LDK does not. As @@ -775,32 +695,102 @@ where } } +impl Listen for Wallet { + fn filtered_block_connected( + &self, _header: &bitcoin::block::Header, + _txdata: &lightning::chain::transaction::TransactionData, _height: u32, + ) { + debug_assert!(false, "Syncing filtered blocks is currently not supported"); + // As far as we can tell this would be a no-op anyways as we don't have to tell BDK about + // the header chain of intermediate blocks. According to the BDK team, it's sufficient to + // only connect full blocks starting from the last point of disagreement. + } + + fn block_connected(&self, block: &bitcoin::Block, height: u32) { + let mut locked_wallet = self.inner.lock().unwrap(); + + let pre_checkpoint = locked_wallet.latest_checkpoint(); + if pre_checkpoint.height() != height - 1 + || pre_checkpoint.hash() != block.header.prev_blockhash + { + log_debug!( + self.logger, + "Detected reorg while applying a connected block to on-chain wallet: new block with hash {} at height {}", + block.header.block_hash(), + height + ); + } + + match locked_wallet.apply_block(block, height) { + Ok(()) => { + if let Err(e) = self.update_payment_store(&mut *locked_wallet) { + log_error!(self.logger, "Failed to update payment store: {}", e); + return; + } + }, + Err(e) => { + log_error!( + self.logger, + "Failed to apply connected block to on-chain wallet: {}", + e + ); + return; + }, + }; + + let mut locked_persister = self.persister.lock().unwrap(); + match locked_wallet.persist(&mut locked_persister) { + Ok(_) => (), + Err(e) => { + log_error!(self.logger, "Failed to persist on-chain wallet: {}", e); + return; + }, + }; + } + + fn blocks_disconnected(&self, _fork_point_block: BestBlock) { + // This is a no-op as we don't have to tell BDK about disconnections. According to the BDK + // team, it's sufficient in case of a reorg to always connect blocks starting from the last + // point of disagreement. + } +} + +impl WalletSource for Wallet { + fn list_confirmed_utxos<'a>( + &'a self, + ) -> Pin, ()>> + Send + 'a>> { + Box::pin(async move { self.list_confirmed_utxos_inner() }) + } + + fn get_change_script<'a>( + &'a self, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { self.get_change_script_inner() }) + } + + fn sign_psbt<'a>( + &'a self, psbt: Psbt, + ) -> Pin> + Send + 'a>> { + Box::pin(async move { self.sign_psbt_inner(psbt) }) + } +} + /// Similar to [`KeysManager`], but overrides the destination and shutdown scripts so they are /// directly spendable by the BDK wallet. -pub(crate) struct WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +pub(crate) struct WalletKeysManager { inner: KeysManager, - wallet: Arc>, - logger: L, + wallet: Arc, + logger: Arc, } -impl WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl WalletKeysManager { /// Constructs a `WalletKeysManager` that overrides the destination and shutdown scripts. /// /// See [`KeysManager::new`] for more information on `seed`, `starting_time_secs`, and /// `starting_time_nanos`. pub fn new( - seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, - wallet: Arc>, logger: L, + seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc, + logger: Arc, ) -> Self { let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); Self { inner, wallet, logger } @@ -819,12 +809,7 @@ where } } -impl NodeSigner for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl NodeSigner for WalletKeysManager { fn get_node_id(&self, recipient: Recipient) -> Result { self.inner.get_node_id(recipient) } @@ -835,8 +820,16 @@ where self.inner.ecdh(recipient, other_key, tweak) } - fn get_inbound_payment_key(&self) -> ExpandedKey { - self.inner.get_inbound_payment_key() + fn get_expanded_key(&self) -> ExpandedKey { + self.inner.get_expanded_key() + } + + fn get_peer_storage_key(&self) -> PeerStorageKey { + self.inner.get_peer_storage_key() + } + + fn get_receive_auth_key(&self) -> lightning::sign::ReceiveAuthKey { + self.inner.get_receive_auth_key() } fn sign_invoice( @@ -854,19 +847,17 @@ where ) -> Result { self.inner.sign_bolt12_invoice(invoice) } + fn sign_message(&self, msg: &[u8]) -> Result { + self.inner.sign_message(msg) + } } -impl OutputSpender for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl OutputSpender for WalletKeysManager { /// See [`KeysManager::spend_spendable_outputs`] for documentation on this method. - fn spend_spendable_outputs( + fn spend_spendable_outputs( &self, descriptors: &[&SpendableOutputDescriptor], outputs: Vec, change_destination_script: ScriptBuf, feerate_sat_per_1000_weight: u32, - locktime: Option, secp_ctx: &Secp256k1, + locktime: Option, secp_ctx: &Secp256k1, ) -> Result { self.inner.spend_spendable_outputs( descriptors, @@ -879,39 +870,21 @@ where } } -impl EntropySource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl EntropySource for WalletKeysManager { fn get_secure_random_bytes(&self) -> [u8; 32] { self.inner.get_secure_random_bytes() } } -impl SignerProvider for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ +impl SignerProvider for WalletKeysManager { type EcdsaSigner = InMemorySigner; - fn generate_channel_keys_id( - &self, inbound: bool, channel_value_satoshis: u64, user_channel_id: u128, - ) -> [u8; 32] { - self.inner.generate_channel_keys_id(inbound, channel_value_satoshis, user_channel_id) - } - - fn derive_channel_signer( - &self, channel_value_satoshis: u64, channel_keys_id: [u8; 32], - ) -> Self::EcdsaSigner { - self.inner.derive_channel_signer(channel_value_satoshis, channel_keys_id) + fn generate_channel_keys_id(&self, inbound: bool, user_channel_id: u128) -> [u8; 32] { + self.inner.generate_channel_keys_id(inbound, user_channel_id) } - fn read_chan_signer(&self, reader: &[u8]) -> Result { - self.inner.read_chan_signer(reader) + fn derive_channel_signer(&self, channel_keys_id: [u8; 32]) -> Self::EcdsaSigner { + self.inner.derive_channel_signer(channel_keys_id) } fn get_destination_script(&self, _channel_keys_id: [u8; 32]) -> Result { @@ -941,16 +914,20 @@ where } } -impl ChangeDestinationSource for WalletKeysManager -where - B::Target: BroadcasterInterface, - E::Target: FeeEstimator, - L::Target: LdkLogger, -{ - fn get_change_destination_script(&self) -> Result { - let address = self.wallet.get_new_internal_address().map_err(|e| { - log_error!(self.logger, "Failed to retrieve new address from wallet: {}", e); - })?; - Ok(address.script_pubkey()) +impl ChangeDestinationSource for WalletKeysManager { + fn get_change_destination_script<'a>( + &self, + ) -> Pin> + Send + 'a>> { + let wallet = Arc::clone(&self.wallet); + let logger = Arc::clone(&self.logger); + Box::pin(async move { + wallet + .get_new_internal_address() + .map_err(|e| { + log_error!(logger, "Failed to retrieve new address from wallet: {}", e); + }) + .map(|addr| addr.script_pubkey()) + .map_err(|_| ()) + }) } } diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 780e9bbf4f..f5bfe76fce 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -21,7 +21,7 @@ use ldk_node::{ use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; -use lightning::util::persist::KVStore; +use lightning::util::persist::KVStoreSync; use lightning::util::test_utils::TestStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; @@ -1236,7 +1236,7 @@ impl TestSyncStore { } } -impl KVStore for TestSyncStore { +impl KVStoreSync for TestSyncStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result> { @@ -1263,12 +1263,14 @@ impl KVStore for TestSyncStore { } fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); - let fs_res = self.fs_store.write(primary_namespace, secondary_namespace, key, buf); - let sqlite_res = self.sqlite_store.write(primary_namespace, secondary_namespace, key, buf); - let test_res = self.test_store.write(primary_namespace, secondary_namespace, key, buf); + let fs_res = self.fs_store.write(primary_namespace, secondary_namespace, key, buf.clone()); + let sqlite_res = + self.sqlite_store.write(primary_namespace, secondary_namespace, key, buf.clone()); + let test_res = + self.test_store.write(primary_namespace, secondary_namespace, key, buf.clone()); assert!(self .do_list(primary_namespace, secondary_namespace) diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 0932116ef2..fa88fe0cce 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -22,13 +22,14 @@ use ldk_node::config::EsploraSyncConfig; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, - QrPaymentResult, SendingParameters, + QrPaymentResult, }; use ldk_node::{Builder, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; -use lightning::util::persist::KVStore; +use lightning::routing::router::RouteParametersConfig; +use lightning::util::persist::KVStoreSync; use lightning_invoice::{Bolt11InvoiceDescription, Description}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -212,11 +213,11 @@ fn multi_hop_sending() { // Sleep a bit for gossip to propagate. std::thread::sleep(std::time::Duration::from_secs(1)); - let sending_params = SendingParameters { - max_total_routing_fee_msat: Some(Some(75_000).into()), - max_total_cltv_expiry_delta: Some(1000), - max_path_count: Some(10), - max_channel_saturation_power_of_half: Some(2), + let route_params = RouteParametersConfig { + max_total_routing_fee_msat: Some(75_000), + max_total_cltv_expiry_delta: 1000, + max_path_count: 10, + max_channel_saturation_power_of_half: 2, }; let invoice_description = @@ -225,7 +226,7 @@ fn multi_hop_sending() { .bolt11_payment() .receive(2_500_000, &invoice_description.clone().into(), 9217) .unwrap(); - nodes[0].bolt11_payment().send(&invoice, Some(sending_params)).unwrap(); + nodes[0].bolt11_payment().send(&invoice, Some(route_params)).unwrap(); expect_event!(nodes[1], PaymentForwarded); @@ -246,7 +247,7 @@ fn start_stop_reinit() { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let test_sync_store: Arc = + let test_sync_store: Arc = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.clone().into())); let sync_config = EsploraSyncConfig { background_sync_config: None }; From 4b45d7c1e6f3494e23f005ce04eb8761d06ec6af Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 15 Aug 2025 09:42:47 +0200 Subject: [PATCH 138/177] Switch to use `rustls-ring` everywhere We switch to use `rustls-ring` everywhere, which is necessary for Swift builds, but also generally makes our lives easier. --- .github/workflows/kotlin.yml | 3 --- Cargo.toml | 12 ++++++------ src/builder.rs | 2 +- 3 files changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/workflows/kotlin.yml b/.github/workflows/kotlin.yml index 5cb1b8c279..a1711ba498 100644 --- a/.github/workflows/kotlin.yml +++ b/.github/workflows/kotlin.yml @@ -39,9 +39,6 @@ jobs: - name: Generate Kotlin JVM run: ./scripts/uniffi_bindgen_generate_kotlin.sh - - name: Install `bindgen-cli` - run: cargo install --force bindgen-cli - - name: Generate Kotlin Android run: ./scripts/uniffi_bindgen_generate_kotlin_android.sh diff --git a/Cargo.toml b/Cargo.toml index aaaa55f39b..9010ad6d59 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,7 +36,7 @@ default = [] #lightning-background-processor = { version = "0.1.0" } #lightning-rapid-gossip-sync = { version = "0.1.0" } #lightning-block-sync = { version = "0.1.0", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum"] } +#lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } #lightning-liquidity = { version = "0.1.0", features = ["std"] } #lightning-macros = { version = "0.1.0" } @@ -48,7 +48,7 @@ default = [] #lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum", "time"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } @@ -60,7 +60,7 @@ lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["esplora-async-https", "electrum", "time"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } @@ -72,13 +72,13 @@ lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", #lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } #lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum", "time"] } +#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } #lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } #lightning-macros = { path = "../rust-lightning/lightning-macros" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} -bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls"]} +bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls-ring"]} bdk_wallet = { version = "2.0.0", default-features = false, features = ["std", "keys-bip39"]} reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } @@ -93,7 +93,7 @@ rand = "0.8.5" chrono = { version = "0.4", default-features = false, features = ["clock"] } tokio = { version = "1.37", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } esplora-client = { version = "0.12", default-features = false, features = ["tokio", "async-https-rustls"] } -electrum-client = { version = "0.24.0", default-features = true } +electrum-client = { version = "0.24.0", default-features = false, features = ["proxy", "use-rustls-ring"] } libc = "0.2" uniffi = { version = "0.28.3", features = ["build"], optional = true } serde = { version = "1.0.210", default-features = false, features = ["std", "derive"] } diff --git a/src/builder.rs b/src/builder.rs index 094c21e726..a46b182e1f 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1688,7 +1688,7 @@ fn optionally_install_rustls_cryptoprovider() { INIT_CRYPTO.call_once(|| { // Ensure we always install a `CryptoProvider` for `rustls` if it was somehow not previously installed by now. if rustls::crypto::CryptoProvider::get_default().is_none() { - let _ = rustls::crypto::aws_lc_rs::default_provider().install_default(); + let _ = rustls::crypto::ring::default_provider().install_default(); } // Refuse to startup without TLS support. Better to catch it now than even later at runtime. From 80ac9f35eaf39847ed6c2438df1c25e0299a6807 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 3 Sep 2025 12:46:07 +0200 Subject: [PATCH 139/177] Use log timestamps with millisecond resolution Helpful to correlate multiple log files throughout time --- src/logger.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/logger.rs b/src/logger.rs index bbd24ec207..40817897c1 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -124,7 +124,7 @@ impl LogWriter for Writer { let log = format!( "{} {:<5} [{}:{}] {}\n", - Utc::now().format("%Y-%m-%d %H:%M:%S"), + Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), record.level.to_string(), record.module_path, record.line, From 66f5c28ca8663a3c9e5b7562660192f3804bec4f Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 19 Aug 2025 15:37:31 +0200 Subject: [PATCH 140/177] Add static invoice support This commit adds support for using ldk-node as a static invoice server. When configured as such, the node persists and retrieves invoices from the configured kv store. Access is guarded by a rate limiter to prevent overload and mitigate potential DoS attacks. In this mode, ldk-node also exposes blinded paths that can be shared with async recipients, allowing them to contact the static invoice server. When ldk-node functions as a recipient, it can communicate with the static invoice server to set up async payments. --- bindings/ldk_node.udl | 9 + src/builder.rs | 2 +- src/config.rs | 3 + src/error.rs | 8 + src/event.rs | 63 +++- src/io/mod.rs | 5 + src/lib.rs | 10 + src/payment/asynchronous/mod.rs | 9 + src/payment/asynchronous/rate_limiter.rs | 96 ++++++ .../asynchronous/static_invoice_store.rs | 277 ++++++++++++++++++ src/payment/bolt12.rs | 105 ++++++- src/payment/mod.rs | 1 + src/types.rs | 2 +- tests/integration_tests_rust.rs | 95 ++++++ 14 files changed, 672 insertions(+), 13 deletions(-) create mode 100644 src/payment/asynchronous/mod.rs create mode 100644 src/payment/asynchronous/rate_limiter.rs create mode 100644 src/payment/asynchronous/static_invoice_store.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index b9bab61e8a..9f0ef697e8 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,6 +13,7 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; RouteParametersConfig? route_parameters; + boolean async_payment_services_enabled; }; dictionary AnchorChannelsConfig { @@ -209,6 +210,12 @@ interface Bolt12Payment { Bolt12Invoice request_refund_payment([ByRef]Refund refund); [Throws=NodeError] Refund initiate_refund(u64 amount_msat, u32 expiry_secs, u64? quantity, string? payer_note); + [Throws=NodeError] + Offer receive_async(); + [Throws=NodeError] + void set_paths_to_static_invoice_server(bytes paths); + [Throws=NodeError] + bytes blinded_paths_for_async_recipient(bytes recipient_id); }; interface SpontaneousPayment { @@ -311,6 +318,8 @@ enum NodeError { "InsufficientFunds", "LiquiditySourceUnavailable", "LiquidityFeeTooHigh", + "InvalidBlindedPaths", + "AsyncPaymentServicesDisabled", }; dictionary NodeStatus { diff --git a/src/builder.rs b/src/builder.rs index a46b182e1f..d330597eed 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1455,7 +1455,7 @@ fn build_with_store_internal( Arc::clone(&channel_manager), message_router, Arc::clone(&channel_manager), - IgnoringMessageHandler {}, + Arc::clone(&channel_manager), IgnoringMessageHandler {}, IgnoringMessageHandler {}, )); diff --git a/src/config.rs b/src/config.rs index 84f62d220a..bb0bd56ba0 100644 --- a/src/config.rs +++ b/src/config.rs @@ -179,6 +179,8 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, + /// Whether to enable the static invoice service to support async payment reception for clients. + pub async_payment_services_enabled: bool, } impl Default for Config { @@ -193,6 +195,7 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, + async_payment_services_enabled: false, } } } diff --git a/src/error.rs b/src/error.rs index 2cb71186d3..eaa022e565 100644 --- a/src/error.rs +++ b/src/error.rs @@ -120,6 +120,10 @@ pub enum Error { LiquiditySourceUnavailable, /// The given operation failed due to the LSP's required opening fee being too high. LiquidityFeeTooHigh, + /// The given blinded paths are invalid. + InvalidBlindedPaths, + /// Asynchronous payment services are disabled. + AsyncPaymentServicesDisabled, } impl fmt::Display for Error { @@ -193,6 +197,10 @@ impl fmt::Display for Error { Self::LiquidityFeeTooHigh => { write!(f, "The given operation failed due to the LSP's required opening fee being too high.") }, + Self::InvalidBlindedPaths => write!(f, "The given blinded paths are invalid."), + Self::AsyncPaymentServicesDisabled => { + write!(f, "Asynchronous payment services are disabled.") + }, } } } diff --git a/src/event.rs b/src/event.rs index bad1b84ab2..7a6dc48329 100644 --- a/src/event.rs +++ b/src/event.rs @@ -6,7 +6,6 @@ // accordance with one or both of these licenses. use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet}; - use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, UserChannelId, @@ -19,6 +18,7 @@ use crate::fee_estimator::ConfirmationTarget; use crate::liquidity::LiquiditySource; use crate::logger::Logger; +use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; use crate::payment::store::{ PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, }; @@ -27,7 +27,7 @@ use crate::io::{ EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, }; -use crate::logger::{log_debug, log_error, log_info, LdkLogger}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; use crate::runtime::Runtime; @@ -458,6 +458,7 @@ where runtime: Arc, logger: L, config: Arc, + static_invoice_store: Option, } impl EventHandler @@ -470,8 +471,9 @@ where channel_manager: Arc, connection_manager: Arc>, output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, - payment_store: Arc, peer_store: Arc>, runtime: Arc, - logger: L, config: Arc, + payment_store: Arc, peer_store: Arc>, + static_invoice_store: Option, runtime: Arc, logger: L, + config: Arc, ) -> Self { Self { event_queue, @@ -487,6 +489,7 @@ where logger, runtime, config, + static_invoice_store, } } @@ -1494,11 +1497,55 @@ where LdkEvent::OnionMessagePeerConnected { .. } => { debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); }, - LdkEvent::PersistStaticInvoice { .. } => { - debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted."); + + LdkEvent::PersistStaticInvoice { + invoice, + invoice_slot, + recipient_id, + invoice_persisted_path, + } => { + if let Some(store) = self.static_invoice_store.as_ref() { + match store + .handle_persist_static_invoice(invoice, invoice_slot, recipient_id) + .await + { + Ok(_) => { + self.channel_manager.static_invoice_persisted(invoice_persisted_path); + }, + Err(e) => { + log_error!(self.logger, "Failed to persist static invoice: {}", e); + return Err(ReplayEvent()); + }, + }; + } }, - LdkEvent::StaticInvoiceRequested { .. } => { - debug_assert!(false, "We currently don't support static invoice persistence, so this event should never be emitted."); + LdkEvent::StaticInvoiceRequested { recipient_id, invoice_slot, reply_path } => { + if let Some(store) = self.static_invoice_store.as_ref() { + let invoice = + store.handle_static_invoice_requested(&recipient_id, invoice_slot).await; + + match invoice { + Ok(Some(invoice)) => { + if let Err(e) = + self.channel_manager.send_static_invoice(invoice, reply_path) + { + log_error!(self.logger, "Failed to send static invoice: {:?}", e); + } + }, + Ok(None) => { + log_trace!( + self.logger, + "No static invoice found for recipient {} and slot {}", + hex_utils::to_string(&recipient_id), + invoice_slot + ); + }, + Err(e) => { + log_error!(self.logger, "Failed to retrieve static invoice: {}", e); + return Err(ReplayEvent()); + }, + } + } }, LdkEvent::FundingTransactionReadyForSigning { .. } => { debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted."); diff --git a/src/io/mod.rs b/src/io/mod.rs index 7a52a5c984..38fba5114f 100644 --- a/src/io/mod.rs +++ b/src/io/mod.rs @@ -73,3 +73,8 @@ pub(crate) const BDK_WALLET_TX_GRAPH_KEY: &str = "tx_graph"; pub(crate) const BDK_WALLET_INDEXER_PRIMARY_NAMESPACE: &str = "bdk_wallet"; pub(crate) const BDK_WALLET_INDEXER_SECONDARY_NAMESPACE: &str = ""; pub(crate) const BDK_WALLET_INDEXER_KEY: &str = "indexer"; + +/// [`StaticInvoice`]s will be persisted under this key. +/// +/// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice +pub(crate) const STATIC_INVOICE_STORE_PRIMARY_NAMESPACE: &str = "static_invoices"; diff --git a/src/lib.rs b/src/lib.rs index 160762dd2c..e7e27273b8 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,6 +136,7 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; +use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, UnifiedQrPayment, @@ -498,6 +499,12 @@ impl Node { Arc::clone(&self.logger), )); + let static_invoice_store = if self.config.async_payment_services_enabled { + Some(StaticInvoiceStore::new(Arc::clone(&self.kv_store))) + } else { + None + }; + let event_handler = Arc::new(EventHandler::new( Arc::clone(&self.event_queue), Arc::clone(&self.wallet), @@ -509,6 +516,7 @@ impl Node { self.liquidity_source.clone(), Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), + static_invoice_store, Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), @@ -818,6 +826,7 @@ impl Node { Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), ) @@ -831,6 +840,7 @@ impl Node { Arc::new(Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), + Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), )) diff --git a/src/payment/asynchronous/mod.rs b/src/payment/asynchronous/mod.rs new file mode 100644 index 0000000000..ebb7a4bd3e --- /dev/null +++ b/src/payment/asynchronous/mod.rs @@ -0,0 +1,9 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +mod rate_limiter; +pub(crate) mod static_invoice_store; diff --git a/src/payment/asynchronous/rate_limiter.rs b/src/payment/asynchronous/rate_limiter.rs new file mode 100644 index 0000000000..153577b16b --- /dev/null +++ b/src/payment/asynchronous/rate_limiter.rs @@ -0,0 +1,96 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! [`RateLimiter`] to control the rate of requests from users. + +use std::collections::HashMap; +use std::time::{Duration, Instant}; + +/// Implements a leaky-bucket style rate limiter parameterized by the max capacity of the bucket, the refill interval, +/// and the max idle duration. +/// +/// For every passing of the refill interval, one token is added to the bucket, up to the maximum capacity. When the +/// bucket has remained at the maximum capacity for longer than the max idle duration, it is removed to prevent memory +/// leakage. +pub(crate) struct RateLimiter { + users: HashMap, Bucket>, + capacity: u32, + refill_interval: Duration, + max_idle: Duration, +} + +struct Bucket { + tokens: u32, + last_refill: Instant, +} + +impl RateLimiter { + pub(crate) fn new(capacity: u32, refill_interval: Duration, max_idle: Duration) -> Self { + Self { users: HashMap::new(), capacity, refill_interval, max_idle } + } + + pub(crate) fn allow(&mut self, user_id: &[u8]) -> bool { + let now = Instant::now(); + + let entry = self.users.entry(user_id.to_vec()); + let is_new_user = matches!(entry, std::collections::hash_map::Entry::Vacant(_)); + + let bucket = entry.or_insert(Bucket { tokens: self.capacity, last_refill: now }); + + let elapsed = now.duration_since(bucket.last_refill); + let tokens_to_add = (elapsed.as_secs_f64() / self.refill_interval.as_secs_f64()) as u32; + + if tokens_to_add > 0 { + bucket.tokens = (bucket.tokens + tokens_to_add).min(self.capacity); + bucket.last_refill = now; + } + + let allow = if bucket.tokens > 0 { + bucket.tokens -= 1; + true + } else { + false + }; + + // Each time a new user is added, we take the opportunity to clean up old rate limits. + if is_new_user { + self.garbage_collect(self.max_idle); + } + + allow + } + + fn garbage_collect(&mut self, max_idle: Duration) { + let now = Instant::now(); + self.users.retain(|_, bucket| now.duration_since(bucket.last_refill) < max_idle); + } +} + +#[cfg(test)] +mod tests { + use crate::payment::asynchronous::rate_limiter::RateLimiter; + + use std::time::Duration; + + #[test] + fn rate_limiter_test() { + // Test + let mut rate_limiter = + RateLimiter::new(3, Duration::from_millis(100), Duration::from_secs(1)); + + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user1")); + assert!(!rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user2")); + + std::thread::sleep(Duration::from_millis(150)); + + assert!(rate_limiter.allow(b"user1")); + assert!(rate_limiter.allow(b"user2")); + } +} diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs new file mode 100644 index 0000000000..eed6720e51 --- /dev/null +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -0,0 +1,277 @@ +// This file is Copyright its original authors, visible in version control history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license , at your option. You may not use this file except in +// accordance with one or both of these licenses. + +//! Store implementation for [`StaticInvoice`]s. + +use crate::hex_utils; +use crate::io::STATIC_INVOICE_STORE_PRIMARY_NAMESPACE; +use crate::payment::asynchronous::rate_limiter::RateLimiter; +use crate::types::DynStore; + +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; + +use lightning::{offers::static_invoice::StaticInvoice, util::ser::Writeable}; + +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +pub(crate) struct StaticInvoiceStore { + kv_store: Arc, + request_rate_limiter: Mutex, + persist_rate_limiter: Mutex, +} + +impl StaticInvoiceStore { + const RATE_LIMITER_BUCKET_CAPACITY: u32 = 5; + const RATE_LIMITER_REFILL_INTERVAL: Duration = Duration::from_millis(100); + const RATE_LIMITER_MAX_IDLE: Duration = Duration::from_secs(600); + + pub(crate) fn new(kv_store: Arc) -> Self { + Self { + kv_store, + request_rate_limiter: Mutex::new(RateLimiter::new( + Self::RATE_LIMITER_BUCKET_CAPACITY, + Self::RATE_LIMITER_REFILL_INTERVAL, + Self::RATE_LIMITER_MAX_IDLE, + )), + persist_rate_limiter: Mutex::new(RateLimiter::new( + Self::RATE_LIMITER_BUCKET_CAPACITY, + Self::RATE_LIMITER_REFILL_INTERVAL, + Self::RATE_LIMITER_MAX_IDLE, + )), + } + } + + fn check_rate_limit( + limiter: &Mutex, recipient_id: &[u8], + ) -> Result<(), lightning::io::Error> { + let mut limiter = limiter.lock().unwrap(); + if !limiter.allow(recipient_id) { + Err(lightning::io::Error::new(lightning::io::ErrorKind::Other, "Rate limit exceeded")) + } else { + Ok(()) + } + } + + pub(crate) async fn handle_static_invoice_requested( + &self, recipient_id: &[u8], invoice_slot: u16, + ) -> Result, lightning::io::Error> { + Self::check_rate_limit(&self.request_rate_limiter, &recipient_id)?; + + let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id); + + self.kv_store + .read(STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, &secondary_namespace, &key) + .and_then(|data| { + data.try_into().map(Some).map_err(|e| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + format!("Failed to parse static invoice: {:?}", e), + ) + }) + }) + .or_else( + |e| { + if e.kind() == lightning::io::ErrorKind::NotFound { + Ok(None) + } else { + Err(e) + } + }, + ) + } + + pub(crate) async fn handle_persist_static_invoice( + &self, invoice: StaticInvoice, invoice_slot: u16, recipient_id: Vec, + ) -> Result<(), lightning::io::Error> { + Self::check_rate_limit(&self.persist_rate_limiter, &recipient_id)?; + + let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, &recipient_id); + + let mut buf = Vec::new(); + invoice.write(&mut buf)?; + + // Static invoices will be persisted at "static_invoices//". + // + // Example: static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/00001 + self.kv_store.write(STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, &secondary_namespace, &key, buf) + } + + fn get_storage_location(invoice_slot: u16, recipient_id: &[u8]) -> (String, String) { + let hash = Sha256::hash(recipient_id).to_byte_array(); + let secondary_namespace = hex_utils::to_string(&hash); + + let key = format!("{:05}", invoice_slot); + (secondary_namespace, key) + } +} + +#[cfg(test)] +mod tests { + use std::{sync::Arc, time::Duration}; + + use bitcoin::{ + key::{Keypair, Secp256k1}, + secp256k1::{PublicKey, SecretKey}, + }; + use lightning::blinded_path::{ + message::BlindedMessagePath, + payment::{BlindedPayInfo, BlindedPaymentPath}, + BlindedHop, + }; + use lightning::ln::inbound_payment::ExpandedKey; + use lightning::offers::{ + nonce::Nonce, + offer::OfferBuilder, + static_invoice::{StaticInvoice, StaticInvoiceBuilder}, + }; + use lightning::sign::EntropySource; + use lightning::util::test_utils::TestStore; + use lightning_types::features::BlindedHopFeatures; + + use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; + use crate::types::DynStore; + + #[tokio::test] + async fn static_invoice_store_test() { + let store: Arc = Arc::new(TestStore::new(false)); + let static_invoice_store = StaticInvoiceStore::new(Arc::clone(&store)); + + let static_invoice = invoice(); + let recipient_id = vec![1, 1, 1]; + assert!(static_invoice_store + .handle_persist_static_invoice(static_invoice.clone(), 0, recipient_id.clone()) + .await + .is_ok()); + + let requested_invoice = + static_invoice_store.handle_static_invoice_requested(&recipient_id, 0).await.unwrap(); + + assert_eq!(requested_invoice.unwrap(), static_invoice); + + assert!(static_invoice_store + .handle_static_invoice_requested(&recipient_id, 1) + .await + .unwrap() + .is_none()); + + assert!(static_invoice_store + .handle_static_invoice_requested(&[2, 2, 2], 0) + .await + .unwrap() + .is_none()); + } + + fn invoice() -> StaticInvoice { + let node_id = recipient_pubkey(); + let payment_paths = payment_paths(); + let now = now(); + let expanded_key = ExpandedKey::new([42; 32]); + let entropy = FixedEntropy {}; + let nonce = Nonce::from_entropy_source(&entropy); + let secp_ctx = Secp256k1::new(); + + let offer = OfferBuilder::deriving_signing_pubkey(node_id, &expanded_key, nonce, &secp_ctx) + .path(blinded_path()) + .build() + .unwrap(); + + StaticInvoiceBuilder::for_offer_using_derived_keys( + &offer, + payment_paths.clone(), + vec![blinded_path()], + now, + &expanded_key, + nonce, + &secp_ctx, + ) + .unwrap() + .build_and_sign(&secp_ctx) + .unwrap() + } + + fn now() -> Duration { + std::time::SystemTime::now() + .duration_since(std::time::SystemTime::UNIX_EPOCH) + .expect("SystemTime::now() should come after SystemTime::UNIX_EPOCH") + } + + fn payment_paths() -> Vec { + vec![ + BlindedPaymentPath::from_blinded_path_and_payinfo( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(44), encrypted_payload: vec![0; 44] }, + ], + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ), + BlindedPaymentPath::from_blinded_path_and_payinfo( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(45), encrypted_payload: vec![0; 45] }, + BlindedHop { blinded_node_id: pubkey(46), encrypted_payload: vec![0; 46] }, + ], + BlindedPayInfo { + fee_base_msat: 1, + fee_proportional_millionths: 1_000, + cltv_expiry_delta: 42, + htlc_minimum_msat: 100, + htlc_maximum_msat: 1_000_000_000_000, + features: BlindedHopFeatures::empty(), + }, + ), + ] + } + + fn blinded_path() -> BlindedMessagePath { + BlindedMessagePath::from_blinded_path( + pubkey(40), + pubkey(41), + vec![ + BlindedHop { blinded_node_id: pubkey(42), encrypted_payload: vec![0; 43] }, + BlindedHop { blinded_node_id: pubkey(43), encrypted_payload: vec![0; 44] }, + ], + ) + } + + fn pubkey(byte: u8) -> PublicKey { + let secp_ctx = Secp256k1::new(); + PublicKey::from_secret_key(&secp_ctx, &privkey(byte)) + } + + fn privkey(byte: u8) -> SecretKey { + SecretKey::from_slice(&[byte; 32]).unwrap() + } + + fn recipient_keys() -> Keypair { + let secp_ctx = Secp256k1::new(); + Keypair::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[43; 32]).unwrap()) + } + + fn recipient_pubkey() -> PublicKey { + recipient_keys().public_key() + } + + struct FixedEntropy; + + impl EntropySource for FixedEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [42; 32] + } + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 4e968deb7a..81349e2bd4 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,18 +9,21 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::LDK_PAYMENT_RETRY_TIMEOUT; +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; use crate::types::{ChannelManager, PaymentStore}; +use lightning::blinded_path::message::BlindedMessagePath; use lightning::ln::channelmanager::{PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::routing::router::RouteParametersConfig; +#[cfg(feature = "uniffi")] +use lightning::util::ser::{Readable, Writeable}; use lightning_types::string::UntrustedString; use rand::RngCore; @@ -54,15 +57,16 @@ pub struct Bolt12Payment { channel_manager: Arc, payment_store: Arc, is_running: Arc>, + config: Arc, logger: Arc, } impl Bolt12Payment { pub(crate) fn new( channel_manager: Arc, payment_store: Arc, - is_running: Arc>, logger: Arc, + config: Arc, is_running: Arc>, logger: Arc, ) -> Self { - Self { channel_manager, payment_store, is_running, logger } + Self { channel_manager, payment_store, config, is_running, logger } } /// Send a payment given an offer. @@ -450,4 +454,99 @@ impl Bolt12Payment { Ok(maybe_wrap(refund)) } + + /// Retrieve an [`Offer`] for receiving async payments as an often-offline recipient. + /// + /// Will only return an offer if [`Bolt12Payment::set_paths_to_static_invoice_server`] was called and we succeeded + /// in interactively building a [`StaticInvoice`] with the static invoice server. + /// + /// Useful for posting offers to receive payments later, such as posting an offer on a website. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + /// [`Offer`]: lightning::offers::offer::Offer + pub fn receive_async(&self) -> Result { + self.channel_manager + .get_async_receive_offer() + .map(maybe_wrap) + .or(Err(Error::OfferCreationFailed)) + } + + /// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a + /// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(not(feature = "uniffi"))] + pub fn set_paths_to_static_invoice_server( + &self, paths: Vec, + ) -> Result<(), Error> { + self.channel_manager + .set_paths_to_static_invoice_server(paths) + .or(Err(Error::InvalidBlindedPaths)) + } + + /// Sets the [`BlindedMessagePath`]s that we will use as an async recipient to interactively build [`Offer`]s with a + /// static invoice server, so the server can serve [`StaticInvoice`]s to payers on our behalf when we're offline. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(feature = "uniffi")] + pub fn set_paths_to_static_invoice_server(&self, paths: Vec) -> Result<(), Error> { + let decoded_paths = as Readable>::read(&mut &paths[..]) + .or(Err(Error::InvalidBlindedPaths))?; + + self.channel_manager + .set_paths_to_static_invoice_server(decoded_paths) + .or(Err(Error::InvalidBlindedPaths)) + } + + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(not(feature = "uniffi"))] + pub fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, + ) -> Result, Error> { + self.blinded_paths_for_async_recipient_internal(recipient_id) + } + + /// [`BlindedMessagePath`]s for an async recipient to communicate with this node and interactively + /// build [`Offer`]s and [`StaticInvoice`]s for receiving async payments. + /// + /// **Caution**: Async payments support is considered experimental. + /// + /// [`Offer`]: lightning::offers::offer::Offer + /// [`StaticInvoice`]: lightning::offers::static_invoice::StaticInvoice + #[cfg(feature = "uniffi")] + pub fn blinded_paths_for_async_recipient( + &self, recipient_id: Vec, + ) -> Result, Error> { + let paths = self.blinded_paths_for_async_recipient_internal(recipient_id)?; + + let mut bytes = Vec::new(); + paths.write(&mut bytes).or(Err(Error::InvalidBlindedPaths))?; + Ok(bytes) + } + + fn blinded_paths_for_async_recipient_internal( + &self, recipient_id: Vec, + ) -> Result, Error> { + if !self.config.async_payment_services_enabled { + return Err(Error::AsyncPaymentServicesDisabled); + } + + self.channel_manager + .blinded_paths_for_async_recipient(recipient_id, None) + .or(Err(Error::InvalidBlindedPaths)) + } } diff --git a/src/payment/mod.rs b/src/payment/mod.rs index 54f7894dcd..f629960e1d 100644 --- a/src/payment/mod.rs +++ b/src/payment/mod.rs @@ -7,6 +7,7 @@ //! Objects for different types of payments. +pub(crate) mod asynchronous; mod bolt11; mod bolt12; mod onchain; diff --git a/src/types.rs b/src/types.rs index b9bc1c317c..3635badffe 100644 --- a/src/types.rs +++ b/src/types.rs @@ -123,7 +123,7 @@ pub(crate) type OnionMessenger = lightning::onion_message::messenger::OnionMesse Arc, Arc, Arc, - IgnoringMessageHandler, + Arc, IgnoringMessageHandler, IgnoringMessageHandler, >; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index fa88fe0cce..77f46091d0 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -1130,6 +1130,101 @@ fn simple_bolt12_send_receive() { assert_eq!(node_a_payments.first().unwrap().amount_msat, Some(overpaid_amount)); } +#[test] +fn static_invoice_server() { + let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + + let config_sender = random_config(true); + let node_sender = setup_node(&chain_source, config_sender, None); + + let config_sender_lsp = random_config(true); + let node_sender_lsp = setup_node(&chain_source, config_sender_lsp, None); + + let mut config_receiver_lsp = random_config(true); + config_receiver_lsp.node_config.async_payment_services_enabled = true; + let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp, None); + + let config_receiver = random_config(true); + let node_receiver = setup_node(&chain_source, config_receiver, None); + + let address_sender = node_sender.onchain_payment().new_address().unwrap(); + let address_sender_lsp = node_sender_lsp.onchain_payment().new_address().unwrap(); + let address_receiver_lsp = node_receiver_lsp.onchain_payment().new_address().unwrap(); + let address_receiver = node_receiver.onchain_payment().new_address().unwrap(); + let premine_amount_sat = 4_000_000; + premine_and_distribute_funds( + &bitcoind.client, + &electrsd.client, + vec![address_sender, address_sender_lsp, address_receiver_lsp, address_receiver], + Amount::from_sat(premine_amount_sat), + ); + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + open_channel(&node_sender, &node_sender_lsp, 400_000, true, &electrsd); + open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd); + open_channel(&node_receiver_lsp, &node_receiver, 400_000, true, &electrsd); + + generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); + + node_sender.sync_wallets().unwrap(); + node_sender_lsp.sync_wallets().unwrap(); + node_receiver_lsp.sync_wallets().unwrap(); + node_receiver.sync_wallets().unwrap(); + + expect_channel_ready_event!(node_sender, node_sender_lsp.node_id()); + expect_channel_ready_event!(node_sender_lsp, node_sender.node_id()); + expect_channel_ready_event!(node_sender_lsp, node_receiver_lsp.node_id()); + expect_channel_ready_event!(node_receiver_lsp, node_sender_lsp.node_id()); + expect_channel_ready_event!(node_receiver_lsp, node_receiver.node_id()); + expect_channel_ready_event!(node_receiver, node_receiver_lsp.node_id()); + + let has_node_announcements = |node: &ldk_node::Node| { + node.network_graph() + .list_nodes() + .iter() + .filter(|n| { + node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) + }) + .count() >= 4 + }; + + // Wait for everyone to see all channels and node announcements. + while node_sender.network_graph().list_channels().len() < 3 + || node_sender_lsp.network_graph().list_channels().len() < 3 + || node_receiver_lsp.network_graph().list_channels().len() < 3 + || node_receiver.network_graph().list_channels().len() < 3 + || !has_node_announcements(&node_sender) + || !has_node_announcements(&node_sender_lsp) + || !has_node_announcements(&node_receiver_lsp) + || !has_node_announcements(&node_receiver) + { + std::thread::sleep(std::time::Duration::from_millis(100)); + } + + let recipient_id = vec![1, 2, 3]; + let blinded_paths = + node_receiver_lsp.bolt12_payment().blinded_paths_for_async_recipient(recipient_id).unwrap(); + node_receiver.bolt12_payment().set_paths_to_static_invoice_server(blinded_paths).unwrap(); + + let offer = loop { + if let Ok(offer) = node_receiver.bolt12_payment().receive_async() { + break offer; + } + + std::thread::sleep(std::time::Duration::from_millis(100)); + }; + + let payment_id = + node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None).unwrap(); + + expect_payment_successful_event!(node_sender, Some(payment_id), None); +} + #[test] fn test_node_announcement_propagation() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); From 006a06e157ef6f9d80584b9be33bfd5b8a47cf02 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 16 Sep 2025 10:19:33 +0200 Subject: [PATCH 141/177] Adapt channel balance reporting to use confirmed candidate With splicing now implemented, a channel may have multiple holder commitment transactions and corresponding balance candidates. ldk-node now reports the confirmed balance candidate rather than a single static balance, ensuring the exposed value matches the channel's onchain state. Other candidate balances remain internal for now. --- Cargo.toml | 24 ++++++++++++------------ src/balance.rs | 30 ++++++++++++++++++------------ 2 files changed, 30 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9010ad6d59..c2b7775ac2 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "4e32d85249359d8ef8ece97d89848e40154363ab", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/balance.rs b/src/balance.rs index d0ebc310b0..7ba4826a94 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -73,7 +73,8 @@ pub struct BalanceDetails { pub enum LightningBalance { /// The channel is not yet closed (or the commitment or closing transaction has not yet /// appeared in a block). The given balance is claimable (less on-chain fees) if the channel is - /// force-closed now. + /// force-closed now. Values do not take into account any pending splices and are only based + /// on the confirmed state of the channel. ClaimableOnChannelClose { /// The identifier of the channel this balance belongs to. channel_id: ChannelId, @@ -224,21 +225,26 @@ impl LightningBalance { ) -> Self { match balance { LdkBalance::ClaimableOnChannelClose { - amount_satoshis, - transaction_fee_satoshis, - outbound_payment_htlc_rounded_msat, - outbound_forwarded_htlc_rounded_msat, - inbound_claiming_htlc_rounded_msat, - inbound_htlc_rounded_msat, - } => Self::ClaimableOnChannelClose { - channel_id, - counterparty_node_id, - amount_satoshis, - transaction_fee_satoshis, + balance_candidates, + confirmed_balance_candidate_index, outbound_payment_htlc_rounded_msat, outbound_forwarded_htlc_rounded_msat, inbound_claiming_htlc_rounded_msat, inbound_htlc_rounded_msat, + } => { + // unwrap safety: confirmed_balance_candidate_index is guaranteed to index into balance_candidates + let balance = balance_candidates.get(confirmed_balance_candidate_index).unwrap(); + + Self::ClaimableOnChannelClose { + channel_id, + counterparty_node_id, + amount_satoshis: balance.amount_satoshis, + transaction_fee_satoshis: balance.transaction_fee_satoshis, + outbound_payment_htlc_rounded_msat, + outbound_forwarded_htlc_rounded_msat, + inbound_claiming_htlc_rounded_msat, + inbound_htlc_rounded_msat, + } }, LdkBalance::ClaimableAwaitingConfirmations { amount_satoshis, From c99ff305edc59c256d3315bcc0327d048b68d000 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 16 Sep 2025 13:34:46 +0200 Subject: [PATCH 142/177] Log to console with node prefix --- tests/common/logging.rs | 27 ++++++++++++++++++++++++++- tests/integration_tests_rust.rs | 17 ++++++++++++++--- 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/tests/common/logging.rs b/tests/common/logging.rs index 6bceac29a9..d7d59ba324 100644 --- a/tests/common/logging.rs +++ b/tests/common/logging.rs @@ -1,5 +1,4 @@ use chrono::Utc; -#[cfg(not(feature = "uniffi"))] use ldk_node::logger::LogRecord; use ldk_node::logger::{LogLevel, LogWriter}; #[cfg(not(feature = "uniffi"))] @@ -143,3 +142,29 @@ pub(crate) fn validate_log_entry(entry: &String) { let msg = &path_and_msg[msg_start_index..]; assert!(!msg.is_empty()); } + +pub(crate) struct MultiNodeLogger { + node_id: String, +} + +impl MultiNodeLogger { + pub(crate) fn new(node_id: String) -> Self { + Self { node_id } + } +} + +impl LogWriter for MultiNodeLogger { + fn log(&self, record: LogRecord) { + let log = format!( + "[{}] {} {:<5} [{}:{}] {}\n", + self.node_id, + Utc::now().format("%Y-%m-%d %H:%M:%S%.3f"), + record.level.to_string(), + record.module_path, + record.line, + record.args + ); + + print!("{}", log); + } +} diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 77f46091d0..c9f2f95fcf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -12,6 +12,7 @@ use common::{ expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, generate_blocks_and_wait, + logging::MultiNodeLogger, logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, @@ -1135,17 +1136,27 @@ fn static_invoice_server() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); - let config_sender = random_config(true); + let mut config_sender = random_config(true); + config_sender.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); let node_sender = setup_node(&chain_source, config_sender, None); - let config_sender_lsp = random_config(true); + let mut config_sender_lsp = random_config(true); + config_sender_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); let node_sender_lsp = setup_node(&chain_source, config_sender_lsp, None); let mut config_receiver_lsp = random_config(true); config_receiver_lsp.node_config.async_payment_services_enabled = true; + config_receiver_lsp.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); + let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp, None); - let config_receiver = random_config(true); + let mut config_receiver = random_config(true); + config_receiver.log_writer = + TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); + let node_receiver = setup_node(&chain_source, config_receiver, None); let address_sender = node_sender.onchain_payment().new_address().unwrap(); From 8d18d1655e92c25e1b0e772a93cf63bf1c0575fa Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 16 Sep 2025 11:28:41 +0200 Subject: [PATCH 143/177] Update static invoice store for invoice requests With the merge of https://github.com/lightningdevkit/rust-lightning/pull/4049, it is now possible for a static invoice server to forward the invoice request to the recipient if they are online. --- Cargo.toml | 24 ++++----- src/event.rs | 26 +++++++--- .../asynchronous/static_invoice_store.rs | 50 ++++++++++++++----- 3 files changed, 70 insertions(+), 30 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index c2b7775ac2..f3038ee966 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "84398d9e5b3dc61c0a5c71972aa944f19948aef0", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/event.rs b/src/event.rs index 7a6dc48329..cd91463790 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1500,13 +1500,19 @@ where LdkEvent::PersistStaticInvoice { invoice, + invoice_request_path, invoice_slot, recipient_id, invoice_persisted_path, } => { if let Some(store) = self.static_invoice_store.as_ref() { match store - .handle_persist_static_invoice(invoice, invoice_slot, recipient_id) + .handle_persist_static_invoice( + invoice, + invoice_request_path, + invoice_slot, + recipient_id, + ) .await { Ok(_) => { @@ -1519,16 +1525,24 @@ where }; } }, - LdkEvent::StaticInvoiceRequested { recipient_id, invoice_slot, reply_path } => { + LdkEvent::StaticInvoiceRequested { + recipient_id, + invoice_slot, + reply_path, + invoice_request, + } => { if let Some(store) = self.static_invoice_store.as_ref() { let invoice = store.handle_static_invoice_requested(&recipient_id, invoice_slot).await; match invoice { - Ok(Some(invoice)) => { - if let Err(e) = - self.channel_manager.send_static_invoice(invoice, reply_path) - { + Ok(Some((invoice, invoice_request_path))) => { + if let Err(e) = self.channel_manager.respond_to_static_invoice_request( + invoice, + reply_path, + invoice_request, + invoice_request_path, + ) { log_error!(self.logger, "Failed to send static invoice: {:?}", e); } }, diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index eed6720e51..f1aa702a48 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -15,11 +15,23 @@ use crate::types::DynStore; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; -use lightning::{offers::static_invoice::StaticInvoice, util::ser::Writeable}; +use lightning::blinded_path::message::BlindedMessagePath; +use lightning::impl_writeable_tlv_based; +use lightning::{offers::static_invoice::StaticInvoice, util::ser::Readable, util::ser::Writeable}; use std::sync::{Arc, Mutex}; use std::time::Duration; +struct PersistedStaticInvoice { + invoice: StaticInvoice, + request_path: BlindedMessagePath, +} + +impl_writeable_tlv_based!(PersistedStaticInvoice, { + (0, invoice, required), + (2, request_path, required) +}); + pub(crate) struct StaticInvoiceStore { kv_store: Arc, request_rate_limiter: Mutex, @@ -60,7 +72,7 @@ impl StaticInvoiceStore { pub(crate) async fn handle_static_invoice_requested( &self, recipient_id: &[u8], invoice_slot: u16, - ) -> Result, lightning::io::Error> { + ) -> Result, lightning::io::Error> { Self::check_rate_limit(&self.request_rate_limiter, &recipient_id)?; let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id); @@ -68,12 +80,16 @@ impl StaticInvoiceStore { self.kv_store .read(STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, &secondary_namespace, &key) .and_then(|data| { - data.try_into().map(Some).map_err(|e| { - lightning::io::Error::new( - lightning::io::ErrorKind::InvalidData, - format!("Failed to parse static invoice: {:?}", e), - ) - }) + PersistedStaticInvoice::read(&mut &*data) + .map(|persisted_invoice| { + Some((persisted_invoice.invoice, persisted_invoice.request_path)) + }) + .map_err(|e| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + format!("Failed to parse static invoice: {:?}", e), + ) + }) }) .or_else( |e| { @@ -87,14 +103,18 @@ impl StaticInvoiceStore { } pub(crate) async fn handle_persist_static_invoice( - &self, invoice: StaticInvoice, invoice_slot: u16, recipient_id: Vec, + &self, invoice: StaticInvoice, invoice_request_path: BlindedMessagePath, invoice_slot: u16, + recipient_id: Vec, ) -> Result<(), lightning::io::Error> { Self::check_rate_limit(&self.persist_rate_limiter, &recipient_id)?; let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, &recipient_id); + let persisted_invoice = + PersistedStaticInvoice { invoice, request_path: invoice_request_path }; + let mut buf = Vec::new(); - invoice.write(&mut buf)?; + persisted_invoice.write(&mut buf)?; // Static invoices will be persisted at "static_invoices//". // @@ -144,15 +164,21 @@ mod tests { let static_invoice = invoice(); let recipient_id = vec![1, 1, 1]; + let invoice_request_path = blinded_path(); assert!(static_invoice_store - .handle_persist_static_invoice(static_invoice.clone(), 0, recipient_id.clone()) + .handle_persist_static_invoice( + static_invoice.clone(), + invoice_request_path.clone(), + 0, + recipient_id.clone() + ) .await .is_ok()); let requested_invoice = static_invoice_store.handle_static_invoice_requested(&recipient_id, 0).await.unwrap(); - assert_eq!(requested_invoice.unwrap(), static_invoice); + assert_eq!(requested_invoice.unwrap(), (static_invoice, invoice_request_path)); assert!(static_invoice_store .handle_static_invoice_requested(&recipient_id, 1) From efbef4c4d57a4059eab7558e8127698c0bc4299c Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Wed, 17 Sep 2025 14:14:48 +0200 Subject: [PATCH 144/177] Update static invoice test to use unannounced channels To better align with the expected real life setup. --- src/builder.rs | 4 ++++ tests/common/mod.rs | 11 +++++++++-- tests/integration_tests_rust.rs | 33 ++++++++++++++++++++++----------- 3 files changed, 35 insertions(+), 13 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index d330597eed..b99c44cecd 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1378,6 +1378,10 @@ fn build_with_store_internal( 100; } + if config.async_payment_services_enabled { + user_config.accept_forwards_to_priv_channels = true; + } + let message_router = Arc::new(MessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); diff --git a/tests/common/mod.rs b/tests/common/mod.rs index f5bfe76fce..70c9a43a81 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -589,6 +589,13 @@ pub(crate) fn bump_fee_and_broadcast( pub fn open_channel( node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, should_announce: bool, electrsd: &ElectrsD, +) -> OutPoint { + open_channel_push_amt(node_a, node_b, funding_amount_sat, None, should_announce, electrsd) +} + +pub fn open_channel_push_amt( + node_a: &TestNode, node_b: &TestNode, funding_amount_sat: u64, push_amount_msat: Option, + should_announce: bool, electrsd: &ElectrsD, ) -> OutPoint { if should_announce { node_a @@ -596,7 +603,7 @@ pub fn open_channel( node_b.node_id(), node_b.listening_addresses().unwrap().first().unwrap().clone(), funding_amount_sat, - None, + push_amount_msat, None, ) .unwrap(); @@ -606,7 +613,7 @@ pub fn open_channel( node_b.node_id(), node_b.listening_addresses().unwrap().first().unwrap().clone(), funding_amount_sat, - None, + push_amount_msat, None, ) .unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index c9f2f95fcf..f2e8407cd6 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -14,9 +14,9 @@ use common::{ generate_blocks_and_wait, logging::MultiNodeLogger, logging::{init_log_logger, validate_log_entry, TestLogWriter}, - open_channel, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, - random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, setup_node, - setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, + random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, + setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; use ldk_node::config::EsploraSyncConfig; @@ -1137,11 +1137,14 @@ fn static_invoice_server() { let chain_source = TestChainSource::Esplora(&electrsd); let mut config_sender = random_config(true); + config_sender.node_config.listening_addresses = None; + config_sender.node_config.node_alias = None; config_sender.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); let node_sender = setup_node(&chain_source, config_sender, None); let mut config_sender_lsp = random_config(true); + config_sender_lsp.node_config.async_payment_services_enabled = true; config_sender_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); let node_sender_lsp = setup_node(&chain_source, config_sender_lsp, None); @@ -1154,9 +1157,10 @@ fn static_invoice_server() { let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp, None); let mut config_receiver = random_config(true); + config_receiver.node_config.listening_addresses = None; + config_receiver.node_config.node_alias = None; config_receiver.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver ".to_string()))); - let node_receiver = setup_node(&chain_source, config_receiver, None); let address_sender = node_sender.onchain_payment().new_address().unwrap(); @@ -1176,9 +1180,16 @@ fn static_invoice_server() { node_receiver_lsp.sync_wallets().unwrap(); node_receiver.sync_wallets().unwrap(); - open_channel(&node_sender, &node_sender_lsp, 400_000, true, &electrsd); + open_channel(&node_sender, &node_sender_lsp, 400_000, false, &electrsd); open_channel(&node_sender_lsp, &node_receiver_lsp, 400_000, true, &electrsd); - open_channel(&node_receiver_lsp, &node_receiver, 400_000, true, &electrsd); + open_channel_push_amt( + &node_receiver, + &node_receiver_lsp, + 400_000, + Some(200_000_000), + false, + &electrsd, + ); generate_blocks_and_wait(&bitcoind.client, &electrsd.client, 6); @@ -1201,14 +1212,14 @@ fn static_invoice_server() { .filter(|n| { node.network_graph().node(n).map_or(false, |info| info.announcement_info.is_some()) }) - .count() >= 4 + .count() >= 2 }; // Wait for everyone to see all channels and node announcements. - while node_sender.network_graph().list_channels().len() < 3 - || node_sender_lsp.network_graph().list_channels().len() < 3 - || node_receiver_lsp.network_graph().list_channels().len() < 3 - || node_receiver.network_graph().list_channels().len() < 3 + while node_sender.network_graph().list_channels().len() < 1 + || node_sender_lsp.network_graph().list_channels().len() < 1 + || node_receiver_lsp.network_graph().list_channels().len() < 1 + || node_receiver.network_graph().list_channels().len() < 1 || !has_node_announcements(&node_sender) || !has_node_announcements(&node_sender_lsp) || !has_node_announcements(&node_receiver_lsp) From 3df14770480c3fadb62b34dc57d23ee5b9b150df Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Fri, 19 Sep 2025 14:42:21 +0200 Subject: [PATCH 145/177] Fix wait_for_tx exponential backoff Backoff wasn't actually working and polling would happen without any delay at all. --- tests/common/mod.rs | 35 +++++++++++++++++------------------ 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 70c9a43a81..0a1e8cbd2c 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -437,32 +437,31 @@ pub(crate) fn wait_for_block(electrs: &E, min_height: usize) { } pub(crate) fn wait_for_tx(electrs: &E, txid: Txid) { - let mut tx_res = electrs.transaction_get(&txid); - loop { - if tx_res.is_ok() { - break; - } - tx_res = exponential_backoff_poll(|| { - electrs.ping().unwrap(); - Some(electrs.transaction_get(&txid)) - }); + if electrs.transaction_get(&txid).is_ok() { + return; } + + exponential_backoff_poll(|| { + electrs.ping().unwrap(); + electrs.transaction_get(&txid).ok() + }); } pub(crate) fn wait_for_outpoint_spend(electrs: &E, outpoint: OutPoint) { let tx = electrs.transaction_get(&outpoint.txid).unwrap(); let txout_script = tx.output.get(outpoint.vout as usize).unwrap().clone().script_pubkey; - let mut is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); - loop { - if is_spent { - break; - } - is_spent = exponential_backoff_poll(|| { - electrs.ping().unwrap(); - Some(!electrs.script_get_history(&txout_script).unwrap().is_empty()) - }); + let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); + if is_spent { + return; } + + exponential_backoff_poll(|| { + electrs.ping().unwrap(); + + let is_spent = !electrs.script_get_history(&txout_script).unwrap().is_empty(); + is_spent.then_some(()) + }); } pub(crate) fn exponential_backoff_poll(mut poll: F) -> T From 97f404f4d895e9f06268facbbe617a40a6358455 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 18 Sep 2025 20:34:20 +0200 Subject: [PATCH 146/177] Adapt to new pay_for_offer call in upstream LDK Updated `pay_for_offer` call with `OptionalOfferPaymentParams` and delegate to `pay_for_offer_with_quantity` when needed. --- Cargo.toml | 24 ++++++++++++------------ src/payment/bolt12.rs | 43 ++++++++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f3038ee966..1d3f45bfa6 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "b002e43ec5f9c1cbdcd1ac8588402c5a65ecd2e4", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 81349e2bd4..601c03d7d6 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -17,7 +17,7 @@ use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, Payme use crate::types::{ChannelManager, PaymentStore}; use lightning::blinded_path::message::BlindedMessagePath; -use lightning::ln::channelmanager::{PaymentId, Retry}; +use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::routing::router::RouteParametersConfig; @@ -102,15 +102,19 @@ impl Bolt12Payment { }, }; - match self.channel_manager.pay_for_offer( - &offer, - quantity, - None, - payer_note.clone(), - payment_id, + let params = OptionalOfferPaymentParams { + payer_note: payer_note.clone(), retry_strategy, route_params_config, - ) { + }; + let res = if let Some(quantity) = quantity { + self.channel_manager + .pay_for_offer_with_quantity(&offer, None, payment_id, params, quantity) + } else { + self.channel_manager.pay_for_offer(&offer, None, payment_id, params) + }; + + match res { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( @@ -209,15 +213,24 @@ impl Bolt12Payment { return Err(Error::InvalidAmount); } - match self.channel_manager.pay_for_offer( - &offer, - quantity, - Some(amount_msat), - payer_note.clone(), - payment_id, + let params = OptionalOfferPaymentParams { + payer_note: payer_note.clone(), retry_strategy, route_params_config, - ) { + }; + let res = if let Some(quantity) = quantity { + self.channel_manager.pay_for_offer_with_quantity( + &offer, + Some(amount_msat), + payment_id, + params, + quantity, + ) + } else { + self.channel_manager.pay_for_offer(&offer, Some(amount_msat), payment_id, params) + }; + + match res { Ok(()) => { let payee_pubkey = offer.issuer_signing_pubkey(); log_info!( From 904a05f7eda483e4122a4191620c03d6441f59e4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 22 Sep 2025 13:11:23 +0200 Subject: [PATCH 147/177] Try to log status code for `reqwest`'s `Request` error kind We attempt to log a status code when `reqwest` returns a `Request` error kind. It might not be the case that the status code would always/ever be set for this error kind. --- src/chain/esplora.rs | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 8e9a4dbd44..2226358c15 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -144,12 +144,22 @@ impl EsploraChainSource { }, Err(e) => match *e { esplora_client::Error::Reqwest(he) => { - log_error!( - self.logger, - "{} of on-chain wallet failed due to HTTP connection error: {}", - if incremental_sync { "Incremental sync" } else { "Sync" }, - he - ); + if let Some(status_code) = he.status() { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP {} error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + status_code, + he, + ); + } else { + log_error!( + self.logger, + "{} of on-chain wallet failed due to HTTP error: {}", + if incremental_sync { "Incremental sync" } else { "Sync" }, + he, + ); + } Err(Error::WalletOperationFailed) }, _ => { From 1192085185eb8bc8b2981c102b596e416276322c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 Sep 2025 09:53:59 +0200 Subject: [PATCH 148/177] Bump LDK and account for `FutureSpawner` move The `FutureSpawner` trait moved to `lightning::util::native_async` now. --- Cargo.toml | 24 ++++++++++++------------ src/gossip.rs | 4 +++- 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1d3f45bfa6..b639b7dc1c 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "50391d3a3efa7a8f32d119d126a633e4b1981ee6", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/gossip.rs b/src/gossip.rs index 258f9f7364..efaf3ce894 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -12,7 +12,9 @@ use crate::runtime::Runtime; use crate::types::{GossipSync, Graph, P2PGossipSync, PeerManager, RapidGossipSync, UtxoLookup}; use crate::Error; -use lightning_block_sync::gossip::{FutureSpawner, GossipVerifier}; +use lightning_block_sync::gossip::GossipVerifier; + +use lightning::util::native_async::FutureSpawner; use std::future::Future; use std::sync::atomic::{AtomicU32, Ordering}; From b7598ae7d947a6593f01649a08e29b0d78253b4e Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Thu, 4 Sep 2025 16:20:50 +0200 Subject: [PATCH 149/177] Add onion mailbox for async receivers This introduces an in-memory mailbox to hold onion messages until the receiver comes online. This is required for async payment `held_htlc_available` messages. The mailbox is bounded by a maximum number of peers and a maximum number of messages per peer. --- bindings/ldk_node.udl | 9 ++- src/builder.rs | 101 ++++++++++++++++++++----- src/config.rs | 16 +++- src/event.rs | 40 ++++++++-- src/lib.rs | 16 ++-- src/payment/asynchronous/mod.rs | 1 + src/payment/asynchronous/om_mailbox.rs | 99 ++++++++++++++++++++++++ src/payment/bolt12.rs | 16 ++-- tests/common/mod.rs | 11 ++- tests/integration_tests_rust.rs | 37 +++++++-- 10 files changed, 298 insertions(+), 48 deletions(-) create mode 100644 src/payment/asynchronous/om_mailbox.rs diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index 9f0ef697e8..a6d867e5aa 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -13,7 +13,6 @@ dictionary Config { u64 probing_liquidity_limit_multiplier; AnchorChannelsConfig? anchor_channels_config; RouteParametersConfig? route_parameters; - boolean async_payment_services_enabled; }; dictionary AnchorChannelsConfig { @@ -96,6 +95,8 @@ interface Builder { [Throws=BuildError] void set_node_alias(string node_alias); [Throws=BuildError] + void set_async_payments_role(AsyncPaymentsRole? role); + [Throws=BuildError] Node build(); [Throws=BuildError] Node build_with_fs_store(); @@ -356,6 +357,7 @@ enum BuildError { "WalletSetupFailed", "LoggerSetupFailed", "NetworkMismatch", + "AsyncPaymentsConfigMismatch", }; [Trait] @@ -720,6 +722,11 @@ enum Currency { "Signet", }; +enum AsyncPaymentsRole { + "Client", + "Server", +}; + dictionary RouteHintHop { PublicKey src_node_id; u64 short_channel_id; diff --git a/src/builder.rs b/src/builder.rs index b99c44cecd..7bca0c2c6a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -7,9 +7,9 @@ use crate::chain::ChainSource; use crate::config::{ - default_user_config, may_announce_channel, AnnounceError, BitcoindRestClientConfig, Config, - ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, - DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, + default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, + BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, + DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; use crate::connection::ConnectionManager; @@ -27,6 +27,7 @@ use crate::liquidity::{ }; use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger}; use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; use crate::peer_store::PeerStore; use crate::runtime::Runtime; use crate::tx_broadcaster::TransactionBroadcaster; @@ -191,6 +192,8 @@ pub enum BuildError { LoggerSetupFailed, /// The given network does not match the node's previously configured network. NetworkMismatch, + /// The role of the node in an asynchronous payments context is not compatible with the current configuration. + AsyncPaymentsConfigMismatch, } impl fmt::Display for BuildError { @@ -219,6 +222,12 @@ impl fmt::Display for BuildError { Self::NetworkMismatch => { write!(f, "Given network does not match the node's previously configured network.") }, + Self::AsyncPaymentsConfigMismatch => { + write!( + f, + "The async payments role is not compatible with the current configuration." + ) + }, } } } @@ -240,6 +249,7 @@ pub struct NodeBuilder { gossip_source_config: Option, liquidity_source_config: Option, log_writer_config: Option, + async_payments_role: Option, runtime_handle: Option, } @@ -266,6 +276,7 @@ impl NodeBuilder { liquidity_source_config, log_writer_config, runtime_handle, + async_payments_role: None, } } @@ -544,6 +555,21 @@ impl NodeBuilder { Ok(self) } + /// Sets the role of the node in an asynchronous payments context. + /// + /// See for more information about the async payments protocol. + pub fn set_async_payments_role( + &mut self, role: Option, + ) -> Result<&mut Self, BuildError> { + if let Some(AsyncPaymentsRole::Server) = role { + may_announce_channel(&self.config) + .map_err(|_| BuildError::AsyncPaymentsConfigMismatch)?; + } + + self.async_payments_role = role; + Ok(self) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result { @@ -700,6 +726,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, runtime, logger, @@ -732,6 +759,7 @@ impl NodeBuilder { self.chain_data_source_config.as_ref(), self.gossip_source_config.as_ref(), self.liquidity_source_config.as_ref(), + self.async_payments_role, seed_bytes, runtime, logger, @@ -989,6 +1017,13 @@ impl ArcedNodeBuilder { self.inner.write().unwrap().set_node_alias(node_alias).map(|_| ()) } + /// Sets the role of the node in an asynchronous payments context. + pub fn set_async_payments_role( + &self, role: Option, + ) -> Result<(), BuildError> { + self.inner.write().unwrap().set_async_payments_role(role).map(|_| ()) + } + /// Builds a [`Node`] instance with a [`SqliteStore`] backend and according to the options /// previously configured. pub fn build(&self) -> Result, BuildError> { @@ -1082,8 +1117,9 @@ impl ArcedNodeBuilder { fn build_with_store_internal( config: Arc, chain_data_source_config: Option<&ChainDataSourceConfig>, gossip_source_config: Option<&GossipSourceConfig>, - liquidity_source_config: Option<&LiquiditySourceConfig>, seed_bytes: [u8; 64], - runtime: Arc, logger: Arc, kv_store: Arc, + liquidity_source_config: Option<&LiquiditySourceConfig>, + async_payments_role: Option, seed_bytes: [u8; 64], runtime: Arc, + logger: Arc, kv_store: Arc, ) -> Result { optionally_install_rustls_cryptoprovider(); @@ -1378,8 +1414,14 @@ fn build_with_store_internal( 100; } - if config.async_payment_services_enabled { - user_config.accept_forwards_to_priv_channels = true; + if let Some(role) = async_payments_role { + match role { + AsyncPaymentsRole::Server => { + user_config.accept_forwards_to_priv_channels = true; + user_config.enable_htlc_hold = true; + }, + AsyncPaymentsRole::Client => user_config.hold_outbound_htlcs_at_next_hop = true, + } } let message_router = @@ -1452,17 +1494,32 @@ fn build_with_store_internal( } // Initialize the PeerManager - let onion_messenger: Arc = Arc::new(OnionMessenger::new( - Arc::clone(&keys_manager), - Arc::clone(&keys_manager), - Arc::clone(&logger), - Arc::clone(&channel_manager), - message_router, - Arc::clone(&channel_manager), - Arc::clone(&channel_manager), - IgnoringMessageHandler {}, - IgnoringMessageHandler {}, - )); + let onion_messenger: Arc = + if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Arc::new(OnionMessenger::new_with_offline_peer_interception( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + } else { + Arc::new(OnionMessenger::new( + Arc::clone(&keys_manager), + Arc::clone(&keys_manager), + Arc::clone(&logger), + Arc::clone(&channel_manager), + message_router, + Arc::clone(&channel_manager), + Arc::clone(&channel_manager), + IgnoringMessageHandler {}, + IgnoringMessageHandler {}, + )) + }; let ephemeral_bytes: [u8; 32] = keys_manager.get_secure_random_bytes(); // Initialize the GossipSource @@ -1649,6 +1706,12 @@ fn build_with_store_internal( }, }; + let om_mailbox = if let Some(AsyncPaymentsRole::Server) = async_payments_role { + Some(Arc::new(OnionMessageMailbox::new())) + } else { + None + }; + let (stop_sender, _) = tokio::sync::watch::channel(()); let (background_processor_stop_sender, _) = tokio::sync::watch::channel(()); let is_running = Arc::new(RwLock::new(false)); @@ -1681,6 +1744,8 @@ fn build_with_store_internal( is_running, is_listening, node_metrics, + om_mailbox, + async_payments_role, }) } diff --git a/src/config.rs b/src/config.rs index bb0bd56ba0..88b70815d5 100644 --- a/src/config.rs +++ b/src/config.rs @@ -179,8 +179,6 @@ pub struct Config { /// **Note:** If unset, default parameters will be used, and you will be able to override the /// parameters on a per-payment basis in the corresponding method calls. pub route_parameters: Option, - /// Whether to enable the static invoice service to support async payment reception for clients. - pub async_payment_services_enabled: bool, } impl Default for Config { @@ -195,7 +193,6 @@ impl Default for Config { anchor_channels_config: Some(AnchorChannelsConfig::default()), route_parameters: None, node_alias: None, - async_payment_services_enabled: false, } } } @@ -537,6 +534,19 @@ impl From for LdkMaxDustHTLCExposure { } } +#[derive(Debug, Clone, Copy)] +/// The role of the node in an asynchronous payments context. +/// +/// See for more information about the async payments protocol. +pub enum AsyncPaymentsRole { + /// Node acts a client in an async payments context. This means that if possible, it will instruct its peers to hold + /// HTLCs for it, so that it can go offline. + Client, + /// Node acts as a server in an async payments context. This means that it will hold async payments HTLCs and onion + /// messages for its peers. + Server, +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/src/event.rs b/src/event.rs index cd91463790..1d1acfafa6 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,7 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::types::{CustomTlvRecord, DynStore, PaymentStore, Sweeper, Wallet}; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet}; use crate::{ hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, UserChannelId, @@ -459,6 +460,8 @@ where logger: L, config: Arc, static_invoice_store: Option, + onion_messenger: Arc, + om_mailbox: Option>, } impl EventHandler @@ -472,7 +475,8 @@ where output_sweeper: Arc, network_graph: Arc, liquidity_source: Option>>>, payment_store: Arc, peer_store: Arc>, - static_invoice_store: Option, runtime: Arc, logger: L, + static_invoice_store: Option, onion_messenger: Arc, + om_mailbox: Option>, runtime: Arc, logger: L, config: Arc, ) -> Self { Self { @@ -490,6 +494,8 @@ where runtime, config, static_invoice_store, + onion_messenger, + om_mailbox, } } @@ -1491,11 +1497,33 @@ where self.bump_tx_event_handler.handle_event(&bte).await; }, - LdkEvent::OnionMessageIntercepted { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + LdkEvent::OnionMessageIntercepted { peer_node_id, message } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + om_mailbox.onion_message_intercepted(peer_node_id, message); + } else { + log_trace!( + self.logger, + "Onion message intercepted, but no onion message mailbox available" + ); + } }, - LdkEvent::OnionMessagePeerConnected { .. } => { - debug_assert!(false, "We currently don't support onion message interception, so this event should never be emitted."); + LdkEvent::OnionMessagePeerConnected { peer_node_id } => { + if let Some(om_mailbox) = self.om_mailbox.as_ref() { + let messages = om_mailbox.onion_message_peer_connected(peer_node_id); + + for message in messages { + if let Err(e) = + self.onion_messenger.forward_onion_message(message, &peer_node_id) + { + log_trace!( + self.logger, + "Failed to forward onion message to peer {}: {:?}", + peer_node_id, + e + ); + } + } + } }, LdkEvent::PersistStaticInvoice { diff --git a/src/lib.rs b/src/lib.rs index e7e27273b8..0463432313 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -127,8 +127,8 @@ pub use builder::NodeBuilder as Builder; use chain::ChainSource; use config::{ - default_user_config, may_announce_channel, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, - PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, + default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, + NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; use event::{EventHandler, EventQueue}; @@ -136,6 +136,7 @@ use gossip::GossipSource; use graph::NetworkGraph; use io::utils::write_node_metrics; use liquidity::{LSPS1Liquidity, LiquiditySource}; +use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ Bolt11Payment, Bolt12Payment, OnchainPayment, PaymentDetails, SpontaneousPayment, @@ -205,6 +206,8 @@ pub struct Node { is_running: Arc>, is_listening: Arc, node_metrics: Arc>, + om_mailbox: Option>, + async_payments_role: Option, } impl Node { @@ -499,7 +502,8 @@ impl Node { Arc::clone(&self.logger), )); - let static_invoice_store = if self.config.async_payment_services_enabled { + let static_invoice_store = if let Some(AsyncPaymentsRole::Server) = self.async_payments_role + { Some(StaticInvoiceStore::new(Arc::clone(&self.kv_store))) } else { None @@ -517,6 +521,8 @@ impl Node { Arc::clone(&self.payment_store), Arc::clone(&self.peer_store), static_invoice_store, + Arc::clone(&self.onion_messenger), + self.om_mailbox.clone(), Arc::clone(&self.runtime), Arc::clone(&self.logger), Arc::clone(&self.config), @@ -826,9 +832,9 @@ impl Node { Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), - Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, ) } @@ -840,9 +846,9 @@ impl Node { Arc::new(Bolt12Payment::new( Arc::clone(&self.channel_manager), Arc::clone(&self.payment_store), - Arc::clone(&self.config), Arc::clone(&self.is_running), Arc::clone(&self.logger), + self.async_payments_role, )) } diff --git a/src/payment/asynchronous/mod.rs b/src/payment/asynchronous/mod.rs index ebb7a4bd3e..c28f6e2433 100644 --- a/src/payment/asynchronous/mod.rs +++ b/src/payment/asynchronous/mod.rs @@ -5,5 +5,6 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +pub(crate) mod om_mailbox; mod rate_limiter; pub(crate) mod static_invoice_store; diff --git a/src/payment/asynchronous/om_mailbox.rs b/src/payment/asynchronous/om_mailbox.rs new file mode 100644 index 0000000000..9a7478706d --- /dev/null +++ b/src/payment/asynchronous/om_mailbox.rs @@ -0,0 +1,99 @@ +use std::collections::{HashMap, VecDeque}; +use std::sync::Mutex; + +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::OnionMessage; + +pub(crate) struct OnionMessageMailbox { + map: Mutex>>, +} + +impl OnionMessageMailbox { + const MAX_MESSAGES_PER_PEER: usize = 30; + const MAX_PEERS: usize = 300; + + pub fn new() -> Self { + Self { map: Mutex::new(HashMap::with_capacity(Self::MAX_PEERS)) } + } + + pub(crate) fn onion_message_intercepted(&self, peer_node_id: PublicKey, message: OnionMessage) { + let mut map = self.map.lock().unwrap(); + + let queue = map.entry(peer_node_id).or_insert_with(VecDeque::new); + if queue.len() >= Self::MAX_MESSAGES_PER_PEER { + queue.pop_front(); + } + queue.push_back(message); + + // Enforce a peers limit. If exceeded, evict the peer with the longest queue. + if map.len() > Self::MAX_PEERS { + let peer_to_remove = + map.iter().max_by_key(|(_, queue)| queue.len()).map(|(peer, _)| *peer).unwrap(); + + map.remove(&peer_to_remove); + } + } + + pub(crate) fn onion_message_peer_connected( + &self, peer_node_id: PublicKey, + ) -> Vec { + let mut map = self.map.lock().unwrap(); + + if let Some(queue) = map.remove(&peer_node_id) { + queue.into() + } else { + Vec::new() + } + } + + #[cfg(test)] + pub(crate) fn is_empty(&self) -> bool { + let map = self.map.lock().unwrap(); + map.is_empty() + } +} + +#[cfg(test)] +mod tests { + use bitcoin::key::Secp256k1; + use bitcoin::secp256k1::{PublicKey, SecretKey}; + use lightning::onion_message; + + use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; + + #[test] + fn onion_message_mailbox() { + let mailbox = OnionMessageMailbox::new(); + + let secp = Secp256k1::new(); + let sk_bytes = [12; 32]; + let sk = SecretKey::from_slice(&sk_bytes).unwrap(); + let peer_node_id = PublicKey::from_secret_key(&secp, &sk); + + let blinding_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let blinding_point = PublicKey::from_secret_key(&secp, &blinding_sk); + + let message_sk = SecretKey::from_slice(&[13; 32]).unwrap(); + let message_point = PublicKey::from_secret_key(&secp, &message_sk); + + let message = lightning::ln::msgs::OnionMessage { + blinding_point, + onion_routing_packet: onion_message::packet::Packet { + version: 0, + public_key: message_point, + hop_data: vec![1, 2, 3], + hmac: [0; 32], + }, + }; + mailbox.onion_message_intercepted(peer_node_id, message.clone()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 1); + assert_eq!(messages[0], message); + + assert!(mailbox.is_empty()); + + let messages = mailbox.onion_message_peer_connected(peer_node_id); + assert_eq!(messages.len(), 0); + } +} diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 601c03d7d6..6cb2f0b85d 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,7 +9,7 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::config::{AsyncPaymentsRole, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::error::Error; use crate::ffi::{maybe_deref, maybe_wrap}; use crate::logger::{log_error, log_info, LdkLogger, Logger}; @@ -57,16 +57,17 @@ pub struct Bolt12Payment { channel_manager: Arc, payment_store: Arc, is_running: Arc>, - config: Arc, logger: Arc, + async_payments_role: Option, } impl Bolt12Payment { pub(crate) fn new( channel_manager: Arc, payment_store: Arc, - config: Arc, is_running: Arc>, logger: Arc, + is_running: Arc>, logger: Arc, + async_payments_role: Option, ) -> Self { - Self { channel_manager, payment_store, config, is_running, logger } + Self { channel_manager, payment_store, is_running, logger, async_payments_role } } /// Send a payment given an offer. @@ -554,8 +555,11 @@ impl Bolt12Payment { fn blinded_paths_for_async_recipient_internal( &self, recipient_id: Vec, ) -> Result, Error> { - if !self.config.async_payment_services_enabled { - return Err(Error::AsyncPaymentServicesDisabled); + match self.async_payments_role { + Some(AsyncPaymentsRole::Server) => {}, + _ => { + return Err(Error::AsyncPaymentServicesDisabled); + }, } self.channel_manager diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 0a1e8cbd2c..aa09b86d0b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -12,7 +12,7 @@ pub(crate) mod logging; use logging::TestLogWriter; -use ldk_node::config::{Config, ElectrumSyncConfig, EsploraSyncConfig}; +use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ @@ -310,6 +310,13 @@ pub(crate) fn setup_two_nodes( pub(crate) fn setup_node( chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, +) -> TestNode { + setup_node_for_async_payments(chain_source, config, seed_bytes, None) +} + +pub(crate) fn setup_node_for_async_payments( + chain_source: &TestChainSource, config: TestConfig, seed_bytes: Option>, + async_payments_role: Option, ) -> TestNode { setup_builder!(builder, config.node_config); match chain_source { @@ -375,6 +382,8 @@ pub(crate) fn setup_node( } } + builder.set_async_payments_role(async_payments_role).unwrap(); + let test_sync_store = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.into())); let node = builder.build_with_store(test_sync_store).unwrap(); node.start().unwrap(); diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index f2e8407cd6..63fc737b35 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -16,10 +16,11 @@ use common::{ logging::{init_log_logger, validate_log_entry, TestLogWriter}, open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, + setup_node, setup_node_for_async_payments, setup_two_nodes, wait_for_tx, TestChainSource, + TestSyncStore, }; -use ldk_node::config::EsploraSyncConfig; +use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, @@ -1132,7 +1133,7 @@ fn simple_bolt12_send_receive() { } #[test] -fn static_invoice_server() { +fn async_payment() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); let chain_source = TestChainSource::Esplora(&electrsd); @@ -1141,20 +1142,33 @@ fn static_invoice_server() { config_sender.node_config.node_alias = None; config_sender.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender ".to_string()))); - let node_sender = setup_node(&chain_source, config_sender, None); + let node_sender = setup_node_for_async_payments( + &chain_source, + config_sender, + None, + Some(AsyncPaymentsRole::Client), + ); let mut config_sender_lsp = random_config(true); - config_sender_lsp.node_config.async_payment_services_enabled = true; config_sender_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("sender_lsp ".to_string()))); - let node_sender_lsp = setup_node(&chain_source, config_sender_lsp, None); + let node_sender_lsp = setup_node_for_async_payments( + &chain_source, + config_sender_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); let mut config_receiver_lsp = random_config(true); - config_receiver_lsp.node_config.async_payment_services_enabled = true; config_receiver_lsp.log_writer = TestLogWriter::Custom(Arc::new(MultiNodeLogger::new("receiver_lsp".to_string()))); - let node_receiver_lsp = setup_node(&chain_source, config_receiver_lsp, None); + let node_receiver_lsp = setup_node_for_async_payments( + &chain_source, + config_receiver_lsp, + None, + Some(AsyncPaymentsRole::Server), + ); let mut config_receiver = random_config(true); config_receiver.node_config.listening_addresses = None; @@ -1241,9 +1255,16 @@ fn static_invoice_server() { std::thread::sleep(std::time::Duration::from_millis(100)); }; + node_receiver.stop().unwrap(); + let payment_id = node_sender.bolt12_payment().send_using_amount(&offer, 5_000, None, None).unwrap(); + // Sleep to allow the payment reach a state where the htlc is held and waiting for the receiver to come online. + std::thread::sleep(std::time::Duration::from_millis(3000)); + + node_receiver.start().unwrap(); + expect_payment_successful_event!(node_sender, Some(payment_id), None); } From 2ae346693d00d6831b92d835d1e47da805d723da Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Sun, 28 Sep 2025 11:03:08 +0200 Subject: [PATCH 150/177] Re-format all imports Nightly `rustfmt` allows to auto-group imports on the module level. While we're not quite convinced to switch to the nightly channel for this yet (mostly because not all contributors would have the right nightly version installed on their machines), we here make use of `cargo +nightly fmt` with some addtional import grouping options as a one-off. This cleans up our imports for the whole crate and gets us to a consistent state everywhere. --- rustfmt.toml | 2 + src/balance.rs | 9 +- src/builder.rs | 78 ++++++++--------- src/chain/bitcoind.rs | 50 ++++++----- src/chain/electrum.rs | 42 ++++------ src/chain/esplora.rs | 27 +++--- src/chain/mod.rs | 18 ++-- src/config.rs | 23 ++--- src/connection.rs | 15 ++-- src/data_store.rs | 16 ++-- src/error.rs | 4 +- src/event.rs | 80 ++++++++---------- src/fee_estimator.rs | 12 +-- src/ffi/types.rs | 84 ++++++++----------- src/gossip.rs | 17 ++-- src/graph.rs | 8 +- src/io/sqlite_store/migrations.rs | 10 +-- src/io/sqlite_store/mod.rs | 10 +-- src/io/test_utils.rs | 10 +-- src/io/utils.rs | 54 ++++++------ src/io/vss_store.rs | 21 +++-- src/lib.rs | 67 ++++++--------- src/liquidity.rs | 35 ++++---- src/logger.rs | 15 ++-- src/message_handler.rs | 13 ++- src/payment/asynchronous/rate_limiter.rs | 4 +- .../asynchronous/static_invoice_store.rs | 43 +++++----- src/payment/bolt11.rs | 28 +++---- src/payment/bolt12.rs | 20 ++--- src/payment/onchain.rs | 8 +- src/payment/spontaneous.rs | 16 ++-- src/payment/store.rs | 11 ++- src/payment/unified_qr.rs | 27 +++--- src/peer_store.rs | 24 +++--- src/runtime.rs | 12 +-- src/tx_broadcaster.rs | 11 +-- src/types.rs | 34 ++++---- src/wallet/mod.rs | 58 ++++++------- src/wallet/persist.rs | 10 +-- src/wallet/ser.rs | 15 ++-- tests/common/logging.rs | 6 +- tests/common/mod.rs | 39 ++++----- tests/integration_tests_cln.rs | 19 ++--- tests/integration_tests_lnd.rs | 28 +++---- tests/integration_tests_rust.rs | 32 +++---- tests/integration_tests_vss.rs | 3 +- tests/reorg_test.rs | 6 +- 47 files changed, 518 insertions(+), 656 deletions(-) diff --git a/rustfmt.toml b/rustfmt.toml index 4f88472bec..66161555ca 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -10,3 +10,5 @@ match_block_trailing_comma = true # UNSTABLE: format_macro_matchers = true # UNSTABLE: format_strings = true # UNSTABLE: group_imports = "StdExternalCrate" +# UNSTABLE: reorder_imports = true +# UNSTABLE: imports_granularity = "Module" diff --git a/src/balance.rs b/src/balance.rs index 7ba4826a94..d96278daeb 100644 --- a/src/balance.rs +++ b/src/balance.rs @@ -5,17 +5,14 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::chain::channelmonitor::Balance as LdkBalance; -use lightning::chain::channelmonitor::BalanceSource; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Amount, BlockHash, Txid}; +use lightning::chain::channelmonitor::{Balance as LdkBalance, BalanceSource}; use lightning::ln::types::ChannelId; use lightning::sign::SpendableOutputDescriptor; use lightning::util::sweep::{OutputSpendStatus, TrackedSpendableOutput}; - use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, BlockHash, Txid}; - /// Details of the known available balances returned by [`Node::list_balances`]. /// /// [`Node::list_balances`]: crate::Node::list_balances diff --git a/src/builder.rs b/src/builder.rs index 7bca0c2c6a..cf414ec576 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -5,13 +5,47 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::collections::HashMap; +use std::convert::TryInto; +use std::default::Default; +use std::path::PathBuf; +use std::sync::atomic::AtomicBool; +use std::sync::{Arc, Mutex, Once, RwLock}; +use std::time::SystemTime; +use std::{fmt, fs}; + +use bdk_wallet::template::Bip84; +use bdk_wallet::{KeychainKind, Wallet as BdkWallet}; +use bip39::Mnemonic; +use bitcoin::bip32::{ChildNumber, Xpriv}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{BlockHash, Network}; +use lightning::chain::{chainmonitor, BestBlock, Watch}; +use lightning::io::Cursor; +use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; +use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; +use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; +use lightning::routing::gossip::NodeAlias; +use lightning::routing::router::DefaultRouter; +use lightning::routing::scoring::{ + ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters, +}; +use lightning::sign::{EntropySource, NodeSigner}; +use lightning::util::persist::{ + read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, + CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use lightning::util::ser::ReadableArgs; +use lightning::util::sweep::OutputSweeper; +use lightning_persister::fs_store::FilesystemStore; +use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; + use crate::chain::ChainSource; use crate::config::{ default_user_config, may_announce_channel, AnnounceError, AsyncPaymentsRole, BitcoindRestClientConfig, Config, ElectrumSyncConfig, EsploraSyncConfig, DEFAULT_ESPLORA_SERVER_URL, DEFAULT_LOG_FILENAME, DEFAULT_LOG_LEVEL, WALLET_KEYS_SEED_LEN, }; - use crate::connection::ConnectionManager; use crate::event::EventQueue; use crate::fee_estimator::OnchainFeeEstimator; @@ -39,48 +73,6 @@ use crate::wallet::persist::KVStoreWalletPersister; use crate::wallet::Wallet; use crate::{Node, NodeMetrics}; -use lightning::chain::{chainmonitor, BestBlock, Watch}; -use lightning::io::Cursor; -use lightning::ln::channelmanager::{self, ChainParameters, ChannelManagerReadArgs}; -use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; -use lightning::ln::peer_handler::{IgnoringMessageHandler, MessageHandler}; -use lightning::routing::gossip::NodeAlias; -use lightning::routing::router::DefaultRouter; -use lightning::routing::scoring::{ - ProbabilisticScorer, ProbabilisticScoringDecayParameters, ProbabilisticScoringFeeParameters, -}; -use lightning::sign::{EntropySource, NodeSigner}; - -use lightning::util::persist::{ - read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, - CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use lightning::util::ser::ReadableArgs; -use lightning::util::sweep::OutputSweeper; - -use lightning_persister::fs_store::FilesystemStore; - -use bdk_wallet::template::Bip84; -use bdk_wallet::KeychainKind; -use bdk_wallet::Wallet as BdkWallet; - -use bip39::Mnemonic; - -use bitcoin::secp256k1::PublicKey; -use bitcoin::{BlockHash, Network}; - -use bitcoin::bip32::{ChildNumber, Xpriv}; -use std::collections::HashMap; -use std::convert::TryInto; -use std::default::Default; -use std::fmt; -use std::fs; -use std::path::PathBuf; -use std::sync::atomic::AtomicBool; -use std::sync::{Arc, Mutex, Once, RwLock}; -use std::time::SystemTime; -use vss_client::headers::{FixedHeaders, LnurlAuthToJwtProvider, VssHeaderProvider}; - const VSS_HARDENED_CHILD_INDEX: u32 = 877; const VSS_LNURL_AUTH_HARDENED_CHILD_INDEX: u32 = 138; const LSPS_HARDENED_CHILD_INDEX: u32 = 577; diff --git a/src/chain/bitcoind.rs b/src/chain/bitcoind.rs index 7157e5a4fd..d4f0cd8910 100644 --- a/src/chain/bitcoind.rs +++ b/src/chain/bitcoind.rs @@ -5,24 +5,17 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; - -use crate::config::{ - BitcoindRestClientConfig, Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, -}; -use crate::fee_estimator::{ - apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, - ConfirmationTarget, OnchainFeeEstimator, -}; -use crate::io::utils::write_node_metrics; -use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; -use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; -use crate::{Error, NodeMetrics}; +use std::collections::{HashMap, VecDeque}; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use base64::prelude::BASE64_STANDARD; +use base64::Engine; +use bitcoin::{BlockHash, FeeRate, Network, Transaction, Txid}; use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; use lightning::chain::Listen; use lightning::util::ser::Writeable; - use lightning_block_sync::gossip::UtxoSource; use lightning_block_sync::http::{HttpEndpoint, JsonResponse}; use lightning_block_sync::init::{synchronize_listeners, validate_best_block_header}; @@ -30,20 +23,23 @@ use lightning_block_sync::poll::{ChainPoller, ChainTip, ValidatedBlockHeader}; use lightning_block_sync::rest::RestClient; use lightning_block_sync::rpc::{RpcClient, RpcError}; use lightning_block_sync::{ - AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, Cache, + AsyncBlockSourceResult, BlockData, BlockHeaderData, BlockSource, BlockSourceErrorKind, Cache, + SpvClient, }; -use lightning_block_sync::{BlockSourceErrorKind, SpvClient}; - use serde::Serialize; -use base64::prelude::BASE64_STANDARD; -use base64::Engine; -use bitcoin::{BlockHash, FeeRate, Network, Transaction, Txid}; - -use std::collections::{HashMap, VecDeque}; -use std::sync::atomic::{AtomicU64, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use crate::config::{ + BitcoindRestClientConfig, Config, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, +}; +use crate::fee_estimator::{ + apply_post_estimation_adjustments, get_all_conf_targets, get_num_block_defaults_for_target, + ConfirmationTarget, OnchainFeeEstimator, +}; +use crate::io::utils::write_node_metrics; +use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; +use crate::{Error, NodeMetrics}; const CHAIN_POLLING_INTERVAL_SECS: u64 = 2; @@ -1417,7 +1413,9 @@ mod tests { use bitcoin::hashes::Hash; use bitcoin::{FeeRate, OutPoint, ScriptBuf, Transaction, TxIn, TxOut, Txid, Witness}; use lightning_block_sync::http::JsonResponse; - use proptest::{arbitrary::any, collection::vec, prop_assert_eq, prop_compose, proptest}; + use proptest::arbitrary::any; + use proptest::collection::vec; + use proptest::{prop_assert_eq, prop_compose, proptest}; use serde_json::json; use crate::chain::bitcoind::{ diff --git a/src/chain/electrum.rs b/src/chain/electrum.rs index 40d929ce7f..dbd0d9f7f9 100644 --- a/src/chain/electrum.rs +++ b/src/chain/electrum.rs @@ -5,8 +5,25 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use bdk_chain::bdk_core::spk_client::{ + FullScanRequest as BdkFullScanRequest, FullScanResponse as BdkFullScanResponse, + SyncRequest as BdkSyncRequest, SyncResponse as BdkSyncResponse, +}; +use bdk_electrum::BdkElectrumClient; +use bdk_wallet::{KeychainKind as BdkKeyChainKind, Update as BdkUpdate}; +use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; +use electrum_client::{ + Batch, Client as ElectrumClient, ConfigBuilder as ElectrumConfigBuilder, ElectrumApi, +}; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::util::ser::Writeable; +use lightning_transaction_sync::ElectrumSyncClient; +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ Config, ElectrumSyncConfig, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, FEE_RATE_CACHE_UPDATE_TIMEOUT_SECS, LDK_WALLET_SYNC_TIMEOUT_SECS, TX_BROADCAST_TIMEOUT_SECS, @@ -22,29 +39,6 @@ use crate::runtime::Runtime; use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::NodeMetrics; -use lightning::chain::{Confirm, Filter, WatchedOutput}; -use lightning::util::ser::Writeable; -use lightning_transaction_sync::ElectrumSyncClient; - -use bdk_chain::bdk_core::spk_client::FullScanRequest as BdkFullScanRequest; -use bdk_chain::bdk_core::spk_client::FullScanResponse as BdkFullScanResponse; -use bdk_chain::bdk_core::spk_client::SyncRequest as BdkSyncRequest; -use bdk_chain::bdk_core::spk_client::SyncResponse as BdkSyncResponse; -use bdk_wallet::KeychainKind as BdkKeyChainKind; -use bdk_wallet::Update as BdkUpdate; - -use bdk_electrum::BdkElectrumClient; - -use electrum_client::Client as ElectrumClient; -use electrum_client::ConfigBuilder as ElectrumConfigBuilder; -use electrum_client::{Batch, ElectrumApi}; - -use bitcoin::{FeeRate, Network, Script, ScriptBuf, Transaction, Txid}; - -use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - const BDK_ELECTRUM_CLIENT_BATCH_SIZE: usize = 5; const ELECTRUM_CLIENT_NUM_RETRIES: u8 = 3; const ELECTRUM_CLIENT_TIMEOUT_SECS: u8 = 10; diff --git a/src/chain/esplora.rs b/src/chain/esplora.rs index 2226358c15..be6f2fb86e 100644 --- a/src/chain/esplora.rs +++ b/src/chain/esplora.rs @@ -5,8 +5,18 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; +use std::collections::HashMap; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use bdk_esplora::EsploraAsyncExt; +use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; +use esplora_client::AsyncClient as EsploraAsyncClient; +use lightning::chain::{Confirm, Filter, WatchedOutput}; +use lightning::util::ser::Writeable; +use lightning_transaction_sync::EsploraSyncClient; +use super::{periodically_archive_fully_resolved_monitors, WalletSyncStatus}; use crate::config::{ Config, EsploraSyncConfig, BDK_CLIENT_CONCURRENCY, BDK_CLIENT_STOP_GAP, BDK_WALLET_SYNC_TIMEOUT_SECS, DEFAULT_ESPLORA_CLIENT_TIMEOUT_SECS, @@ -21,21 +31,6 @@ use crate::logger::{log_bytes, log_error, log_info, log_trace, LdkLogger, Logger use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; -use lightning::chain::{Confirm, Filter, WatchedOutput}; -use lightning::util::ser::Writeable; - -use lightning_transaction_sync::EsploraSyncClient; - -use bdk_esplora::EsploraAsyncExt; - -use esplora_client::AsyncClient as EsploraAsyncClient; - -use bitcoin::{FeeRate, Network, Script, Transaction, Txid}; - -use std::collections::HashMap; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; - pub(super) struct EsploraChainSource { pub(super) sync_config: EsploraSyncConfig, esplora_client: EsploraAsyncClient, diff --git a/src/chain/mod.rs b/src/chain/mod.rs index f3a29e9843..309d60eabc 100644 --- a/src/chain/mod.rs +++ b/src/chain/mod.rs @@ -9,6 +9,14 @@ mod bitcoind; mod electrum; mod esplora; +use std::collections::HashMap; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use bitcoin::{Script, Txid}; +use lightning::chain::Filter; +use lightning_block_sync::gossip::UtxoSource; + use crate::chain::bitcoind::BitcoindChainSource; use crate::chain::electrum::ElectrumChainSource; use crate::chain::esplora::EsploraChainSource; @@ -23,16 +31,6 @@ use crate::runtime::Runtime; use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet}; use crate::{Error, NodeMetrics}; -use lightning::chain::Filter; - -use lightning_block_sync::gossip::UtxoSource; - -use bitcoin::{Script, Txid}; - -use std::collections::HashMap; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - pub(crate) enum WalletSyncStatus { Completed, InProgress { subscribers: tokio::sync::broadcast::Sender> }, diff --git a/src/config.rs b/src/config.rs index 88b70815d5..d221dd6c3a 100644 --- a/src/config.rs +++ b/src/config.rs @@ -7,20 +7,19 @@ //! Objects for configuring the node. -use crate::logger::LogLevel; +use std::fmt; +use std::time::Duration; +use bitcoin::secp256k1::PublicKey; +use bitcoin::Network; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; use lightning::routing::router::RouteParametersConfig; -use lightning::util::config::ChannelConfig as LdkChannelConfig; -use lightning::util::config::MaxDustHTLCExposure as LdkMaxDustHTLCExposure; -use lightning::util::config::UserConfig; +use lightning::util::config::{ + ChannelConfig as LdkChannelConfig, MaxDustHTLCExposure as LdkMaxDustHTLCExposure, UserConfig, +}; -use bitcoin::secp256k1::PublicKey; -use bitcoin::Network; - -use std::fmt; -use std::time::Duration; +use crate::logger::LogLevel; // Config defaults const DEFAULT_NETWORK: Network = Network::Bitcoin; @@ -551,11 +550,7 @@ pub enum AsyncPaymentsRole { mod tests { use std::str::FromStr; - use super::may_announce_channel; - use super::AnnounceError; - use super::Config; - use super::NodeAlias; - use super::SocketAddress; + use super::{may_announce_channel, AnnounceError, Config, NodeAlias, SocketAddress}; #[test] fn node_announce_channel() { diff --git a/src/connection.rs b/src/connection.rs index c4cde717af..e3a25f3577 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -5,20 +5,19 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::logger::{log_error, log_info, LdkLogger}; -use crate::types::PeerManager; -use crate::Error; - -use lightning::ln::msgs::SocketAddress; - -use bitcoin::secp256k1::PublicKey; - use std::collections::hash_map::{self, HashMap}; use std::net::ToSocketAddrs; use std::ops::Deref; use std::sync::{Arc, Mutex}; use std::time::Duration; +use bitcoin::secp256k1::PublicKey; +use lightning::ln::msgs::SocketAddress; + +use crate::logger::{log_error, log_info, LdkLogger}; +use crate::types::PeerManager; +use crate::Error; + pub(crate) struct ConnectionManager where L::Target: LdkLogger, diff --git a/src/data_store.rs b/src/data_store.rs index 45802c272b..f9dbaa788f 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -5,16 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::logger::{log_error, LdkLogger}; -use crate::types::DynStore; -use crate::Error; +use std::collections::{hash_map, HashMap}; +use std::ops::Deref; +use std::sync::{Arc, Mutex}; use lightning::util::ser::{Readable, Writeable}; -use std::collections::hash_map; -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, Mutex}; +use crate::logger::{log_error, LdkLogger}; +use crate::types::DynStore; +use crate::Error; pub(crate) trait StorableObject: Clone + Readable + Writeable { type Id: StorableObjectId; @@ -164,9 +163,8 @@ mod tests { use lightning::impl_writeable_tlv_based; use lightning::util::test_utils::{TestLogger, TestStore}; - use crate::hex_utils; - use super::*; + use crate::hex_utils; #[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct TestObjectId { diff --git a/src/error.rs b/src/error.rs index eaa022e565..ae47c5ba8c 100644 --- a/src/error.rs +++ b/src/error.rs @@ -5,14 +5,14 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::fmt; + use bdk_chain::bitcoin::psbt::ExtractTxError as BdkExtractTxError; use bdk_chain::local_chain::CannotConnectError as BdkChainConnectionError; use bdk_chain::tx_graph::CalculateFeeError as BdkChainCalculateFeeError; use bdk_wallet::error::CreateTxError as BdkCreateTxError; use bdk_wallet::signer::SignerError as BdkSignerError; -use std::fmt; - #[derive(Copy, Clone, Debug, PartialEq, Eq)] /// An error that possibly needs to be handled by the user. pub enum Error { diff --git a/src/event.rs b/src/event.rs index 1d1acfafa6..1236c7cf2e 100644 --- a/src/event.rs +++ b/src/event.rs @@ -5,36 +5,19 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; -use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet}; -use crate::{ - hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, - UserChannelId, -}; - -use crate::config::{may_announce_channel, Config}; -use crate::connection::ConnectionManager; -use crate::data_store::DataStoreUpdateResult; -use crate::fee_estimator::ConfirmationTarget; -use crate::liquidity::LiquiditySource; -use crate::logger::Logger; - -use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; -use crate::payment::store::{ - PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, -}; - -use crate::io::{ - EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger}; - -use crate::runtime::Runtime; +use core::future::Future; +use core::task::{Poll, Waker}; +use std::collections::VecDeque; +use std::ops::Deref; +use std::sync::{Arc, Condvar, Mutex}; +use bitcoin::blockdata::locktime::absolute::LockTime; +use bitcoin::secp256k1::PublicKey; +use bitcoin::{Amount, OutPoint}; use lightning::events::bump_transaction::BumpTransactionEvent; -use lightning::events::{ClosureReason, PaymentPurpose, ReplayEvent}; -use lightning::events::{Event as LdkEvent, PaymentFailureReason}; +use lightning::events::{ + ClosureReason, Event as LdkEvent, PaymentFailureReason, PaymentPurpose, ReplayEvent, +}; use lightning::impl_writeable_tlv_based_enum; use lightning::ln::channelmanager::PaymentId; use lightning::ln::types::ChannelId; @@ -44,22 +27,31 @@ use lightning::util::config::{ }; use lightning::util::errors::APIError; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; - -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - use lightning_liquidity::lsps2::utils::compute_opening_fee; - -use bitcoin::blockdata::locktime::absolute::LockTime; -use bitcoin::secp256k1::PublicKey; -use bitcoin::{Amount, OutPoint}; - +use lightning_types::payment::{PaymentHash, PaymentPreimage}; use rand::{thread_rng, Rng}; -use core::future::Future; -use core::task::{Poll, Waker}; -use std::collections::VecDeque; -use std::ops::Deref; -use std::sync::{Arc, Condvar, Mutex}; +use crate::config::{may_announce_channel, Config}; +use crate::connection::ConnectionManager; +use crate::data_store::DataStoreUpdateResult; +use crate::fee_estimator::ConfirmationTarget; +use crate::io::{ + EVENT_QUEUE_PERSISTENCE_KEY, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use crate::liquidity::LiquiditySource; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox; +use crate::payment::asynchronous::static_invoice_store::StaticInvoiceStore; +use crate::payment::store::{ + PaymentDetails, PaymentDetailsUpdate, PaymentDirection, PaymentKind, PaymentStatus, +}; +use crate::runtime::Runtime; +use crate::types::{CustomTlvRecord, DynStore, OnionMessenger, PaymentStore, Sweeper, Wallet}; +use crate::{ + hex_utils, BumpTransactionEventHandler, ChannelManager, Error, Graph, PeerInfo, PeerStore, + UserChannelId, +}; /// An event emitted by [`Node`], which should be handled by the user. /// @@ -1599,11 +1591,13 @@ where #[cfg(test)] mod tests { - use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; use std::sync::atomic::{AtomicU16, Ordering}; use std::time::Duration; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use super::*; + #[tokio::test] async fn event_queue_persistence() { let store: Arc = Arc::new(TestStore::new(false)); diff --git a/src/fee_estimator.rs b/src/fee_estimator.rs index f8ddcd5fd2..b787ecd336 100644 --- a/src/fee_estimator.rs +++ b/src/fee_estimator.rs @@ -5,15 +5,15 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::chain::chaininterface::ConfirmationTarget as LdkConfirmationTarget; -use lightning::chain::chaininterface::FeeEstimator as LdkFeeEstimator; -use lightning::chain::chaininterface::FEERATE_FLOOR_SATS_PER_KW; - -use bitcoin::FeeRate; - use std::collections::HashMap; use std::sync::RwLock; +use bitcoin::FeeRate; +use lightning::chain::chaininterface::{ + ConfirmationTarget as LdkConfirmationTarget, FeeEstimator as LdkFeeEstimator, + FEERATE_FLOOR_SATS_PER_KW, +}; + #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub(crate) enum ConfirmationTarget { /// The default target for onchain payments. diff --git a/src/ffi/types.rs b/src/ffi/types.rs index 02d3217870..b64bd730eb 100644 --- a/src/ffi/types.rs +++ b/src/ffi/types.rs @@ -10,63 +10,52 @@ // // Make sure to add any re-exported items that need to be used in uniffi below. -pub use crate::config::{ - default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, - EsploraSyncConfig, MaxDustHTLCExposure, -}; -pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; -pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; -pub use crate::logger::{LogLevel, LogRecord, LogWriter}; -pub use crate::payment::store::{ - ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, -}; -pub use crate::payment::QrPaymentResult; +use std::convert::TryInto; +use std::ops::Deref; +use std::str::FromStr; +use std::sync::Arc; +use std::time::Duration; +pub use bip39::Mnemonic; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::secp256k1::PublicKey; +pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; pub use lightning::chain::channelmonitor::BalanceSource; pub use lightning::events::{ClosureReason, PaymentFailureReason}; +use lightning::ln::channelmanager::PaymentId; pub use lightning::ln::types::ChannelId; +use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; pub use lightning::offers::offer::OfferId; +use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; +use lightning::offers::refund::Refund as LdkRefund; pub use lightning::routing::gossip::{NodeAlias, NodeId, RoutingFees}; pub use lightning::routing::router::RouteParametersConfig; -pub use lightning_types::string::UntrustedString; - -pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; - +use lightning::util::ser::Writeable; +use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; pub use lightning_invoice::{Description, SignedRawBolt11Invoice}; - pub use lightning_liquidity::lsps0::ser::LSPSDateTime; pub use lightning_liquidity::lsps1::msgs::{ LSPS1ChannelInfo, LSPS1OrderId, LSPS1OrderParams, LSPS1PaymentState, }; - -pub use bitcoin::{Address, BlockHash, FeeRate, Network, OutPoint, Txid}; - -pub use bip39::Mnemonic; - +pub use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; +pub use lightning_types::string::UntrustedString; pub use vss_client::headers::{VssHeaderProvider, VssHeaderProviderError}; -use crate::UniffiCustomTypeConverter; - use crate::builder::sanitize_alias; +pub use crate::config::{ + default_config, AnchorChannelsConfig, BackgroundSyncConfig, ElectrumSyncConfig, + EsploraSyncConfig, MaxDustHTLCExposure, +}; use crate::error::Error; -use crate::hex_utils; -use crate::{SocketAddress, UserChannelId}; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; -use bitcoin::secp256k1::PublicKey; -use lightning::ln::channelmanager::PaymentId; -use lightning::offers::invoice::Bolt12Invoice as LdkBolt12Invoice; -use lightning::offers::offer::{Amount as LdkAmount, Offer as LdkOffer}; -use lightning::offers::refund::Refund as LdkRefund; -use lightning::util::ser::Writeable; -use lightning_invoice::{Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescriptionRef}; - -use std::convert::TryInto; -use std::ops::Deref; -use std::str::FromStr; -use std::sync::Arc; -use std::time::Duration; +pub use crate::graph::{ChannelInfo, ChannelUpdateInfo, NodeAnnouncementInfo, NodeInfo}; +pub use crate::liquidity::{LSPS1OrderStatus, LSPS2ServiceConfig}; +pub use crate::logger::{LogLevel, LogRecord, LogWriter}; +pub use crate::payment::store::{ + ConfirmationStatus, LSPFeeLimits, PaymentDirection, PaymentKind, PaymentStatus, +}; +pub use crate::payment::QrPaymentResult; +use crate::{hex_utils, SocketAddress, UniffiCustomTypeConverter, UserChannelId}; impl UniffiCustomTypeConverter for PublicKey { type Builtin = String; @@ -1177,16 +1166,13 @@ impl UniffiCustomTypeConverter for LSPSDateTime { #[cfg(test)] mod tests { - use std::{ - num::NonZeroU64, - time::{SystemTime, UNIX_EPOCH}, - }; + use std::num::NonZeroU64; + use std::time::{SystemTime, UNIX_EPOCH}; + + use lightning::offers::offer::{OfferBuilder, Quantity}; + use lightning::offers::refund::RefundBuilder; use super::*; - use lightning::offers::{ - offer::{OfferBuilder, Quantity}, - refund::RefundBuilder, - }; fn create_test_bolt11_invoice() -> (LdkBolt11Invoice, Bolt11Invoice) { let invoice_string = "lnbc1pn8g249pp5f6ytj32ty90jhvw69enf30hwfgdhyymjewywcmfjevflg6s4z86qdqqcqzzgxqyz5vqrzjqwnvuc0u4txn35cafc7w94gxvq5p3cu9dd95f7hlrh0fvs46wpvhdfjjzh2j9f7ye5qqqqryqqqqthqqpysp5mm832athgcal3m7h35sc29j63lmgzvwc5smfjh2es65elc2ns7dq9qrsgqu2xcje2gsnjp0wn97aknyd3h58an7sjj6nhcrm40846jxphv47958c6th76whmec8ttr2wmg6sxwchvxmsc00kqrzqcga6lvsf9jtqgqy5yexa"; diff --git a/src/gossip.rs b/src/gossip.rs index efaf3ce894..01aff47425 100644 --- a/src/gossip.rs +++ b/src/gossip.rs @@ -5,6 +5,14 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::future::Future; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; +use std::time::Duration; + +use lightning::util::native_async::FutureSpawner; +use lightning_block_sync::gossip::GossipVerifier; + use crate::chain::ChainSource; use crate::config::RGS_SYNC_TIMEOUT_SECS; use crate::logger::{log_trace, LdkLogger, Logger}; @@ -12,15 +20,6 @@ use crate::runtime::Runtime; use crate::types::{GossipSync, Graph, P2PGossipSync, PeerManager, RapidGossipSync, UtxoLookup}; use crate::Error; -use lightning_block_sync::gossip::GossipVerifier; - -use lightning::util::native_async::FutureSpawner; - -use std::future::Future; -use std::sync::atomic::{AtomicU32, Ordering}; -use std::sync::Arc; -use std::time::Duration; - pub(crate) enum GossipSource { P2PNetwork { gossip_sync: Arc, diff --git a/src/graph.rs b/src/graph.rs index 3e4e58c880..f2daebb9f3 100644 --- a/src/graph.rs +++ b/src/graph.rs @@ -7,19 +7,17 @@ //! Objects for querying the network graph. -use crate::types::Graph; - -use lightning::routing::gossip::NodeId; +use std::sync::Arc; #[cfg(feature = "uniffi")] use lightning::ln::msgs::SocketAddress; +use lightning::routing::gossip::NodeId; #[cfg(feature = "uniffi")] use lightning::routing::gossip::RoutingFees; - #[cfg(not(feature = "uniffi"))] use lightning::routing::gossip::{ChannelInfo, NodeInfo}; -use std::sync::Arc; +use crate::types::Graph; /// Represents the network as nodes and channels between them. pub struct NetworkGraph { diff --git a/src/io/sqlite_store/migrations.rs b/src/io/sqlite_store/migrations.rs index 15e60bcc2b..abfbdf6ef0 100644 --- a/src/io/sqlite_store/migrations.rs +++ b/src/io/sqlite_store/migrations.rs @@ -5,9 +5,8 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use rusqlite::Connection; - use lightning::io; +use rusqlite::Connection; pub(super) fn migrate_schema( connection: &mut Connection, kv_table_name: &str, from_version: u16, to_version: u16, @@ -75,14 +74,13 @@ pub(super) fn migrate_schema( #[cfg(test)] mod tests { - use crate::io::sqlite_store::SqliteStore; - use crate::io::test_utils::{do_read_write_remove_list_persist, random_storage_path}; + use std::fs; use lightning::util::persist::KVStoreSync; - use rusqlite::{named_params, Connection}; - use std::fs; + use crate::io::sqlite_store::SqliteStore; + use crate::io::test_utils::{do_read_write_remove_list_persist, random_storage_path}; #[test] fn rwrl_post_schema_1_migration() { diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index 4006ab2cc4..d18c7440d4 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -6,18 +6,16 @@ // accordance with one or both of these licenses. //! Objects related to [`SqliteStore`] live here. -use crate::io::utils::check_namespace_key_validity; +use std::fs; +use std::path::PathBuf; +use std::sync::{Arc, Mutex}; use lightning::io; use lightning::util::persist::KVStoreSync; - use lightning_types::string::PrintableString; - use rusqlite::{named_params, Connection}; -use std::fs; -use std::path::PathBuf; -use std::sync::{Arc, Mutex}; +use crate::io::utils::check_namespace_key_validity; mod migrations; diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index 244dd9cdc5..0676648510 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -5,22 +5,20 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::panic::RefUnwindSafe; +use std::path::PathBuf; + +use lightning::events::ClosureReason; use lightning::ln::functional_test_utils::{ connect_block, create_announced_chan_between_nodes, create_chanmon_cfgs, create_dummy_block, create_network, create_node_cfgs, create_node_chanmgrs, send_payment, }; use lightning::util::persist::{read_channel_monitors, KVStoreSync, KVSTORE_NAMESPACE_KEY_MAX_LEN}; - -use lightning::events::ClosureReason; use lightning::util::test_utils; use lightning::{check_added_monitors, check_closed_broadcast, check_closed_event}; - use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use std::panic::RefUnwindSafe; -use std::path::PathBuf; - pub(crate) fn random_storage_path() -> PathBuf { let mut temp_path = std::env::temp_dir(); let mut rng = thread_rng(); diff --git a/src/io/utils.rs b/src/io/utils.rs index 51e7be5054..0cc910ad76 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -5,20 +5,20 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use super::*; -use crate::config::WALLET_KEYS_SEED_LEN; - -use crate::chain::ChainSource; -use crate::fee_estimator::OnchainFeeEstimator; -use crate::io::{ - NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, -}; -use crate::logger::{log_error, LdkLogger, Logger}; -use crate::peer_store::PeerStore; -use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; -use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; -use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; +use std::fs; +use std::io::Write; +use std::ops::Deref; +use std::path::Path; +use std::sync::Arc; +use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; +use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; +use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; +use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; +use bdk_chain::ConfirmationBlockTime; +use bdk_wallet::ChangeSet as BdkWalletChangeSet; +use bip39::Mnemonic; +use bitcoin::Network; use lightning::io::Cursor; use lightning::ln::msgs::DecodeError; use lightning::routing::gossip::NetworkGraph; @@ -32,25 +32,21 @@ use lightning::util::persist::{ }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; use lightning::util::sweep::OutputSweeper; - use lightning_types::string::PrintableString; - -use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; -use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; -use bdk_chain::miniscript::{Descriptor, DescriptorPublicKey}; -use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; -use bdk_chain::ConfirmationBlockTime; -use bdk_wallet::ChangeSet as BdkWalletChangeSet; - -use bip39::Mnemonic; -use bitcoin::Network; use rand::{thread_rng, RngCore}; -use std::fs; -use std::io::Write; -use std::ops::Deref; -use std::path::Path; -use std::sync::Arc; +use super::*; +use crate::chain::ChainSource; +use crate::config::WALLET_KEYS_SEED_LEN; +use crate::fee_estimator::OnchainFeeEstimator; +use crate::io::{ + NODE_METRICS_KEY, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, +}; +use crate::logger::{log_error, LdkLogger, Logger}; +use crate::peer_store::PeerStore; +use crate::types::{Broadcaster, DynStore, KeysManager, Sweeper}; +use crate::wallet::ser::{ChangeSetDeserWrapper, ChangeSetSerWrapper}; +use crate::{Error, EventQueue, NodeMetrics, PaymentDetails}; /// Generates a random [BIP 39] mnemonic. /// diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 87f966a9ba..a03aafc44b 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -5,18 +5,16 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::io::utils::check_namespace_key_validity; -use crate::runtime::Runtime; +#[cfg(test)] +use std::panic::RefUnwindSafe; +use std::sync::Arc; +use std::time::Duration; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use lightning::io::{self, Error, ErrorKind}; use lightning::util::persist::KVStoreSync; use prost::Message; use rand::RngCore; -#[cfg(test)] -use std::panic::RefUnwindSafe; -use std::sync::Arc; -use std::time::Duration; use vss_client::client::VssClient; use vss_client::error::VssError; use vss_client::headers::VssHeaderProvider; @@ -31,6 +29,9 @@ use vss_client::util::retry::{ }; use vss_client::util::storable_builder::{EntropySource, StorableBuilder}; +use crate::io::utils::check_namespace_key_validity; +use crate::runtime::Runtime; + type CustomRetryPolicy = FilteredRetryPolicy< JitteredRetryPolicy< MaxTotalDelayRetryPolicy>>, @@ -256,14 +257,16 @@ impl RefUnwindSafe for VssStore {} #[cfg(test)] #[cfg(vss_test)] mod tests { - use super::*; - use crate::io::test_utils::do_read_write_remove_list_persist; + use std::collections::HashMap; + use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng, RngCore}; - use std::collections::HashMap; use tokio::runtime; use vss_client::headers::FixedHeaders; + use super::*; + use crate::io::test_utils::do_read_write_remove_list_persist; + #[test] fn vss_read_write_remove_list_persist() { let runtime = Arc::new(Runtime::new().unwrap()); diff --git a/src/lib.rs b/src/lib.rs index 0463432313..0f547ce1de 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -99,43 +99,45 @@ mod tx_broadcaster; mod types; mod wallet; -pub use bip39; -pub use bitcoin; -pub use lightning; -pub use lightning_invoice; -pub use lightning_liquidity; -pub use lightning_types; -pub use tokio; -pub use vss_client; +use std::default::Default; +use std::net::ToSocketAddrs; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; pub use balance::{BalanceDetails, LightningBalance, PendingSweepBalance}; -pub use error::Error as NodeError; -use error::Error; - -pub use event::Event; - -pub use io::utils::generate_entropy_mnemonic; - -#[cfg(feature = "uniffi")] -use ffi::*; - +use bitcoin::secp256k1::PublicKey; #[cfg(feature = "uniffi")] pub use builder::ArcedNodeBuilder as Builder; pub use builder::BuildError; #[cfg(not(feature = "uniffi"))] pub use builder::NodeBuilder as Builder; - use chain::ChainSource; use config::{ default_user_config, may_announce_channel, AsyncPaymentsRole, ChannelConfig, Config, NODE_ANN_BCAST_INTERVAL, PEER_RECONNECTION_INTERVAL, RGS_SYNC_INTERVAL, }; use connection::ConnectionManager; +pub use error::Error as NodeError; +use error::Error; +pub use event::Event; use event::{EventHandler, EventQueue}; +#[cfg(feature = "uniffi")] +use ffi::*; use gossip::GossipSource; use graph::NetworkGraph; +pub use io::utils::generate_entropy_mnemonic; use io::utils::write_node_metrics; +use lightning::chain::BestBlock; +use lightning::events::bump_transaction::Wallet as LdkWallet; +use lightning::impl_writeable_tlv_based; +use lightning::ln::channel_state::ChannelShutdownState; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::msgs::SocketAddress; +use lightning::routing::gossip::NodeAlias; +use lightning_background_processor::process_events_async_with_kv_store_sync; use liquidity::{LSPS1Liquidity, LiquiditySource}; +use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; use payment::asynchronous::static_invoice_store::StaticInvoiceStore; use payment::{ @@ -143,34 +145,17 @@ use payment::{ UnifiedQrPayment, }; use peer_store::{PeerInfo, PeerStore}; +use rand::Rng; use runtime::Runtime; use types::{ Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, }; pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; - -use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; - -use lightning::chain::BestBlock; -use lightning::events::bump_transaction::Wallet as LdkWallet; -use lightning::impl_writeable_tlv_based; -use lightning::ln::channel_state::ChannelShutdownState; -use lightning::ln::channelmanager::PaymentId; -use lightning::ln::msgs::SocketAddress; -use lightning::routing::gossip::NodeAlias; - -use lightning_background_processor::process_events_async_with_kv_store_sync; - -use bitcoin::secp256k1::PublicKey; - -use rand::Rng; - -use std::default::Default; -use std::net::ToSocketAddrs; -use std::sync::atomic::{AtomicBool, Ordering}; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; +pub use { + bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, + vss_client, +}; #[cfg(feature = "uniffi")] uniffi::include_scaffolding!("ldk_node"); diff --git a/src/liquidity.rs b/src/liquidity.rs index 5d0bf5afec..ae31f9ace0 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -7,21 +7,20 @@ //! Objects related to liquidity management. -use crate::chain::ChainSource; -use crate::connection::ConnectionManager; -use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; -use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; -use crate::{total_anchor_channels_reserve_sats, Config, Error}; +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, Mutex, RwLock}; +use std::time::Duration; +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use chrono::Utc; use lightning::events::HTLCHandlingFailureType; use lightning::ln::channelmanager::{InterceptId, MIN_FINAL_CLTV_EXPIRY_DELTA}; use lightning::ln::msgs::SocketAddress; use lightning::ln::types::ChannelId; use lightning::routing::router::{RouteHint, RouteHintHop}; - use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees}; - use lightning_liquidity::events::LiquidityEvent; use lightning_liquidity::lsps0::ser::{LSPSDateTime, LSPSRequestId}; use lightning_liquidity::lsps1::client::LSPS1ClientConfig as LdkLSPS1ClientConfig; @@ -35,22 +34,16 @@ use lightning_liquidity::lsps2::msgs::{LSPS2OpeningFeeParams, LSPS2RawOpeningFee use lightning_liquidity::lsps2::service::LSPS2ServiceConfig as LdkLSPS2ServiceConfig; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; - use lightning_types::payment::PaymentHash; - -use bitcoin::hashes::{sha256, Hash}; -use bitcoin::secp256k1::{PublicKey, Secp256k1}; - -use tokio::sync::oneshot; - -use chrono::Utc; - use rand::Rng; +use tokio::sync::oneshot; -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, Mutex, RwLock}; -use std::time::Duration; +use crate::chain::ChainSource; +use crate::connection::ConnectionManager; +use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; +use crate::runtime::Runtime; +use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; +use crate::{total_anchor_channels_reserve_sats, Config, Error}; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; diff --git a/src/logger.rs b/src/logger.rs index 40817897c1..4eaefad746 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -7,15 +7,6 @@ //! Logging-related objects. -pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord}; -pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace}; - -pub use lightning::util::logger::Level as LogLevel; - -use chrono::Utc; -use log::Level as LogFacadeLevel; -use log::Record as LogFacadeRecord; - #[cfg(not(feature = "uniffi"))] use core::fmt; use std::fs; @@ -23,6 +14,12 @@ use std::io::Write; use std::path::Path; use std::sync::Arc; +use chrono::Utc; +pub use lightning::util::logger::Level as LogLevel; +pub(crate) use lightning::util::logger::{Logger as LdkLogger, Record as LdkRecord}; +pub(crate) use lightning::{log_bytes, log_debug, log_error, log_info, log_trace}; +use log::{Level as LogFacadeLevel, Record as LogFacadeRecord}; + /// A unit of logging output with metadata to enable filtering `module_path`, /// `file`, and `line` to inform on log's source. #[cfg(not(feature = "uniffi"))] diff --git a/src/message_handler.rs b/src/message_handler.rs index 25995a4815..fc206ec4da 100644 --- a/src/message_handler.rs +++ b/src/message_handler.rs @@ -5,21 +5,18 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::liquidity::LiquiditySource; +use std::ops::Deref; +use std::sync::Arc; +use bitcoin::secp256k1::PublicKey; use lightning::ln::peer_handler::CustomMessageHandler; use lightning::ln::wire::CustomMessageReader; use lightning::util::logger::Logger; use lightning::util::ser::LengthLimitedRead; - -use lightning_types::features::{InitFeatures, NodeFeatures}; - use lightning_liquidity::lsps0::ser::RawLSPSMessage; +use lightning_types::features::{InitFeatures, NodeFeatures}; -use bitcoin::secp256k1::PublicKey; - -use std::ops::Deref; -use std::sync::Arc; +use crate::liquidity::LiquiditySource; pub(crate) enum NodeCustomMessageHandler where diff --git a/src/payment/asynchronous/rate_limiter.rs b/src/payment/asynchronous/rate_limiter.rs index 153577b16b..671b1dc72a 100644 --- a/src/payment/asynchronous/rate_limiter.rs +++ b/src/payment/asynchronous/rate_limiter.rs @@ -72,10 +72,10 @@ impl RateLimiter { #[cfg(test)] mod tests { - use crate::payment::asynchronous::rate_limiter::RateLimiter; - use std::time::Duration; + use crate::payment::asynchronous::rate_limiter::RateLimiter; + #[test] fn rate_limiter_test() { // Test diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index f1aa702a48..e81fd8216c 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -7,20 +7,20 @@ //! Store implementation for [`StaticInvoice`]s. -use crate::hex_utils; -use crate::io::STATIC_INVOICE_STORE_PRIMARY_NAMESPACE; -use crate::payment::asynchronous::rate_limiter::RateLimiter; -use crate::types::DynStore; +use std::sync::{Arc, Mutex}; +use std::time::Duration; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::Hash; - use lightning::blinded_path::message::BlindedMessagePath; use lightning::impl_writeable_tlv_based; -use lightning::{offers::static_invoice::StaticInvoice, util::ser::Readable, util::ser::Writeable}; +use lightning::offers::static_invoice::StaticInvoice; +use lightning::util::ser::{Readable, Writeable}; -use std::sync::{Arc, Mutex}; -use std::time::Duration; +use crate::hex_utils; +use crate::io::STATIC_INVOICE_STORE_PRIMARY_NAMESPACE; +use crate::payment::asynchronous::rate_limiter::RateLimiter; +use crate::types::DynStore; struct PersistedStaticInvoice { invoice: StaticInvoice, @@ -133,23 +133,18 @@ impl StaticInvoiceStore { #[cfg(test)] mod tests { - use std::{sync::Arc, time::Duration}; - - use bitcoin::{ - key::{Keypair, Secp256k1}, - secp256k1::{PublicKey, SecretKey}, - }; - use lightning::blinded_path::{ - message::BlindedMessagePath, - payment::{BlindedPayInfo, BlindedPaymentPath}, - BlindedHop, - }; + use std::sync::Arc; + use std::time::Duration; + + use bitcoin::key::{Keypair, Secp256k1}; + use bitcoin::secp256k1::{PublicKey, SecretKey}; + use lightning::blinded_path::message::BlindedMessagePath; + use lightning::blinded_path::payment::{BlindedPayInfo, BlindedPaymentPath}; + use lightning::blinded_path::BlindedHop; use lightning::ln::inbound_payment::ExpandedKey; - use lightning::offers::{ - nonce::Nonce, - offer::OfferBuilder, - static_invoice::{StaticInvoice, StaticInvoiceBuilder}, - }; + use lightning::offers::nonce::Nonce; + use lightning::offers::offer::OfferBuilder; + use lightning::offers::static_invoice::{StaticInvoice, StaticInvoiceBuilder}; use lightning::sign::EntropySource; use lightning::util::test_utils::TestStore; use lightning_types::features::BlindedHopFeatures; diff --git a/src/payment/bolt11.rs b/src/payment/bolt11.rs index 7dcb2817c6..60c313381a 100644 --- a/src/payment/bolt11.rs +++ b/src/payment/bolt11.rs @@ -9,6 +9,19 @@ //! //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md +use std::sync::{Arc, RwLock}; + +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use lightning::ln::channelmanager::{ + Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, +}; +use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; +use lightning_invoice::{ + Bolt11Invoice as LdkBolt11Invoice, Bolt11InvoiceDescription as LdkBolt11InvoiceDescription, +}; +use lightning_types::payment::{PaymentHash, PaymentPreimage}; + use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; use crate::connection::ConnectionManager; use crate::data_store::DataStoreUpdateResult; @@ -24,21 +37,6 @@ use crate::peer_store::{PeerInfo, PeerStore}; use crate::runtime::Runtime; use crate::types::{ChannelManager, PaymentStore}; -use lightning::ln::channelmanager::{ - Bolt11InvoiceParameters, Bolt11PaymentError, PaymentId, Retry, RetryableSendFailure, -}; -use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; - -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - -use lightning_invoice::Bolt11Invoice as LdkBolt11Invoice; -use lightning_invoice::Bolt11InvoiceDescription as LdkBolt11InvoiceDescription; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::Hash; - -use std::sync::{Arc, RwLock}; - #[cfg(not(feature = "uniffi"))] type Bolt11Invoice = LdkBolt11Invoice; #[cfg(feature = "uniffi")] diff --git a/src/payment/bolt12.rs b/src/payment/bolt12.rs index 6cb2f0b85d..337eedf968 100644 --- a/src/payment/bolt12.rs +++ b/src/payment/bolt12.rs @@ -9,28 +9,26 @@ //! //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::config::{AsyncPaymentsRole, LDK_PAYMENT_RETRY_TIMEOUT}; -use crate::error::Error; -use crate::ffi::{maybe_deref, maybe_wrap}; -use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::types::{ChannelManager, PaymentStore}; +use std::num::NonZeroU64; +use std::sync::{Arc, RwLock}; +use std::time::{Duration, SystemTime, UNIX_EPOCH}; use lightning::blinded_path::message::BlindedMessagePath; use lightning::ln::channelmanager::{OptionalOfferPaymentParams, PaymentId, Retry}; use lightning::offers::offer::{Amount, Offer as LdkOffer, Quantity}; use lightning::offers::parse::Bolt12SemanticError; use lightning::routing::router::RouteParametersConfig; - #[cfg(feature = "uniffi")] use lightning::util::ser::{Readable, Writeable}; use lightning_types::string::UntrustedString; - use rand::RngCore; -use std::num::NonZeroU64; -use std::sync::{Arc, RwLock}; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use crate::config::{AsyncPaymentsRole, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::error::Error; +use crate::ffi::{maybe_deref, maybe_wrap}; +use crate::logger::{log_error, log_info, LdkLogger, Logger}; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{ChannelManager, PaymentStore}; #[cfg(not(feature = "uniffi"))] type Bolt12Invoice = lightning::offers::invoice::Bolt12Invoice; diff --git a/src/payment/onchain.rs b/src/payment/onchain.rs index 2614e55cec..c5100d7723 100644 --- a/src/payment/onchain.rs +++ b/src/payment/onchain.rs @@ -7,16 +7,16 @@ //! Holds a payment handler allowing to send and receive on-chain payments. +use std::sync::{Arc, RwLock}; + +use bitcoin::{Address, Txid}; + use crate::config::Config; use crate::error::Error; use crate::logger::{log_info, LdkLogger, Logger}; use crate::types::{ChannelManager, Wallet}; use crate::wallet::OnchainSendAmount; -use bitcoin::{Address, Txid}; - -use std::sync::{Arc, RwLock}; - #[cfg(not(feature = "uniffi"))] type FeeRate = bitcoin::FeeRate; #[cfg(feature = "uniffi")] diff --git a/src/payment/spontaneous.rs b/src/payment/spontaneous.rs index 181307a0f5..6c074f308f 100644 --- a/src/payment/spontaneous.rs +++ b/src/payment/spontaneous.rs @@ -7,21 +7,19 @@ //! Holds a payment handler allowing to send spontaneous ("keysend") payments. -use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; -use crate::error::Error; -use crate::logger::{log_error, log_info, LdkLogger, Logger}; -use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; -use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; +use std::sync::{Arc, RwLock}; +use bitcoin::secp256k1::PublicKey; use lightning::ln::channelmanager::{PaymentId, RecipientOnionFields, Retry, RetryableSendFailure}; use lightning::routing::router::{PaymentParameters, RouteParameters, RouteParametersConfig}; use lightning::sign::EntropySource; - use lightning_types::payment::{PaymentHash, PaymentPreimage}; -use bitcoin::secp256k1::PublicKey; - -use std::sync::{Arc, RwLock}; +use crate::config::{Config, LDK_PAYMENT_RETRY_TIMEOUT}; +use crate::error::Error; +use crate::logger::{log_error, log_info, LdkLogger, Logger}; +use crate::payment::store::{PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus}; +use crate::types::{ChannelManager, CustomTlvRecord, KeysManager, PaymentStore}; // The default `final_cltv_expiry_delta` we apply when not set. const LDK_DEFAULT_FINAL_CLTV_EXPIRY_DELTA: u32 = 144; diff --git a/src/payment/store.rs b/src/payment/store.rs index 568394b488..b17898d9ce 100644 --- a/src/payment/store.rs +++ b/src/payment/store.rs @@ -5,6 +5,9 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::time::{Duration, SystemTime, UNIX_EPOCH}; + +use bitcoin::{BlockHash, Txid}; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::DecodeError; use lightning::offers::offer::OfferId; @@ -13,14 +16,9 @@ use lightning::{ _init_and_read_len_prefixed_tlv_fields, impl_writeable_tlv_based, impl_writeable_tlv_based_enum, write_tlv_fields, }; - use lightning_types::payment::{PaymentHash, PaymentPreimage, PaymentSecret}; use lightning_types::string::UntrustedString; -use bitcoin::{BlockHash, Txid}; - -use std::time::{Duration, SystemTime, UNIX_EPOCH}; - use crate::data_store::{StorableObject, StorableObjectId, StorableObjectUpdate}; use crate::hex_utils; @@ -607,10 +605,11 @@ impl StorableObjectUpdate for PaymentDetailsUpdate { #[cfg(test)] mod tests { - use super::*; use bitcoin::io::Cursor; use lightning::util::ser::Readable; + use super::*; + /// We refactored `PaymentDetails` to hold a payment id and moved some required fields into /// `PaymentKind`. Here, we keep the old layout available in order test de/ser compatibility. #[derive(Clone, Debug, PartialEq, Eq)] diff --git a/src/payment/unified_qr.rs b/src/payment/unified_qr.rs index af5ee1c7b2..fc2eca1500 100644 --- a/src/payment/unified_qr.rs +++ b/src/payment/unified_qr.rs @@ -11,23 +11,22 @@ //! [BIP 21]: https://github.com/bitcoin/bips/blob/master/bip-0021.mediawiki //! [BOLT 11]: https://github.com/lightning/bolts/blob/master/11-payment-encoding.md //! [BOLT 12]: https://github.com/lightning/bolts/blob/master/12-offer-encoding.md -use crate::error::Error; -use crate::ffi::maybe_wrap; -use crate::logger::{log_error, LdkLogger, Logger}; -use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; -use crate::Config; - -use lightning::ln::channelmanager::PaymentId; -use lightning::offers::offer::Offer; -use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; +use std::sync::Arc; +use std::vec::IntoIter; use bip21::de::ParamKind; use bip21::{DeserializationError, DeserializeParams, Param, SerializeParams}; use bitcoin::address::{NetworkChecked, NetworkUnchecked}; use bitcoin::{Amount, Txid}; +use lightning::ln::channelmanager::PaymentId; +use lightning::offers::offer::Offer; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; -use std::sync::Arc; -use std::vec::IntoIter; +use crate::error::Error; +use crate::ffi::maybe_wrap; +use crate::logger::{log_error, LdkLogger, Logger}; +use crate::payment::{Bolt11Payment, Bolt12Payment, OnchainPayment}; +use crate::Config; type Uri<'a> = bip21::Uri<'a, NetworkChecked, Extras>; @@ -303,10 +302,12 @@ impl DeserializationError for Extras { #[cfg(test)] mod tests { + use std::str::FromStr; + + use bitcoin::{Address, Network}; + use super::*; use crate::payment::unified_qr::Extras; - use bitcoin::{Address, Network}; - use std::str::FromStr; #[test] fn parse_uri() { diff --git a/src/peer_store.rs b/src/peer_store.rs index cf3755d237..5ebdc04191 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -5,6 +5,14 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::collections::HashMap; +use std::ops::Deref; +use std::sync::{Arc, RwLock}; + +use bitcoin::secp256k1::PublicKey; +use lightning::impl_writeable_tlv_based; +use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; + use crate::io::{ PEER_INFO_PERSISTENCE_KEY, PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, @@ -13,15 +21,6 @@ use crate::logger::{log_error, LdkLogger}; use crate::types::DynStore; use crate::{Error, SocketAddress}; -use lightning::impl_writeable_tlv_based; -use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; - -use bitcoin::secp256k1::PublicKey; - -use std::collections::HashMap; -use std::ops::Deref; -use std::sync::{Arc, RwLock}; - pub struct PeerStore where L::Target: LdkLogger, @@ -149,12 +148,13 @@ impl_writeable_tlv_based!(PeerInfo, { #[cfg(test)] mod tests { - use super::*; - use lightning::util::test_utils::{TestLogger, TestStore}; - use std::str::FromStr; use std::sync::Arc; + use lightning::util::test_utils::{TestLogger, TestStore}; + + use super::*; + #[test] fn peer_info_persistence() { let store: Arc = Arc::new(TestStore::new(false)); diff --git a/src/runtime.rs b/src/runtime.rs index b30790a041..2275d5bea1 100644 --- a/src/runtime.rs +++ b/src/runtime.rs @@ -5,17 +5,17 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::future::Future; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use tokio::task::{JoinHandle, JoinSet}; + use crate::config::{ BACKGROUND_TASK_SHUTDOWN_TIMEOUT_SECS, LDK_EVENT_HANDLER_SHUTDOWN_TIMEOUT_SECS, }; use crate::logger::{log_debug, log_error, log_trace, LdkLogger, Logger}; -use tokio::task::{JoinHandle, JoinSet}; - -use std::future::Future; -use std::sync::{Arc, Mutex}; -use std::time::Duration; - pub(crate) struct Runtime { mode: RuntimeMode, background_tasks: Mutex>, diff --git a/src/tx_broadcaster.rs b/src/tx_broadcaster.rs index 4d9397a610..12a1fe650c 100644 --- a/src/tx_broadcaster.rs +++ b/src/tx_broadcaster.rs @@ -5,16 +5,13 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::logger::{log_error, LdkLogger}; - -use lightning::chain::chaininterface::BroadcasterInterface; +use std::ops::Deref; use bitcoin::Transaction; +use lightning::chain::chaininterface::BroadcasterInterface; +use tokio::sync::{mpsc, Mutex, MutexGuard}; -use tokio::sync::mpsc; -use tokio::sync::{Mutex, MutexGuard}; - -use std::ops::Deref; +use crate::logger::{log_error, LdkLogger}; const BCAST_PACKAGE_QUEUE_SIZE: usize = 50; diff --git a/src/types.rs b/src/types.rs index 3635badffe..f152772a12 100644 --- a/src/types.rs +++ b/src/types.rs @@ -5,41 +5,35 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use crate::chain::ChainSource; -use crate::config::ChannelConfig; -use crate::data_store::DataStore; -use crate::fee_estimator::OnchainFeeEstimator; -use crate::gossip::RuntimeSpawner; -use crate::logger::Logger; -use crate::message_handler::NodeCustomMessageHandler; -use crate::payment::PaymentDetails; +use std::sync::{Arc, Mutex}; +use bitcoin::secp256k1::PublicKey; +use bitcoin::OutPoint; use lightning::chain::chainmonitor; use lightning::impl_writeable_tlv_based; use lightning::ln::channel_state::ChannelDetails as LdkChannelDetails; -use lightning::ln::msgs::RoutingMessageHandler; -use lightning::ln::msgs::SocketAddress; +use lightning::ln::msgs::{RoutingMessageHandler, SocketAddress}; use lightning::ln::peer_handler::IgnoringMessageHandler; use lightning::ln::types::ChannelId; use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; use lightning::sign::InMemorySigner; -use lightning::util::persist::KVStoreSync; -use lightning::util::persist::KVStoreSyncWrapper; +use lightning::util::persist::{KVStoreSync, KVStoreSyncWrapper}; use lightning::util::ser::{Readable, Writeable, Writer}; - use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; - -use lightning_net_tokio::SocketDescriptor; - use lightning_liquidity::utils::time::DefaultTimeProvider; +use lightning_net_tokio::SocketDescriptor; -use bitcoin::secp256k1::PublicKey; -use bitcoin::OutPoint; - -use std::sync::{Arc, Mutex}; +use crate::chain::ChainSource; +use crate::config::ChannelConfig; +use crate::data_store::DataStore; +use crate::fee_estimator::OnchainFeeEstimator; +use crate::gossip::RuntimeSpawner; +use crate::logger::Logger; +use crate::message_handler::NodeCustomMessageHandler; +use crate::payment::PaymentDetails; pub(crate) type DynStore = dyn KVStoreSync + Sync + Send; diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c03353ef87..0ce4628d40 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -5,37 +5,13 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use persist::KVStoreWalletPersister; - -use crate::config::Config; -use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; - -use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; -use crate::payment::store::ConfirmationStatus; -use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; -use crate::types::{Broadcaster, PaymentStore}; -use crate::Error; - -use lightning::chain::chaininterface::BroadcasterInterface; -use lightning::chain::channelmonitor::ANTI_REORG_DELAY; -use lightning::chain::{BestBlock, Listen}; - -use lightning::events::bump_transaction::{Utxo, WalletSource}; -use lightning::ln::channelmanager::PaymentId; -use lightning::ln::inbound_payment::ExpandedKey; -use lightning::ln::msgs::UnsignedGossipMessage; -use lightning::ln::script::ShutdownScript; -use lightning::sign::{ - ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, - PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, -}; - -use lightning::util::message_signing; -use lightning_invoice::RawBolt11Invoice; +use std::future::Future; +use std::pin::Pin; +use std::str::FromStr; +use std::sync::{Arc, Mutex}; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update}; - use bitcoin::address::NetworkUnchecked; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -49,11 +25,29 @@ use bitcoin::{ Address, Amount, FeeRate, Network, ScriptBuf, Transaction, TxOut, Txid, WPubkeyHash, WitnessProgram, WitnessVersion, }; +use lightning::chain::chaininterface::BroadcasterInterface; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::{BestBlock, Listen}; +use lightning::events::bump_transaction::{Utxo, WalletSource}; +use lightning::ln::channelmanager::PaymentId; +use lightning::ln::inbound_payment::ExpandedKey; +use lightning::ln::msgs::UnsignedGossipMessage; +use lightning::ln::script::ShutdownScript; +use lightning::sign::{ + ChangeDestinationSource, EntropySource, InMemorySigner, KeysManager, NodeSigner, OutputSpender, + PeerStorageKey, Recipient, SignerProvider, SpendableOutputDescriptor, +}; +use lightning::util::message_signing; +use lightning_invoice::RawBolt11Invoice; +use persist::KVStoreWalletPersister; -use std::future::Future; -use std::pin::Pin; -use std::str::FromStr; -use std::sync::{Arc, Mutex}; +use crate::config::Config; +use crate::fee_estimator::{ConfirmationTarget, FeeEstimator, OnchainFeeEstimator}; +use crate::logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; +use crate::payment::store::ConfirmationStatus; +use crate::payment::{PaymentDetails, PaymentDirection, PaymentStatus}; +use crate::types::{Broadcaster, PaymentStore}; +use crate::Error; pub(crate) enum OnchainSendAmount { ExactRetainingReserve { amount_sats: u64, cur_anchor_reserve_sats: u64 }, diff --git a/src/wallet/persist.rs b/src/wallet/persist.rs index d9e4e71359..5c8668937e 100644 --- a/src/wallet/persist.rs +++ b/src/wallet/persist.rs @@ -5,6 +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. +use std::sync::Arc; + +use bdk_chain::Merge; +use bdk_wallet::{ChangeSet, WalletPersister}; + use crate::io::utils::{ read_bdk_wallet_change_set, write_bdk_wallet_change_descriptor, write_bdk_wallet_descriptor, write_bdk_wallet_indexer, write_bdk_wallet_local_chain, write_bdk_wallet_network, @@ -12,11 +17,6 @@ use crate::io::utils::{ }; use crate::logger::{log_error, LdkLogger, Logger}; use crate::types::DynStore; - -use bdk_chain::Merge; -use bdk_wallet::{ChangeSet, WalletPersister}; - -use std::sync::Arc; pub(crate) struct KVStoreWalletPersister { latest_change_set: Option, kv_store: Arc, diff --git a/src/wallet/ser.rs b/src/wallet/ser.rs index ae1509bdf0..c1ad984e62 100644 --- a/src/wallet/ser.rs +++ b/src/wallet/ser.rs @@ -5,26 +5,23 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. -use lightning::ln::msgs::DecodeError; -use lightning::util::ser::{BigSize, Readable, RequiredWrapper, Writeable, Writer}; -use lightning::{decode_tlv_stream, encode_tlv_stream, read_tlv_fields, write_tlv_fields}; +use std::collections::{BTreeMap, BTreeSet}; +use std::str::FromStr; +use std::sync::Arc; use bdk_chain::bdk_core::{BlockId, ConfirmationBlockTime}; use bdk_chain::indexer::keychain_txout::ChangeSet as BdkIndexerChangeSet; use bdk_chain::local_chain::ChangeSet as BdkLocalChainChangeSet; use bdk_chain::tx_graph::ChangeSet as BdkTxGraphChangeSet; use bdk_chain::DescriptorId; - use bdk_wallet::descriptor::Descriptor; use bdk_wallet::keys::DescriptorPublicKey; - use bitcoin::hashes::sha256::Hash as Sha256Hash; use bitcoin::p2p::Magic; use bitcoin::{BlockHash, Network, OutPoint, Transaction, TxOut, Txid}; - -use std::collections::{BTreeMap, BTreeSet}; -use std::str::FromStr; -use std::sync::Arc; +use lightning::ln::msgs::DecodeError; +use lightning::util::ser::{BigSize, Readable, RequiredWrapper, Writeable, Writer}; +use lightning::{decode_tlv_stream, encode_tlv_stream, read_tlv_fields, write_tlv_fields}; const CHANGESET_SERIALIZATION_VERSION: u8 = 1; diff --git a/tests/common/logging.rs b/tests/common/logging.rs index d7d59ba324..3ff24d34d4 100644 --- a/tests/common/logging.rs +++ b/tests/common/logging.rs @@ -1,10 +1,10 @@ +use std::sync::{Arc, Mutex}; + use chrono::Utc; -use ldk_node::logger::LogRecord; -use ldk_node::logger::{LogLevel, LogWriter}; +use ldk_node::logger::{LogLevel, LogRecord, LogWriter}; #[cfg(not(feature = "uniffi"))] use log::Record as LogFacadeRecord; use log::{Level as LogFacadeLevel, LevelFilter as LogFacadeLevelFilter, Log as LogFacadeLog}; -use std::sync::{Arc, Mutex}; #[derive(Clone)] pub(crate) enum TestLogWriter { diff --git a/tests/common/mod.rs b/tests/common/mod.rs index aa09b86d0b..98c96e307b 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,46 +10,39 @@ pub(crate) mod logging; -use logging::TestLogWriter; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::path::PathBuf; +use std::sync::{Arc, RwLock}; +use std::time::Duration; +use bitcoin::hashes::hex::FromHex; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::Hash; +use bitcoin::{ + Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness, +}; +use electrsd::corepc_node::{Client as BitcoindClient, Node as BitcoinD}; +use electrsd::{corepc_node, ElectrsD}; +use electrum_client::ElectrumApi; use ldk_node::config::{AsyncPaymentsRole, Config, ElectrumSyncConfig, EsploraSyncConfig}; use ldk_node::io::sqlite_store::SqliteStore; use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, }; - use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; use lightning::util::persist::KVStoreSync; use lightning::util::test_utils::TestStore; - use lightning_invoice::{Bolt11InvoiceDescription, Description}; -use lightning_types::payment::{PaymentHash, PaymentPreimage}; - use lightning_persister::fs_store::FilesystemStore; - -use bitcoin::hashes::sha256::Hash as Sha256; -use bitcoin::hashes::{hex::FromHex, Hash}; -use bitcoin::{ - Address, Amount, Network, OutPoint, ScriptBuf, Sequence, Transaction, Txid, Witness, -}; - -use electrsd::corepc_node::Client as BitcoindClient; -use electrsd::corepc_node::Node as BitcoinD; -use electrsd::{corepc_node, ElectrsD}; -use electrum_client::ElectrumApi; - +use lightning_types::payment::{PaymentHash, PaymentPreimage}; +use logging::TestLogWriter; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; use serde_json::{json, Value}; -use std::collections::{HashMap, HashSet}; -use std::env; -use std::path::PathBuf; -use std::sync::{Arc, RwLock}; -use std::time::Duration; - macro_rules! expect_event { ($node: expr, $event_type: ident) => {{ match $node.wait_next_event() { diff --git a/tests/integration_tests_cln.rs b/tests/integration_tests_cln.rs index f77311fb2b..6fc72b2c25 100644 --- a/tests/integration_tests_cln.rs +++ b/tests/integration_tests_cln.rs @@ -9,27 +9,22 @@ mod common; -use ldk_node::bitcoin::secp256k1::PublicKey; -use ldk_node::bitcoin::Amount; -use ldk_node::lightning::ln::msgs::SocketAddress; -use ldk_node::{Builder, Event}; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use std::default::Default; +use std::str::FromStr; use clightningrpc::lightningrpc::LightningRPC; use clightningrpc::responses::NetworkAddress; - use electrsd::corepc_client::client_sync::Auth; use electrsd::corepc_node::Client as BitcoindClient; - use electrum_client::Client as ElectrumClient; -use lightning_invoice::Bolt11Invoice; - +use ldk_node::bitcoin::secp256k1::PublicKey; +use ldk_node::bitcoin::Amount; +use ldk_node::lightning::ln::msgs::SocketAddress; +use ldk_node::{Builder, Event}; +use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, Description}; use rand::distributions::Alphanumeric; use rand::{thread_rng, Rng}; -use std::default::Default; -use std::str::FromStr; - #[test] fn test_cln() { // Setup bitcoind / electrs clients diff --git a/tests/integration_tests_lnd.rs b/tests/integration_tests_lnd.rs index 0232e8f2ea..7dfc1e4f92 100755 --- a/tests/integration_tests_lnd.rs +++ b/tests/integration_tests_lnd.rs @@ -2,29 +2,25 @@ mod common; +use std::default::Default; +use std::str::FromStr; + +use bitcoin::hex::DisplayHex; +use electrsd::corepc_client::client_sync::Auth; +use electrsd::corepc_node::Client as BitcoindClient; +use electrum_client::Client as ElectrumClient; use ldk_node::bitcoin::secp256k1::PublicKey; use ldk_node::bitcoin::Amount; use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::{Builder, Event}; - +use lightning_invoice::{Bolt11InvoiceDescription, Description}; +use lnd_grpc_rust::lnrpc::invoice::InvoiceState::Settled as LndInvoiceStateSettled; use lnd_grpc_rust::lnrpc::{ - invoice::InvoiceState::Settled as LndInvoiceStateSettled, GetInfoRequest as LndGetInfoRequest, - GetInfoResponse as LndGetInfoResponse, Invoice as LndInvoice, - ListInvoiceRequest as LndListInvoiceRequest, QueryRoutesRequest as LndQueryRoutesRequest, - Route as LndRoute, SendRequest as LndSendRequest, + GetInfoRequest as LndGetInfoRequest, GetInfoResponse as LndGetInfoResponse, + Invoice as LndInvoice, ListInvoiceRequest as LndListInvoiceRequest, + QueryRoutesRequest as LndQueryRoutesRequest, Route as LndRoute, SendRequest as LndSendRequest, }; use lnd_grpc_rust::{connect, LndClient}; - -use electrsd::corepc_client::client_sync::Auth; -use electrsd::corepc_node::Client as BitcoindClient; - -use electrum_client::Client as ElectrumClient; -use lightning_invoice::{Bolt11InvoiceDescription, Description}; - -use bitcoin::hex::DisplayHex; - -use std::default::Default; -use std::str::FromStr; use tokio::fs; #[tokio::test(flavor = "multi_thread", worker_threads = 1)] diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 63fc737b35..0db30ea1c6 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -7,19 +7,24 @@ mod common; +use std::collections::HashSet; +use std::str::FromStr; +use std::sync::Arc; + +use bitcoin::address::NetworkUnchecked; +use bitcoin::hashes::sha256::Hash as Sha256Hash; +use bitcoin::hashes::Hash; +use bitcoin::{Address, Amount, ScriptBuf}; +use common::logging::{init_log_logger, validate_log_entry, MultiNodeLogger, TestLogWriter}; use common::{ bump_fee_and_broadcast, distribute_funds_unconfirmed, do_channel_full_cycle, expect_channel_pending_event, expect_channel_ready_event, expect_event, expect_payment_claimable_event, expect_payment_received_event, expect_payment_successful_event, - generate_blocks_and_wait, - logging::MultiNodeLogger, - logging::{init_log_logger, validate_log_entry, TestLogWriter}, - open_channel, open_channel_push_amt, premine_and_distribute_funds, premine_blocks, prepare_rbf, - random_config, random_listening_addresses, setup_bitcoind_and_electrsd, setup_builder, - setup_node, setup_node_for_async_payments, setup_two_nodes, wait_for_tx, TestChainSource, - TestSyncStore, + generate_blocks_and_wait, open_channel, open_channel_push_amt, premine_and_distribute_funds, + premine_blocks, prepare_rbf, random_config, random_listening_addresses, + setup_bitcoind_and_electrsd, setup_builder, setup_node, setup_node_for_async_payments, + setup_two_nodes, wait_for_tx, TestChainSource, TestSyncStore, }; - use ldk_node::config::{AsyncPaymentsRole, EsploraSyncConfig}; use ldk_node::liquidity::LSPS2ServiceConfig; use ldk_node::payment::{ @@ -27,25 +32,14 @@ use ldk_node::payment::{ QrPaymentResult, }; use ldk_node::{Builder, Event, NodeError}; - use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; use lightning::util::persist::KVStoreSync; - use lightning_invoice::{Bolt11InvoiceDescription, Description}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; - -use bitcoin::address::NetworkUnchecked; -use bitcoin::hashes::sha256::Hash as Sha256Hash; -use bitcoin::hashes::Hash; -use bitcoin::{Address, Amount, ScriptBuf}; use log::LevelFilter; -use std::collections::HashSet; -use std::str::FromStr; -use std::sync::Arc; - #[test] fn channel_full_cycle() { let (bitcoind, electrsd) = setup_bitcoind_and_electrsd(); diff --git a/tests/integration_tests_vss.rs b/tests/integration_tests_vss.rs index 9d6ec158c9..bdd8760034 100644 --- a/tests/integration_tests_vss.rs +++ b/tests/integration_tests_vss.rs @@ -9,9 +9,10 @@ mod common; -use ldk_node::Builder; use std::collections::HashMap; +use ldk_node::Builder; + #[test] fn channel_full_cycle_with_vss_store() { let (bitcoind, electrsd) = common::setup_bitcoind_and_electrsd(); diff --git a/tests/reorg_test.rs b/tests/reorg_test.rs index 707b67e888..03ace908fb 100644 --- a/tests/reorg_test.rs +++ b/tests/reorg_test.rs @@ -1,9 +1,11 @@ mod common; +use std::collections::HashMap; + use bitcoin::Amount; use ldk_node::payment::{PaymentDirection, PaymentKind}; use ldk_node::{Event, LightningBalance, PendingSweepBalance}; -use proptest::{prelude::prop, proptest}; -use std::collections::HashMap; +use proptest::prelude::prop; +use proptest::proptest; use crate::common::{ expect_event, generate_blocks_and_wait, invalidate_blocks, open_channel, From 11b7eb5f8976da4118186ea1e815d03938cce279 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 22 Sep 2025 18:11:36 +0200 Subject: [PATCH 151/177] Use cancellable task for inbound connections So that we'll cleanly signal and wait for termination. --- src/lib.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 0f547ce1de..a8f2f87eb0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -313,6 +313,7 @@ impl Node { bind_addrs.extend(resolved_address); } + let runtime = Arc::clone(&self.runtime); self.runtime.spawn_cancellable_background_task(async move { { let listener = @@ -338,7 +339,7 @@ impl Node { } res = listener.accept() => { let tcp_stream = res.unwrap().0; - tokio::spawn(async move { + runtime.spawn_cancellable_background_task(async move { lightning_net_tokio::setup_inbound( Arc::clone(&peer_mgr), tcp_stream.into_std().unwrap(), From 6f1edcdb8cf842aad519333fc684e9507df8dc6d Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Mon, 22 Sep 2025 14:38:32 +0200 Subject: [PATCH 152/177] Listen on all provided addresses Previously ldk-node would start binding after the first successful bind to an address. --- bindings/ldk_node.udl | 1 - src/builder.rs | 3 - src/lib.rs | 97 ++++++++++++++++++--------------- tests/integration_tests_rust.rs | 24 +++++--- 4 files changed, 67 insertions(+), 58 deletions(-) diff --git a/bindings/ldk_node.udl b/bindings/ldk_node.udl index a6d867e5aa..bd1e4fc430 100644 --- a/bindings/ldk_node.udl +++ b/bindings/ldk_node.udl @@ -325,7 +325,6 @@ enum NodeError { dictionary NodeStatus { boolean is_running; - boolean is_listening; BestBlock current_best_block; u64? latest_lightning_wallet_sync_timestamp; u64? latest_onchain_wallet_sync_timestamp; diff --git a/src/builder.rs b/src/builder.rs index cf414ec576..0b3ea3101a 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -9,7 +9,6 @@ use std::collections::HashMap; use std::convert::TryInto; use std::default::Default; use std::path::PathBuf; -use std::sync::atomic::AtomicBool; use std::sync::{Arc, Mutex, Once, RwLock}; use std::time::SystemTime; use std::{fmt, fs}; @@ -1133,7 +1132,6 @@ fn build_with_store_internal( } // Initialize the status fields. - let is_listening = Arc::new(AtomicBool::new(false)); let node_metrics = match read_node_metrics(Arc::clone(&kv_store), Arc::clone(&logger)) { Ok(metrics) => Arc::new(RwLock::new(metrics)), Err(e) => { @@ -1734,7 +1732,6 @@ fn build_with_store_internal( peer_store, payment_store, is_running, - is_listening, node_metrics, om_mailbox, async_payments_role, diff --git a/src/lib.rs b/src/lib.rs index a8f2f87eb0..a075cfac5b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -101,7 +101,6 @@ mod wallet; use std::default::Default; use std::net::ToSocketAddrs; -use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, Mutex, RwLock}; use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; @@ -189,7 +188,6 @@ pub struct Node { peer_store: Arc>>, payment_store: Arc, is_running: Arc>, - is_listening: Arc, node_metrics: Arc>, om_mailbox: Option>, async_payments_role: Option, @@ -293,9 +291,7 @@ impl Node { if let Some(listening_addresses) = &self.config.listening_addresses { // Setup networking let peer_manager_connection_handler = Arc::clone(&self.peer_manager); - let mut stop_listen = self.stop_sender.subscribe(); let listening_logger = Arc::clone(&self.logger); - let listening_indicator = Arc::clone(&self.is_listening); let mut bind_addrs = Vec::with_capacity(listening_addresses.len()); @@ -313,46 +309,62 @@ impl Node { bind_addrs.extend(resolved_address); } - let runtime = Arc::clone(&self.runtime); - self.runtime.spawn_cancellable_background_task(async move { - { - let listener = - tokio::net::TcpListener::bind(&*bind_addrs).await - .unwrap_or_else(|e| { - log_error!(listening_logger, "Failed to bind to listen addresses/ports - is something else already listening on it?: {}", e); - panic!( - "Failed to bind to listen address/port - is something else already listening on it?", - ); - }); - - listening_indicator.store(true, Ordering::Release); - - loop { - let peer_mgr = Arc::clone(&peer_manager_connection_handler); - tokio::select! { - _ = stop_listen.changed() => { - log_debug!( - listening_logger, - "Stopping listening to inbound connections." + let logger = Arc::clone(&listening_logger); + let listeners = self.runtime.block_on(async move { + let mut listeners = Vec::new(); + + // Try to bind to all addresses + for addr in &*bind_addrs { + match tokio::net::TcpListener::bind(addr).await { + Ok(listener) => { + log_trace!(logger, "Listener bound to {}", addr); + listeners.push(listener); + }, + Err(e) => { + log_error!( + logger, + "Failed to bind to {}: {} - is something else already listening?", + addr, + e ); - break; - } - res = listener.accept() => { - let tcp_stream = res.unwrap().0; - runtime.spawn_cancellable_background_task(async move { - lightning_net_tokio::setup_inbound( - Arc::clone(&peer_mgr), - tcp_stream.into_std().unwrap(), - ) - .await; - }); - } + return Err(Error::InvalidSocketAddress); + }, } } - } - listening_indicator.store(false, Ordering::Release); - }); + Ok(listeners) + })?; + + for listener in listeners { + let logger = Arc::clone(&listening_logger); + let peer_mgr = Arc::clone(&peer_manager_connection_handler); + let mut stop_listen = self.stop_sender.subscribe(); + let runtime = Arc::clone(&self.runtime); + self.runtime.spawn_cancellable_background_task(async move { + loop { + tokio::select! { + _ = stop_listen.changed() => { + log_debug!( + logger, + "Stopping listening to inbound connections." + ); + break; + } + res = listener.accept() => { + let tcp_stream = res.unwrap().0; + let peer_mgr = Arc::clone(&peer_mgr); + runtime.spawn_cancellable_background_task(async move { + lightning_net_tokio::setup_inbound( + Arc::clone(&peer_mgr), + tcp_stream.into_std().unwrap(), + ) + .await; + }); + } + } + } + }); + } } // Regularly reconnect to persisted peers. @@ -667,7 +679,6 @@ impl Node { /// Returns the status of the [`Node`]. pub fn status(&self) -> NodeStatus { let is_running = *self.is_running.read().unwrap(); - let is_listening = self.is_listening.load(Ordering::Acquire); let current_best_block = self.channel_manager.current_best_block().into(); let locked_node_metrics = self.node_metrics.read().unwrap(); let latest_lightning_wallet_sync_timestamp = @@ -685,7 +696,6 @@ impl Node { NodeStatus { is_running, - is_listening, current_best_block, latest_lightning_wallet_sync_timestamp, latest_onchain_wallet_sync_timestamp, @@ -1496,9 +1506,6 @@ impl Drop for Node { pub struct NodeStatus { /// Indicates whether the [`Node`] is running. pub is_running: bool, - /// Indicates whether the [`Node`] is listening for incoming connections on the addresses - /// configured via [`Config::listening_addresses`]. - pub is_listening: bool, /// The best block to which our Lightning wallet is currently synced. pub current_best_block: BestBlock, /// The timestamp, in seconds since start of the UNIX epoch, when we last successfully synced diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index 0db30ea1c6..cca52ae2de 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -817,6 +817,21 @@ fn sign_verify_msg() { assert!(node.verify_signature(msg, sig.as_str(), &pkey)); } +#[test] +fn connection_multi_listen() { + let (_bitcoind, electrsd) = setup_bitcoind_and_electrsd(); + let chain_source = TestChainSource::Esplora(&electrsd); + let (node_a, node_b) = setup_two_nodes(&chain_source, false, false, false); + + let node_id_b = node_b.node_id(); + + let node_addrs_b = node_b.listening_addresses().unwrap(); + for node_addr_b in &node_addrs_b { + node_a.connect(node_id_b, node_addr_b.clone(), false).unwrap(); + node_a.disconnect(node_id_b).unwrap(); + } +} + #[test] fn connection_restart_behavior() { do_connection_restart_behavior(true); @@ -832,11 +847,6 @@ fn do_connection_restart_behavior(persist: bool) { let node_id_b = node_b.node_id(); let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - - while !node_b.status().is_listening { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - node_a.connect(node_id_b, node_addr_b, persist).unwrap(); let peer_details_a = node_a.list_peers().first().unwrap().clone(); @@ -886,10 +896,6 @@ fn concurrent_connections_succeed() { let node_id_b = node_b.node_id(); let node_addr_b = node_b.listening_addresses().unwrap().first().unwrap().clone(); - while !node_b.status().is_listening { - std::thread::sleep(std::time::Duration::from_millis(10)); - } - let mut handles = Vec::new(); for _ in 0..10 { let thread_node = Arc::clone(&node_a); From 0f621ff117e9a144ff790ea6172d5c202132a5c7 Mon Sep 17 00:00:00 2001 From: Joost Jager Date: Tue, 23 Sep 2025 10:26:47 +0200 Subject: [PATCH 153/177] Do not use random ports from ephemeral range To avoid conflicts ("port in use") with ports that were used for outgoing connections and are now in the TIME_WAIT state. --- tests/common/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 98c96e307b..1331fc0477 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -195,7 +195,7 @@ pub(crate) fn random_storage_path() -> PathBuf { pub(crate) fn random_port() -> u16 { let mut rng = thread_rng(); - rng.gen_range(5000..65535) + rng.gen_range(5000..32768) } pub(crate) fn random_listening_addresses() -> Vec { From 52a5f9aa1bbf7058455240818596cd4d48e62e8c Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 09:58:56 +0200 Subject: [PATCH 154/177] Move current VSS `KVStoreSync` logic to `_internal` methods .. first step to make review easier. --- src/io/vss_store.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index a03aafc44b..64143dd427 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -126,10 +126,8 @@ impl VssStore { } Ok(keys) } -} -impl KVStoreSync for VssStore { - fn read( + fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; @@ -160,7 +158,7 @@ impl KVStoreSync for VssStore { Ok(self.storable_builder.deconstruct(storable)?.0) } - fn write( + fn write_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; @@ -188,7 +186,7 @@ impl KVStoreSync for VssStore { Ok(()) } - fn remove( + fn remove_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; @@ -211,7 +209,9 @@ impl KVStoreSync for VssStore { Ok(()) } - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + fn list_internal( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; let keys = self @@ -229,6 +229,30 @@ impl KVStoreSync for VssStore { } } +impl KVStoreSync for VssStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + self.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + self.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> io::Result<()> { + self.remove_internal(primary_namespace, secondary_namespace, key, lazy) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + self.list_internal(primary_namespace, secondary_namespace) + } +} + fn derive_data_encryption_and_obfuscation_keys(vss_seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) { let hkdf = |initial_key_material: &[u8], salt: &[u8]| -> [u8; 32] { let mut engine = HmacEngine::::new(salt); From bc313f94bf1190b44bda0eaa6151bda8c719e8d9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 10:03:30 +0200 Subject: [PATCH 155/177] Make VSS internal methods `async`, move `block_on` to `impl KVStoreSync` .. as we're gonna reuse the `async` `_internal` methods shortly. --- src/io/vss_store.rs | 34 +++++++++++++++++++--------------- 1 file changed, 19 insertions(+), 15 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 64143dd427..02cb54e78a 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -127,7 +127,7 @@ impl VssStore { Ok(keys) } - fn read_internal( + async fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; @@ -135,7 +135,7 @@ impl VssStore { store_id: self.store_id.clone(), key: self.build_key(primary_namespace, secondary_namespace, key)?, }; - let resp = self.runtime.block_on(self.client.get_object(&request)).map_err(|e| { + let resp = self.client.get_object(&request).await.map_err(|e| { let msg = format!( "Failed to read from key {}/{}/{}: {}", primary_namespace, secondary_namespace, key, e @@ -145,6 +145,7 @@ impl VssStore { _ => Error::new(ErrorKind::Other, msg), } })?; + // unwrap safety: resp.value must be always present for a non-erroneous VSS response, otherwise // it is an API-violation which is converted to [`VssError::InternalServerError`] in [`VssClient`] let storable = Storable::decode(&resp.value.unwrap().value[..]).map_err(|e| { @@ -158,7 +159,7 @@ impl VssStore { Ok(self.storable_builder.deconstruct(storable)?.0) } - fn write_internal( + async fn write_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; @@ -175,7 +176,7 @@ impl VssStore { delete_items: vec![], }; - self.runtime.block_on(self.client.put_object(&request)).map_err(|e| { + self.client.put_object(&request).await.map_err(|e| { let msg = format!( "Failed to write to key {}/{}/{}: {}", primary_namespace, secondary_namespace, key, e @@ -186,7 +187,7 @@ impl VssStore { Ok(()) } - fn remove_internal( + async fn remove_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; @@ -199,25 +200,24 @@ impl VssStore { }), }; - self.runtime.block_on(self.client.delete_object(&request)).map_err(|e| { + self.client.delete_object(&request).await.map_err(|e| { let msg = format!( "Failed to delete key {}/{}/{}: {}", primary_namespace, secondary_namespace, key, e ); Error::new(ErrorKind::Other, msg) })?; + Ok(()) } - fn list_internal( + async fn list_internal( &self, primary_namespace: &str, secondary_namespace: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; - let keys = self - .runtime - .block_on(self.list_all_keys(primary_namespace, secondary_namespace)) - .map_err(|e| { + let keys = + self.list_all_keys(primary_namespace, secondary_namespace).await.map_err(|e| { let msg = format!( "Failed to retrieve keys in namespace: {}/{} : {}", primary_namespace, secondary_namespace, e @@ -233,23 +233,27 @@ impl KVStoreSync for VssStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { - self.read_internal(primary_namespace, secondary_namespace, key) + let fut = self.read_internal(primary_namespace, secondary_namespace, key); + self.runtime.block_on(fut) } fn write( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { - self.write_internal(primary_namespace, secondary_namespace, key, buf) + let fut = self.write_internal(primary_namespace, secondary_namespace, key, buf); + self.runtime.block_on(fut) } fn remove( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, ) -> io::Result<()> { - self.remove_internal(primary_namespace, secondary_namespace, key, lazy) + let fut = self.remove_internal(primary_namespace, secondary_namespace, key, lazy); + self.runtime.block_on(fut) } fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { - self.list_internal(primary_namespace, secondary_namespace) + let fut = self.list_internal(primary_namespace, secondary_namespace); + self.runtime.block_on(fut) } } From 2b40a8064077246c198afb530af36318033f13c8 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 10:40:22 +0200 Subject: [PATCH 156/177] Split `VssStore` into `VssStore` and `VssStoreInner` .. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily. --- src/io/vss_store.rs | 78 ++++++++++++++++++++++++++------------------- 1 file changed, 46 insertions(+), 32 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 02cb54e78a..b85477f5e9 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -41,17 +41,59 @@ type CustomRetryPolicy = FilteredRetryPolicy< /// A [`KVStoreSync`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. pub struct VssStore { + inner: Arc, + runtime: Arc, +} + +impl VssStore { + pub(crate) fn new( + base_url: String, store_id: String, vss_seed: [u8; 32], + header_provider: Arc, runtime: Arc, + ) -> Self { + let inner = Arc::new(VssStoreInner::new(base_url, store_id, vss_seed, header_provider)); + Self { inner, runtime } + } +} + +impl KVStoreSync for VssStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + let fut = self.inner.read_internal(primary_namespace, secondary_namespace, key); + self.runtime.block_on(fut) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + let fut = self.inner.write_internal(primary_namespace, secondary_namespace, key, buf); + self.runtime.block_on(fut) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> io::Result<()> { + let fut = self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy); + self.runtime.block_on(fut) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + let fut = self.inner.list_internal(primary_namespace, secondary_namespace); + self.runtime.block_on(fut) + } +} + +struct VssStoreInner { client: VssClient, store_id: String, - runtime: Arc, storable_builder: StorableBuilder, key_obfuscator: KeyObfuscator, } -impl VssStore { +impl VssStoreInner { pub(crate) fn new( base_url: String, store_id: String, vss_seed: [u8; 32], - header_provider: Arc, runtime: Arc, + header_provider: Arc, ) -> Self { let (data_encryption_key, obfuscation_master_key) = derive_data_encryption_and_obfuscation_keys(&vss_seed); @@ -71,7 +113,7 @@ impl VssStore { }) as _); let client = VssClient::new_with_headers(base_url, retry_policy, header_provider); - Self { client, store_id, runtime, storable_builder, key_obfuscator } + Self { client, store_id, storable_builder, key_obfuscator } } fn build_key( @@ -229,34 +271,6 @@ impl VssStore { } } -impl KVStoreSync for VssStore { - fn read( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> io::Result> { - let fut = self.read_internal(primary_namespace, secondary_namespace, key); - self.runtime.block_on(fut) - } - - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, - ) -> io::Result<()> { - let fut = self.write_internal(primary_namespace, secondary_namespace, key, buf); - self.runtime.block_on(fut) - } - - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, - ) -> io::Result<()> { - let fut = self.remove_internal(primary_namespace, secondary_namespace, key, lazy); - self.runtime.block_on(fut) - } - - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { - let fut = self.list_internal(primary_namespace, secondary_namespace); - self.runtime.block_on(fut) - } -} - fn derive_data_encryption_and_obfuscation_keys(vss_seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) { let hkdf = |initial_key_material: &[u8], salt: &[u8]| -> [u8; 32] { let mut engine = HmacEngine::::new(salt); From 679b6d3e4167484bc5b19066df40b3771814808a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 26 Sep 2025 09:12:52 +0200 Subject: [PATCH 157/177] Refactor infallible `build_key` to not return an error --- src/io/vss_store.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index b85477f5e9..7c3ccdd860 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -116,14 +116,12 @@ impl VssStoreInner { Self { client, store_id, storable_builder, key_obfuscator } } - fn build_key( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> io::Result { + fn build_key(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> String { let obfuscated_key = self.key_obfuscator.obfuscate(key); if primary_namespace.is_empty() { - Ok(obfuscated_key) + obfuscated_key } else { - Ok(format!("{}#{}#{}", primary_namespace, secondary_namespace, obfuscated_key)) + format!("{}#{}#{}", primary_namespace, secondary_namespace, obfuscated_key) } } @@ -175,7 +173,7 @@ impl VssStoreInner { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; let request = GetObjectRequest { store_id: self.store_id.clone(), - key: self.build_key(primary_namespace, secondary_namespace, key)?, + key: self.build_key(primary_namespace, secondary_namespace, key), }; let resp = self.client.get_object(&request).await.map_err(|e| { let msg = format!( @@ -211,7 +209,7 @@ impl VssStoreInner { store_id: self.store_id.clone(), global_version: None, transaction_items: vec![KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key)?, + key: self.build_key(primary_namespace, secondary_namespace, key), version, value: storable.encode_to_vec(), }], @@ -236,7 +234,7 @@ impl VssStoreInner { let request = DeleteObjectRequest { store_id: self.store_id.clone(), key_value: Some(KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key)?, + key: self.build_key(primary_namespace, secondary_namespace, key), version: -1, value: vec![], }), From 2b117f5584548f40ad024dfe54ae3c6c739cfeab Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 10:47:46 +0200 Subject: [PATCH 158/177] Implement `KVStore` for `VssStore` We implement the async `KVStore` trait for `VssStore`. --- src/io/vss_store.rs | 283 ++++++++++++++++++++++++++++++++++++-------- 1 file changed, 234 insertions(+), 49 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index 7c3ccdd860..b71d4144e1 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -5,16 +5,22 @@ // http://opensource.org/licenses/MIT>, at your option. You may not use this file except in // accordance with one or both of these licenses. +use std::boxed::Box; +use std::collections::HashMap; +use std::future::Future; #[cfg(test)] use std::panic::RefUnwindSafe; -use std::sync::Arc; +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; +use std::sync::{Arc, Mutex}; use std::time::Duration; use bitcoin::hashes::{sha256, Hash, HashEngine, Hmac, HmacEngine}; use lightning::io::{self, Error, ErrorKind}; -use lightning::util::persist::KVStoreSync; +use lightning::util::persist::{KVStore, KVStoreSync}; use prost::Message; use rand::RngCore; +use tokio::sync::RwLock; use vss_client::client::VssClient; use vss_client::error::VssError; use vss_client::headers::VssHeaderProvider; @@ -42,6 +48,9 @@ type CustomRetryPolicy = FilteredRetryPolicy< /// A [`KVStoreSync`] implementation that writes to and reads from a [VSS](https://github.com/lightningdevkit/vss-server/blob/main/README.md) backend. pub struct VssStore { inner: Arc, + // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list + // operations aren't sensitive to the order of execution. + next_version: AtomicU64, runtime: Arc, } @@ -51,7 +60,32 @@ impl VssStore { header_provider: Arc, runtime: Arc, ) -> Self { let inner = Arc::new(VssStoreInner::new(base_url, store_id, vss_seed, header_provider)); - Self { inner, runtime } + let next_version = AtomicU64::new(1); + Self { inner, next_version, runtime } + } + + // Same logic as for the obfuscated keys below, but just for locking, using the plaintext keys + fn build_locking_key( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> String { + if primary_namespace.is_empty() { + key.to_owned() + } else { + format!("{}#{}#{}", primary_namespace, secondary_namespace, key) + } + } + + fn get_new_version_and_lock_ref(&self, locking_key: String) -> (Arc>, u64) { + let version = self.next_version.fetch_add(1, Ordering::Relaxed); + if version == u64::MAX { + panic!("VssStore version counter overflowed"); + } + + // Get a reference to the inner lock. We do this early so that the arc can double as an in-flight counter for + // cleaning up unused locks. + let inner_lock_ref = self.inner.get_inner_lock_ref(locking_key); + + (inner_lock_ref, version) } } @@ -66,14 +100,34 @@ impl KVStoreSync for VssStore { fn write( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { - let fut = self.inner.write_internal(primary_namespace, secondary_namespace, key, buf); + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let fut = self.inner.write_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + buf, + ); self.runtime.block_on(fut) } fn remove( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, ) -> io::Result<()> { - let fut = self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy); + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let fut = self.inner.remove_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + lazy, + ); self.runtime.block_on(fut) } @@ -83,11 +137,82 @@ impl KVStoreSync for VssStore { } } +impl KVStore for VssStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner.read_internal(&primary_namespace, &secondary_namespace, &key).await + }) + } + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner + .write_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + buf, + ) + .await + }) + } + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { + inner + .remove_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + lazy, + ) + .await + }) + } + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + Box::pin(async move { inner.list_internal(&primary_namespace, &secondary_namespace).await }) + } +} + struct VssStoreInner { client: VssClient, store_id: String, storable_builder: StorableBuilder, key_obfuscator: KeyObfuscator, + // Per-key locks that ensures that we don't have concurrent writes to the same namespace/key. + // The lock also encapsulates the latest written version per key. + locks: Mutex>>>, } impl VssStoreInner { @@ -113,10 +238,18 @@ impl VssStoreInner { }) as _); let client = VssClient::new_with_headers(base_url, retry_policy, header_provider); - Self { client, store_id, storable_builder, key_obfuscator } + let locks = Mutex::new(HashMap::new()); + Self { client, store_id, storable_builder, key_obfuscator, locks } } - fn build_key(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> String { + fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { + let mut outer_lock = self.locks.lock().unwrap(); + Arc::clone(&outer_lock.entry(locking_key).or_default()) + } + + fn build_obfuscated_key( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> String { let obfuscated_key = self.key_obfuscator.obfuscate(key); if primary_namespace.is_empty() { obfuscated_key @@ -171,10 +304,9 @@ impl VssStoreInner { &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; - let request = GetObjectRequest { - store_id: self.store_id.clone(), - key: self.build_key(primary_namespace, secondary_namespace, key), - }; + + let obfuscated_key = self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let request = GetObjectRequest { store_id: self.store_id.clone(), key: obfuscated_key }; let resp = self.client.get_object(&request).await.map_err(|e| { let msg = format!( "Failed to read from key {}/{}/{}: {}", @@ -200,55 +332,65 @@ impl VssStoreInner { } async fn write_internal( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; - let version = -1; - let storable = self.storable_builder.build(buf, version); - let request = PutObjectRequest { - store_id: self.store_id.clone(), - global_version: None, - transaction_items: vec![KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key), - version, - value: storable.encode_to_vec(), - }], - delete_items: vec![], - }; - self.client.put_object(&request).await.map_err(|e| { - let msg = format!( - "Failed to write to key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - Error::new(ErrorKind::Other, msg) - })?; + self.execute_locked_write(inner_lock_ref, locking_key, version, async move || { + let obfuscated_key = + self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let vss_version = -1; + let storable = self.storable_builder.build(buf, vss_version); + let request = PutObjectRequest { + store_id: self.store_id.clone(), + global_version: None, + transaction_items: vec![KeyValue { + key: obfuscated_key, + version: vss_version, + value: storable.encode_to_vec(), + }], + delete_items: vec![], + }; - Ok(()) + self.client.put_object(&request).await.map_err(|e| { + let msg = format!( + "Failed to write to key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + Error::new(ErrorKind::Other, msg) + })?; + + Ok(()) + }) + .await } async fn remove_internal( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; - let request = DeleteObjectRequest { - store_id: self.store_id.clone(), - key_value: Some(KeyValue { - key: self.build_key(primary_namespace, secondary_namespace, key), - version: -1, - value: vec![], - }), - }; - self.client.delete_object(&request).await.map_err(|e| { - let msg = format!( - "Failed to delete key {}/{}/{}: {}", - primary_namespace, secondary_namespace, key, e - ); - Error::new(ErrorKind::Other, msg) - })?; + self.execute_locked_write(inner_lock_ref, locking_key, version, async move || { + let obfuscated_key = + self.build_obfuscated_key(primary_namespace, secondary_namespace, key); + let request = DeleteObjectRequest { + store_id: self.store_id.clone(), + key_value: Some(KeyValue { key: obfuscated_key, version: -1, value: vec![] }), + }; - Ok(()) + self.client.delete_object(&request).await.map_err(|e| { + let msg = format!( + "Failed to delete key {}/{}/{}: {}", + primary_namespace, secondary_namespace, key, e + ); + Error::new(ErrorKind::Other, msg) + })?; + + Ok(()) + }) + .await } async fn list_internal( @@ -267,6 +409,49 @@ impl VssStoreInner { Ok(keys) } + + async fn execute_locked_write< + F: Future>, + FN: FnOnce() -> F, + >( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, callback: FN, + ) -> Result<(), lightning::io::Error> { + let res = { + let mut last_written_version = inner_lock_ref.write().await; + + // Check if we already have a newer version written/removed. This is used in async contexts to realize eventual + // consistency. + let is_stale_version = version <= *last_written_version; + + // If the version is not stale, we execute the callback. Otherwise we can and must skip writing. + if is_stale_version { + Ok(()) + } else { + callback().await.map(|_| { + *last_written_version = version; + }) + } + }; + + self.clean_locks(&inner_lock_ref, locking_key); + + res + } + + fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { + // If there no arcs in use elsewhere, this means that there are no in-flight writes. We can remove the map entry + // to prevent leaking memory. The two arcs that are expected are the one in the map and the one held here in + // inner_lock_ref. The outer lock is obtained first, to avoid a new arc being cloned after we've already + // counted. + let mut outer_lock = self.locks.lock().unwrap(); + + let strong_count = Arc::strong_count(&inner_lock_ref); + debug_assert!(strong_count >= 2, "Unexpected VssStore strong count"); + + if strong_count == 2 { + outer_lock.remove(&locking_key); + } + } } fn derive_data_encryption_and_obfuscation_keys(vss_seed: &[u8; 32]) -> ([u8; 32], [u8; 32]) { From ce731688acc24c70266349fd57e383ade5d9590b Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 7 Oct 2025 10:42:38 +0200 Subject: [PATCH 159/177] f No need to use `RwLock` if we ever only `write` it --- src/io/vss_store.rs | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index b71d4144e1..c5442d1ac0 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -20,7 +20,6 @@ use lightning::io::{self, Error, ErrorKind}; use lightning::util::persist::{KVStore, KVStoreSync}; use prost::Message; use rand::RngCore; -use tokio::sync::RwLock; use vss_client::client::VssClient; use vss_client::error::VssError; use vss_client::headers::VssHeaderProvider; @@ -75,7 +74,9 @@ impl VssStore { } } - fn get_new_version_and_lock_ref(&self, locking_key: String) -> (Arc>, u64) { + fn get_new_version_and_lock_ref( + &self, locking_key: String, + ) -> (Arc>, u64) { let version = self.next_version.fetch_add(1, Ordering::Relaxed); if version == u64::MAX { panic!("VssStore version counter overflowed"); @@ -212,7 +213,7 @@ struct VssStoreInner { key_obfuscator: KeyObfuscator, // Per-key locks that ensures that we don't have concurrent writes to the same namespace/key. // The lock also encapsulates the latest written version per key. - locks: Mutex>>>, + locks: Mutex>>>, } impl VssStoreInner { @@ -242,7 +243,7 @@ impl VssStoreInner { Self { client, store_id, storable_builder, key_obfuscator, locks } } - fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { + fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { let mut outer_lock = self.locks.lock().unwrap(); Arc::clone(&outer_lock.entry(locking_key).or_default()) } @@ -332,7 +333,7 @@ impl VssStoreInner { } async fn write_internal( - &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; @@ -367,7 +368,7 @@ impl VssStoreInner { } async fn remove_internal( - &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; @@ -414,10 +415,11 @@ impl VssStoreInner { F: Future>, FN: FnOnce() -> F, >( - &self, inner_lock_ref: Arc>, locking_key: String, version: u64, callback: FN, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + callback: FN, ) -> Result<(), lightning::io::Error> { let res = { - let mut last_written_version = inner_lock_ref.write().await; + let mut last_written_version = inner_lock_ref.lock().await; // Check if we already have a newer version written/removed. This is used in async contexts to realize eventual // consistency. @@ -438,7 +440,7 @@ impl VssStoreInner { res } - fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { + fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { // If there no arcs in use elsewhere, this means that there are no in-flight writes. We can remove the map entry // to prevent leaking memory. The two arcs that are expected are the one in the map and the one held here in // inner_lock_ref. The outer lock is obtained first, to avoid a new arc being cloned after we've already From 76c75fe590495f9ac62fd6196df3402d0d168e5a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 11:00:04 +0200 Subject: [PATCH 160/177] Move `SqliteStore` logic to `_internal` methods .. to be easier reusable via `KVStore` also --- src/io/sqlite_store/mod.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index d18c7440d4..582dd831b9 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -126,10 +126,8 @@ impl SqliteStore { pub fn get_data_dir(&self) -> PathBuf { self.data_dir.clone() } -} -impl KVStoreSync for SqliteStore { - fn read( + fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "read")?; @@ -177,7 +175,7 @@ impl KVStoreSync for SqliteStore { Ok(res) } - fn write( + fn write_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; @@ -213,7 +211,7 @@ impl KVStoreSync for SqliteStore { }) } - fn remove( + fn remove_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; @@ -245,7 +243,9 @@ impl KVStoreSync for SqliteStore { Ok(()) } - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + fn list_internal( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> io::Result> { check_namespace_key_validity(primary_namespace, secondary_namespace, None, "list")?; let locked_conn = self.connection.lock().unwrap(); @@ -285,6 +285,30 @@ impl KVStoreSync for SqliteStore { } } +impl KVStoreSync for SqliteStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + self.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + self.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> io::Result<()> { + self.remove_internal(primary_namespace, secondary_namespace, key, lazy) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + self.list_internal(primary_namespace, secondary_namespace) + } +} + #[cfg(test)] mod tests { use super::*; From c0473cc98da39df40954242a543277578d8cc6b5 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 11:18:42 +0200 Subject: [PATCH 161/177] Split `SqliteStore` into `SqliteStore` and `SqliteStoreInner` .. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily. --- src/io/sqlite_store/mod.rs | 79 ++++++++++++++++++++++---------------- 1 file changed, 46 insertions(+), 33 deletions(-) diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index 582dd831b9..2ab5c11a65 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -37,9 +37,7 @@ const SCHEMA_USER_VERSION: u16 = 2; /// /// [SQLite]: https://sqlite.org pub struct SqliteStore { - connection: Arc>, - data_dir: PathBuf, - kv_table_name: String, + inner: Arc, } impl SqliteStore { @@ -51,6 +49,50 @@ impl SqliteStore { /// Similarly, the given `kv_table_name` will be used or default to [`DEFAULT_KV_TABLE_NAME`]. pub fn new( data_dir: PathBuf, db_file_name: Option, kv_table_name: Option, + ) -> io::Result { + let inner = Arc::new(SqliteStoreInner::new(data_dir, db_file_name, kv_table_name)?); + Ok(Self { inner }) + } + + /// Returns the data directory. + pub fn get_data_dir(&self) -> PathBuf { + self.inner.data_dir.clone() + } +} + +impl KVStoreSync for SqliteStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> io::Result> { + self.inner.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> io::Result<()> { + self.inner.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> io::Result<()> { + self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy) + } + + fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { + self.inner.list_internal(primary_namespace, secondary_namespace) + } +} + +struct SqliteStoreInner { + connection: Arc>, + data_dir: PathBuf, + kv_table_name: String, +} + +impl SqliteStoreInner { + fn new( + data_dir: PathBuf, db_file_name: Option, kv_table_name: Option, ) -> io::Result { let db_file_name = db_file_name.unwrap_or(DEFAULT_SQLITE_DB_FILE_NAME.to_string()); let kv_table_name = kv_table_name.unwrap_or(DEFAULT_KV_TABLE_NAME.to_string()); @@ -122,11 +164,6 @@ impl SqliteStore { Ok(Self { connection, data_dir, kv_table_name }) } - /// Returns the data directory. - pub fn get_data_dir(&self) -> PathBuf { - self.data_dir.clone() - } - fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result> { @@ -285,30 +322,6 @@ impl SqliteStore { } } -impl KVStoreSync for SqliteStore { - fn read( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> io::Result> { - self.read_internal(primary_namespace, secondary_namespace, key) - } - - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, - ) -> io::Result<()> { - self.write_internal(primary_namespace, secondary_namespace, key, buf) - } - - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, - ) -> io::Result<()> { - self.remove_internal(primary_namespace, secondary_namespace, key, lazy) - } - - fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { - self.list_internal(primary_namespace, secondary_namespace) - } -} - #[cfg(test)] mod tests { use super::*; @@ -318,7 +331,7 @@ mod tests { impl Drop for SqliteStore { fn drop(&mut self) { - match fs::remove_dir_all(&self.data_dir) { + match fs::remove_dir_all(&self.inner.data_dir) { Err(e) => println!("Failed to remove test store directory: {}", e), _ => {}, } From 5c09a31d9d2c00529110537182222063753ea596 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 7 Oct 2025 11:13:06 +0200 Subject: [PATCH 162/177] Bump BDK to 2.2, rust-bitcoin to 0.32.7 --- Cargo.toml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b639b7dc1c..30864e9176 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -79,12 +79,12 @@ lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} bdk_electrum = { version = "0.23.0", default-features = false, features = ["use-rustls-ring"]} -bdk_wallet = { version = "2.0.0", default-features = false, features = ["std", "keys-bip39"]} +bdk_wallet = { version = "2.2.0", default-features = false, features = ["std", "keys-bip39"]} reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] } rustls = { version = "0.23", default-features = false } rusqlite = { version = "0.31.0", features = ["bundled"] } -bitcoin = "0.32.4" +bitcoin = "0.32.7" bip39 = "2.0.0" bip21 = { version = "0.5", features = ["std"], default-features = false } From 496552b108340e9263d9ebed220826b5b00ef41a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 12 Sep 2025 11:42:47 +0200 Subject: [PATCH 163/177] Implement `KVStore` for `SqliteStore` --- src/io/sqlite_store/mod.rs | 301 ++++++++++++++++++++++++++++++------- 1 file changed, 248 insertions(+), 53 deletions(-) diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index 2ab5c11a65..6ba41f714c 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -6,12 +6,17 @@ // accordance with one or both of these licenses. //! Objects related to [`SqliteStore`] live here. +use std::boxed::Box; +use std::collections::HashMap; use std::fs; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; +use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::{Arc, Mutex}; use lightning::io; -use lightning::util::persist::KVStoreSync; +use lightning::util::persist::{KVStore, KVStoreSync}; use lightning_types::string::PrintableString; use rusqlite::{named_params, Connection}; @@ -38,6 +43,10 @@ const SCHEMA_USER_VERSION: u16 = 2; /// [SQLite]: https://sqlite.org pub struct SqliteStore { inner: Arc, + + // Version counter to ensure that writes are applied in the correct order. It is assumed that read and list + // operations aren't sensitive to the order of execution. + next_write_version: AtomicU64, } impl SqliteStore { @@ -51,7 +60,27 @@ impl SqliteStore { data_dir: PathBuf, db_file_name: Option, kv_table_name: Option, ) -> io::Result { let inner = Arc::new(SqliteStoreInner::new(data_dir, db_file_name, kv_table_name)?); - Ok(Self { inner }) + let next_write_version = AtomicU64::new(1); + Ok(Self { inner, next_write_version }) + } + + fn build_locking_key( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> String { + format!("{}#{}#{}", primary_namespace, secondary_namespace, key) + } + + fn get_new_version_and_lock_ref(&self, locking_key: String) -> (Arc>, u64) { + let version = self.next_write_version.fetch_add(1, Ordering::Relaxed); + if version == u64::MAX { + panic!("SqliteStore version counter overflowed"); + } + + // Get a reference to the inner lock. We do this early so that the arc can double as an in-flight counter for + // cleaning up unused locks. + let inner_lock_ref = self.inner.get_inner_lock_ref(locking_key); + + (inner_lock_ref, version) } /// Returns the data directory. @@ -60,6 +89,99 @@ impl SqliteStore { } } +impl KVStore for SqliteStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.read_internal(&primary_namespace, &secondary_namespace, &key) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.write_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + buf, + ) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Pin> + Send>> { + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.remove_internal( + inner_lock_ref, + locking_key, + version, + &primary_namespace, + &secondary_namespace, + &key, + lazy, + ) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.list_internal(&primary_namespace, &secondary_namespace) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } +} + impl KVStoreSync for SqliteStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, @@ -70,13 +192,33 @@ impl KVStoreSync for SqliteStore { fn write( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { - self.inner.write_internal(primary_namespace, secondary_namespace, key, buf) + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + self.inner.write_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + buf, + ) } fn remove( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, ) -> io::Result<()> { - self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy) + let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); + let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); + self.inner.remove_internal( + inner_lock_ref, + locking_key, + version, + primary_namespace, + secondary_namespace, + key, + lazy, + ) } fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { @@ -88,6 +230,7 @@ struct SqliteStoreInner { connection: Arc>, data_dir: PathBuf, kv_table_name: String, + write_version_locks: Mutex>>>, } impl SqliteStoreInner { @@ -161,7 +304,13 @@ impl SqliteStoreInner { })?; let connection = Arc::new(Mutex::new(connection)); - Ok(Self { connection, data_dir, kv_table_name }) + let write_version_locks = Mutex::new(HashMap::new()); + Ok(Self { connection, data_dir, kv_table_name, write_version_locks }) + } + + fn get_inner_lock_ref(&self, locking_key: String) -> Arc> { + let mut outer_lock = self.write_version_locks.lock().unwrap(); + Arc::clone(&outer_lock.entry(locking_key).or_default()) } fn read_internal( @@ -213,71 +362,77 @@ impl SqliteStoreInner { } fn write_internal( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "write")?; - let locked_conn = self.connection.lock().unwrap(); + self.execute_locked_write(inner_lock_ref, locking_key, version, || { + let locked_conn = self.connection.lock().unwrap(); - let sql = format!( - "INSERT OR REPLACE INTO {} (primary_namespace, secondary_namespace, key, value) VALUES (:primary_namespace, :secondary_namespace, :key, :value);", - self.kv_table_name - ); + let sql = format!( + "INSERT OR REPLACE INTO {} (primary_namespace, secondary_namespace, key, value) VALUES (:primary_namespace, :secondary_namespace, :key, :value);", + self.kv_table_name + ); - let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { - let msg = format!("Failed to prepare statement: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - })?; + let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { + let msg = format!("Failed to prepare statement: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; - stmt.execute(named_params! { - ":primary_namespace": primary_namespace, - ":secondary_namespace": secondary_namespace, - ":key": key, - ":value": buf, - }) - .map(|_| ()) - .map_err(|e| { - let msg = format!( - "Failed to write to key {}/{}/{}: {}", - PrintableString(primary_namespace), - PrintableString(secondary_namespace), - PrintableString(key), - e - ); - io::Error::new(io::ErrorKind::Other, msg) + stmt.execute(named_params! { + ":primary_namespace": primary_namespace, + ":secondary_namespace": secondary_namespace, + ":key": key, + ":value": buf, + }) + .map(|_| ()) + .map_err(|e| { + let msg = format!( + "Failed to write to key {}/{}/{}: {}", + PrintableString(primary_namespace), + PrintableString(secondary_namespace), + PrintableString(key), + e + ); + io::Error::new(io::ErrorKind::Other, msg) + }) }) } fn remove_internal( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, + primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; - let locked_conn = self.connection.lock().unwrap(); + self.execute_locked_write(inner_lock_ref, locking_key, version, || { + let locked_conn = self.connection.lock().unwrap(); - let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name); + let sql = format!("DELETE FROM {} WHERE primary_namespace=:primary_namespace AND secondary_namespace=:secondary_namespace AND key=:key;", self.kv_table_name); - let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { - let msg = format!("Failed to prepare statement: {}", e); - io::Error::new(io::ErrorKind::Other, msg) - })?; + let mut stmt = locked_conn.prepare_cached(&sql).map_err(|e| { + let msg = format!("Failed to prepare statement: {}", e); + io::Error::new(io::ErrorKind::Other, msg) + })?; - stmt.execute(named_params! { - ":primary_namespace": primary_namespace, - ":secondary_namespace": secondary_namespace, - ":key": key, + stmt.execute(named_params! { + ":primary_namespace": primary_namespace, + ":secondary_namespace": secondary_namespace, + ":key": key, + }) + .map_err(|e| { + let msg = format!( + "Failed to delete key {}/{}/{}: {}", + PrintableString(primary_namespace), + PrintableString(secondary_namespace), + PrintableString(key), + e + ); + io::Error::new(io::ErrorKind::Other, msg) + })?; + Ok(()) }) - .map_err(|e| { - let msg = format!( - "Failed to delete key {}/{}/{}: {}", - PrintableString(primary_namespace), - PrintableString(secondary_namespace), - PrintableString(key), - e - ); - io::Error::new(io::ErrorKind::Other, msg) - })?; - Ok(()) } fn list_internal( @@ -320,6 +475,46 @@ impl SqliteStoreInner { Ok(keys) } + + fn execute_locked_write Result<(), lightning::io::Error>>( + &self, inner_lock_ref: Arc>, locking_key: String, version: u64, callback: F, + ) -> Result<(), lightning::io::Error> { + let res = { + let mut last_written_version = inner_lock_ref.lock().unwrap(); + + // Check if we already have a newer version written/removed. This is used in async contexts to realize eventual + // consistency. + let is_stale_version = version <= *last_written_version; + + // If the version is not stale, we execute the callback. Otherwise we can and must skip writing. + if is_stale_version { + Ok(()) + } else { + callback().map(|_| { + *last_written_version = version; + }) + } + }; + + self.clean_locks(&inner_lock_ref, locking_key); + + res + } + + fn clean_locks(&self, inner_lock_ref: &Arc>, locking_key: String) { + // If there no arcs in use elsewhere, this means that there are no in-flight writes. We can remove the map entry + // to prevent leaking memory. The two arcs that are expected are the one in the map and the one held here in + // inner_lock_ref. The outer lock is obtained first, to avoid a new arc being cloned after we've already + // counted. + let mut outer_lock = self.write_version_locks.lock().unwrap(); + + let strong_count = Arc::strong_count(&inner_lock_ref); + debug_assert!(strong_count >= 2, "Unexpected SqliteStore strong count"); + + if strong_count == 2 { + outer_lock.remove(&locking_key); + } + } } #[cfg(test)] From f535b50e97da73e39f1f19b90b3a28228cdb9e9d Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 18 Sep 2025 11:13:46 +0200 Subject: [PATCH 164/177] Move `TestStoreSync` logic to `_internal` methods .. to be easier reusable via `KVStore` also --- tests/common/mod.rs | 36 ++++++++++++++++++++++++++++++------ 1 file changed, 30 insertions(+), 6 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 1331fc0477..3e1cf899f7 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1242,10 +1242,8 @@ impl TestSyncStore { }, } } -} -impl KVStoreSync for TestSyncStore { - fn read( + fn read_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result> { let _guard = self.serializer.read().unwrap(); @@ -1270,7 +1268,7 @@ impl KVStoreSync for TestSyncStore { } } - fn write( + fn write_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); @@ -1299,7 +1297,7 @@ impl KVStoreSync for TestSyncStore { } } - fn remove( + fn remove_internal( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); @@ -1327,10 +1325,36 @@ impl KVStoreSync for TestSyncStore { } } - fn list( + fn list_internal( &self, primary_namespace: &str, secondary_namespace: &str, ) -> lightning::io::Result> { let _guard = self.serializer.read().unwrap(); self.do_list(primary_namespace, secondary_namespace) } } + +impl KVStoreSync for TestSyncStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result> { + self.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> lightning::io::Result<()> { + self.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> lightning::io::Result<()> { + self.remove_internal(primary_namespace, secondary_namespace, key, lazy) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result> { + self.list_internal(primary_namespace, secondary_namespace) + } +} From b2fca59f65dbc9af56275fa756b013a1aa27c9e9 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 18 Sep 2025 11:16:36 +0200 Subject: [PATCH 165/177] Split `TestSyncStore` into `TestSyncStore` and `TestSyncStoreInner` .. where the former holds the latter in an `Arc` that can be used in async/`Future` contexts more easily. --- tests/common/mod.rs | 67 ++++++++++++++++++++++++++------------------- 1 file changed, 39 insertions(+), 28 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3e1cf899f7..3e72a3df48 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1190,14 +1190,51 @@ pub(crate) fn do_channel_full_cycle( // A `KVStore` impl for testing purposes that wraps all our `KVStore`s and asserts their synchronicity. pub(crate) struct TestSyncStore { + inner: Arc, +} + +impl TestSyncStore { + pub(crate) fn new(dest_dir: PathBuf) -> Self { + let inner = Arc::new(TestSyncStoreInner::new(dest_dir)); + Self { inner } + } +} + +impl KVStoreSync for TestSyncStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result> { + self.inner.read_internal(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> lightning::io::Result<()> { + self.inner.write_internal(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> lightning::io::Result<()> { + self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result> { + self.inner.list_internal(primary_namespace, secondary_namespace) + } +} + +struct TestSyncStoreInner { serializer: RwLock<()>, test_store: TestStore, fs_store: FilesystemStore, sqlite_store: SqliteStore, } -impl TestSyncStore { - pub(crate) fn new(dest_dir: PathBuf) -> Self { +impl TestSyncStoreInner { + fn new(dest_dir: PathBuf) -> Self { let serializer = RwLock::new(()); let mut fs_dir = dest_dir.clone(); fs_dir.push("fs_store"); @@ -1332,29 +1369,3 @@ impl TestSyncStore { self.do_list(primary_namespace, secondary_namespace) } } - -impl KVStoreSync for TestSyncStore { - fn read( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, - ) -> lightning::io::Result> { - self.read_internal(primary_namespace, secondary_namespace, key) - } - - fn write( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, - ) -> lightning::io::Result<()> { - self.write_internal(primary_namespace, secondary_namespace, key, buf) - } - - fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, - ) -> lightning::io::Result<()> { - self.remove_internal(primary_namespace, secondary_namespace, key, lazy) - } - - fn list( - &self, primary_namespace: &str, secondary_namespace: &str, - ) -> lightning::io::Result> { - self.list_internal(primary_namespace, secondary_namespace) - } -} From 8aa68ff00605828570c11404936b818625ec41ea Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 18 Sep 2025 11:18:54 +0200 Subject: [PATCH 166/177] Implement `KVStore` for `TestSyncStore` --- tests/common/mod.rs | 137 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 121 insertions(+), 16 deletions(-) diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 3e72a3df48..817d0edc55 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -10,9 +10,12 @@ pub(crate) mod logging; +use std::boxed::Box; use std::collections::{HashMap, HashSet}; use std::env; +use std::future::Future; use std::path::PathBuf; +use std::pin::Pin; use std::sync::{Arc, RwLock}; use std::time::Duration; @@ -31,9 +34,10 @@ use ldk_node::payment::{PaymentDirection, PaymentKind, PaymentStatus}; use ldk_node::{ Builder, CustomTlvRecord, Event, LightningBalance, Node, NodeError, PendingSweepBalance, }; +use lightning::io; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; -use lightning::util::persist::KVStoreSync; +use lightning::util::persist::{KVStore, KVStoreSync}; use lightning::util::test_utils::TestStore; use lightning_invoice::{Bolt11InvoiceDescription, Description}; use lightning_persister::fs_store::FilesystemStore; @@ -1200,6 +1204,76 @@ impl TestSyncStore { } } +impl KVStore for TestSyncStore { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.read_internal(&primary_namespace, &secondary_namespace, &key) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, + ) -> Pin> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.write_internal(&primary_namespace, &secondary_namespace, &key, buf) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> Pin> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let key = key.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.remove_internal(&primary_namespace, &secondary_namespace, &key, lazy) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> Pin, io::Error>> + Send>> { + let primary_namespace = primary_namespace.to_string(); + let secondary_namespace = secondary_namespace.to_string(); + let inner = Arc::clone(&self.inner); + let fut = tokio::task::spawn_blocking(move || { + inner.list_internal(&primary_namespace, &secondary_namespace) + }); + Box::pin(async move { + fut.await.unwrap_or_else(|e| { + let msg = format!("Failed to IO operation due join error: {}", e); + Err(io::Error::new(io::ErrorKind::Other, msg)) + }) + }) + } +} + impl KVStoreSync for TestSyncStore { fn read( &self, primary_namespace: &str, secondary_namespace: &str, key: &str, @@ -1254,9 +1328,10 @@ impl TestSyncStoreInner { fn do_list( &self, primary_namespace: &str, secondary_namespace: &str, ) -> lightning::io::Result> { - let fs_res = self.fs_store.list(primary_namespace, secondary_namespace); - let sqlite_res = self.sqlite_store.list(primary_namespace, secondary_namespace); - let test_res = self.test_store.list(primary_namespace, secondary_namespace); + let fs_res = KVStoreSync::list(&self.fs_store, primary_namespace, secondary_namespace); + let sqlite_res = + KVStoreSync::list(&self.sqlite_store, primary_namespace, secondary_namespace); + let test_res = KVStoreSync::list(&self.test_store, primary_namespace, secondary_namespace); match fs_res { Ok(mut list) => { @@ -1285,9 +1360,11 @@ impl TestSyncStoreInner { ) -> lightning::io::Result> { let _guard = self.serializer.read().unwrap(); - let fs_res = self.fs_store.read(primary_namespace, secondary_namespace, key); - let sqlite_res = self.sqlite_store.read(primary_namespace, secondary_namespace, key); - let test_res = self.test_store.read(primary_namespace, secondary_namespace, key); + let fs_res = KVStoreSync::read(&self.fs_store, primary_namespace, secondary_namespace, key); + let sqlite_res = + KVStoreSync::read(&self.sqlite_store, primary_namespace, secondary_namespace, key); + let test_res = + KVStoreSync::read(&self.test_store, primary_namespace, secondary_namespace, key); match fs_res { Ok(read) => { @@ -1309,11 +1386,27 @@ impl TestSyncStoreInner { &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: Vec, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); - let fs_res = self.fs_store.write(primary_namespace, secondary_namespace, key, buf.clone()); - let sqlite_res = - self.sqlite_store.write(primary_namespace, secondary_namespace, key, buf.clone()); - let test_res = - self.test_store.write(primary_namespace, secondary_namespace, key, buf.clone()); + let fs_res = KVStoreSync::write( + &self.fs_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); + let sqlite_res = KVStoreSync::write( + &self.sqlite_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); + let test_res = KVStoreSync::write( + &self.test_store, + primary_namespace, + secondary_namespace, + key, + buf.clone(), + ); assert!(self .do_list(primary_namespace, secondary_namespace) @@ -1338,10 +1431,22 @@ impl TestSyncStoreInner { &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); - let fs_res = self.fs_store.remove(primary_namespace, secondary_namespace, key, lazy); - let sqlite_res = - self.sqlite_store.remove(primary_namespace, secondary_namespace, key, lazy); - let test_res = self.test_store.remove(primary_namespace, secondary_namespace, key, lazy); + let fs_res = + KVStoreSync::remove(&self.fs_store, primary_namespace, secondary_namespace, key, lazy); + let sqlite_res = KVStoreSync::remove( + &self.sqlite_store, + primary_namespace, + secondary_namespace, + key, + lazy, + ); + let test_res = KVStoreSync::remove( + &self.test_store, + primary_namespace, + secondary_namespace, + key, + lazy, + ); assert!(!self .do_list(primary_namespace, secondary_namespace) From c9e3f7150718dbd5e6bd13b702cfb85f30b47f94 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 17 Sep 2025 12:57:25 +0200 Subject: [PATCH 167/177] Require both types of `KVStore` As an intermediary step, we require any store to implement both `KVStore` and `KVStoreSync`, allowing us to switch over step-by-step. We already switch to the fully-async background processor variant here. --- Cargo.toml | 8 +- src/builder.rs | 7 +- src/data_store.rs | 49 ++++--- src/event.rs | 45 +++--- src/io/utils.rs | 128 ++++++++++-------- src/lib.rs | 41 +++--- .../asynchronous/static_invoice_store.rs | 59 ++++---- src/peer_store.rs | 59 ++++---- src/types.rs | 18 ++- tests/integration_tests_rust.rs | 5 +- 10 files changed, 236 insertions(+), 183 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b639b7dc1c..7385b5c466 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,7 +32,7 @@ default = [] #lightning-types = { version = "0.2.0" } #lightning-invoice = { version = "0.33.0", features = ["std"] } #lightning-net-tokio = { version = "0.1.0" } -#lightning-persister = { version = "0.1.0" } +#lightning-persister = { version = "0.1.0", features = ["tokio"] } #lightning-background-processor = { version = "0.1.0" } #lightning-rapid-gossip-sync = { version = "0.1.0" } #lightning-block-sync = { version = "0.1.0", features = ["rest-client", "rpc-client", "tokio"] } @@ -44,7 +44,7 @@ default = [] #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } #lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["tokio"] } #lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["rest-client", "rpc-client", "tokio"] } @@ -56,7 +56,7 @@ lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = " lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std"] } lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["tokio"] } lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["rest-client", "rpc-client", "tokio"] } @@ -68,7 +68,7 @@ lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", #lightning-types = { path = "../rust-lightning/lightning-types" } #lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } #lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister" } +#lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } #lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } #lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } #lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } diff --git a/src/builder.rs b/src/builder.rs index 0b3ea3101a..3396a52a03 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -31,7 +31,7 @@ use lightning::routing::scoring::{ }; use lightning::sign::{EntropySource, NodeSigner}; use lightning::util::persist::{ - read_channel_monitors, CHANNEL_MANAGER_PERSISTENCE_KEY, + read_channel_monitors, KVStoreSync, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::ReadableArgs; @@ -1419,7 +1419,8 @@ fn build_with_store_internal( // Initialize the ChannelManager let channel_manager = { - if let Ok(res) = kv_store.read( + if let Ok(res) = KVStoreSync::read( + &*kv_store, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, CHANNEL_MANAGER_PERSISTENCE_KEY, @@ -1657,7 +1658,7 @@ fn build_with_store_internal( Ok(output_sweeper) => Arc::new(output_sweeper), Err(e) => { if e.kind() == std::io::ErrorKind::NotFound { - Arc::new(OutputSweeper::new_with_kv_store_sync( + Arc::new(OutputSweeper::new( channel_manager.current_best_block(), Arc::clone(&tx_broadcaster), Arc::clone(&fee_estimator), diff --git a/src/data_store.rs b/src/data_store.rs index f9dbaa788f..83cbf4476b 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -9,6 +9,7 @@ use std::collections::{hash_map, HashMap}; use std::ops::Deref; use std::sync::{Arc, Mutex}; +use lightning::util::persist::KVStoreSync; use lightning::util::ser::{Readable, Writeable}; use crate::logger::{log_error, LdkLogger}; @@ -97,19 +98,24 @@ where let removed = self.objects.lock().unwrap().remove(id).is_some(); if removed { let store_key = id.encode_to_hex_str(); - self.kv_store - .remove(&self.primary_namespace, &self.secondary_namespace, &store_key, false) - .map_err(|e| { - log_error!( - self.logger, - "Removing object data for key {}/{}/{} failed due to: {}", - &self.primary_namespace, - &self.secondary_namespace, - store_key, - e - ); - Error::PersistenceFailed - })?; + KVStoreSync::remove( + &*self.kv_store, + &self.primary_namespace, + &self.secondary_namespace, + &store_key, + false, + ) + .map_err(|e| { + log_error!( + self.logger, + "Removing object data for key {}/{}/{} failed due to: {}", + &self.primary_namespace, + &self.secondary_namespace, + store_key, + e + ); + Error::PersistenceFailed + })?; } Ok(()) } @@ -141,9 +147,14 @@ where fn persist(&self, object: &SO) -> Result<(), Error> { let store_key = object.id().encode_to_hex_str(); let data = object.encode(); - self.kv_store - .write(&self.primary_namespace, &self.secondary_namespace, &store_key, data) - .map_err(|e| { + KVStoreSync::write( + &*self.kv_store, + &self.primary_namespace, + &self.secondary_namespace, + &store_key, + data, + ) + .map_err(|e| { log_error!( self.logger, "Write for key {}/{}/{} failed due to: {}", @@ -241,13 +252,15 @@ mod tests { let store_key = id.encode_to_hex_str(); // Check we start empty. - assert!(store.read(&primary_namespace, &secondary_namespace, &store_key).is_err()); + assert!(KVStoreSync::read(&*store, &primary_namespace, &secondary_namespace, &store_key) + .is_err()); // Check we successfully store an object and return `false` let object = TestObject { id, data: [23u8; 3] }; assert_eq!(Ok(false), data_store.insert(object.clone())); assert_eq!(Some(object), data_store.get(&id)); - assert!(store.read(&primary_namespace, &secondary_namespace, &store_key).is_ok()); + assert!(KVStoreSync::read(&*store, &primary_namespace, &secondary_namespace, &store_key) + .is_ok()); // Test re-insertion returns `true` let mut override_object = object.clone(); diff --git a/src/event.rs b/src/event.rs index 1236c7cf2e..824cba6949 100644 --- a/src/event.rs +++ b/src/event.rs @@ -26,6 +26,7 @@ use lightning::util::config::{ ChannelConfigOverrides, ChannelConfigUpdate, ChannelHandshakeConfigUpdate, }; use lightning::util::errors::APIError; +use lightning::util::persist::KVStoreSync; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use lightning_liquidity::lsps2::utils::compute_opening_fee; use lightning_types::payment::{PaymentHash, PaymentPreimage}; @@ -348,24 +349,24 @@ where fn persist_queue(&self, locked_queue: &VecDeque) -> Result<(), Error> { let data = EventQueueSerWrapper(locked_queue).encode(); - self.kv_store - .write( + KVStoreSync::write( + &*self.kv_store, + EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_KEY, + data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_KEY, - data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_KEY, - e - ); - Error::PersistenceFailed - })?; + e + ); + Error::PersistenceFailed + })?; Ok(()) } } @@ -1620,13 +1621,13 @@ mod tests { } // Check we can read back what we persisted. - let persisted_bytes = store - .read( - EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, - EVENT_QUEUE_PERSISTENCE_KEY, - ) - .unwrap(); + let persisted_bytes = KVStoreSync::read( + &*store, + EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, + EVENT_QUEUE_PERSISTENCE_KEY, + ) + .unwrap(); let deser_event_queue = EventQueue::read(&mut &persisted_bytes[..], (Arc::clone(&store), logger)).unwrap(); assert_eq!(deser_event_queue.wait_next_event(), expected_event); diff --git a/src/io/utils.rs b/src/io/utils.rs index 0cc910ad76..cb3ca0847e 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -24,11 +24,12 @@ use lightning::ln::msgs::DecodeError; use lightning::routing::gossip::NetworkGraph; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringDecayParameters}; use lightning::util::persist::{ - KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, NETWORK_GRAPH_PERSISTENCE_KEY, - NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, - OUTPUT_SWEEPER_PERSISTENCE_KEY, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, - OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, - SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, + KVStoreSync, KVSTORE_NAMESPACE_KEY_ALPHABET, KVSTORE_NAMESPACE_KEY_MAX_LEN, + NETWORK_GRAPH_PERSISTENCE_KEY, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, + NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY, + OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, + SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; use lightning::util::sweep::OutputSweeper; @@ -131,7 +132,8 @@ pub(crate) fn read_network_graph( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_KEY, @@ -150,7 +152,8 @@ where L::Target: LdkLogger, { let params = ProbabilisticScoringDecayParameters::default(); - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, SCORER_PERSISTENCE_SECONDARY_NAMESPACE, SCORER_PERSISTENCE_KEY, @@ -169,7 +172,8 @@ pub(crate) fn read_event_queue( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, EVENT_QUEUE_PERSISTENCE_PRIMARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_SECONDARY_NAMESPACE, EVENT_QUEUE_PERSISTENCE_KEY, @@ -187,7 +191,8 @@ pub(crate) fn read_peer_info( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PEER_INFO_PERSISTENCE_KEY, @@ -207,11 +212,13 @@ where { let mut res = Vec::new(); - for stored_key in kv_store.list( + for stored_key in KVStoreSync::list( + &*kv_store, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, )? { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE, &stored_key, @@ -234,7 +241,8 @@ pub(crate) fn read_output_sweeper( chain_data_source: Arc, keys_manager: Arc, kv_store: Arc, logger: Arc, ) -> Result { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, OUTPUT_SWEEPER_PERSISTENCE_PRIMARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_SECONDARY_NAMESPACE, OUTPUT_SWEEPER_PERSISTENCE_KEY, @@ -248,7 +256,7 @@ pub(crate) fn read_output_sweeper( kv_store, logger.clone(), ); - OutputSweeper::read_with_kv_store_sync(&mut reader, args).map_err(|e| { + OutputSweeper::read(&mut reader, args).map_err(|e| { log_error!(logger, "Failed to deserialize OutputSweeper: {}", e); std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to deserialize OutputSweeper") }) @@ -260,7 +268,8 @@ pub(crate) fn read_node_metrics( where L::Target: LdkLogger, { - let mut reader = Cursor::new(kv_store.read( + let mut reader = Cursor::new(KVStoreSync::read( + &*kv_store, NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, NODE_METRICS_KEY, @@ -278,24 +287,24 @@ where L::Target: LdkLogger, { let data = node_metrics.encode(); - kv_store - .write( + KVStoreSync::write( + &*kv_store, + NODE_METRICS_PRIMARY_NAMESPACE, + NODE_METRICS_SECONDARY_NAMESPACE, + NODE_METRICS_KEY, + data, + ) + .map_err(|e| { + log_error!( + logger, + "Writing data to key {}/{}/{} failed due to: {}", NODE_METRICS_PRIMARY_NAMESPACE, NODE_METRICS_SECONDARY_NAMESPACE, NODE_METRICS_KEY, - data, - ) - .map_err(|e| { - log_error!( - logger, - "Writing data to key {}/{}/{} failed due to: {}", - NODE_METRICS_PRIMARY_NAMESPACE, - NODE_METRICS_SECONDARY_NAMESPACE, - NODE_METRICS_KEY, - e - ); - Error::PersistenceFailed - }) + e + ); + Error::PersistenceFailed + }) } pub(crate) fn is_valid_kvstore_str(key: &str) -> bool { @@ -397,24 +406,26 @@ macro_rules! impl_read_write_change_set_type { where L::Target: LdkLogger, { - let bytes = match kv_store.read($primary_namespace, $secondary_namespace, $key) { - Ok(bytes) => bytes, - Err(e) => { - if e.kind() == lightning::io::ErrorKind::NotFound { - return Ok(None); - } else { - log_error!( - logger, - "Reading data from key {}/{}/{} failed due to: {}", - $primary_namespace, - $secondary_namespace, - $key, - e - ); - return Err(e.into()); - } - }, - }; + let bytes = + match KVStoreSync::read(&*kv_store, $primary_namespace, $secondary_namespace, $key) + { + Ok(bytes) => bytes, + Err(e) => { + if e.kind() == lightning::io::ErrorKind::NotFound { + return Ok(None); + } else { + log_error!( + logger, + "Reading data from key {}/{}/{} failed due to: {}", + $primary_namespace, + $secondary_namespace, + $key, + e + ); + return Err(e.into()); + } + }, + }; let mut reader = Cursor::new(bytes); let res: Result, DecodeError> = @@ -438,17 +449,18 @@ macro_rules! impl_read_write_change_set_type { L::Target: LdkLogger, { let data = ChangeSetSerWrapper(value).encode(); - kv_store.write($primary_namespace, $secondary_namespace, $key, data).map_err(|e| { - log_error!( - logger, - "Writing data to key {}/{}/{} failed due to: {}", - $primary_namespace, - $secondary_namespace, - $key, - e - ); - e.into() - }) + KVStoreSync::write(&*kv_store, $primary_namespace, $secondary_namespace, $key, data) + .map_err(|e| { + log_error!( + logger, + "Writing data to key {}/{}/{} failed due to: {}", + $primary_namespace, + $secondary_namespace, + $key, + e + ); + e.into() + }) } }; } diff --git a/src/lib.rs b/src/lib.rs index a075cfac5b..c235d2a88e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -134,7 +134,8 @@ use lightning::ln::channel_state::ChannelShutdownState; use lightning::ln::channelmanager::PaymentId; use lightning::ln::msgs::SocketAddress; use lightning::routing::gossip::NodeAlias; -use lightning_background_processor::process_events_async_with_kv_store_sync; +use lightning::util::persist::KVStoreSync; +use lightning_background_processor::process_events_async; use liquidity::{LSPS1Liquidity, LiquiditySource}; use logger::{log_debug, log_error, log_info, log_trace, LdkLogger, Logger}; use payment::asynchronous::om_mailbox::OnionMessageMailbox; @@ -147,10 +148,12 @@ use peer_store::{PeerInfo, PeerStore}; use rand::Rng; use runtime::Runtime; use types::{ - Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, DynStore, Graph, - KeysManager, OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, + Broadcaster, BumpTransactionEventHandler, ChainMonitor, ChannelManager, Graph, KeysManager, + OnionMessenger, PaymentStore, PeerManager, Router, Scorer, Sweeper, Wallet, +}; +pub use types::{ + ChannelDetails, CustomTlvRecord, DynStore, PeerDetails, SyncAndAsyncKVStore, UserChannelId, }; -pub use types::{ChannelDetails, CustomTlvRecord, PeerDetails, UserChannelId}; pub use { bip39, bitcoin, lightning, lightning_invoice, lightning_liquidity, lightning_types, tokio, vss_client, @@ -562,7 +565,7 @@ impl Node { }; self.runtime.spawn_background_processor_task(async move { - process_events_async_with_kv_store_sync( + process_events_async( background_persister, |e| background_event_handler.handle_event(e), background_chain_mon, @@ -1478,20 +1481,20 @@ impl Node { /// Exports the current state of the scorer. The result can be shared with and merged by light nodes that only have /// a limited view of the network. pub fn export_pathfinding_scores(&self) -> Result, Error> { - self.kv_store - .read( - lightning::util::persist::SCORER_PERSISTENCE_PRIMARY_NAMESPACE, - lightning::util::persist::SCORER_PERSISTENCE_SECONDARY_NAMESPACE, - lightning::util::persist::SCORER_PERSISTENCE_KEY, - ) - .map_err(|e| { - log_error!( - self.logger, - "Failed to access store while exporting pathfinding scores: {}", - e - ); - Error::PersistenceFailed - }) + KVStoreSync::read( + &*self.kv_store, + lightning::util::persist::SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + lightning::util::persist::SCORER_PERSISTENCE_SECONDARY_NAMESPACE, + lightning::util::persist::SCORER_PERSISTENCE_KEY, + ) + .map_err(|e| { + log_error!( + self.logger, + "Failed to access store while exporting pathfinding scores: {}", + e + ); + Error::PersistenceFailed + }) } } diff --git a/src/payment/asynchronous/static_invoice_store.rs b/src/payment/asynchronous/static_invoice_store.rs index e81fd8216c..a7e2d2f9e7 100644 --- a/src/payment/asynchronous/static_invoice_store.rs +++ b/src/payment/asynchronous/static_invoice_store.rs @@ -15,6 +15,7 @@ use bitcoin::hashes::Hash; use lightning::blinded_path::message::BlindedMessagePath; use lightning::impl_writeable_tlv_based; use lightning::offers::static_invoice::StaticInvoice; +use lightning::util::persist::KVStoreSync; use lightning::util::ser::{Readable, Writeable}; use crate::hex_utils; @@ -77,29 +78,33 @@ impl StaticInvoiceStore { let (secondary_namespace, key) = Self::get_storage_location(invoice_slot, recipient_id); - self.kv_store - .read(STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, &secondary_namespace, &key) - .and_then(|data| { - PersistedStaticInvoice::read(&mut &*data) - .map(|persisted_invoice| { - Some((persisted_invoice.invoice, persisted_invoice.request_path)) - }) - .map_err(|e| { - lightning::io::Error::new( - lightning::io::ErrorKind::InvalidData, - format!("Failed to parse static invoice: {:?}", e), - ) - }) - }) - .or_else( - |e| { - if e.kind() == lightning::io::ErrorKind::NotFound { - Ok(None) - } else { - Err(e) - } - }, - ) + KVStoreSync::read( + &*self.kv_store, + STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, + &secondary_namespace, + &key, + ) + .and_then(|data| { + PersistedStaticInvoice::read(&mut &*data) + .map(|persisted_invoice| { + Some((persisted_invoice.invoice, persisted_invoice.request_path)) + }) + .map_err(|e| { + lightning::io::Error::new( + lightning::io::ErrorKind::InvalidData, + format!("Failed to parse static invoice: {:?}", e), + ) + }) + }) + .or_else( + |e| { + if e.kind() == lightning::io::ErrorKind::NotFound { + Ok(None) + } else { + Err(e) + } + }, + ) } pub(crate) async fn handle_persist_static_invoice( @@ -119,7 +124,13 @@ impl StaticInvoiceStore { // Static invoices will be persisted at "static_invoices//". // // Example: static_invoices/039058c6f2c0cb492c533b0a4d14ef77cc0f78abccced5287d84a1a2011cfb81/00001 - self.kv_store.write(STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, &secondary_namespace, &key, buf) + KVStoreSync::write( + &*self.kv_store, + STATIC_INVOICE_STORE_PRIMARY_NAMESPACE, + &secondary_namespace, + &key, + buf, + ) } fn get_storage_location(invoice_slot: u16, recipient_id: &[u8]) -> (String, String) { diff --git a/src/peer_store.rs b/src/peer_store.rs index 5ebdc04191..82c80c396f 100644 --- a/src/peer_store.rs +++ b/src/peer_store.rs @@ -11,6 +11,7 @@ use std::sync::{Arc, RwLock}; use bitcoin::secp256k1::PublicKey; use lightning::impl_writeable_tlv_based; +use lightning::util::persist::KVStoreSync; use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer}; use crate::io::{ @@ -67,24 +68,24 @@ where fn persist_peers(&self, locked_peers: &HashMap) -> Result<(), Error> { let data = PeerStoreSerWrapper(&*locked_peers).encode(); - self.kv_store - .write( + KVStoreSync::write( + &*self.kv_store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + data, + ) + .map_err(|e| { + log_error!( + self.logger, + "Write for key {}/{}/{} failed due to: {}", PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, PEER_INFO_PERSISTENCE_KEY, - data, - ) - .map_err(|e| { - log_error!( - self.logger, - "Write for key {}/{}/{} failed due to: {}", - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - e - ); - Error::PersistenceFailed - })?; + e + ); + Error::PersistenceFailed + })?; Ok(()) } } @@ -167,23 +168,23 @@ mod tests { .unwrap(); let address = SocketAddress::from_str("127.0.0.1:9738").unwrap(); let expected_peer_info = PeerInfo { node_id, address }; - assert!(store - .read( - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - ) - .is_err()); + assert!(KVStoreSync::read( + &*store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + ) + .is_err()); peer_store.add_peer(expected_peer_info.clone()).unwrap(); // Check we can read back what we persisted. - let persisted_bytes = store - .read( - PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, - PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, - PEER_INFO_PERSISTENCE_KEY, - ) - .unwrap(); + let persisted_bytes = KVStoreSync::read( + &*store, + PEER_INFO_PERSISTENCE_PRIMARY_NAMESPACE, + PEER_INFO_PERSISTENCE_SECONDARY_NAMESPACE, + PEER_INFO_PERSISTENCE_KEY, + ) + .unwrap(); let deser_peer_store = PeerStore::read(&mut &persisted_bytes[..], (Arc::clone(&store), logger)).unwrap(); diff --git a/src/types.rs b/src/types.rs index f152772a12..4f5229dd2e 100644 --- a/src/types.rs +++ b/src/types.rs @@ -19,7 +19,7 @@ use lightning::routing::gossip; use lightning::routing::router::DefaultRouter; use lightning::routing::scoring::{ProbabilisticScorer, ProbabilisticScoringFeeParameters}; use lightning::sign::InMemorySigner; -use lightning::util::persist::{KVStoreSync, KVStoreSyncWrapper}; +use lightning::util::persist::{KVStore, KVStoreSync}; use lightning::util::ser::{Readable, Writeable, Writer}; use lightning::util::sweep::OutputSweeper; use lightning_block_sync::gossip::{GossipVerifier, UtxoSource}; @@ -35,7 +35,19 @@ use crate::logger::Logger; use crate::message_handler::NodeCustomMessageHandler; use crate::payment::PaymentDetails; -pub(crate) type DynStore = dyn KVStoreSync + Sync + Send; +/// A supertrait that requires that a type implements both [`KVStore`] and [`KVStoreSync`] at the +/// same time. +pub trait SyncAndAsyncKVStore: KVStore + KVStoreSync {} + +impl SyncAndAsyncKVStore for T +where + T: KVStore, + T: KVStoreSync, +{ +} + +/// A type alias for [`SyncAndAsyncKVStore`] with `Sync`/`Send` markers; +pub type DynStore = dyn SyncAndAsyncKVStore + Sync + Send; pub(crate) type ChainMonitor = chainmonitor::ChainMonitor< InMemorySigner, @@ -133,7 +145,7 @@ pub(crate) type Sweeper = OutputSweeper< Arc, Arc, Arc, - KVStoreSyncWrapper>, + Arc, Arc, Arc, >; diff --git a/tests/integration_tests_rust.rs b/tests/integration_tests_rust.rs index cca52ae2de..64a78e11bf 100644 --- a/tests/integration_tests_rust.rs +++ b/tests/integration_tests_rust.rs @@ -31,11 +31,10 @@ use ldk_node::payment::{ ConfirmationStatus, PaymentDetails, PaymentDirection, PaymentKind, PaymentStatus, QrPaymentResult, }; -use ldk_node::{Builder, Event, NodeError}; +use ldk_node::{Builder, DynStore, Event, NodeError}; use lightning::ln::channelmanager::PaymentId; use lightning::routing::gossip::{NodeAlias, NodeId}; use lightning::routing::router::RouteParametersConfig; -use lightning::util::persist::KVStoreSync; use lightning_invoice::{Bolt11InvoiceDescription, Description}; use lightning_types::payment::{PaymentHash, PaymentPreimage}; use log::LevelFilter; @@ -243,7 +242,7 @@ fn start_stop_reinit() { let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap()); - let test_sync_store: Arc = + let test_sync_store: Arc = Arc::new(TestSyncStore::new(config.node_config.storage_dir_path.clone().into())); let sync_config = EsploraSyncConfig { background_sync_config: None }; From e92cadac471c7f28eb6fe0ddc170c39f497b29e0 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 8 Oct 2025 10:09:52 +0200 Subject: [PATCH 168/177] Use `#[allow(deprecated)]` to silence new deprecation warnings The `bdk_wallet` release 2.2 deprecated BDK's signer API in favor of using `bitcoin::psbt::sign`. As the best approach for switching to that API is currently not entirely clear, we intermittently allow for the use of the deprecated APIs to silence the warnings and unbreak our CI. --- src/error.rs | 2 ++ src/wallet/mod.rs | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/error.rs b/src/error.rs index ae47c5ba8c..7e9dbac20a 100644 --- a/src/error.rs +++ b/src/error.rs @@ -11,6 +11,7 @@ use bdk_chain::bitcoin::psbt::ExtractTxError as BdkExtractTxError; use bdk_chain::local_chain::CannotConnectError as BdkChainConnectionError; use bdk_chain::tx_graph::CalculateFeeError as BdkChainCalculateFeeError; use bdk_wallet::error::CreateTxError as BdkCreateTxError; +#[allow(deprecated)] use bdk_wallet::signer::SignerError as BdkSignerError; #[derive(Copy, Clone, Debug, PartialEq, Eq)] @@ -207,6 +208,7 @@ impl fmt::Display for Error { impl std::error::Error for Error {} +#[allow(deprecated)] impl From for Error { fn from(_: BdkSignerError) -> Self { Self::OnchainTxSigningFailed diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 0ce4628d40..6d79fe02f3 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -11,7 +11,9 @@ use std::str::FromStr; use std::sync::{Arc, Mutex}; use bdk_chain::spk_client::{FullScanRequest, SyncRequest}; -use bdk_wallet::{Balance, KeychainKind, PersistedWallet, SignOptions, Update}; +#[allow(deprecated)] +use bdk_wallet::SignOptions; +use bdk_wallet::{Balance, KeychainKind, PersistedWallet, Update}; use bitcoin::address::NetworkUnchecked; use bitcoin::blockdata::constants::WITNESS_SCALE_FACTOR; use bitcoin::blockdata::locktime::absolute::LockTime; @@ -222,6 +224,7 @@ impl Wallet { Ok(()) } + #[allow(deprecated)] pub(crate) fn create_funding_transaction( &self, output_script: ScriptBuf, amount: Amount, confirmation_target: ConfirmationTarget, locktime: LockTime, @@ -338,6 +341,7 @@ impl Wallet { .map_err(|_| Error::InvalidAddress) } + #[allow(deprecated)] pub(crate) fn send_to_address( &self, address: &bitcoin::Address, send_amount: OnchainSendAmount, fee_rate: Option, @@ -647,6 +651,7 @@ impl Wallet { Ok(utxos) } + #[allow(deprecated)] fn get_change_script_inner(&self) -> Result { let mut locked_wallet = self.inner.lock().unwrap(); let mut locked_persister = self.persister.lock().unwrap(); @@ -659,6 +664,7 @@ impl Wallet { Ok(address_info.address.script_pubkey()) } + #[allow(deprecated)] fn sign_psbt_inner(&self, mut psbt: Psbt) -> Result { let locked_wallet = self.inner.lock().unwrap(); From a404a94df7e0b99c45bb2c21f4aaf4799363f6e4 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 Sep 2025 09:10:47 +0200 Subject: [PATCH 169/177] Account for `LiquidityManager` persistence We recently implemented persistence for the `lightning-liquidity` service state. Here we make corresponding changes in LDK Node to have our service state persisted. --- Cargo.toml | 24 +++++----- src/builder.rs | 4 +- src/event.rs | 117 +++++++++++++++++++++++++---------------------- src/liquidity.rs | 91 ++++++++++++++++++++---------------- src/types.rs | 1 + 5 files changed, 130 insertions(+), 107 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 9829b7cb75..6e404bd92e 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -109,7 +109,7 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "3e21ba37a133977d4247e86f25df983b39326994", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/builder.rs b/src/builder.rs index 3396a52a03..0f627a2fe0 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1557,6 +1557,7 @@ fn build_with_store_internal( Arc::clone(&channel_manager), Arc::clone(&keys_manager), Arc::clone(&chain_source), + Arc::clone(&kv_store), Arc::clone(&config), Arc::clone(&logger), ); @@ -1590,7 +1591,8 @@ fn build_with_store_internal( liquidity_source_builder.lsps2_service(promise_secret, config.clone()) }); - let liquidity_source = Arc::new(liquidity_source_builder.build()); + let liquidity_source = runtime + .block_on(async move { liquidity_source_builder.build().await.map(Arc::new) })?; let custom_message_handler = Arc::new(NodeCustomMessageHandler::new_liquidity(Arc::clone(&liquidity_source))); (Some(liquidity_source), custom_message_handler) diff --git a/src/event.rs b/src/event.rs index 824cba6949..df6649e054 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1046,7 +1046,7 @@ where LdkEvent::ProbeFailed { .. } => {}, LdkEvent::HTLCHandlingFailed { failure_type, .. } => { if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_htlc_handling_failed(failure_type); + liquidity_source.handle_htlc_handling_failed(failure_type).await; } }, LdkEvent::SpendableOutputs { outputs, channel_id } => { @@ -1229,40 +1229,46 @@ where claim_from_onchain_tx, outbound_amount_forwarded_msat, } => { - let read_only_network_graph = self.network_graph.read_only(); - let nodes = read_only_network_graph.nodes(); - let channels = self.channel_manager.list_channels(); - - let node_str = |channel_id: &Option| { - channel_id - .and_then(|channel_id| channels.iter().find(|c| c.channel_id == channel_id)) - .and_then(|channel| { - nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) - }) - .map_or("private_node".to_string(), |node| { - node.announcement_info - .as_ref() - .map_or("unnamed node".to_string(), |ann| { - format!("node {}", ann.alias()) - }) - }) - }; - let channel_str = |channel_id: &Option| { - channel_id - .map(|channel_id| format!(" with channel {}", channel_id)) - .unwrap_or_default() - }; - let from_prev_str = format!( - " from {}{}", - node_str(&prev_channel_id), - channel_str(&prev_channel_id) - ); - let to_next_str = - format!(" to {}{}", node_str(&next_channel_id), channel_str(&next_channel_id)); + { + let read_only_network_graph = self.network_graph.read_only(); + let nodes = read_only_network_graph.nodes(); + let channels = self.channel_manager.list_channels(); + + let node_str = |channel_id: &Option| { + channel_id + .and_then(|channel_id| { + channels.iter().find(|c| c.channel_id == channel_id) + }) + .and_then(|channel| { + nodes.get(&NodeId::from_pubkey(&channel.counterparty.node_id)) + }) + .map_or("private_node".to_string(), |node| { + node.announcement_info + .as_ref() + .map_or("unnamed node".to_string(), |ann| { + format!("node {}", ann.alias()) + }) + }) + }; + let channel_str = |channel_id: &Option| { + channel_id + .map(|channel_id| format!(" with channel {}", channel_id)) + .unwrap_or_default() + }; + let from_prev_str = format!( + " from {}{}", + node_str(&prev_channel_id), + channel_str(&prev_channel_id) + ); + let to_next_str = format!( + " to {}{}", + node_str(&next_channel_id), + channel_str(&next_channel_id) + ); - let fee_earned = total_fee_earned_msat.unwrap_or(0); - if claim_from_onchain_tx { - log_info!( + let fee_earned = total_fee_earned_msat.unwrap_or(0); + if claim_from_onchain_tx { + log_info!( self.logger, "Forwarded payment{}{} of {}msat, earning {}msat in fees from claiming onchain.", from_prev_str, @@ -1270,19 +1276,20 @@ where outbound_amount_forwarded_msat.unwrap_or(0), fee_earned, ); - } else { - log_info!( - self.logger, - "Forwarded payment{}{} of {}msat, earning {}msat in fees.", - from_prev_str, - to_next_str, - outbound_amount_forwarded_msat.unwrap_or(0), - fee_earned, - ); + } else { + log_info!( + self.logger, + "Forwarded payment{}{} of {}msat, earning {}msat in fees.", + from_prev_str, + to_next_str, + outbound_amount_forwarded_msat.unwrap_or(0), + fee_earned, + ); + } } if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_payment_forwarded(next_channel_id); + liquidity_source.handle_payment_forwarded(next_channel_id).await; } let event = Event::PaymentForwarded { @@ -1375,11 +1382,9 @@ where ); if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_channel_ready( - user_channel_id, - &channel_id, - &counterparty_node_id, - ); + liquidity_source + .handle_channel_ready(user_channel_id, &channel_id, &counterparty_node_id) + .await; } let event = Event::ChannelReady { @@ -1428,12 +1433,14 @@ where .. } => { if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_htlc_intercepted( - requested_next_hop_scid, - intercept_id, - expected_outbound_amount_msat, - payment_hash, - ); + liquidity_source + .handle_htlc_intercepted( + requested_next_hop_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) + .await; } }, LdkEvent::InvoiceReceived { .. } => { diff --git a/src/liquidity.rs b/src/liquidity.rs index ae31f9ace0..a09848b38d 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -38,11 +38,12 @@ use lightning_types::payment::PaymentHash; use rand::Rng; use tokio::sync::oneshot; +use crate::builder::BuildError; use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet}; +use crate::types::{ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; @@ -140,6 +141,7 @@ where channel_manager: Arc, keys_manager: Arc, chain_source: Arc, + kv_store: Arc, config: Arc, logger: L, } @@ -150,7 +152,7 @@ where { pub(crate) fn new( wallet: Arc, channel_manager: Arc, keys_manager: Arc, - chain_source: Arc, config: Arc, logger: L, + chain_source: Arc, kv_store: Arc, config: Arc, logger: L, ) -> Self { let lsps1_client = None; let lsps2_client = None; @@ -163,6 +165,7 @@ where channel_manager, keys_manager, chain_source, + kv_store, config, logger, } @@ -213,7 +216,7 @@ where self } - pub(crate) fn build(self) -> LiquiditySource { + pub(crate) async fn build(self) -> Result, BuildError> { let liquidity_service_config = self.lsps2_service.as_ref().map(|s| { let lsps2_service_config = Some(s.ldk_service_config.clone()); let lsps5_service_config = None; @@ -230,17 +233,22 @@ where lsps5_client_config, }); - let liquidity_manager = Arc::new(LiquidityManager::new( - Arc::clone(&self.keys_manager), - Arc::clone(&self.keys_manager), - Arc::clone(&self.channel_manager), - Some(Arc::clone(&self.chain_source)), - None, - liquidity_service_config, - liquidity_client_config, - )); + let liquidity_manager = Arc::new( + LiquidityManager::new( + Arc::clone(&self.keys_manager), + Arc::clone(&self.keys_manager), + Arc::clone(&self.channel_manager), + Some(Arc::clone(&self.chain_source)), + None, + Arc::clone(&self.kv_store), + liquidity_service_config, + liquidity_client_config, + ) + .await + .map_err(|_| BuildError::ReadFailed)?, + ); - LiquiditySource { + Ok(LiquiditySource { lsps1_client: self.lsps1_client, lsps2_client: self.lsps2_client, lsps2_service: self.lsps2_service, @@ -251,7 +259,7 @@ where liquidity_manager, config: self.config, logger: self.logger, - } + }) } } @@ -574,14 +582,17 @@ where } } - match lsps2_service_handler.invoice_parameters_generated( - &counterparty_node_id, - request_id, - intercept_scid, - LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, - LSPS2_CLIENT_TRUSTS_LSP_MODE, - user_channel_id, - ) { + match lsps2_service_handler + .invoice_parameters_generated( + &counterparty_node_id, + request_id, + intercept_scid, + LSPS2_CHANNEL_CLTV_EXPIRY_DELTA, + LSPS2_CLIENT_TRUSTS_LSP_MODE, + user_channel_id, + ) + .await + { Ok(()) => {}, Err(e) => { log_error!( @@ -1239,15 +1250,14 @@ where }) } - pub(crate) fn handle_channel_ready( + pub(crate) async fn handle_channel_ready( &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, ) { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.channel_ready( - user_channel_id, - channel_id, - counterparty_node_id, - ) { + if let Err(e) = lsps2_service_handler + .channel_ready(user_channel_id, channel_id, counterparty_node_id) + .await + { log_error!( self.logger, "LSPS2 service failed to handle ChannelReady event: {:?}", @@ -1257,17 +1267,20 @@ where } } - pub(crate) fn handle_htlc_intercepted( + pub(crate) async fn handle_htlc_intercepted( &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, payment_hash: PaymentHash, ) { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.htlc_intercepted( - intercept_scid, - intercept_id, - expected_outbound_amount_msat, - payment_hash, - ) { + if let Err(e) = lsps2_service_handler + .htlc_intercepted( + intercept_scid, + intercept_id, + expected_outbound_amount_msat, + payment_hash, + ) + .await + { log_error!( self.logger, "LSPS2 service failed to handle HTLCIntercepted event: {:?}", @@ -1277,9 +1290,9 @@ where } } - pub(crate) fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { + pub(crate) async fn handle_htlc_handling_failed(&self, failure_type: HTLCHandlingFailureType) { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type) { + if let Err(e) = lsps2_service_handler.htlc_handling_failed(failure_type).await { log_error!( self.logger, "LSPS2 service failed to handle HTLCHandlingFailed event: {:?}", @@ -1289,10 +1302,10 @@ where } } - pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option) { + pub(crate) async fn handle_payment_forwarded(&self, next_channel_id: Option) { if let Some(next_channel_id) = next_channel_id { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id) { + if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id).await { log_error!( self.logger, "LSPS2 service failed to handle PaymentForwarded: {:?}", diff --git a/src/types.rs b/src/types.rs index 4f5229dd2e..ccfde2766a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -75,6 +75,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< Arc, Arc, Arc, + Arc, Arc, >; From 351a0a91dfe0dc8816527cf5b3d9aab994ac243a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 Sep 2025 16:19:55 +0200 Subject: [PATCH 170/177] Account for dropped `Arc` for `DefaultTimeProvider` --- src/types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.rs b/src/types.rs index ccfde2766a..252efd042a 100644 --- a/src/types.rs +++ b/src/types.rs @@ -76,7 +76,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< Arc, Arc, Arc, - Arc, + DefaultTimeProvider, >; pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< From 9977903132e56afa78804b2e3de1bee29a9b410a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 25 Sep 2025 16:20:22 +0200 Subject: [PATCH 171/177] Account for `lazy` being dropped from `KVStore::remove` --- Cargo.toml | 50 +++++++++++++++++++------------------- src/data_store.rs | 1 - src/io/sqlite_store/mod.rs | 8 +++--- src/io/test_utils.rs | 4 +-- src/io/vss_store.rs | 8 +++--- tests/common/mod.rs | 30 ++++++++--------------- 6 files changed, 43 insertions(+), 58 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6e404bd92e..f684c68b30 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,29 +52,29 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } - -#lightning = { path = "../rust-lightning/lightning", features = ["std"] } -#lightning-types = { path = "../rust-lightning/lightning-types" } -#lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } -#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -#lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } -#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } -#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } -#lightning-macros = { path = "../rust-lightning/lightning-macros" } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } +#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } +#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["tokio"] } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } +#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } + +lightning = { path = "../rust-lightning/lightning", features = ["std"] } +lightning-types = { path = "../rust-lightning/lightning-types" } +lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } +lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } +lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } +lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } +lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } +lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } +lightning-macros = { path = "../rust-lightning/lightning-macros" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -109,8 +109,8 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std", "_test_utils"] } -#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std", "_test_utils"] } +lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/data_store.rs b/src/data_store.rs index 83cbf4476b..ce4b294e0a 100644 --- a/src/data_store.rs +++ b/src/data_store.rs @@ -103,7 +103,6 @@ where &self.primary_namespace, &self.secondary_namespace, &store_key, - false, ) .map_err(|e| { log_error!( diff --git a/src/io/sqlite_store/mod.rs b/src/io/sqlite_store/mod.rs index 6ba41f714c..c41df8ea04 100644 --- a/src/io/sqlite_store/mod.rs +++ b/src/io/sqlite_store/mod.rs @@ -137,7 +137,7 @@ impl KVStore for SqliteStore { } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> Pin> + Send>> { let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); @@ -153,7 +153,6 @@ impl KVStore for SqliteStore { &primary_namespace, &secondary_namespace, &key, - lazy, ) }); Box::pin(async move { @@ -206,7 +205,7 @@ impl KVStoreSync for SqliteStore { } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); @@ -217,7 +216,6 @@ impl KVStoreSync for SqliteStore { primary_namespace, secondary_namespace, key, - lazy, ) } @@ -402,7 +400,7 @@ impl SqliteStoreInner { fn remove_internal( &self, inner_lock_ref: Arc>, locking_key: String, version: u64, - primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; diff --git a/src/io/test_utils.rs b/src/io/test_utils.rs index 0676648510..22f1a4ea5a 100644 --- a/src/io/test_utils.rs +++ b/src/io/test_utils.rs @@ -55,7 +55,7 @@ pub(crate) fn do_read_write_remove_list_persist( let read_data = kv_store.read(primary_namespace, secondary_namespace, key).unwrap(); assert_eq!(data, &*read_data); - kv_store.remove(primary_namespace, secondary_namespace, key, false).unwrap(); + kv_store.remove(primary_namespace, secondary_namespace, key).unwrap(); let listed_keys = kv_store.list(primary_namespace, secondary_namespace).unwrap(); assert_eq!(listed_keys.len(), 0); @@ -71,7 +71,7 @@ pub(crate) fn do_read_write_remove_list_persist( let read_data = kv_store.read(&max_chars, &max_chars, &max_chars).unwrap(); assert_eq!(data, &*read_data); - kv_store.remove(&max_chars, &max_chars, &max_chars, false).unwrap(); + kv_store.remove(&max_chars, &max_chars, &max_chars).unwrap(); let listed_keys = kv_store.list(&max_chars, &max_chars).unwrap(); assert_eq!(listed_keys.len(), 0); diff --git a/src/io/vss_store.rs b/src/io/vss_store.rs index c5442d1ac0..134ff7af2f 100644 --- a/src/io/vss_store.rs +++ b/src/io/vss_store.rs @@ -116,7 +116,7 @@ impl KVStoreSync for VssStore { } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); @@ -127,7 +127,6 @@ impl KVStoreSync for VssStore { primary_namespace, secondary_namespace, key, - lazy, ); self.runtime.block_on(fut) } @@ -174,7 +173,7 @@ impl KVStore for VssStore { }) } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> Pin> + Send>> { let locking_key = self.build_locking_key(primary_namespace, secondary_namespace, key); let (inner_lock_ref, version) = self.get_new_version_and_lock_ref(locking_key.clone()); @@ -191,7 +190,6 @@ impl KVStore for VssStore { &primary_namespace, &secondary_namespace, &key, - lazy, ) .await }) @@ -369,7 +367,7 @@ impl VssStoreInner { async fn remove_internal( &self, inner_lock_ref: Arc>, locking_key: String, version: u64, - primary_namespace: &str, secondary_namespace: &str, key: &str, _lazy: bool, + primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> io::Result<()> { check_namespace_key_validity(primary_namespace, secondary_namespace, Some(key), "remove")?; diff --git a/tests/common/mod.rs b/tests/common/mod.rs index 817d0edc55..3ac0e84327 100644 --- a/tests/common/mod.rs +++ b/tests/common/mod.rs @@ -1240,14 +1240,14 @@ impl KVStore for TestSyncStore { }) } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> Pin> + Send>> { let primary_namespace = primary_namespace.to_string(); let secondary_namespace = secondary_namespace.to_string(); let key = key.to_string(); let inner = Arc::clone(&self.inner); let fut = tokio::task::spawn_blocking(move || { - inner.remove_internal(&primary_namespace, &secondary_namespace, &key, lazy) + inner.remove_internal(&primary_namespace, &secondary_namespace, &key) }); Box::pin(async move { fut.await.unwrap_or_else(|e| { @@ -1288,9 +1288,9 @@ impl KVStoreSync for TestSyncStore { } fn remove( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result<()> { - self.inner.remove_internal(primary_namespace, secondary_namespace, key, lazy) + self.inner.remove_internal(primary_namespace, secondary_namespace, key) } fn list( @@ -1428,25 +1428,15 @@ impl TestSyncStoreInner { } fn remove_internal( - &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, ) -> lightning::io::Result<()> { let _guard = self.serializer.write().unwrap(); let fs_res = - KVStoreSync::remove(&self.fs_store, primary_namespace, secondary_namespace, key, lazy); - let sqlite_res = KVStoreSync::remove( - &self.sqlite_store, - primary_namespace, - secondary_namespace, - key, - lazy, - ); - let test_res = KVStoreSync::remove( - &self.test_store, - primary_namespace, - secondary_namespace, - key, - lazy, - ); + KVStoreSync::remove(&self.fs_store, primary_namespace, secondary_namespace, key); + let sqlite_res = + KVStoreSync::remove(&self.sqlite_store, primary_namespace, secondary_namespace, key); + let test_res = + KVStoreSync::remove(&self.test_store, primary_namespace, secondary_namespace, key); assert!(!self .do_list(primary_namespace, secondary_namespace) From 7855d38c2f31f79a43bad13f97cc6ee9872bef56 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 30 Sep 2025 10:01:04 +0200 Subject: [PATCH 172/177] Account for lifetime change in `get_change_destination_script` --- src/wallet/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index 6d79fe02f3..c72c5b9f32 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -916,7 +916,7 @@ impl SignerProvider for WalletKeysManager { impl ChangeDestinationSource for WalletKeysManager { fn get_change_destination_script<'a>( - &self, + &'a self, ) -> Pin> + Send + 'a>> { let wallet = Arc::clone(&self.wallet); let logger = Arc::clone(&self.logger); From cabcbb3713f99c5ff1727a113311892de70e7d14 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 30 Sep 2025 10:07:56 +0200 Subject: [PATCH 173/177] Account for `LiquidityManager` taking a broadcaster --- src/builder.rs | 1 + src/event.rs | 11 ++++++++++- src/liquidity.rs | 18 ++++++++++++++---- src/types.rs | 1 + 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/src/builder.rs b/src/builder.rs index 0f627a2fe0..b64fc23a00 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -1557,6 +1557,7 @@ fn build_with_store_internal( Arc::clone(&channel_manager), Arc::clone(&keys_manager), Arc::clone(&chain_source), + Arc::clone(&tx_broadcaster), Arc::clone(&kv_store), Arc::clone(&config), Arc::clone(&logger), diff --git a/src/event.rs b/src/event.rs index df6649e054..e14479972d 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1289,7 +1289,16 @@ where } if let Some(liquidity_source) = self.liquidity_source.as_ref() { - liquidity_source.handle_payment_forwarded(next_channel_id).await; + if let Some(skimmed_fee_msat) = skimmed_fee_msat { + liquidity_source + .handle_payment_forwarded(next_channel_id, skimmed_fee_msat) + .await; + } else { + debug_assert!( + false, + "We expect skimmed_fee_msat to be set since LDK 0.0.122" + ); + } } let event = Event::PaymentForwarded { diff --git a/src/liquidity.rs b/src/liquidity.rs index a09848b38d..81d48e530f 100644 --- a/src/liquidity.rs +++ b/src/liquidity.rs @@ -43,7 +43,9 @@ use crate::chain::ChainSource; use crate::connection::ConnectionManager; use crate::logger::{log_debug, log_error, log_info, LdkLogger, Logger}; use crate::runtime::Runtime; -use crate::types::{ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet}; +use crate::types::{ + Broadcaster, ChannelManager, DynStore, KeysManager, LiquidityManager, PeerManager, Wallet, +}; use crate::{total_anchor_channels_reserve_sats, Config, Error}; const LIQUIDITY_REQUEST_TIMEOUT_SECS: u64 = 5; @@ -141,6 +143,7 @@ where channel_manager: Arc, keys_manager: Arc, chain_source: Arc, + tx_broadcaster: Arc, kv_store: Arc, config: Arc, logger: L, @@ -152,7 +155,8 @@ where { pub(crate) fn new( wallet: Arc, channel_manager: Arc, keys_manager: Arc, - chain_source: Arc, kv_store: Arc, config: Arc, logger: L, + chain_source: Arc, tx_broadcaster: Arc, kv_store: Arc, + config: Arc, logger: L, ) -> Self { let lsps1_client = None; let lsps2_client = None; @@ -165,6 +169,7 @@ where channel_manager, keys_manager, chain_source, + tx_broadcaster, kv_store, config, logger, @@ -241,6 +246,7 @@ where Some(Arc::clone(&self.chain_source)), None, Arc::clone(&self.kv_store), + Arc::clone(&self.tx_broadcaster), liquidity_service_config, liquidity_client_config, ) @@ -1302,10 +1308,14 @@ where } } - pub(crate) async fn handle_payment_forwarded(&self, next_channel_id: Option) { + pub(crate) async fn handle_payment_forwarded( + &self, next_channel_id: Option, skimmed_fee_msat: u64, + ) { if let Some(next_channel_id) = next_channel_id { if let Some(lsps2_service_handler) = self.liquidity_manager.lsps2_service_handler() { - if let Err(e) = lsps2_service_handler.payment_forwarded(next_channel_id).await { + if let Err(e) = + lsps2_service_handler.payment_forwarded(next_channel_id, skimmed_fee_msat).await + { log_error!( self.logger, "LSPS2 service failed to handle PaymentForwarded: {:?}", diff --git a/src/types.rs b/src/types.rs index 252efd042a..ddd5879852 100644 --- a/src/types.rs +++ b/src/types.rs @@ -77,6 +77,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager< Arc, Arc, DefaultTimeProvider, + Arc, >; pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager< From 9ec89fe73e2817191446c325b83f376fd51eb365 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 2 Oct 2025 14:24:48 +0200 Subject: [PATCH 174/177] Account for `OutputSweeper::read` being dropped --- src/io/utils.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/io/utils.rs b/src/io/utils.rs index cb3ca0847e..1556314c4d 100644 --- a/src/io/utils.rs +++ b/src/io/utils.rs @@ -32,7 +32,6 @@ use lightning::util::persist::{ SCORER_PERSISTENCE_SECONDARY_NAMESPACE, }; use lightning::util::ser::{Readable, ReadableArgs, Writeable}; -use lightning::util::sweep::OutputSweeper; use lightning_types::string::PrintableString; use rand::{thread_rng, RngCore}; @@ -256,10 +255,11 @@ pub(crate) fn read_output_sweeper( kv_store, logger.clone(), ); - OutputSweeper::read(&mut reader, args).map_err(|e| { + let (_, sweeper) = <(_, Sweeper)>::read(&mut reader, args).map_err(|e| { log_error!(logger, "Failed to deserialize OutputSweeper: {}", e); std::io::Error::new(std::io::ErrorKind::InvalidData, "Failed to deserialize OutputSweeper") - }) + })?; + Ok(sweeper) } pub(crate) fn read_node_metrics( From 22761575ac279cdec0a7dc59dace5458b8d587b8 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 13 Oct 2025 10:11:08 +0200 Subject: [PATCH 175/177] Bump to latest LDK `main` - We fix an oversight introduced in 9977903132e56afa78804b2e3de1bee29a9b410a where we switched `Cargo.toml` to use local dependencies instead of pointing to an appropriate upstream commit - We account for Splicing-related events - We account for `KeysManager` taking a new flag for v2-payment-key derivation. --- Cargo.toml | 50 +++++++++++++++++++++++------------------------ src/event.rs | 12 ++++++++++++ src/wallet/mod.rs | 2 +- 3 files changed, 38 insertions(+), 26 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index f684c68b30..e2afb0be73 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -52,29 +52,29 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } -#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std"] } -#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["tokio"] } -#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } -#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204" } - -lightning = { path = "../rust-lightning/lightning", features = ["std"] } -lightning-types = { path = "../rust-lightning/lightning-types" } -lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } -lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } -lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } -lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } -lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } -lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } -lightning-macros = { path = "../rust-lightning/lightning-macros" } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["tokio"] } +lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } + +#lightning = { path = "../rust-lightning/lightning", features = ["std"] } +#lightning-types = { path = "../rust-lightning/lightning-types" } +#lightning-invoice = { path = "../rust-lightning/lightning-invoice", features = ["std"] } +#lightning-net-tokio = { path = "../rust-lightning/lightning-net-tokio" } +#lightning-persister = { path = "../rust-lightning/lightning-persister", features = ["tokio"] } +#lightning-background-processor = { path = "../rust-lightning/lightning-background-processor" } +#lightning-rapid-gossip-sync = { path = "../rust-lightning/lightning-rapid-gossip-sync" } +#lightning-block-sync = { path = "../rust-lightning/lightning-block-sync", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { path = "../rust-lightning/lightning-transaction-sync", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +#lightning-liquidity = { path = "../rust-lightning/lightning-liquidity", features = ["std"] } +#lightning-macros = { path = "../rust-lightning/lightning-macros" } bdk_chain = { version = "0.23.0", default-features = false, features = ["std"] } bdk_esplora = { version = "0.22.0", default-features = false, features = ["async-https-rustls", "tokio"]} @@ -109,8 +109,8 @@ winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] #lightning = { version = "0.1.0", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "d0765847c85f1c3dc753c17c3e05dbcb1d300204", features = ["std", "_test_utils"] } -lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } +lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std", "_test_utils"] } +#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" diff --git a/src/event.rs b/src/event.rs index e14479972d..79fc5e5db7 100644 --- a/src/event.rs +++ b/src/event.rs @@ -1601,6 +1601,18 @@ where LdkEvent::FundingTransactionReadyForSigning { .. } => { debug_assert!(false, "We currently don't support interactive-tx, so this event should never be emitted."); }, + LdkEvent::SplicePending { .. } => { + debug_assert!( + false, + "We currently don't support splicing, so this event should never be emitted." + ); + }, + LdkEvent::SpliceFailed { .. } => { + debug_assert!( + false, + "We currently don't support splicing, so this event should never be emitted." + ); + }, } Ok(()) } diff --git a/src/wallet/mod.rs b/src/wallet/mod.rs index c72c5b9f32..8f8151b9cb 100644 --- a/src/wallet/mod.rs +++ b/src/wallet/mod.rs @@ -792,7 +792,7 @@ impl WalletKeysManager { seed: &[u8; 32], starting_time_secs: u64, starting_time_nanos: u32, wallet: Arc, logger: Arc, ) -> Self { - let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos); + let inner = KeysManager::new(seed, starting_time_secs, starting_time_nanos, true); Self { inner, wallet, logger } } From 813b162778c28fc004f0dd11f4965e429cc43307 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 14 Oct 2025 09:37:48 +0200 Subject: [PATCH 176/177] Bump LDK to v0.2.0-beta1 We update our LDK dependency to the just-released v0.2.0-beta1. --- Cargo.toml | 48 ++++++++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e2afb0be73..7b888c9297 100755 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,17 +28,17 @@ panic = 'abort' # Abort on panic default = [] [dependencies] -#lightning = { version = "0.1.0", features = ["std"] } -#lightning-types = { version = "0.2.0" } -#lightning-invoice = { version = "0.33.0", features = ["std"] } -#lightning-net-tokio = { version = "0.1.0" } -#lightning-persister = { version = "0.1.0", features = ["tokio"] } -#lightning-background-processor = { version = "0.1.0" } -#lightning-rapid-gossip-sync = { version = "0.1.0" } -#lightning-block-sync = { version = "0.1.0", features = ["rest-client", "rpc-client", "tokio"] } -#lightning-transaction-sync = { version = "0.1.0", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } -#lightning-liquidity = { version = "0.1.0", features = ["std"] } -#lightning-macros = { version = "0.1.0" } +lightning = { version = "0.2.0-beta1", features = ["std"] } +lightning-types = { version = "0.3.0-beta1" } +lightning-invoice = { version = "0.34.0-beta1", features = ["std"] } +lightning-net-tokio = { version = "0.2.0-beta1" } +lightning-persister = { version = "0.2.0-beta1", features = ["tokio"] } +lightning-background-processor = { version = "0.2.0-beta1" } +lightning-rapid-gossip-sync = { version = "0.2.0-beta1" } +lightning-block-sync = { version = "0.2.0-beta1", features = ["rest-client", "rpc-client", "tokio"] } +lightning-transaction-sync = { version = "0.2.0-beta1", features = ["esplora-async-https", "time", "electrum-rustls-ring"] } +lightning-liquidity = { version = "0.2.0-beta1", features = ["std"] } +lightning-macros = { version = "0.2.0-beta1" } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main", features = ["std"] } #lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } @@ -52,17 +52,17 @@ default = [] #lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } #lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", branch = "main" } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } -lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } -lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } -lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } -lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["tokio"] } -lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } -lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } -lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["rest-client", "rpc-client", "tokio"] } -lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } -lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } -lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +#lightning-types = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-invoice = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std"] } +#lightning-net-tokio = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-persister = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["tokio"] } +#lightning-background-processor = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-rapid-gossip-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-block-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["rest-client", "rpc-client", "tokio"] } +#lightning-transaction-sync = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["esplora-async-https", "electrum-rustls-ring", "time"] } +#lightning-liquidity = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } +#lightning-macros = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03" } #lightning = { path = "../rust-lightning/lightning", features = ["std"] } #lightning-types = { path = "../rust-lightning/lightning-types" } @@ -107,9 +107,9 @@ prost = { version = "0.11.6", default-features = false} winapi = { version = "0.3", features = ["winbase"] } [dev-dependencies] -#lightning = { version = "0.1.0", features = ["std", "_test_utils"] } +lightning = { version = "0.2.0-beta1", features = ["std", "_test_utils"] } #lightning = { git = "https://github.com/lightningdevkit/rust-lightning", branch="main", features = ["std", "_test_utils"] } -lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std", "_test_utils"] } +#lightning = { git = "https://github.com/lightningdevkit/rust-lightning", rev = "21e9a9c0ef80021d0669f2a366f55d08ba8d9b03", features = ["std", "_test_utils"] } #lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] } proptest = "1.0.0" regex = "1.5.6" From 4419953a71f0d7f8ffcb2ba48a82a0624763fd98 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 14 Oct 2025 11:12:24 +0200 Subject: [PATCH 177/177] Switch to use Rust VSS server in CI Since we just deprecated the Java version of the VSS server, we here switch our CI over to use the Rust version. --- .github/workflows/vss-integration.yml | 51 +++------------------------ 1 file changed, 5 insertions(+), 46 deletions(-) diff --git a/.github/workflows/vss-integration.yml b/.github/workflows/vss-integration.yml index f7a2307803..81b63fdf93 100644 --- a/.github/workflows/vss-integration.yml +++ b/.github/workflows/vss-integration.yml @@ -18,7 +18,7 @@ jobs: env: POSTGRES_DB: postgres POSTGRES_USER: postgres - POSTGRES_PASSWORD: YOU_MUST_CHANGE_THIS_PASSWORD + POSTGRES_PASSWORD: postgres options: >- --health-cmd pg_isready --health-interval 10s @@ -36,54 +36,13 @@ jobs: repository: lightningdevkit/vss-server path: vss-server - - name: Set up Java - uses: actions/setup-java@v3 - with: - distribution: 'corretto' - java-version: '17' - - - name: Start Tomcat + - name: Build and Deploy VSS Server run: | - docker run -d --network=host --name tomcat tomcat:latest - - - name: Setup Gradle - uses: gradle/gradle-build-action@v2 - with: - gradle-version: release-candidate - - - name: Create database table - run: | - psql -h localhost -U postgres -d postgres -f ./vss-server/java/app/src/main/java/org/vss/impl/postgres/sql/v0_create_vss_db.sql - env: - PGPASSWORD: YOU_MUST_CHANGE_THIS_PASSWORD - - - name: Build and Deploy VSS - run: | - # Print Info - java -version - gradle --version - - GRADLE_VERSION=$(gradle --version | awk '/^Gradle/ {print $2}' | head -1) - if [ -z "$GRADLE_VERSION" ]; then - echo "Error: Failed to extract Gradle version." >&2 - exit 1 - fi - echo "Extracted Gradle Version: $GRADLE_VERSION" - - cd vss-server/java - gradle wrapper --gradle-version $GRADLE_VERSION - ./gradlew --version - ./gradlew build - - docker cp app/build/libs/vss-1.0.war tomcat:/usr/local/tomcat/webapps/vss.war - cd ../ - - name: Run VSS Integration tests against vss-instance. + cd vss-server/rust + cargo run server/vss-server-config.toml& + - name: Run VSS Integration tests run: | cd ldk-node export TEST_VSS_BASE_URL="http://localhost:8080/vss" RUSTFLAGS="--cfg vss_test" cargo build --verbose --color always RUSTFLAGS="--cfg vss_test" cargo test --test integration_tests_vss - - - name: Cleanup - run: | - docker stop tomcat && docker rm tomcat