Skip to content
Closed
Changes from all commits
Commits
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
24 changes: 12 additions & 12 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -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/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] }
lightning-types = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-invoice = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["std"] }
lightning-net-tokio = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-persister = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-background-processor = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-rapid-gossip-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-block-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["rest-client", "rpc-client", "tokio"] }
lightning-transaction-sync = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp", features = ["esplora-async-https", "electrum-rustls-ring", "time"] }
lightning-liquidity = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }
lightning-macros = { git = "https://github.com/martinsaposnic/rust-lightning", branch = "client-trusts-lsp" }

#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/martinsaposnic/rust-lightning", branch="client-trusts-lsp", features = ["std", "_test_utils"] }
#lightning = { path = "../rust-lightning/lightning", features = ["std", "_test_utils"] }
proptest = "1.0.0"
regex = "1.5.6"
8 changes: 8 additions & 0 deletions bindings/ldk_node.udl
Original file line number Diff line number Diff line change
@@ -44,6 +44,7 @@ dictionary LSPS2ServiceConfig {
u32 max_client_to_self_delay;
u64 min_payment_size_msat;
u64 max_payment_size_msat;
boolean client_trusts_lsp;
};

enum LogLevel {
@@ -194,6 +195,13 @@ interface Bolt11Payment {
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);
[Throws=NodeError]
JitChannelManualClaim receive_via_jit_channel_manual_claim(u64 amount_msat, [ByRef]Bolt11InvoiceDescription description, u32 expiry_secs, u64? max_total_lsp_fee_limit_msat);
};

dictionary JitChannelManualClaim {
Bolt11Invoice invoice;
PaymentPreimage preimage;
};

