Skip to content

Commit 5672a5c

Browse files
Matt Coralloclaude
andcommitted
Receive BOLT12 payments through LSPS2 JIT channels
Wrap the default router with the LSPS2-aware `LSPS2Router` and wire it to the LSPS2 client handler, so JIT-channel blinded payment paths are injected into BOLT12 invoices based on the latest invoice parameters negotiated with our configured LSPs. If liquidity sources are configured, we negotiate the parameters with the cheapest LSP once at startup. From then on they simply apply to all BOLT12 receives, i.e., any offer created via the regular receive APIs will have JIT-channel paths injected into its invoices, and we force a refresh of any static invoices stored with a static invoice server so they include the new paths, too. As the router encodes the used parameters in the paths' payment metadata, they are available again upon receipt and used to verify any skimmed channel opening fee against what was negotiated. Parameters previously negotiated with LSPs that are no longer configured are wiped early during node building, before the node could create any invoices or connect to peers. The BOLT11 JIT receive flows are refactored to share the same negotiation logic. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 76f59ae commit 5672a5c

5 files changed

Lines changed: 290 additions & 47 deletions

File tree

src/builder.rs

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ use lightning::util::persist::{
4343
use lightning::util::ser::ReadableArgs;
4444
use lightning::util::sweep::OutputSweeper;
4545
use lightning_dns_resolver::OMDomainResolver;
46+
use lightning_liquidity::lsps2::router::LSPS2Router;
4647
use vss_client::headers::VssHeaderProvider;
4748

4849
use crate::chain::ChainSource;
@@ -70,7 +71,7 @@ use crate::io::{
7071
};
7172
use crate::liquidity::{LSPS2ServiceConfig, LiquiditySourceBuilder, LspConfig};
7273
use crate::lnurl_auth::LnurlAuth;
73-
use crate::logger::{log_error, LdkLogger, LogLevel, LogWriter, Logger};
74+
use crate::logger::{log_error, log_info, LdkLogger, LogLevel, LogWriter, Logger};
7475
use crate::message_handler::NodeCustomMessageHandler;
7576
use crate::payment::asynchronous::om_mailbox::OnionMessageMailbox;
7677
use crate::peer_store::PeerStore;
@@ -1762,13 +1763,17 @@ fn build_with_store_internal(
17621763
}
17631764

17641765
let scoring_fee_params = ProbabilisticScoringFeeParameters::default();
1765-
let router = Arc::new(DefaultRouter::new(
1766+
let inner_router = DefaultRouter::new(
17661767
Arc::clone(&network_graph),
17671768
Arc::clone(&logger),
17681769
Arc::clone(&keys_manager),
17691770
Arc::clone(&scorer),
17701771
scoring_fee_params,
1771-
));
1772+
);
1773+
// Wrap the default router to inject LSPS2 JIT-channel blinded payment paths into BOLT12
1774+
// invoices based on the latest invoice parameters negotiated with our configured LSPs. The
1775+
// LSPS2 client handler is wired up below, once the liquidity source is built.
1776+
let router = Arc::new(LSPS2Router::new(inner_router));
17721777

17731778
let mut user_config = default_user_config(&config);
17741779

@@ -1993,6 +1998,50 @@ fn build_with_store_internal(
19931998
let custom_message_handler =
19941999
Arc::new(NodeCustomMessageHandler::new(Arc::clone(&liquidity_source)));
19952000

2001+
// Wire up the LSPS2 client handler, having the router inject JIT-channel blinded payment
2002+
// paths based on the latest invoice parameters negotiated with our configured LSPs.
2003+
//
2004+
// Beforehand, wipe any parameters previously negotiated with LSPs that are no longer
2005+
// configured, making sure the router won't have payments routed through them anymore.
2006+
if let Some(lsps2_client_handler) =
2007+
liquidity_source.liquidity_manager().lsps2_client_handler_arc()
2008+
{
2009+
let stale_lsp_node_ids = lsps2_client_handler
2010+
.latest_invoice_params()
2011+
.into_iter()
2012+
.map(|invoice_params| invoice_params.counterparty_node_id)
2013+
.filter(|counterparty_node_id| {
2014+
!liquidity_source_config.map_or(false, |lsc| {
2015+
lsc.lsp_nodes.iter().any(|n| n.node_id == *counterparty_node_id)
2016+
})
2017+
})
2018+
.collect::<Vec<_>>();
2019+
2020+
for counterparty_node_id in stale_lsp_node_ids {
2021+
let wipe_handler = Arc::clone(&lsps2_client_handler);
2022+
let wipe_logger = Arc::clone(&logger);
2023+
runtime.spawn_background_task(async move {
2024+
log_info!(
2025+
wipe_logger,
2026+
"Wiping LSPS2 invoice parameters previously negotiated with now-unconfigured LSP {}",
2027+
counterparty_node_id,
2028+
);
2029+
if let Err(e) =
2030+
wipe_handler.clear_latest_invoice_params(&counterparty_node_id).await
2031+
{
2032+
log_error!(
2033+
wipe_logger,
2034+
"Failed to wipe LSPS2 invoice parameters for LSP {}: {:?}",
2035+
counterparty_node_id,
2036+
e
2037+
);
2038+
}
2039+
});
2040+
}
2041+
2042+
router.set_lsps2_client_handler(lsps2_client_handler);
2043+
}
2044+
19962045
(liquidity_source, custom_message_handler)
19972046
};
19982047

src/event.rs

Lines changed: 100 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77

88
use core::future::Future;
99
use core::task::{Poll, Waker};
10-
use std::collections::VecDeque;
10+
use std::collections::{BTreeMap, VecDeque};
1111
use std::ops::Deref;
1212
use std::sync::{Arc, Mutex};
1313

@@ -30,6 +30,8 @@ use lightning::util::errors::APIError;
3030
use lightning::util::persist::KVStore;
3131
use lightning::util::ser::{Readable, ReadableArgs, Writeable, Writer};
3232
use lightning::{impl_ser_tlv_based, impl_ser_tlv_based_enum};
33+
use lightning_liquidity::lsps2::client::LSPS2InvoiceParameters;
34+
use lightning_liquidity::lsps2::router::LSPS2_PAYMENT_METADATA_KEY;
3335
use lightning_liquidity::lsps2::utils::compute_opening_fee;
3436
use lightning_types::payment::{PaymentHash, PaymentPreimage};
3537

@@ -611,6 +613,21 @@ where
611613
})
612614
}
613615

