Skip to content

Commit

Permalink
Check and claim underpaying HTLCs
Browse files Browse the repository at this point in the history
We allow the user to optionally set a maximum LSP fee limit, and
generally check that the LSP won't skim off too much from the inbound
payment.
  • Loading branch information
tnull committed Jan 11, 2024
1 parent 2d15838 commit b338467
Show file tree
Hide file tree
Showing 7 changed files with 102 additions and 16 deletions.
6 changes: 3 additions & 3 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
Expand Up @@ -85,9 +85,7 @@ interface LDKNode {
[Throws=NodeError]
Bolt11Invoice receive_variable_amount_payment([ByRef]string description, u32 expiry_secs);
[Throws=NodeError]
Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs);
[Throws=NodeError]
Bolt11Invoice receive_variable_amount_payment_via_jit_channel([ByRef]string description, u32 expiry_secs);
Bolt11Invoice receive_payment_via_jit_channel(u64 amount_msat, [ByRef]string description, u32 expiry_secs, u64? max_lsp_fee_limit_msat);
PaymentDetails? payment([ByRef]PaymentHash payment_hash);
[Throws=NodeError]
void remove_payment([ByRef]PaymentHash payment_hash);
Expand Down Expand Up @@ -134,6 +132,7 @@ enum NodeError {
"DuplicatePayment",
"InsufficientFunds",
"LiquiditySourceUnavailable",
"LiquidityFeeTooHigh",
};

[Error]
Expand Down Expand Up @@ -186,6 +185,7 @@ dictionary PaymentDetails {
u64? amount_msat;
PaymentDirection direction;
PaymentStatus status;
u64? maximum_counterparty_skimmed_fee_msat;
};

dictionary OutPoint {
Expand Down
6 changes: 6 additions & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -686,6 +686,12 @@ fn build_with_store_internal<K: KVStore + Sync + Send + 'static>(
// generating the events otherwise.
user_config.manually_accept_inbound_channels = true;
}

if liquidity_source_config.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;
}
let channel_manager = {
if let Ok(res) = kv_store.read(
CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE,
Expand Down
5 changes: 5 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,8 @@ pub enum Error {
InsufficientFunds,
/// The given operation failed due to the required liquidity source being unavailable.
LiquiditySourceUnavailable,
/// The given operation failed due to the LSP's required opening fee being too high.
LiquidityFeeTooHigh,
}

impl fmt::Display for Error {
Expand Down Expand Up @@ -114,6 +116,9 @@ impl fmt::Display for Error {
Self::LiquiditySourceUnavailable => {
write!(f, "The given operation failed due to the required liquidity source being unavailable.")
}
Self::LiquidityFeeTooHigh => {
write!(f, "The given operation failed due to the LSP's required opening fee being too high.")
}
}
}
}
Expand Down
26 changes: 25 additions & 1 deletion src/event.rs
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,7 @@ where
via_user_channel_id: _,
claim_deadline: _,
onion_fields: _,
counterparty_skimmed_fee_msat: _,
counterparty_skimmed_fee_msat,
} => {
if let Some(info) = self.payment_store.get(&payment_hash) {
if info.status == PaymentStatus::Succeeded {
Expand All @@ -380,6 +380,29 @@ where
});
return;
}

let maximum_counterparty_skimmed_fee_msat =
info.maximum_counterparty_skimmed_fee_msat.unwrap_or(0);
if counterparty_skimmed_fee_msat > maximum_counterparty_skimmed_fee_msat {
log_info!(
self.logger,
"Refusing inbound payment with hash {} as the counterparty-withheld fee of {}msat exceeds our limit of {}msat",
hex_utils::to_string(&payment_hash.0),
counterparty_skimmed_fee_msat,
maximum_counterparty_skimmed_fee_msat,
);
self.channel_manager.fail_htlc_backwards(&payment_hash);

let update = PaymentDetailsUpdate {
status: Some(PaymentStatus::Failed),
..PaymentDetailsUpdate::new(payment_hash)
};
self.payment_store.update(&update).unwrap_or_else(|e| {
log_error!(self.logger, "Failed to access payment store: {}", e);
panic!("Failed to access payment store");
});
return;
}
}

