diff --git a/Cargo.lock b/Cargo.lock index ee6d2996386..879b47179e5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -64,7 +64,7 @@ checksum = "7fdf0c124883ef234a6262e43b9ed1d214e9f9c8744a88f1f2451c2b6efe4290" dependencies = [ "async-trait", "bit-vec", - "derive_more", + "derive_more 0.99.17", "log", "parity-scale-codec", ] @@ -1722,6 +1722,27 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "derive_more" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" +dependencies = [ + "derive_more-impl", +] + +[[package]] +name = "derive_more-impl" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", + "unicode-xid", +] + [[package]] name = "devimint" version = "0.6.0-alpha" @@ -2128,7 +2149,7 @@ dependencies = [ "cfg-if", "derive-deftly", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "educe", "fedimint-tor-dirmgr", "fs-mistrust", @@ -2676,6 +2697,7 @@ dependencies = [ "ldk-node", "lightning", "lightning-invoice", + "lockable", "prost 0.13.3", "rand", "reqwest 0.12.9", @@ -3278,7 +3300,7 @@ dependencies = [ "async-trait", "base64ct", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "digest", "educe", "event-listener 5.2.0", @@ -5143,6 +5165,19 @@ dependencies = [ "scopeguard", ] +[[package]] +name = "lockable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e251a4db7189cc05077d5585541ec5a8c4fb47e561edff84c46c1e35671027b" +dependencies = [ + "derive_more 1.0.0", + "futures", + "itertools 0.13.0", + "lru", + "tokio", +] + [[package]] name = "log" version = "0.4.21" @@ -6700,7 +6735,7 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a1e9cd092ef5e122f1a34f3fe15de8e9685f8f610e31c4c0643976aa5e31737" dependencies = [ - "derive_more", + "derive_more 0.99.17", "educe", "either", "fluid-let", @@ -7757,7 +7792,7 @@ dependencies = [ "bitflags 2.4.2", "bytes", "caret", - "derive_more", + "derive_more 0.99.17", "educe", "paste", "rand", @@ -7780,7 +7815,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d788592147d24f6269cea9bf43c18393905a8778540f4ab77065306e8158d105" dependencies = [ "caret", - "derive_more", + "derive_more 0.99.17", "digest", "thiserror", "tor-bytes", @@ -7796,7 +7831,7 @@ checksum = "48f70c19181bb19d58eb8e135146d617c6b7497c69e08a23c0967f42040926b4" dependencies = [ "async-trait", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "educe", "futures", "postage", @@ -7843,7 +7878,7 @@ dependencies = [ "bounded-vec-deque", "cfg-if", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "downcast-rs", "dyn-clone", "educe", @@ -7927,7 +7962,7 @@ checksum = "4ad3fc105b351b327a7f21348fa6ebf8b89c8e2106be77c94eb8d69f01dbfaa7" dependencies = [ "async-compression", "base64ct", - "derive_more", + "derive_more 0.99.17", "futures", "hex", "http 1.1.0", @@ -7954,7 +7989,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cab4f75d5d8e00890261b9ff22e7c40354fdf88b375d83a6974d8128207f4db3" dependencies = [ "backtrace", - "derive_more", + "derive_more 0.99.17", "futures", "once_cell", "paste", @@ -7975,7 +8010,7 @@ dependencies = [ "base64ct", "derive-deftly", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "dyn-clone", "educe", "futures", @@ -8014,7 +8049,7 @@ checksum = "40668cea7d86ebcff26a07ed728f6006f9a89f320e32b659990d12602ae9c980" dependencies = [ "async-trait", "derive-deftly", - "derive_more", + "derive_more 0.99.17", "educe", "either", "futures", @@ -8054,7 +8089,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6045a6105e8159e86a73d9e925c67d7ef85dec1cbf160230c02fd94430ea7192" dependencies = [ "data-encoding", - "derive_more", + "derive_more 0.99.17", "digest", "itertools 0.13.0", "paste", @@ -8080,7 +8115,7 @@ dependencies = [ "arrayvec", "derive-deftly", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "downcast-rs", "dyn-clone", "fs-mistrust", @@ -8113,7 +8148,7 @@ dependencies = [ "caret", "derive-deftly", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "hex", "itertools 0.13.0", "safelog", @@ -8138,7 +8173,7 @@ dependencies = [ "base64ct", "ctr", "curve25519-dalek", - "derive_more", + "derive_more 0.99.17", "digest", "ed25519-dalek", "educe", @@ -8182,7 +8217,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b39f8f3dcad5504634b66a4ae209a2d0dd9936752f6e5a3722c8ff6831ab5e" dependencies = [ "bitflags 2.4.2", - "derive_more", + "derive_more 0.99.17", "digest", "futures", "hex", @@ -8218,7 +8253,7 @@ dependencies = [ "bitflags 2.4.2", "cipher", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "digest", "educe", "hex", @@ -8257,7 +8292,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4c114ebe5597c0e6c5e692130f9343a46673e821f3318b69791a84076628c4e2" dependencies = [ "derive-deftly", - "derive_more", + "derive_more 0.99.17", "filetime", "fs-mistrust", "fslock", @@ -8284,7 +8319,7 @@ dependencies = [ "cipher", "coarsetime", "derive_builder_fork_arti", - "derive_more", + "derive_more 0.99.17", "digest", "educe", "futures", @@ -8353,7 +8388,7 @@ dependencies = [ "async-trait", "async_executors", "coarsetime", - "derive_more", + "derive_more 0.99.17", "educe", "futures", "futures-rustls", @@ -8378,7 +8413,7 @@ dependencies = [ "async-trait", "backtrace", "derive-deftly", - "derive_more", + "derive_more 0.99.17", "educe", "futures", "humantime", @@ -8415,7 +8450,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dae1da9b62c697ba9924f26051ed346ae3b8b57eab7b4d367cb2f5462e05902a" dependencies = [ - "derive_more", + "derive_more 0.99.17", "thiserror", ] @@ -8632,6 +8667,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "untrusted" version = "0.7.1" diff --git a/gateway/ln-gateway/Cargo.toml b/gateway/ln-gateway/Cargo.toml index fc6210d0830..40202379509 100644 --- a/gateway/ln-gateway/Cargo.toml +++ b/gateway/ln-gateway/Cargo.toml @@ -59,6 +59,7 @@ hex = { workspace = true } ldk-node = "0.4.2" lightning = { workspace = true } lightning-invoice = { workspace = true } +lockable = "0.1.1" prost = "0.13.3" rand = { workspace = true } reqwest = { workspace = true } diff --git a/gateway/ln-gateway/src/lightning/ldk.rs b/gateway/ln-gateway/src/lightning/ldk.rs index 4aeff647485..d55f7761ef7 100644 --- a/gateway/ln-gateway/src/lightning/ldk.rs +++ b/gateway/ln-gateway/src/lightning/ldk.rs @@ -15,6 +15,7 @@ use ldk_node::lightning::ln::msgs::SocketAddress; use ldk_node::lightning::ln::PaymentHash; use ldk_node::lightning::routing::gossip::NodeAlias; use ldk_node::payment::{PaymentKind, PaymentStatus, SendingParameters}; +use lightning::ln::channelmanager::PaymentId; use lightning::ln::PaymentPreimage; use lightning::util::scid_utils::scid_from_parts; use lightning_invoice::Bolt11Invoice; @@ -43,6 +44,11 @@ pub struct GatewayLdkClient { /// The HTLC stream, until it is taken by calling /// `ILnRpcClient::route_htlcs`. htlc_stream_receiver_or: Option>, + + /// Lock pool used to ensure that our implementation of `ILnRpcClient::pay` + /// doesn't allow for multiple simultaneous calls with the same invoice to + /// execute in parallel. This helps ensure that the function is idempotent. + outbound_lightning_payment_lock_pool: lockable::LockPool, } impl std::fmt::Debug for GatewayLdkClient { @@ -116,6 +122,7 @@ impl GatewayLdkClient { esplora_client: esplora_client::Builder::new(esplora_server_url).build_async()?, task_group, htlc_stream_receiver_or: Some(htlc_stream_receiver), + outbound_lightning_payment_lock_pool: lockable::LockPool::new(), }) } @@ -283,22 +290,45 @@ impl ILnRpcClient for GatewayLdkClient { max_delay: u64, max_fee: Amount, ) -> Result { - let payment_id = match self.node.bolt11_payment().send( - &invoice, - Some(SendingParameters { - max_total_routing_fee_msat: Some(Some(max_fee.msats)), - max_total_cltv_expiry_delta: Some(max_delay as u32), - max_path_count: None, - max_channel_saturation_power_of_half: None, - }), - ) { - Ok(payment_id) => payment_id, - Err(e) => { - return Err(LightningRpcError::FailedPayment { - failure_reason: format!("LDK payment failed to initialize: {e:?}"), - }); - } - }; + let payment_id = PaymentId(*invoice.payment_hash().as_byte_array()); + + // Lock by the payment hash to prevent multiple simultaneous calls with the same + // invoice from executing. This prevents `ldk-node::Bolt11Payment::send()` from + // being called multiple times with the same invoice. This is important because + // `ldk-node::Bolt11Payment::send()` is not idempotent, but this function must + // be idempotent. + let _payment_lock_guard = self + .outbound_lightning_payment_lock_pool + .async_lock(payment_id) + .await; + + // If a payment is not known to the node we can initiate it, and if it is known + // we can skip calling `ldk-node::Bolt11Payment::send()` and wait for the + // payment to complete. The lock guard above guarantees that this block is only + // executed once at a time for a given payment hash, ensuring that there is no + // race condition between checking if a payment is known and initiating a new + // payment if it isn't. + if self.node.payment(&payment_id).is_none() { + assert_eq!( + self.node + .bolt11_payment() + .send( + &invoice, + Some(SendingParameters { + max_total_routing_fee_msat: Some(Some(max_fee.msats)), + max_total_cltv_expiry_delta: Some(max_delay as u32), + max_path_count: None, + max_channel_saturation_power_of_half: None, + }), + ) + // TODO: Investigate whether all error types returned by `Bolt11Payment::send()` + // result in idempotency. + .map_err(|e| LightningRpcError::FailedPayment { + failure_reason: format!("LDK payment failed to initialize: {e:?}"), + })?, + payment_id + ); + } // TODO: Find a way to avoid looping/polling to know when a payment is // completed. `ldk-node` provides `PaymentSuccessful` and `PaymentFailed`