Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
50 commits
Select commit Hold shift + click to select a range
d910c8b
lsps2: Move client into module directory
tnull Jul 16, 2026
0de4f6c
lsps2: Add single-use payment leases
tnull Jul 16, 2026
aeb4f15
f Document payment leases and use odd TLV types
tnull Oct 26, 2026
4ec3542
lsps2: Persist payment leases
tnull Jul 16, 2026
2a13df1
lsps2: Prune stale payment leases
tnull Jul 16, 2026
1fc7cee
lsps2: Configure a global fee limit
tnull Jul 21, 2026
5c266ff
f Document the fixed-amount fee cap
tnull Oct 29, 2026
6f31bdc
bolt11: Consume LSPS2 payment leases
tnull Jul 16, 2026
d912533
f Document that leases are consumed at selection
tnull Oct 29, 2026
304614f
lsps2: Reject oversized CLTV deltas
tnull Aug 14, 2026
b9fb27f
bolt12: Move payment into module directory
tnull Jul 20, 2026
9558f8e
lsps2: Persist lease cache targets
tnull Jul 21, 2026
250698b
f Document lease cache targets and use odd TLV types
tnull Oct 26, 2026
3ebb650
bolt12: Register LSPS2 lease targets
tnull Jul 16, 2026
8ea1f92
f Skip lease cache targets without an LSPS2 source
tnull Oct 26, 2026
373262c
f Register lease targets for unified offers
tnull Oct 29, 2026
9d6836e
payment: Move payment metadata
tnull Jul 16, 2026
310abc7
lsps2: Add JIT payment router
tnull Jul 21, 2026
07609aa
f Document direct path rejection and use odd TLV types
tnull Oct 26, 2026
c49ff5b
bolt12: Wrap offers message handling
tnull Jul 20, 2026
7446567
bolt12: Sync offers flow block state
tnull Jul 20, 2026
12ec0d3
bolt12: Install LSPS2 router
tnull Jul 16, 2026
5929b77
f Note BOLT12 direct path rejection in the changelog
tnull Oct 26, 2026
47b362e
bolt12: Handle invoice requests locally
tnull Jul 20, 2026
58e7c74
bolt12: Respond to requests from leases
tnull Jul 16, 2026
9d58d7c
bolt12: Bound pending JIT requests
tnull Jul 20, 2026
c5b73a6
f Preserve slots for distinct JIT requests
tnull Oct 16, 2026
f213155
f Keep JIT responses until invoice expiry
tnull Oct 16, 2026
f3f002d
lsps2: Extend opening parameter validity
tnull Jul 16, 2026
e5f24f9
bolt12: Validate LSPS2 opening fees
tnull Jul 16, 2026
50e727b
lsps2: Serialize lease negotiation
tnull Jul 16, 2026
552cfb8
lsps2: Refill consumed payment leases
tnull Jul 16, 2026
89c3c4c
f Refill leases for newly registered offer targets
tnull Oct 26, 2026
ede361d
f Drop cached leases of removed cache targets
tnull Oct 26, 2026
d0d29bb
f Drop leases of cache targets expired across restarts
tnull Oct 29, 2026
911c72f
lsps2: Restore cached payment leases
tnull Jul 21, 2026
fb2c35c
lsps2: Retry lease negotiation failures
tnull Jul 16, 2026
34a4ec9
f Handle LSPS2 rejections without timeout
tnull Oct 16, 2026
d1fc1d1
lsps2: Match variable leases to payments
tnull Jul 21, 2026
f2984c1
f Cap variable lease fees at the resolved amount
tnull Oct 29, 2026
8a990d4
lsps2: Separate cache lease validity
tnull Aug 14, 2026
5d87e14
f Only require cached leases to outlive their invoice
tnull Oct 26, 2026
4d590a6
f Document on-demand lease negotiation
tnull Oct 29, 2026
654a1c5
lsps2: Test BOLT12 JIT payments
tnull Jul 16, 2026
5e3e2d6
lsps2: Test multi-LSP BOLT12 selection
tnull Jul 16, 2026
4c627a9
lsps2: Test restored BOLT12 leases
tnull Jul 16, 2026
141fc1d
f Test that unified offers pre-negotiate leases
tnull Oct 29, 2026
39dce83
lsps2: Test payment lease state
tnull Jul 16, 2026
eb772a9
lsps2: Filter direct fallback capacity
tnull Aug 14, 2026
cc6de28
f Exclude private capacity from direct fallback
tnull Oct 16, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
change once after such a migration. Payment contents and completeness are unaffected.
- Pending JIT-channel payments created before upgrading may fail after upgrade because the
prior LSPS2 fee-limit state stored in `PaymentKind::Bolt11Jit` is not migrated.
- Creating a BOLT 12 invoice for a known amount, including for refunds, now fails instead of
falling back to a direct path to the node if its channels cannot receive that amount and no
LSPS2 just-in-time channel is available. Such invoices could not be paid. This also applies
to nodes without a configured LSP.
Comment on lines +24 to +27

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🤖 The CHANGELOG still doesn't have a feature entry for receiving BOLT12 payments over LSPS2 JIT channels — only the fee-limit and skim-field notes. And one wording point in the direct-path compatibility note: for refunds, "no LSPS2 just-in-time channel is available" is always the case — the router never uses a lease for a refund — so the entry reads as if an LSP could help there.