interface Bolt12Payment {
1 change: 1 addition & 0 deletions src/builder.rs
Original file line number Diff line number Diff line change
@@ -1507,6 +1507,7 @@ fn build_with_store_internal(
Arc::clone(&chain_source),
Arc::clone(&config),
Arc::clone(&logger),
Arc::clone(&tx_broadcaster),
);

lsc.lsps1_client.as_ref().map(|config| {
55 changes: 44 additions & 11 deletions src/event.rs
Original file line number Diff line number Diff line change
@@ -497,7 +497,7 @@ where
counterparty_node_id,
channel_value_satoshis,
output_script,
..
user_channel_id,
} => {
// Construct the raw transaction with the output that is paid the amount of the
// channel.
@@ -516,12 +516,43 @@ where
locktime,
) {
Ok(final_tx) => {
// Give the funding transaction back to LDK for opening the channel.
match self.channel_manager.funding_transaction_generated(
temporary_channel_id,
counterparty_node_id,
final_tx,
) {
let needs_manual_broadcast =
match self.liquidity_source.as_ref().map(|ls| {
ls.as_ref().lsps2_channel_needs_manual_broadcast(
counterparty_node_id,
user_channel_id,
)
}) {
Some(Ok(v)) => v,
Some(Err(e)) => {
log_error!(self.logger, "Failed to determine if channel needs manual broadcast: {:?}", e);
false
},
None => false,
};

let result = if needs_manual_broadcast {
self.liquidity_source.as_ref().map(|ls| {
ls.lsps2_store_funding_transaction(
user_channel_id,
counterparty_node_id,
final_tx.clone(),
);
});
self.channel_manager.funding_transaction_generated_manual_broadcast(
temporary_channel_id,
counterparty_node_id,
final_tx,
)
} else {
self.channel_manager.funding_transaction_generated(
temporary_channel_id,
counterparty_node_id,
final_tx,
)
};

match result {
Ok(()) => {},
Err(APIError::APIMisuseError { err }) => {
log_error!(self.logger, "Panicking due to APIMisuseError: {}", err);
@@ -560,8 +591,10 @@ where
},
}
},
LdkEvent::FundingTxBroadcastSafe { .. } => {
debug_assert!(false, "We currently only support safe funding, so this event should never be emitted.");
LdkEvent::FundingTxBroadcastSafe { user_channel_id, counterparty_node_id, .. } => {
self.liquidity_source.as_ref().map(|ls| {
ls.lsps2_funding_tx_broadcast_safe(user_channel_id, counterparty_node_id);
});
},
LdkEvent::PaymentClaimable {
payment_hash,
@@ -686,7 +719,7 @@ where
match info.kind {
PaymentKind::Bolt11 { preimage, .. }
| PaymentKind::Bolt11Jit { preimage, .. } => {
if purpose.preimage().is_none() {
if preimage.is_none() || purpose.preimage().is_none() {
debug_assert!(
preimage.is_none(),
"We would have registered the preimage if we knew"
@@ -1280,7 +1313,7 @@ where
}

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, skimmed_fee_msat);
}

let event = Event::PaymentForwarded {
9 changes: 9 additions & 0 deletions src/ffi/types.rs
Original file line number Diff line number Diff line change
@@ -1175,6 +1175,15 @@ impl UniffiCustomTypeConverter for LSPSDateTime {
}
}

/// A payable invoice and its corresponding preimage for manual claiming via a JIT channel.
#[derive(Debug, Clone)]
pub struct JitChannelManualClaim {
/// The payable invoice.
pub invoice: Arc<Bolt11Invoice>,
/// The payment preimage.
pub preimage: PaymentPreimage,
}

#[cfg(test)]
mod tests {
use std::{
96 changes: 91 additions & 5 deletions src/liquidity.rs
Original file line number Diff line number Diff line change
@@ -11,14 +11,17 @@ 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::{
Broadcaster, ChannelManager, KeysManager, LiquidityManager, PeerManager, Wallet,
};
use crate::{total_anchor_channels_reserve_sats, Config, Error};

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::util::errors::APIError;

use lightning_invoice::{Bolt11Invoice, Bolt11InvoiceDescription, InvoiceBuilder, RoutingFees};

@@ -40,6 +43,7 @@ use lightning_types::payment::PaymentHash;

use bitcoin::hashes::{sha256, Hash};
use bitcoin::secp256k1::{PublicKey, Secp256k1};
use bitcoin::Transaction;

use tokio::sync::oneshot;

@@ -55,7 +59,6 @@ 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 {
@@ -134,6 +137,8 @@ pub struct LSPS2ServiceConfig {
pub min_payment_size_msat: u64,
/// The maximum payment size that we will accept when opening a channel.
pub max_payment_size_msat: u64,
/// Use the client trusts lsp model
pub client_trusts_lsp: bool,
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm trying to understand the client_trusts_lsp configuration here. According to bLIP-52, I expect the LSP to dynamically switch from lsp_trusts_client to client_trusts_lsp mode upon detecting an attack, without requiring a restart.
However, this configuration appears to be static and set at node startup. If that's the case, what happens when:

  1. A node initially running in lsp_trusts_client mode detects an attack
  2. The node restarts with client_trusts_lsp: true to switch modes
  3. There's an existing outbound JIT channel where the client expects the LSP to broadcast the funding transaction before sending the preimage to claim.

What are the potential consequences for that in-flight JIT channel during the mode transition?

Copy link
Author

Choose a reason for hiding this comment

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

just pushed a fixup that makes the flag dynamic

There's an existing outbound JIT channel where the client expects the LSP to broadcast the funding transaction before sending the preimage to claim.

I just posted a question about this https://discord.com/channels/915026692102316113/994015949176963183/1397280758196080660

my current interpretation about this is that once lsps2.buy succeeds, that flag is part of the contract for this flow. the LSP can always abort and make you start over, but it cannot change the trust model mid negotiation

we will see what they respond on discord

Copy link
Author

Choose a reason for hiding this comment

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

my current interpretation about this is that once lsps2.buy succeeds, that flag is part of the contract for this flow. the LSP can always abort and make you start over, but it cannot change the trust model mid negotiation

I created a test that shows how this works (test lsps2_in_flight_under_attack_switch)

Copy link
Collaborator

Choose a reason for hiding this comment

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

my current interpretation about this is that once lsps2.buy succeeds, that flag is part of the contract for this flow. the LSP can always abort and make you start over, but it cannot change the trust model mid negotiation

Yes, as also noted on Discord, I agree with that interpretation and even think that for now we can leave the flag to be statically determined at startup.

}

pub(crate) struct LiquiditySourceBuilder<L: Deref>
@@ -149,6 +154,7 @@ where
chain_source: Arc<ChainSource>,
config: Arc<Config>,
logger: L,
broadcaster: Arc<Broadcaster>,
}

impl<L: Deref> LiquiditySourceBuilder<L>
@@ -158,6 +164,7 @@ where
pub(crate) fn new(
wallet: Arc<Wallet>, channel_manager: Arc<ChannelManager>, keys_manager: Arc<KeysManager>,
chain_source: Arc<ChainSource>, config: Arc<Config>, logger: L,
broadcaster: Arc<Broadcaster>,
) -> Self {
let lsps1_client = None;
let lsps2_client = None;
@@ -172,6 +179,7 @@ where
chain_source,
config,
logger,
broadcaster,
}
}

@@ -242,6 +250,7 @@ where
Arc::clone(&self.keys_manager),
Arc::clone(&self.channel_manager),
Some(Arc::clone(&self.chain_source)),
Arc::clone(&self.broadcaster),
None,
liquidity_service_config,
liquidity_client_config,
@@ -298,6 +307,79 @@ where
self.lsps2_client.as_ref().map(|s| (s.lsp_node_id, s.lsp_address.clone()))
}

pub(crate) fn lsps2_channel_needs_manual_broadcast(
&self, counterparty_node_id: PublicKey, user_channel_id: u128,
) -> Result<bool, APIError> {
// if we are not in a client_trusts_lsp model, we don't check and just return false
if !self.is_client_trusts_lsp() {
log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP.");
return Ok(false);
}

// if we are in a client_trusts_lsp model, then we check if the LSP has an LSPS2 operation in progress
self.lsps2_service.as_ref().map_or(Ok(false), |_| {
let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler();
if let Some(handler) = lsps2_service_handler {
handler.channel_needs_manual_broadcast(user_channel_id, &counterparty_node_id)
} else {
log_error!(self.logger, "LSPS2 service handler is not available.");
Ok(false)
}
})
}

pub(crate) fn lsps2_store_funding_transaction(
&self, user_channel_id: u128, counterparty_node_id: PublicKey, funding_tx: Transaction,
) {
if !self.is_client_trusts_lsp() {
log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP.");
return;
}
self.lsps2_service.as_ref().map(|_| {
let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler();
if let Some(handler) = lsps2_service_handler {
handler
.store_funding_transaction(user_channel_id, &counterparty_node_id, funding_tx)
.unwrap_or_else(|e| {
debug_assert!(false, "Failed to store funding transaction: {:?}", e);
log_error!(self.logger, "Failed to store funding transaction: {:?}", e);
});
} else {
log_error!(self.logger, "LSPS2 service handler is not available.");
}
});
}

pub(crate) fn lsps2_funding_tx_broadcast_safe(
&self, user_channel_id: u128, counterparty_node_id: PublicKey,
) {
if !self.is_client_trusts_lsp() {
log_debug!(self.logger, "Skipping funding transaction broadcast as client trusts LSP.");
return;
}
self.lsps2_service.as_ref().map(|_| {
let lsps2_service_handler = self.liquidity_manager.lsps2_service_handler();
if let Some(handler) = lsps2_service_handler {
handler
.set_funding_tx_broadcast_safe(user_channel_id, &counterparty_node_id)
.unwrap_or_else(|e| {
debug_assert!(false, "Failed to store funding transaction: {:?}", e);
log_error!(self.logger, "Failed to store funding transaction: {:?}", e);
});
} else {
log_error!(self.logger, "LSPS2 service handler is not available.");
}
});
}

fn is_client_trusts_lsp(&self) -> bool {
if let Some(lsps2_service) = self.lsps2_service.as_ref() {
lsps2_service.service_config.client_trusts_lsp
} else {
false
}
}

pub(crate) async fn handle_next_event(&self) {
match self.liquidity_manager.next_event_async().await {
LiquidityEvent::LSPS1Client(LSPS1ClientEvent::SupportedOptionsReady {
@@ -586,7 +668,7 @@ where
request_id,
intercept_scid,
LSPS2_CHANNEL_CLTV_EXPIRY_DELTA,
LSPS2_CLIENT_TRUSTS_LSP_MODE,
service_config.client_trusts_lsp,
user_channel_id,
) {
Ok(()) => {},
@@ -1296,10 +1378,14 @@ where
}
}

pub(crate) fn handle_payment_forwarded(&self, next_channel_id: Option<ChannelId>) {
pub(crate) fn handle_payment_forwarded(
&self, next_channel_id: Option<ChannelId>, skimmed_fee_msat: Option<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) {
if let Err(e) = lsps2_service_handler
.payment_forwarded(next_channel_id, skimmed_fee_msat.unwrap_or(0))
{
log_error!(
self.logger,
"LSPS2 service failed to handle PaymentForwarded: {:?}",
52 changes: 45 additions & 7 deletions src/payment/bolt11.rs
Original file line number Diff line number Diff line change
@@ -49,6 +49,14 @@ type Bolt11InvoiceDescription = LdkBolt11InvoiceDescription;
#[cfg(feature = "uniffi")]
type Bolt11InvoiceDescription = crate::ffi::Bolt11InvoiceDescription;

#[cfg(not(feature = "uniffi"))]
pub struct JitChannelManualClaim {
pub invoice: Bolt11Invoice,
pub preimage: PaymentPreimage,
}
#[cfg(feature = "uniffi")]
type JitChannelManualClaim = crate::ffi::JitChannelManualClaim;

/// A payment handler allowing to create and pay [BOLT 11] invoices.
///
/// Should be retrieved by calling [`Node::bolt11_payment`].
@@ -544,13 +552,14 @@ impl Bolt11Payment {
max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_try_convert_enum(description)?;
let invoice = 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,
None,
true,
)?;
Ok(maybe_wrap(invoice))
}
@@ -583,17 +592,40 @@ impl Bolt11Payment {
max_total_lsp_fee_limit_msat: Option<u64>, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_try_convert_enum(description)?;
let invoice = 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,
Some(payment_hash),
true,
)?;
Ok(maybe_wrap(invoice))
}

/// Returns a payable invoice for manual claiming via a JIT channel.
///
/// Similar to `receive_via_jit_channel` but requires manual claiming via `claim_for_hash`.
pub fn receive_via_jit_channel_manual_claim(
&self, amount_msat: u64, description: &Bolt11InvoiceDescription, expiry_secs: u32,
max_total_lsp_fee_limit_msat: Option<u64>,
) -> Result<JitChannelManualClaim, Error> {
let description = maybe_try_convert_enum(description)?;
let (invoice, preimage) = self.receive_via_jit_channel_inner(
Some(amount_msat),
&description,
expiry_secs,
max_total_lsp_fee_limit_msat,
None,
None,
false,
)?;
let preimage = preimage.ok_or(Error::InvoiceCreationFailed)?;
let invoice = maybe_wrap(invoice);
Ok(JitChannelManualClaim { invoice, preimage })
}

/// 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.
///
@@ -610,13 +642,14 @@ impl Bolt11Payment {
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_try_convert_enum(description)?;
let invoice = 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,
None,
true,
)?;
Ok(maybe_wrap(invoice))
}
@@ -650,13 +683,14 @@ impl Bolt11Payment {
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: PaymentHash,
) -> Result<Bolt11Invoice, Error> {
let description = maybe_try_convert_enum(description)?;
let invoice = 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,
Some(payment_hash),
true,
)?;
Ok(maybe_wrap(invoice))
}
@@ -665,7 +699,8 @@ impl Bolt11Payment {
&self, amount_msat: Option<u64>, description: &LdkBolt11InvoiceDescription,
expiry_secs: u32, max_total_lsp_fee_limit_msat: Option<u64>,
max_proportional_lsp_fee_limit_ppm_msat: Option<u64>, payment_hash: Option<PaymentHash>,
) -> Result<LdkBolt11Invoice, Error> {
auto_claim: bool,
) -> Result<(LdkBolt11Invoice, Option<PaymentPreimage>), Error> {
let liquidity_source =
self.liquidity_source.as_ref().ok_or(Error::LiquiditySourceUnavailable)?;

@@ -723,9 +758,12 @@ impl Bolt11Payment {
let id = PaymentId(payment_hash.0);
let preimage =
self.channel_manager.get_payment_preimage(payment_hash, payment_secret.clone()).ok();

let stored_preimage = if auto_claim { preimage } else { None };

let kind = PaymentKind::Bolt11Jit {
hash: payment_hash,
preimage,
preimage: stored_preimage,
secret: Some(payment_secret.clone()),
counterparty_skimmed_fee_msat: None,
lsp_fee_limits,
@@ -743,7 +781,7 @@ impl Bolt11Payment {
// Persist LSP peer to make sure we reconnect on restart.
self.peer_store.add_peer(peer_info)?;

Ok(invoice)
Ok((invoice, preimage))
}

/// Sends payment probes over all paths of a route that would be used to pay the given invoice.
1 change: 1 addition & 0 deletions src/types.rs
Original file line number Diff line number Diff line change
@@ -70,6 +70,7 @@ pub(crate) type LiquidityManager = lightning_liquidity::LiquidityManager<
Arc<ChannelManager>,
Arc<ChainSource>,
Arc<DefaultTimeProvider>,
Arc<Broadcaster>,
>;

pub(crate) type ChannelManager = lightning::ln::channelmanager::ChannelManager<
253 changes: 253 additions & 0 deletions tests/integration_tests_rust.rs
Original file line number Diff line number Diff line change
@@ -1406,6 +1406,7 @@ fn lsps2_client_service_integration() {
min_channel_lifetime: 100,
min_channel_opening_fee_msat: 0,
max_client_to_self_delay: 1024,
client_trusts_lsp: false,
};

let service_config = random_config(true);
@@ -1700,3 +1701,255 @@ async fn drop_in_async_context() {
let node = setup_node(&chain_source, config, Some(seed_bytes));
node.stop().unwrap();
}

#[test]
fn lsps2_client_trusts_lsp() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());

let sync_config = EsploraSyncConfig { background_sync_config: None };

// 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,
client_trusts_lsp: true,
};

let service_config = random_config(true);
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();
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.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.clone(), None);
let client_node = client_builder.build().unwrap();
client_node.start().unwrap();

let payer_config = random_config(true);
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();

let service_addr_onchain = service_node.onchain_payment().new_address().unwrap();
let client_addr_onchain = client_node.onchain_payment().new_address().unwrap();
let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain],
Amount::from_sat(premine_amount_sat),
);
service_node.sync_wallets().unwrap();
client_node.sync_wallets().unwrap();
payer_node.sync_wallets().unwrap();
println!("Premine complete!");
// Open a channel payer -> service that will allow paying the JIT invoice
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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len();

let invoice_description =
Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
let jit_amount_msat = 100_000_000;

println!("Generating JIT invoice!");
let res = client_node
.bolt11_payment()
.receive_via_jit_channel_manual_claim(
jit_amount_msat,
&invoice_description.into(),
1024,
None,
)
.unwrap();
let jit_invoice = res.invoice;
let preimage = res.preimage;

// 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();
println!("Payment ID: {:?}", payment_id);
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());
println!("Try to find funding tx... It won't be found yet, as the client has not claimed it.");
let mut funding_tx_found = false;
for _ in 0..50 {
std::thread::sleep(std::time::Duration::from_millis(100));
let current_mempool = bitcoind.client.get_raw_mempool().unwrap();
if current_mempool.0.len() > initial_mempool_size {
funding_tx_found = true;
break;
}
}
assert!(!funding_tx_found, "Funding transaction should NOT be broadcast yet");
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 manual_payment_hash = PaymentHash(Sha256Hash::hash(&preimage.0).to_byte_array());
let _ = expect_payment_claimable_event!(
client_node,
payment_id,
manual_payment_hash,
expected_received_amount_msat
);

client_node
.bolt11_payment()
.claim_for_hash(manual_payment_hash, jit_amount_msat, preimage)
.unwrap();

expect_payment_successful_event!(payer_node, Some(payment_id), None);

let _ = expect_payment_received_event!(client_node, expected_received_amount_msat).unwrap();

println!("Waiting for funding transaction to be broadcast...");
let mut funding_tx_found = false;
for _ in 0..500 {
std::thread::sleep(std::time::Duration::from_millis(100));
let current_mempool = bitcoind.client.get_raw_mempool().unwrap();
if current_mempool.0.len() > initial_mempool_size {
funding_tx_found = true;
break;
}
}

assert!(funding_tx_found, "Funding transaction should be broadcast after the client claims it");
}
#[test]
fn lsps2_lsp_trusts_client_but_client_does_not_claim() {
let (bitcoind, electrsd) = setup_bitcoind_and_electrsd();

let esplora_url = format!("http://{}", electrsd.esplora_url.as_ref().unwrap());

let sync_config = EsploraSyncConfig { background_sync_config: None };

// 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,
client_trusts_lsp: false,
};

let service_config = random_config(true);
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();
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.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.clone(), None);
let client_node = client_builder.build().unwrap();
client_node.start().unwrap();

let payer_config = random_config(true);
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();

let service_addr_onchain = service_node.onchain_payment().new_address().unwrap();
let client_addr_onchain = client_node.onchain_payment().new_address().unwrap();
let payer_addr_onchain = 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_onchain, client_addr_onchain, payer_addr_onchain],
Amount::from_sat(premine_amount_sat),
);
service_node.sync_wallets().unwrap();
client_node.sync_wallets().unwrap();
payer_node.sync_wallets().unwrap();
println!("Premine complete!");
// Open a channel payer -> service that will allow paying the JIT invoice
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 initial_mempool_size = bitcoind.client.get_raw_mempool().unwrap().0.len();

let invoice_description =
Bolt11InvoiceDescription::Direct(Description::new(String::from("asdf")).unwrap());
let jit_amount_msat = 100_000_000;

println!("Generating JIT invoice!");
let res = client_node
.bolt11_payment()
.receive_via_jit_channel_manual_claim(
jit_amount_msat,
&invoice_description.into(),
1024,
None,
)
.unwrap();
let jit_invoice = res.invoice;

// 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());
println!("Waiting for funding transaction to be broadcast... It will be there because LSP trusts the client, even though the client has not claimed it yet.");
let mut funding_tx_found = false;
for _ in 0..500 {
std::thread::sleep(std::time::Duration::from_millis(100));
let current_mempool = bitcoind.client.get_raw_mempool().unwrap();
if current_mempool.0.len() > initial_mempool_size {
funding_tx_found = true;
break;
}
}
assert!(funding_tx_found, "Funding transaction should be broadcast");
}