Skip to content

Commit 49f497f

Browse files
committed
Implement LSPS2 invoice creation flow
1 parent 338b9c5 commit 49f497f

File tree

8 files changed

+400
-16
lines changed

8 files changed

+400
-16
lines changed

bindings/ldk_node.udl

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,8 @@ interface LDKNode {
8787
Bolt11Invoice receive_payment(u64 amount_msat, [ByRef]string description, u32 expiry_secs);
8888
[Throws=NodeError]
8989
Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs);
90+
[Throws=NodeError]
91+
Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
9092
PaymentDetails? payment([ByRef]PaymentHash payment_hash);
9193
[Throws=NodeError]
9294
void remove_payment([ByRef]PaymentHash payment_hash);
@@ -118,6 +120,7 @@ enum NodeError {
118120
"MessageSigningFailed",
119121
"TxSyncFailed",
120122
"GossipUpdateFailed",
123+
"LiquidityRequestFailed",
121124
"InvalidAddress",
122125
"InvalidSocketAddress",
123126
"InvalidPublicKey",
@@ -132,6 +135,7 @@ enum NodeError {
132135
"DuplicatePayment",
133136
"InsufficientFunds",
134137
"LiquiditySourceUnavailable",
138+
"LiquidityFeeTooHigh",
135139
};
136140

137141
[Error]
@@ -170,12 +174,8 @@ enum PaymentStatus {
170174
"Failed",
171175
};
172176

173-
[NonExhaustive]
174-
enum Network {
175-
"Bitcoin",
176-
"Testnet",
177-
"Signet",
178-
"Regtest",
177+
dictionary LSPFeeLimits {
178+
u64? max_total_opening_fee_msat;
179179
};
180180

181181
dictionary PaymentDetails {
@@ -185,6 +185,15 @@ dictionary PaymentDetails {
185185
u64? amount_msat;
186186
PaymentDirection direction;
187187
PaymentStatus status;
188+
LSPFeeLimits? lsp_fee_limits;
189+
};
190+
191+
[NonExhaustive]
192+
enum Network {
193+
"Bitcoin",
194+
"Testnet",
195+
"Signet",
196+
"Regtest",
188197
};
189198

190199
dictionary OutPoint {

src/builder.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -694,6 +694,12 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
694694
// generating the events otherwise.
695695
user_config.manually_accept_inbound_channels = true;
696696
}
697+
698+
if liquidity_source_config.is_some() {
699+
// Generally allow claiming underpaying HTLCs as the LSP will skim off some fee. We'll
700+
// check that they don't take too much before claiming.
701+
user_config.channel_config.accept_underpaying_htlcs = true;
702+
}
697703
let channel_manager = {
698704
if let Ok(res) = kv_store.read(
699705
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,

src/error.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub enum Error {
3737
TxSyncFailed,
3838
/// A gossip updating operation failed.
3939
GossipUpdateFailed,
40+
/// A liquidity request operation failed.
41+
LiquidityRequestFailed,
4042
/// The given address is invalid.
4143
InvalidAddress,
4244
/// The given network address is invalid.
@@ -65,6 +67,8 @@ pub enum Error {
6567
InsufficientFunds,
6668
/// The given operation failed due to the required liquidity source being unavailable.
6769
LiquiditySourceUnavailable,
70+
/// The given operation failed due to the LSP's required opening fee being too high.
71+
LiquidityFeeTooHigh,
6872
}
6973

7074
impl fmt::Display for Error {
@@ -91,6 +95,7 @@ impl fmt::Display for Error {
9195
Self::MessageSigningFailed => write!(f, "Failed to sign given message."),
9296
Self::TxSyncFailed => write!(f, "Failed to sync transactions."),
9397
Self::GossipUpdateFailed => write!(f, "Failed to update gossip data."),
98+
Self::LiquidityRequestFailed => write!(f, "Failed to request inbound liquidity."),
9499
Self::InvalidAddress => write!(f, "The given address is invalid."),
95100
Self::InvalidSocketAddress => write!(f, "The given network address is invalid."),
96101
Self::InvalidPublicKey => write!(f, "The given public key is invalid."),
@@ -111,6 +116,9 @@ impl fmt::Display for Error {
111116
Self::LiquiditySourceUnavailable => {
112117
write!(f, "The given operation failed due to the required liquidity source being unavailable.")
113118
}
119+
Self::LiquidityFeeTooHigh => {
120+
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
121+
}
114122
}
115123
}
116124
}

src/event.rs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -395,7 +395,7 @@ where
395395
via_user_channel_id: _,
396396
claim_deadline: _,
397397
onion_fields: _,
398-
counterparty_skimmed_fee_msat: _,
398+
counterparty_skimmed_fee_msat,
399399
} => {
400400
if let Some(info) = self.payment_store.get(&payment_hash) {
401401
if info.status == PaymentStatus::Succeeded {
@@ -417,6 +417,29 @@ where
417417
});
418418
return;
419419
}
420+
421+
let max_total_opening_fee_msat =
422+
info.lsp_fee_limits.and_then(|l| l.max_total_opening_fee_msat).unwrap_or(0);
423+
if counterparty_skimmed_fee_msat > max_total_opening_fee_msat {
424+
log_info!(
425+
self.logger,
426+
"Refusing inbound payment with hash {} as the counterparty-withheld fee of {}msat exceeds our limit of {}msat",
427+
hex_utils::to_string(&payment_hash.0),
428+
counterparty_skimmed_fee_msat,
429+
max_total_opening_fee_msat,
430+
);
431+
self.channel_manager.fail_htlc_backwards(&payment_hash);
432+
433+
let update = PaymentDetailsUpdate {
434+
status: Some(PaymentStatus::Failed),
435+
..PaymentDetailsUpdate::new(payment_hash)
436+
};
437+
self.payment_store.update(&update).unwrap_or_else(|e| {
438+
log_error!(self.logger, "Failed to access payment store: {}", e);
439+
panic!("Failed to access payment store");
440+
});
441+
return;
442+
}
420443
}
421444

422445
log_info!(
@@ -510,6 +533,7 @@ where
510533
amount_msat: Some(amount_msat),
511534
direction: PaymentDirection::Inbound,
512535
status: PaymentStatus::Succeeded,
536+
lsp_fee_limits: None,
513537
};
514538

515539
match self.payment_store.insert(payment) {

src/lib.rs

Lines changed: 103 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ use event::{EventHandler, EventQueue};
120120
use gossip::GossipSource;
121121
use liquidity::LiquiditySource;
122122
use payment_store::PaymentStore;
123-
pub use payment_store::{PaymentDetails, PaymentDirection, PaymentStatus};
123+
pub use payment_store::{LSPFeeLimits, PaymentDetails, PaymentDirection, PaymentStatus};
124124
use peer_store::{PeerInfo, PeerStore};
125125
use types::{
126126
Broadcaster, ChainMonitor, ChannelManager, FeeEstimator, KeysManager, NetworkGraph,
@@ -1217,6 +1217,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
12171217
amount_msat: invoice.amount_milli_satoshis(),
12181218
direction: PaymentDirection::Outbound,
12191219
status: PaymentStatus::Pending,
1220+
lsp_fee_limits: None,
12201221
};
12211222
self.payment_store.insert(payment)?;
12221223

@@ -1236,6 +1237,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
12361237
amount_msat: invoice.amount_milli_satoshis(),
12371238
direction: PaymentDirection::Outbound,
12381239
status: PaymentStatus::Failed,
1240+
lsp_fee_limits: None,
12391241
};
12401242

12411243
self.payment_store.insert(payment)?;
@@ -1323,6 +1325,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
13231325
amount_msat: Some(amount_msat),
13241326
direction: PaymentDirection::Outbound,
13251327
status: PaymentStatus::Pending,
1328+
lsp_fee_limits: None,
13261329
};
13271330
self.payment_store.insert(payment)?;
13281331

@@ -1343,6 +1346,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
13431346
amount_msat: Some(amount_msat),
13441347
direction: PaymentDirection::Outbound,
13451348
status: PaymentStatus::Failed,
1349+
lsp_fee_limits: None,
13461350
};
13471351
self.payment_store.insert(payment)?;
13481352

@@ -1397,6 +1401,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
13971401
status: PaymentStatus::Pending,
13981402
direction: PaymentDirection::Outbound,
13991403
amount_msat: Some(amount_msat),
1404+
lsp_fee_limits: None,
14001405
};
14011406
self.payment_store.insert(payment)?;
14021407

@@ -1417,6 +1422,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
14171422
status: PaymentStatus::Failed,
14181423
direction: PaymentDirection::Outbound,
14191424
amount_msat: Some(amount_msat),
1425+
lsp_fee_limits: None,
14201426
};
14211427