616+
fn lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
617+
payment_metadata: Option<&BTreeMap<u64, Vec<u8>>>, payment_size_msat: u64,
618+
) -> Option<u64> {
619+
// For BOLT12 payments, the router encoded the negotiated LSPS2 invoice parameters in the
620+
// payment metadata of any JIT-channel blinded payment paths it constructed. Recompute the
621+
// maximum acceptable opening fee from the negotiated opening fee parameters.
622+
let encoded_params = payment_metadata?.get(&LSPS2_PAYMENT_METADATA_KEY)?;
623+
let invoice_params = LSPS2InvoiceParameters::read(&mut &encoded_params[..]).ok()?;
624+
compute_opening_fee(
625+
payment_size_msat,
626+
invoice_params.opening_fee_params.min_fee_msat,
627+
invoice_params.opening_fee_params.proportional as u64,
628+
)
629+
}
630+
614631
pub async fn handle_event(&self, event: LdkEvent) -> Result<(), ReplayEvent> {
615632
match event {
616633
LdkEvent::FundingGenerationReady {
@@ -798,13 +815,19 @@ where
798815
.and_then(|metadata| {
799816
Self::lsps2_max_total_opening_fee_msat(metadata, amount_msat)
800817
}),
818+
PaymentPurpose::Bolt12OfferPayment { payment_context, .. } => {
819+
Self::lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
820+
payment_context.payment_metadata.as_ref(),
821+
amount_msat + counterparty_skimmed_fee_msat,
822+
)
823+
},
801824
_ => None,
802825
};
803826

804827
let Some(max_total_opening_fee_msat) = max_total_opening_fee_msat else {
805828
log_info!(
806829
self.logger,
807-
"Refusing inbound payment with hash {} as the counterparty withheld {}msat without valid BOLT11 LSPS2 payment metadata",
830+
"Refusing inbound payment with hash {} as the counterparty withheld {}msat without valid LSPS2 payment metadata",
808831
hex_utils::to_string(&payment_hash.0),
809832
counterparty_skimmed_fee_msat,
810833
);
@@ -828,18 +851,24 @@ where
828851
match &info.kind {
829852
PaymentKind::Bolt11 { .. } => {
830853
let update = PaymentDetailsUpdate {
831-
counterparty_skimmed_fee_msat: Some(Some(counterparty_skimmed_fee_msat)),
854+
counterparty_skimmed_fee_msat: Some(Some(
855+
counterparty_skimmed_fee_msat,
856+
)),
832857
..PaymentDetailsUpdate::new(payment_id)
833858
};
834859
match self.payment_store.update(update).await {
835860
Ok(_) => (),
836861
Err(e) => {
837-
log_error!(self.logger, "Failed to access payment store: {}", e);
862+
log_error!(
863+
self.logger,
864+
"Failed to access payment store: {}",
865+
e
866+
);
838867
return Err(ReplayEvent());
839868
},
840869
};
841870
},
842-
_ => debug_assert!(false, "We only expect the counterparty to get away with withholding fees for BOLT11 payments."),
871+
_ => {},
843872
}
844873
}
845874
}
@@ -1948,6 +1977,72 @@ mod tests {
19481977
);
19491978
}
19501979