log_info!(
Expand Down Expand Up @@ -473,6 +496,7 @@ where
amount_msat: Some(amount_msat),
direction: PaymentDirection::Inbound,
status: PaymentStatus::Succeeded,
maximum_counterparty_skimmed_fee_msat: None,
};

match self.payment_store.insert(payment) {
Expand Down
37 changes: 31 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1197,6 +1197,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat: invoice.amount_milli_satoshis(),
direction: PaymentDirection::Outbound,
status: PaymentStatus::Pending,
maximum_counterparty_skimmed_fee_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1216,6 +1217,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat: invoice.amount_milli_satoshis(),
direction: PaymentDirection::Outbound,
status: PaymentStatus::Failed,
maximum_counterparty_skimmed_fee_msat: None,
};

self.payment_store.insert(payment)?;
Expand Down Expand Up @@ -1303,6 +1305,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat: Some(amount_msat),
direction: PaymentDirection::Outbound,
status: PaymentStatus::Pending,
maximum_counterparty_skimmed_fee_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1323,6 +1326,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat: Some(amount_msat),
direction: PaymentDirection::Outbound,
status: PaymentStatus::Failed,
maximum_counterparty_skimmed_fee_msat: None,
};
self.payment_store.insert(payment)?;

Expand Down Expand Up @@ -1377,6 +1381,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
status: PaymentStatus::Pending,
direction: PaymentDirection::Outbound,
amount_msat: Some(amount_msat),
maximum_counterparty_skimmed_fee_msat: None,
};
self.payment_store.insert(payment)?;

Expand All @@ -1397,6 +1402,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
status: PaymentStatus::Failed,
direction: PaymentDirection::Outbound,
amount_msat: Some(amount_msat),
maximum_counterparty_skimmed_fee_msat: None,
};

self.payment_store.insert(payment)?;
Expand Down Expand Up @@ -1570,6 +1576,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
maximum_counterparty_skimmed_fee_msat: None,
};

self.payment_store.insert(payment)?;
Expand All @@ -1583,18 +1590,30 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
/// 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_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.
///
/// [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/blob/main/LSPS2/README.md
pub fn receive_payment_via_jit_channel(
&self, amount_msat: u64, description: &str, expiry_secs: u32,
max_lsp_fee_limit_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
self.receive_payment_via_jit_channel_inner(Some(amount_msat), description, expiry_secs)
self.receive_payment_via_jit_channel_inner(
Some(amount_msat),
description,
expiry_secs,
max_lsp_fee_limit_msat,
)
}