- Upgrading from LDK Node v0.1 is no longer supported if the event queue still contains
a persisted `ChannelClosed` event.
- Users of the VSS storage backend must upgrade their VSS server to at least version
Expand Down Expand Up @@ -51,6 +55,11 @@
This replaces the previous unpaginated `Node::list_payments`, and
`Node::list_payments_with_filter` has been removed; filter the returned pages instead.
- `Node::payment` now returns a `Result`, as retrieving a payment may fail.
- LSPS2 just-in-time receive fee limits are now configured globally through
`Config::lsps2_max_total_lsp_fee_limit_msat`. The corresponding per-call fee-limit arguments
have been removed from the BOLT 11 receive APIs.
- `PaymentKind::Bolt12Offer` now exposes `counterparty_skimmed_fee_msat` for the fee deducted by
an LSP when an inbound payment is received through an LSPS2 just-in-time channel.
- The Bitcoin Core RPC and REST chain-source builder methods now accept an optional
`wallet_rescan_from_height` argument. Passing a height lets fresh wallets rescan from a known
birthday block instead of checkpointing at the current tip, which is useful when restoring a
Expand Down
47 changes: 37 additions & 10 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ use crate::io::{
PENDING_PAYMENT_INFO_PERSISTENCE_PRIMARY_NAMESPACE,
PENDING_PAYMENT_INFO_PERSISTENCE_SECONDARY_NAMESPACE,
};
use crate::liquidity::client::lsps2::router::LSPS2Router;
use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig};
use crate::lnurl_auth::LnurlAuth;
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
Expand All @@ -98,6 +99,7 @@ use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
use crate::payment::forwarding_store::ForwardingStore;
#[cfg(feature = "unified-payments")]
use crate::payment::HRNResolver;
use crate::payment::NodeOffersMessageHandler;
use crate::peer_store::PeerStore;
use crate::probing::{
HighDegreeStrategy, Prober, ProbingConfig, ProbingStrategy, ProbingStrategyKind,
Expand Down Expand Up @@ -2127,12 +2129,16 @@ fn build_with_store_internal(
}

let scoring_fee_params = ProbabilisticScoringFeeParameters::default();
let router = Arc::new(DefaultRouter::new(
let router = Arc::new(LSPS2Router::new(
DefaultRouter::new(
Arc::clone(&network_graph),
Arc::clone(&logger),
Arc::clone(&keys_manager),
Arc::clone(&scorer),
scoring_fee_params,
),
Arc::clone(&network_graph),
Arc::clone(&logger),
Arc::clone(&keys_manager),
Arc::clone(&scorer),
scoring_fee_params,
));

let mut user_config = default_user_config(&config);
Expand Down Expand Up @@ -2216,6 +2222,15 @@ fn build_with_store_internal(
};

let channel_manager = Arc::new(channel_manager);
let offers_message_handler = Arc::new(NodeOffersMessageHandler::new(
config.network,
cur_time.as_secs().try_into().map_err(|_| BuildError::InvalidSystemTime)?,
Arc::clone(&channel_manager),
Arc::clone(&keys_manager),
Arc::clone(&router),
Arc::clone(&message_router),
Arc::clone(&logger),
));

// Give ChannelMonitors to ChainMonitor
for (_blockhash, channel_monitor) in channel_monitors.into_iter() {
Expand Down Expand Up @@ -2295,7 +2310,7 @@ fn build_with_store_internal(
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&offers_message_handler),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
Expand All @@ -2308,7 +2323,7 @@ fn build_with_store_internal(
Arc::clone(&logger),
Arc::clone(&channel_manager),
message_router,
Arc::clone(&channel_manager),
Arc::clone(&offers_message_handler),
Arc::clone(&channel_manager),
Arc::clone(&om_resolver),
IgnoringMessageHandler {},
Expand Down Expand Up @@ -2350,6 +2365,7 @@ fn build_with_store_internal(
Arc::clone(&keys_manager),
Arc::clone(&tx_broadcaster),
Arc::clone(&kv_store),
Arc::downgrade(&runtime),
Arc::clone(&config),
Arc::clone(&logger),
);
Expand Down Expand Up @@ -2435,6 +2451,12 @@ fn build_with_store_internal(
Arc::clone(&keys_manager),
Arc::clone(&logger),
));
offers_message_handler.initialize_jit_handling(
Arc::clone(&runtime),
liquidity_source.lsps2_client(),
Arc::downgrade(&connection_manager),
Arc::downgrade(&onion_messenger),
);

let output_sweeper = match sweeper_bytes_res {
Ok(output_sweeper) => Arc::new(output_sweeper),
Expand Down Expand Up @@ -2513,12 +2535,16 @@ fn build_with_store_internal(
if let Some(penalty) = probing_cfg.diversity_penalty_msat {
probing_fee_params.probing_diversity_penalty_msat = penalty;
}
let probing_router = Arc::new(DefaultRouter::new(
let probing_router = Arc::new(LSPS2Router::new(
DefaultRouter::new(
Arc::clone(&network_graph),
Arc::clone(&logger),
Arc::clone(&keys_manager),
Arc::clone(&scorer),
probing_fee_params,
),
Arc::clone(&network_graph),
Arc::clone(&logger),
Arc::clone(&keys_manager),
Arc::clone(&scorer),
probing_fee_params,
));
Arc::new(HighDegreeStrategy::new(
Arc::clone(&network_graph),
Expand Down Expand Up @@ -2586,6 +2612,7 @@ fn build_with_store_internal(
output_sweeper,
peer_manager,
onion_messenger,
offers_message_handler,
connection_manager,
keys_manager,
network_graph,
Expand Down
24 changes: 18 additions & 6 deletions src/chain/bitcoind.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ use crate::fee_estimator::{
};
use crate::io::utils::update_and_persist_node_metrics;
use crate::logger::{log_bytes, log_debug, log_error, log_info, log_trace, LdkLogger, Logger};
use crate::payment::NodeOffersMessageHandler;
use crate::tx_broadcaster::SortedTransactions;
use crate::types::{Broadcaster, ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
use crate::{Error, PersistedNodeMetrics};
Expand Down Expand Up @@ -257,8 +258,8 @@ impl BitcoindChainSource {
pub(super) async fn initialize_wallets(
&self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>,
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
tx_broadcaster: &Broadcaster,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>, tx_broadcaster: &Broadcaster,
) -> Result<(), Error> {
// First register for the wallet polling status to make sure `Node::sync_wallets` calls
// wait on the result before proceeding.
Expand All @@ -283,11 +284,13 @@ impl BitcoindChainSource {

let onchain_wallet_best_block = onchain_wallet.current_best_block();
let channel_manager_best_block = channel_manager.current_best_block();
let offers_best_block = offers_message_handler.current_best_block();
let sweeper_best_block = output_sweeper.current_best_block();

let chain_listeners = vec![
(onchain_wallet_best_block, &*onchain_wallet as &(dyn Listen + Send + Sync)),
(channel_manager_best_block, &*channel_manager as &(dyn Listen + Send + Sync)),
(offers_best_block, &*offers_message_handler as &(dyn Listen + Send + Sync)),
(sweeper_best_block, &*output_sweeper as &(dyn Listen + Send + Sync)),
];

Expand All @@ -309,6 +312,7 @@ impl BitcoindChainSource {
header_cache,
Arc::clone(&onchain_wallet),
Arc::clone(&channel_manager),
Arc::clone(&offers_message_handler),
Arc::clone(&chain_monitor),
Arc::clone(&output_sweeper),
);
Expand Down Expand Up @@ -350,7 +354,8 @@ impl BitcoindChainSource {
pub(super) async fn continuously_sync_wallets(
&self, mut stop_sync_receiver: tokio::sync::watch::Receiver<()>,
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) {
let mut chain_polling_interval =
tokio::time::interval(Duration::from_secs(CHAIN_POLLING_INTERVAL_SECS));
Expand Down Expand Up @@ -389,6 +394,7 @@ impl BitcoindChainSource {
_ = self.poll_and_update_listeners(
Arc::clone(&onchain_wallet),
Arc::clone(&channel_manager),
Arc::clone(&offers_message_handler),
Arc::clone(&chain_monitor),
Arc::clone(&output_sweeper)
) => {}
Expand Down Expand Up @@ -446,7 +452,8 @@ impl BitcoindChainSource {

pub(super) async fn poll_and_update_listeners(
&self, onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let receiver_res = {
let mut status_lock = self.wallet_polling_status.lock().expect("lock");
Expand All @@ -468,6 +475,7 @@ impl BitcoindChainSource {
.poll_and_update_listeners_inner(
onchain_wallet,
channel_manager,
offers_message_handler,
chain_monitor,
output_sweeper,
)
Expand All @@ -480,7 +488,8 @@ impl BitcoindChainSource {

async fn poll_and_update_listeners_inner(
&self, onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let mut spv_client_lock = self.spv_client.lock().await;
if spv_client_lock.is_none() {
Expand All @@ -495,6 +504,7 @@ impl BitcoindChainSource {
HeaderCache::new(),
Arc::clone(&onchain_wallet),
Arc::clone(&channel_manager),
Arc::clone(&offers_message_handler),
chain_monitor,
output_sweeper,
));
Expand Down Expand Up @@ -561,12 +571,14 @@ impl BitcoindChainSource {
fn new_spv_client(
&self, chain_tip: ValidatedBlockHeader, header_cache: HeaderCache,
onchain_wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>,
chain_monitor: Arc<ChainMonitor>, output_sweeper: Arc<Sweeper>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> BitcoindSpvClient {
let chain_poller = ChainPoller::new(Arc::clone(&self.api_client), self.config.network);
let chain_listener = Arc::new(ChainListener {
onchain_wallet: Arc::downgrade(&onchain_wallet),
channel_manager: Arc::downgrade(&channel_manager),
offers_message_handler: Arc::downgrade(&offers_message_handler),
chain_monitor: Arc::downgrade(&chain_monitor),
output_sweeper: Arc::downgrade(&output_sweeper),
});
Expand Down
19 changes: 15 additions & 4 deletions src/chain/electrum.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ use crate::fee_estimator::{
};
use crate::io::utils::update_and_persist_node_metrics;
use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger};
use crate::payment::NodeOffersMessageHandler;
use crate::runtime::Runtime;
use crate::tx_broadcaster::SortedTransactions;
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
Expand Down Expand Up @@ -230,7 +231,8 @@ impl ElectrumChainSource {
}

pub(crate) async fn sync_lightning_wallet(
&self, channel_manager: Arc<ChannelManager>, chain_monitor: Arc<ChainMonitor>,
&self, channel_manager: Arc<ChannelManager>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let receiver_res = {
Expand All @@ -248,23 +250,32 @@ impl ElectrumChainSource {
let sync_guard =
WalletSyncGuard::new(&self.lightning_wallet_sync_status, Error::TxSyncFailed);

let res =
self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await;
let res = self
.sync_lightning_wallet_inner(
channel_manager,
offers_message_handler,
chain_monitor,
output_sweeper,
)
.await;

sync_guard.complete(res);

res
}

async fn sync_lightning_wallet_inner(
&self, channel_manager: Arc<ChannelManager>, chain_monitor: Arc<ChainMonitor>,
&self, channel_manager: Arc<ChannelManager>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let sync_cman = Arc::clone(&channel_manager);
let sync_offers = Arc::clone(&offers_message_handler);
let sync_cmon = Arc::clone(&chain_monitor);
let sync_sweeper = Arc::clone(&output_sweeper);
let confirmables: Vec<Arc<dyn Confirm + Sync + Send>> = vec![
sync_cman as Arc<dyn Confirm + Sync + Send>,
sync_offers as Arc<dyn Confirm + Sync + Send>,
sync_cmon as Arc<dyn Confirm + Sync + Send>,
sync_sweeper as Arc<dyn Confirm + Sync + Send>,
];
Expand Down
19 changes: 15 additions & 4 deletions src/chain/esplora.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ use crate::fee_estimator::{
};
use crate::io::utils::update_and_persist_node_metrics;
use crate::logger::{log_bytes, log_debug, log_error, log_trace, log_warn, LdkLogger, Logger};
use crate::payment::NodeOffersMessageHandler;
use crate::tx_broadcaster::SortedTransactions;
use crate::types::{ChainMonitor, ChannelManager, DynStore, Sweeper, Wallet};
use crate::{Error, PersistedNodeMetrics};
Expand Down Expand Up @@ -270,7 +271,8 @@ impl EsploraChainSource {
}

pub(super) async fn sync_lightning_wallet(
&self, channel_manager: Arc<ChannelManager>, chain_monitor: Arc<ChainMonitor>,
&self, channel_manager: Arc<ChannelManager>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let receiver_res = {
Expand All @@ -288,23 +290,32 @@ impl EsploraChainSource {
let sync_guard =
WalletSyncGuard::new(&self.lightning_wallet_sync_status, Error::WalletOperationFailed);

let res =
self.sync_lightning_wallet_inner(channel_manager, chain_monitor, output_sweeper).await;
let res = self
.sync_lightning_wallet_inner(
channel_manager,
offers_message_handler,
chain_monitor,
output_sweeper,
)
.await;

sync_guard.complete(res);

res
}

async fn sync_lightning_wallet_inner(
&self, channel_manager: Arc<ChannelManager>, chain_monitor: Arc<ChainMonitor>,
&self, channel_manager: Arc<ChannelManager>,
offers_message_handler: Arc<NodeOffersMessageHandler>, chain_monitor: Arc<ChainMonitor>,
output_sweeper: Arc<Sweeper>,
) -> Result<(), Error> {
let sync_cman = Arc::clone(&channel_manager);
let sync_offers = Arc::clone(&offers_message_handler);
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_offers as &(dyn Confirm + Sync + Send),
&*sync_cmon as &(dyn Confirm + Sync + Send),
&*sync_sweeper as &(dyn Confirm + Sync + Send),
];
Expand Down
Loading
Loading