14221428
self.payment_store.insert(payment)?;
@@ -1590,13 +1596,109 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
15901596
amount_msat,
15911597
direction: PaymentDirection::Inbound,
15921598
status: PaymentStatus::Pending,
1599+
lsp_fee_limits: None,
15931600
};
15941601

15951602
self.payment_store.insert(payment)?;
15961603

15971604
Ok(invoice)
15981605
}
15991606

1607+
/// Returns a payable invoice that can be used to request a payment of the amount given and
1608+
/// receive it via a newly created just-in-time (JIT) channel.
1609+
///
1610+
/// When the returned invoice is paid, the configured [LSPS2]-compliant LSP will open a channel
1611+
/// to us, supplying just-in-time inbound liquidity.
1612+
///
1613+
/// If set, `max_total_lsp_fee_limit_msat` will limit how much fee we allow the LSP to take for opening the
1614+
/// channel to us. We'll use its cheapest offer otherwise.
1615+
///
1616+
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
1617+
pub fn receive_payment_via_jit_channel(
1618+
&self, amount_msat: u64, description: &str, expiry_secs: u32,
1619+
max_total_lsp_fee_limit_msat: Option<u64>,
1620+
) -> Result<Bolt11Invoice, Error> {
1621+
self.receive_payment_via_jit_channel_inner(
1622+
Some(amount_msat),
1623+
description,
1624+
expiry_secs,
1625+
max_total_lsp_fee_limit_msat,
1626+
)
1627+
}
1628+
1629+
fn receive_payment_via_jit_channel_inner(
1630+
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
1631+
max_total_lsp_fee_limit_msat: Option<u64>,
1632+
) -> Result<Bolt11Invoice, Error> {
1633+
let liquidity_source =
1634+
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
1635+
1636+
let (node_id, address) = liquidity_source
1637+
.get_liquidity_source_details()
1638+
.ok_or(Error::LiquiditySourceUnavailable)?;
1639+
1640+
let rt_lock = self.runtime.read().unwrap();
1641+
let runtime = rt_lock.as_ref().unwrap();
1642+
1643+
let peer_info = PeerInfo { node_id, address };
1644+
1645+
let con_node_id = peer_info.node_id;
1646+
let con_addr = peer_info.address.clone();
1647+
let con_logger = Arc::clone(&self.logger);
1648+
let con_pm = Arc::clone(&self.peer_manager);
1649+
1650+
// We need to use our main runtime here as a local runtime might not be around to poll
1651+
// connection futures going forward.
1652+
tokio::task::block_in_place(move || {
1653+
runtime.block_on(async move {
1654+
connect_peer_if_necessary(con_node_id, con_addr, con_pm, con_logger).await
1655+
})
1656+
})?;
1657+
1658+
log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);
1659+
1660+
let liquidity_source = Arc::clone(&liquidity_source);
1661+
let (invoice, lsp_total_opening_fee) = tokio::task::block_in_place(move || {
1662+
runtime.block_on(async move {
1663+
if let Some(amount_msat) = amount_msat {
1664+
liquidity_source
1665+
.lsps2_receive_to_jit_channel(
1666+
amount_msat,
1667+
description,
1668+
expiry_secs,
1669+
max_total_lsp_fee_limit_msat,
1670+
)
1671+
.await
1672+
.map(|(invoice, total_fee)| (invoice, Some(total_fee)))
1673+
} else {
1674+
// TODO: will be implemented in the next commit
1675+
Err(Error::LiquidityRequestFailed)
1676+
}
1677+
})
1678+
})?;
1679+
1680+
// Register payment in payment store.
1681+
let payment_hash = PaymentHash(invoice.payment_hash().to_byte_array());
1682+
let lsp_fee_limits =
1683+
Some(LSPFeeLimits { max_total_opening_fee_msat: lsp_total_opening_fee });
1684+
let payment = PaymentDetails {
1685+
hash: payment_hash,
1686+
preimage: None,
1687+
secret: Some(invoice.payment_secret().clone()),
1688+
amount_msat,
1689+
direction: PaymentDirection::Inbound,
1690+
status: PaymentStatus::Pending,
1691+
lsp_fee_limits,
1692+
};
1693+
1694+
self.payment_store.insert(payment)?;
1695+
1696+
// Persist LSP peer to make sure we reconnect on restart.
1697+
self.peer_store.add_peer(peer_info)?;
1698+
1699+
Ok(invoice)
1700+
}
1701+
16001702
/// Retrieve the details of a specific payment with the given hash.
16011703
///
16021704
/// Returns `Some` if the payment was known and `None` otherwise.

0 commit comments

Comments
 (0)