fn receive_payment_via_jit_channel_inner(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
max_lsp_fee_limit_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let (node_id, address) = self
.liquidity_source
let liquidity_source =
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;

let (node_id, address) = liquidity_source
.get_liquidity_source_details()
.ok_or(Error::LiquiditySourceUnavailable)?;

Expand All @@ -1618,11 +1637,16 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {

log_info!(self.logger, "Connected to LSP {}@{}. ", peer_info.node_id, peer_info.address);

let liquidity_source = Arc::clone(&self.liquidity_source);
let invoice = tokio::task::block_in_place(move || {
let liquidity_source = Arc::clone(&liquidity_source);
let (invoice, lsp_opening_fee) = tokio::task::block_in_place(move || {
runtime.block_on(async move {
liquidity_source
.lsps2_receive_to_jit_channel(amount_msat, description, expiry_secs)
.lsps2_receive_to_jit_channel(
amount_msat,
description,
expiry_secs,
max_lsp_fee_limit_msat,
)
.await
})
})?;
Expand All @@ -1636,6 +1660,7 @@ impl<K: KVStore + Sync + Send + 'static> Node<K> {
amount_msat,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
maximum_counterparty_skimmed_fee_msat: Some(lsp_opening_fee),
};

self.payment_store.insert(payment)?;
Expand Down
17 changes: 11 additions & 6 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
Expand Up @@ -75,10 +75,7 @@ where
}

pub(crate) fn get_liquidity_source_details(&self) -> Option<(PublicKey, SocketAddress)> {
match self {
Self::None => None,
Self::LSPS2 { node_id, address, .. } => Some((*node_id, address.clone())),
}
self.lsps2_service.as_ref().map(|s| (s.node_id, s.address.clone()))
}

pub(crate) async fn handle_next_event(&self) {
Expand Down Expand Up @@ -203,7 +200,8 @@ where

pub(crate) async fn lsps2_receive_to_jit_channel(
&self, amount_msat: Option<u64>, description: &str, expiry_secs: u32,
) -> Result<Bolt11Invoice, Error> {
max_lsp_fee_limit_msat: Option<u64>,
) -> Result<(Bolt11Invoice, u64), Error> {
let lsps2_service = self.lsps2_service.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;
let user_channel_id: u128 = rand::thread_rng().gen::<u128>();

Expand Down Expand Up @@ -263,6 +261,13 @@ where
Error::LiquidityRequestFailed
})?;

if let Some(max_lsp_fee_limit_msat) = max_lsp_fee_limit_msat {
if min_opening_fee_msat > max_lsp_fee_limit_msat {
log_error!(self.logger, "Failed to request inbound JIT channel as LSP's requested opening fee of {}msat exceeds our fee limit of {}msat", min_opening_fee_msat, max_lsp_fee_limit_msat);
return Err(Error::LiquidityFeeTooHigh);
}
}

log_debug!(
self.logger,
"Choosing cheapest liquidity offer, will pay {}msat in LSP fees",
Expand Down Expand Up @@ -342,7 +347,7 @@ where
})?;

log_info!(self.logger, "JIT-channel invoice created: {}", invoice);
Ok(invoice)
Ok((invoice, min_opening_fee_msat))
}
}

Expand Down
21 changes: 21 additions & 0 deletions src/payment_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,21 @@ pub struct PaymentDetails {
pub direction: PaymentDirection,
/// The status of the payment.
pub status: PaymentStatus,
/// The maximal amount we allow our counterparty to withhold from us when forwarding the
/// payment.
///
/// This is usually only `Some` for payments received via a JIT-channel, in which case the first
/// inbound payment will pay for the LSP's channel opening fees.
///
/// See [`LdkChannelConfig::accept_underpaying_htlcs`] for more information.
///
/// [`LdkChannelConfig::accept_underpaying_htlcs`]: lightning::util::config::ChannelConfig::accept_underpaying_htlcs
pub maximum_counterparty_skimmed_fee_msat: Option<u64>,
}

impl_writeable_tlv_based!(PaymentDetails, {
(0, hash, required),
(1, maximum_counterparty_skimmed_fee_msat, option),
(2, preimage, required),
(4, secret, required),
(6, amount_msat, required),
Expand Down Expand Up @@ -80,6 +91,7 @@ pub(crate) struct PaymentDetailsUpdate {
pub amount_msat: Option<Option<u64>>,
pub direction: Option<PaymentDirection>,
pub status: Option<PaymentStatus>,
pub maximum_counterparty_skimmed_fee_msat: Option<Option<u64>>,
}

impl PaymentDetailsUpdate {
Expand All @@ -91,6 +103,7 @@ impl PaymentDetailsUpdate {
amount_msat: None,
direction: None,
status: None,
maximum_counterparty_skimmed_fee_msat: None,
}
}
}
Expand Down Expand Up @@ -171,6 +184,13 @@ where
payment.status = status;
}

if let Some(maximum_counterparty_skimmed_fee_msat) =
update.maximum_counterparty_skimmed_fee_msat
{
payment.maximum_counterparty_skimmed_fee_msat =
maximum_counterparty_skimmed_fee_msat
}

self.persist_info(&update.hash, payment)?;
updated = true;
}
Expand Down Expand Up @@ -247,6 +267,7 @@ mod tests {
amount_msat: None,
direction: PaymentDirection::Inbound,
status: PaymentStatus::Pending,
maximum_counterparty_skimmed_fee_msat: None,
};

assert_eq!(Ok(false), payment_store.insert(payment.clone()));
Expand Down

0 comments on commit b338467

Please sign in to comment.