1980+
#[test]
1981+
fn lsps2_bolt12_payment_metadata_decodes_fee_limit() {
1982+
use lightning_liquidity::lsps0::ser::LSPSDateTime;
1983+
use lightning_liquidity::lsps2::msgs::LSPS2OpeningFeeParams;
1984+
1985+
use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey};
1986+
1987+
use std::str::FromStr;
1988+
1989+
let counterparty_node_id = PublicKey::from_secret_key(
1990+
&Secp256k1::new(),
1991+
&SecretKey::from_slice(&[42; 32]).unwrap(),
1992+
);
1993+
let invoice_params = LSPS2InvoiceParameters {
1994+
counterparty_node_id,
1995+
intercept_scid: 42,
1996+
cltv_expiry_delta: 144,
1997+
opening_fee_params: LSPS2OpeningFeeParams {
1998+
min_fee_msat: 21_000,
1999+
proportional: 10_000,
2000+
valid_until: LSPSDateTime::from_str("2035-05-20T08:30:45Z").unwrap(),
2001+
min_lifetime: 144,
2002+
max_client_to_self_delay: 128,
2003+
min_payment_size_msat: 1,
2004+
max_payment_size_msat: 100_000_000,
2005+
promise: "promise".to_string(),
2006+
},
2007+
};
2008+
2009+
let mut payment_metadata = BTreeMap::new();
2010+
payment_metadata.insert(LSPS2_PAYMENT_METADATA_KEY, invoice_params.encode());
2011+
2012+
// max(min_fee_msat, proportional * payment_size / 1_000_000) = max(21_000, 10_000_000)
2013+
assert_eq!(
2014+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
2015+
Some(&payment_metadata),
2016+
1_000_000_000,
2017+
),
2018+
Some(10_000_000)
2019+
);
2020+
2021+
// Missing metadata, missing key, or malformed parameters are rejected.
2022+
assert_eq!(
2023+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
2024+
None, 1_000_000,
2025+
),
2026+
None
2027+
);
2028+
assert_eq!(
2029+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
2030+
Some(&BTreeMap::new()),
2031+
1_000_000,
2032+
),
2033+
None
2034+
);
2035+
let mut malformed_metadata = BTreeMap::new();
2036+
malformed_metadata.insert(LSPS2_PAYMENT_METADATA_KEY, vec![0xff]);
2037+
assert_eq!(
2038+
EventHandler::<Arc<TestLogger>>::lsps2_max_total_opening_fee_msat_from_bolt12_metadata(
2039+
Some(&malformed_metadata),
2040+
1_000_000,
2041+
),
2042+
None
2043+
);
2044+
}
2045+
19512046
#[test]
19522047
fn lsps2_payment_metadata_missing_or_malformed_limit_is_rejected() {
19532048
let empty_metadata = PaymentMetadata { lsps2_parameters: None }.encode();

src/lib.rs

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -746,6 +746,49 @@ impl Node {
746746
}
747747
});
748748

749+
// If we have configured liquidity sources, negotiate LSPS2 JIT-channel invoice parameters
750+
// with the cheapest LSP once at startup. The negotiated parameters are stored in the
751+
// LSPS2 client handler and will be used by our router to inject JIT-channel blinded
752+
// payment paths into all BOLT12 invoices going forward.
753+
if !self.liquidity_source.get_all_lsp_details().is_empty() {
754+
let negotiation_liquidity_source = Arc::clone(&self.liquidity_source);
755+
let negotiation_cm = Arc::clone(&self.connection_manager);
756+
let negotiation_chan_man = Arc::clone(&self.channel_manager);
757+
let negotiation_peer_store = Arc::clone(&self.peer_store);
758+
let negotiation_logger = Arc::clone(&self.logger);
759+
self.runtime.spawn_background_task(async move {
760+
match negotiation_liquidity_source
761+
.lsps2_client()
762+
.lsps2_negotiate_variable_amount_jit_invoice_params(None, negotiation_cm)
763+
.await
764+
{
765+
Ok((_, _, chosen_lsp)) => {
766+
let peer_info =
767+
PeerInfo { node_id: chosen_lsp.node_id, address: chosen_lsp.address };
768+
if let Err(e) = negotiation_peer_store.add_peer(peer_info).await {
769+
log_error!(
770+
negotiation_logger,
771+
"Failed to add LSP to peer store: {}",
772+
e
773+
);
774+
}
775+
776+
// Any static invoices stored with a static invoice server were built
777+
// before the newly-negotiated parameters were available. Force a refresh
778+
// so they include the JIT-channel payment paths going forward.
779+
negotiation_chan_man.refresh_static_invoices();
780+
},
781+
Err(e) => {
782+
log_error!(
783+
negotiation_logger,
784+
"Failed to negotiate LSPS2 JIT-channel parameters for BOLT12 receives: {}",
785+
e
786+
);
787+
},
788+
}
789+
});
790+
}
791+
749792
log_info!(self.logger, "Startup complete.");
750793
*is_running_lock = true;
751794
Ok(())

0 commit comments

Comments
 (0)