From b8b807124fd6a0168e7c007d37fdfda2bf54172f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 3 Dec 2024 09:45:19 +0100 Subject: [PATCH 01/15] Add `lightning-liquidity` crate to the workspace We upstream the `lightning-liquidity` into the `rust-lightning` workspace. Files are copied over as per commit c80eb75f5a31bea5c2b73e41c50ca382ec0020f8. --- Cargo.toml | 1 + ci/ci-tests.sh | 6 +- lightning-liquidity/Cargo.toml | 47 + lightning-liquidity/README.md | 20 + lightning-liquidity/src/events.rs | 236 +++ lightning-liquidity/src/lib.rs | 69 + lightning-liquidity/src/lsps0/client.rs | 155 ++ lightning-liquidity/src/lsps0/event.rs | 25 + lightning-liquidity/src/lsps0/mod.rs | 16 + lightning-liquidity/src/lsps0/msgs.rs | 216 +++ lightning-liquidity/src/lsps0/ser.rs | 713 ++++++++ lightning-liquidity/src/lsps0/service.rs | 122 ++ lightning-liquidity/src/lsps1/client.rs | 471 +++++ lightning-liquidity/src/lsps1/event.rs | 183 ++ lightning-liquidity/src/lsps1/mod.rs | 16 + lightning-liquidity/src/lsps1/msgs.rs | 473 +++++ lightning-liquidity/src/lsps1/service.rs | 459 +++++ lightning-liquidity/src/lsps2/client.rs | 359 ++++ lightning-liquidity/src/lsps2/event.rs | 125 ++ lightning-liquidity/src/lsps2/mod.rs | 17 + lightning-liquidity/src/lsps2/msgs.rs | 434 +++++ .../src/lsps2/payment_queue.rs | 119 ++ lightning-liquidity/src/lsps2/service.rs | 1603 +++++++++++++++++ lightning-liquidity/src/lsps2/utils.rs | 86 + lightning-liquidity/src/manager.rs | 665 +++++++ lightning-liquidity/src/message_queue.rs | 51 + lightning-liquidity/src/sync/mod.rs | 9 + lightning-liquidity/src/sync/nostd_sync.rs | 111 ++ lightning-liquidity/src/tests/mod.rs | 1 + lightning-liquidity/src/tests/utils.rs | 58 + lightning-liquidity/src/utils.rs | 53 + lightning-liquidity/tests/common/mod.rs | 685 +++++++ .../tests/lsps2_integration_tests.rs | 241 +++ 33 files changed, 7844 insertions(+), 1 deletion(-) create mode 100644 lightning-liquidity/Cargo.toml create mode 100644 lightning-liquidity/README.md create mode 100644 lightning-liquidity/src/events.rs create mode 100644 lightning-liquidity/src/lib.rs create mode 100644 lightning-liquidity/src/lsps0/client.rs create mode 100644 lightning-liquidity/src/lsps0/event.rs create mode 100644 lightning-liquidity/src/lsps0/mod.rs create mode 100644 lightning-liquidity/src/lsps0/msgs.rs create mode 100644 lightning-liquidity/src/lsps0/ser.rs create mode 100644 lightning-liquidity/src/lsps0/service.rs create mode 100644 lightning-liquidity/src/lsps1/client.rs create mode 100644 lightning-liquidity/src/lsps1/event.rs create mode 100644 lightning-liquidity/src/lsps1/mod.rs create mode 100644 lightning-liquidity/src/lsps1/msgs.rs create mode 100644 lightning-liquidity/src/lsps1/service.rs create mode 100644 lightning-liquidity/src/lsps2/client.rs create mode 100644 lightning-liquidity/src/lsps2/event.rs create mode 100644 lightning-liquidity/src/lsps2/mod.rs create mode 100644 lightning-liquidity/src/lsps2/msgs.rs create mode 100644 lightning-liquidity/src/lsps2/payment_queue.rs create mode 100644 lightning-liquidity/src/lsps2/service.rs create mode 100644 lightning-liquidity/src/lsps2/utils.rs create mode 100644 lightning-liquidity/src/manager.rs create mode 100644 lightning-liquidity/src/message_queue.rs create mode 100644 lightning-liquidity/src/sync/mod.rs create mode 100644 lightning-liquidity/src/sync/nostd_sync.rs create mode 100644 lightning-liquidity/src/tests/mod.rs create mode 100644 lightning-liquidity/src/tests/utils.rs create mode 100644 lightning-liquidity/src/utils.rs create mode 100644 lightning-liquidity/tests/common/mod.rs create mode 100644 lightning-liquidity/tests/lsps2_integration_tests.rs diff --git a/Cargo.toml b/Cargo.toml index 2250f06b3ee..61293c17d71 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ members = [ "lightning-transaction-sync", "lightning-macros", "lightning-dns-resolver", + "lightning-liquidity", "possiblyrandom", ] diff --git a/ci/ci-tests.sh b/ci/ci-tests.sh index 8ad6875476f..e23a57d9a15 100755 --- a/ci/ci-tests.sh +++ b/ci/ci-tests.sh @@ -35,6 +35,9 @@ PIN_RELEASE_DEPS # pin the release dependencies in our main workspace # Starting with version 0.5.9 (there is no .6-.8), the `home` crate has an MSRV of rustc 1.70.0. [ "$RUSTC_MINOR_VERSION" -lt 70 ] && cargo update -p home --precise "0.5.5" --verbose +# proptest 1.3.0 requires rustc 1.64.0 +[ "$RUSTC_MINOR_VERSION" -lt 64 ] && cargo update -p proptest --precise "1.2.0" --verbose + export RUST_BACKTRACE=1 echo -e "\n\nChecking the full workspace." @@ -55,6 +58,7 @@ WORKSPACE_MEMBERS=( lightning-transaction-sync lightning-macros lightning-dns-resolver + lightning-liquidity possiblyrandom ) @@ -107,7 +111,7 @@ echo -e "\n\nTest backtrace-debug builds" cargo test -p lightning --verbose --color always --features backtrace echo -e "\n\nTesting no_std builds" -for DIR in lightning-invoice lightning-rapid-gossip-sync; do +for DIR in lightning-invoice lightning-rapid-gossip-sync lightning-liquidity; do cargo test -p $DIR --verbose --color always --no-default-features done diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml new file mode 100644 index 00000000000..17eae8762b6 --- /dev/null +++ b/lightning-liquidity/Cargo.toml @@ -0,0 +1,47 @@ +[package] +name = "lightning-liquidity" +version = "0.1.0-alpha.6" +authors = ["John Cantrell ", "Elias Rohrer "] +homepage = "https://lightningdevkit.org/" +license = "MIT OR Apache-2.0" +edition = "2021" +description = "Types and primitives to integrate a spec-compliant LSP with an LDK-based node." +repository = "https://github.com/lightningdevkit/lightning-liquidity/" +readme = "README.md" +keywords = ["bitcoin", "lightning", "ldk", "bdk"] +categories = ["cryptography::cryptocurrencies"] + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[features] +default = ["std"] +std = [] + +[dependencies] +lightning = { version = "0.0.124", path = "../lightning", default-features = false } +lightning-types = { version = "0.1", path = "../lightning-types", default-features = false } +lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default-features = false, features = ["serde"] } + +bitcoin = { version = "0.32.2", default-features = false, features = ["serde"] } +hashbrown = { version = "0.8" } + +chrono = { version = "0.4", default-features = false, features = ["serde", "alloc"] } +serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } +serde_json = "1.0" + +[dev-dependencies] +lightning = { version = "0.0.124", path = "../lightning", default-features = false, features = ["_test_utils"] } +lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default-features = false, features = ["serde", "std"] } +lightning-persister = { version = "0.0.124", path = "../lightning-persister", default-features = false } +lightning-background-processor = { version = "0.0.124", path = "../lightning-background-processor", default-features = false, features = ["std"] } + +proptest = "1.0.0" +tokio = { version = "1.35", default-features = false, features = [ "rt-multi-thread", "time", "sync", "macros" ] } + +[lints.rust.unexpected_cfgs] +level = "forbid" +# When adding a new cfg attribute, ensure that it is added to this list. +check-cfg = [ + "cfg(lsps1_service)", + "cfg(c_bindings)", +] diff --git a/lightning-liquidity/README.md b/lightning-liquidity/README.md new file mode 100644 index 00000000000..ace18ca0a13 --- /dev/null +++ b/lightning-liquidity/README.md @@ -0,0 +1,20 @@ +# lightning-liquidity + +The goal of this crate is to provide types and primitives to integrate a spec-compliant LSP with an LDK-based node. To this end, this crate provides client-side as well as service-side logic to implement the [LSP specifications]. + +Currently the following specifications are supported: +- [LSPS0] defines the transport protocol with the LSP over which the other protocols communicate. +- [LSPS1] allows to order Lightning channels from an LSP. This is useful when the client needs +inbound Lightning liquidity for which they are willing and able to pay in bitcoin. +- [LSPS2] allows to generate a special invoice for which, when paid, an LSP will open a "just-in-time". +This is useful for the initial on-boarding of clients as the channel opening fees are deducted +from the incoming payment, i.e., no funds are required client-side to initiate this flow. + +To get started, you'll want to setup a `LiquidityManager` and configure it to be the `CustomMessageHandler` of your LDK node. You can then call `LiquidityManager::lsps1_client_handler` / `LiquidityManager::lsps2_client_handler`, or `LiquidityManager::lsps2_service_handler`, to access the respective client-side or service-side handlers. + +`LiquidityManager` uses an eventing system to notify the user about important updates to the protocol flow. To this end, you will need to handle events emitted via one of the event handling methods provided by `LiquidityManager`, e.g., `LiquidityManager::next_event`. + +[LSP specifications]: https://github.com/BitcoinAndLightningLayerSpecs/lsp +[LSPS0]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0 +[LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 +[LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2 diff --git a/lightning-liquidity/src/events.rs b/lightning-liquidity/src/events.rs new file mode 100644 index 00000000000..e71afdca442 --- /dev/null +++ b/lightning-liquidity/src/events.rs @@ -0,0 +1,236 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Events are surfaced by the library to indicate some action must be taken +//! by the end-user. +//! +//! Because we don't have a built-in runtime, it's up to the end-user to poll +//! [`LiquidityManager::get_and_clear_pending_events`] to receive events. +//! +//! [`LiquidityManager::get_and_clear_pending_events`]: crate::LiquidityManager::get_and_clear_pending_events + +use crate::lsps0; +use crate::lsps1; +use crate::lsps2; +use crate::prelude::{Vec, VecDeque}; +use crate::sync::{Arc, Mutex}; + +use core::future::Future; +use core::task::{Poll, Waker}; + +pub(crate) struct EventQueue { + queue: Arc>>, + waker: Arc>>, + #[cfg(feature = "std")] + condvar: std::sync::Condvar, +} + +impl EventQueue { + pub fn new() -> Self { + let queue = Arc::new(Mutex::new(VecDeque::new())); + let waker = Arc::new(Mutex::new(None)); + #[cfg(feature = "std")] + { + let condvar = std::sync::Condvar::new(); + Self { queue, waker, condvar } + } + #[cfg(not(feature = "std"))] + Self { queue, waker } + } + + pub fn enqueue(&self, event: Event) { + { + let mut queue = self.queue.lock().unwrap(); + queue.push_back(event); + } + + if let Some(waker) = self.waker.lock().unwrap().take() { + waker.wake(); + } + #[cfg(feature = "std")] + self.condvar.notify_one(); + } + + pub fn next_event(&self) -> Option { + self.queue.lock().unwrap().pop_front() + } + + pub async fn next_event_async(&self) -> Event { + EventFuture { event_queue: Arc::clone(&self.queue), waker: Arc::clone(&self.waker) }.await + } + + #[cfg(feature = "std")] + pub fn wait_next_event(&self) -> Event { + let mut queue = + self.condvar.wait_while(self.queue.lock().unwrap(), |queue| queue.is_empty()).unwrap(); + + let event = queue.pop_front().expect("non-empty queue"); + let should_notify = !queue.is_empty(); + + drop(queue); + + if should_notify { + if let Some(waker) = self.waker.lock().unwrap().take() { + waker.wake(); + } + + self.condvar.notify_one(); + } + + event + } + + pub fn get_and_clear_pending_events(&self) -> Vec { + self.queue.lock().unwrap().drain(..).collect() + } +} + +/// An event which you should probably take some action in response to. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Event { + /// An LSPS0 client event. + LSPS0Client(lsps0::event::LSPS0ClientEvent), + /// An LSPS1 (Channel Request) client event. + LSPS1Client(lsps1::event::LSPS1ClientEvent), + /// An LSPS1 (Channel Request) server event. + #[cfg(lsps1_service)] + LSPS1Service(lsps1::event::LSPS1ServiceEvent), + /// An LSPS2 (JIT Channel) client event. + LSPS2Client(lsps2::event::LSPS2ClientEvent), + /// An LSPS2 (JIT Channel) server event. + LSPS2Service(lsps2::event::LSPS2ServiceEvent), +} + +struct EventFuture { + event_queue: Arc>>, + waker: Arc>>, +} + +impl Future for EventFuture { + type Output = Event; + + fn poll( + self: core::pin::Pin<&mut Self>, cx: &mut core::task::Context<'_>, + ) -> core::task::Poll { + if let Some(event) = self.event_queue.lock().unwrap().pop_front() { + Poll::Ready(event) + } else { + *self.waker.lock().unwrap() = Some(cx.waker().clone()); + Poll::Pending + } + } +} + +#[cfg(test)] +mod tests { + #[tokio::test] + #[cfg(feature = "std")] + async fn event_queue_works() { + use super::*; + use crate::lsps0::event::LSPS0ClientEvent; + use bitcoin::secp256k1::{PublicKey, Secp256k1, SecretKey}; + use core::sync::atomic::{AtomicU16, Ordering}; + use std::sync::Arc; + use std::time::Duration; + + let event_queue = Arc::new(EventQueue::new()); + assert_eq!(event_queue.next_event(), None); + + let secp_ctx = Secp256k1::new(); + let counterparty_node_id = + PublicKey::from_secret_key(&secp_ctx, &SecretKey::from_slice(&[42; 32]).unwrap()); + let expected_event = Event::LSPS0Client(LSPS0ClientEvent::ListProtocolsResponse { + counterparty_node_id, + protocols: Vec::new(), + }); + + for _ in 0..3 { + event_queue.enqueue(expected_event.clone()); + } + + assert_eq!(event_queue.wait_next_event(), expected_event); + assert_eq!(event_queue.next_event_async().await, expected_event); + assert_eq!(event_queue.next_event(), Some(expected_event.clone())); + assert_eq!(event_queue.next_event(), None); + + // Check `next_event_async` won't return if the queue is empty and always rather timeout. + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(10)) => { + // Timeout + } + _ = event_queue.next_event_async() => { + panic!(); + } + } + assert_eq!(event_queue.next_event(), None); + + // Check we get the expected number of events when polling/enqueuing concurrently. + let enqueued_events = AtomicU16::new(0); + let received_events = AtomicU16::new(0); + let mut delayed_enqueue = false; + + for _ in 0..25 { + event_queue.enqueue(expected_event.clone()); + enqueued_events.fetch_add(1, Ordering::SeqCst); + } + + loop { + tokio::select! { + _ = tokio::time::sleep(Duration::from_millis(10)), if !delayed_enqueue => { + event_queue.enqueue(expected_event.clone()); + enqueued_events.fetch_add(1, Ordering::SeqCst); + delayed_enqueue = true; + } + e = event_queue.next_event_async() => { + assert_eq!(e, expected_event); + received_events.fetch_add(1, Ordering::SeqCst); + + event_queue.enqueue(expected_event.clone()); + enqueued_events.fetch_add(1, Ordering::SeqCst); + } + e = event_queue.next_event_async() => { + assert_eq!(e, expected_event); + received_events.fetch_add(1, Ordering::SeqCst); + } + } + + if delayed_enqueue + && received_events.load(Ordering::SeqCst) == enqueued_events.load(Ordering::SeqCst) + { + break; + } + } + assert_eq!(event_queue.next_event(), None); + + // Check we operate correctly, even when mixing and matching blocking and async API calls. + let (tx, mut rx) = tokio::sync::watch::channel(()); + let thread_queue = Arc::clone(&event_queue); + let thread_event = expected_event.clone(); + std::thread::spawn(move || { + let e = thread_queue.wait_next_event(); + assert_eq!(e, thread_event); + tx.send(()).unwrap(); + }); + + let thread_queue = Arc::clone(&event_queue); + let thread_event = expected_event.clone(); + std::thread::spawn(move || { + // Sleep a bit before we enqueue the events everybody is waiting for. + std::thread::sleep(Duration::from_millis(20)); + thread_queue.enqueue(thread_event.clone()); + thread_queue.enqueue(thread_event.clone()); + }); + + let e = event_queue.next_event_async().await; + assert_eq!(e, expected_event.clone()); + + rx.changed().await.unwrap(); + assert_eq!(event_queue.next_event(), None); + } +} diff --git a/lightning-liquidity/src/lib.rs b/lightning-liquidity/src/lib.rs new file mode 100644 index 00000000000..f665dcdc2b1 --- /dev/null +++ b/lightning-liquidity/src/lib.rs @@ -0,0 +1,69 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. +#![crate_name = "lightning_liquidity"] + +//! The goal of this crate is to provide types and primitives to integrate a spec-compliant LSP with an LDK-based node. To this end, this crate provides client-side as well as service-side logic to implement the [LSP specifications]. +//! +//! Currently the following specifications are supported: +//! - [LSPS0] defines the transport protocol with the LSP over which the other protocols communicate. +//! - [LSPS1] allows to order Lightning channels from an LSP. This is useful when the client needs +//! inbound Lightning liquidity for which they are willing and able to pay in bitcoin. +//! - [LSPS2] allows to generate a special invoice for which, when paid, an LSP will open a "just-in-time". +//! This is useful for the initial on-boarding of clients as the channel opening fees are deducted +//! from the incoming payment, i.e., no funds are required client-side to initiate this flow. +//! +//! To get started, you'll want to setup a [`LiquidityManager`] and configure it to be the +//! [`CustomMessageHandler`] of your LDK node. You can then for example call +//! [`LiquidityManager::lsps1_client_handler`] / [`LiquidityManager::lsps2_client_handler`], or +//! [`LiquidityManager::lsps2_service_handler`], to access the respective client-side or +//! service-side handlers. +//! +//! [`LiquidityManager`] uses an eventing system to notify the user about important updates to the +//! protocol flow. To this end, you will need to handle events emitted via one of the event +//! handling methods provided by [`LiquidityManager`], e.g., [`LiquidityManager::next_event`]. +//! +//! [LSP specifications]: https://github.com/BitcoinAndLightningLayerSpecs/lsp +//! [LSPS0]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0 +//! [LSPS1]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1 +//! [LSPS2]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2 +//! [`CustomMessageHandler`]: lightning::ln::peer_handler::CustomMessageHandler +//! [`LiquidityManager::next_event`]: crate::LiquidityManager::next_event +#![deny(missing_docs)] +#![deny(rustdoc::broken_intra_doc_links)] +#![deny(rustdoc::private_intra_doc_links)] +#![allow(bare_trait_objects)] +#![allow(ellipsis_inclusive_range_patterns)] +#![allow(clippy::drop_non_drop)] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(not(feature = "std"), no_std)] + +#[macro_use] +extern crate alloc; + +mod prelude { + #![allow(unused_imports)] + pub use alloc::{boxed::Box, collections::VecDeque, string::String, vec, vec::Vec}; + pub use hashbrown::{hash_map, HashMap, HashSet}; + + pub use alloc::borrow::ToOwned; + pub use alloc::string::ToString; +} + +pub mod events; +pub mod lsps0; +pub mod lsps1; +pub mod lsps2; +mod manager; +pub mod message_queue; +mod sync; +#[cfg(test)] +mod tests; +mod utils; + +pub use manager::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig}; diff --git a/lightning-liquidity/src/lsps0/client.rs b/lightning-liquidity/src/lsps0/client.rs new file mode 100644 index 00000000000..a04cf86289b --- /dev/null +++ b/lightning-liquidity/src/lsps0/client.rs @@ -0,0 +1,155 @@ +//! Contains the main LSPS2 client-side object, [`LSPS0ClientHandler`]. +//! +//! Please refer to the [LSPS0 +//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more +//! information. + +use crate::events::{Event, EventQueue}; +use crate::lsps0::event::LSPS0ClientEvent; +use crate::lsps0::msgs::{ + LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsRequest, ListProtocolsResponse, +}; +use crate::lsps0::ser::{ProtocolMessageHandler, ResponseError}; +use crate::message_queue::MessageQueue; +use crate::sync::Arc; +use crate::utils; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +/// A message handler capable of sending and handling LSPS0 messages. +pub struct LSPS0ClientHandler +where + ES::Target: EntropySource, +{ + entropy_source: ES, + pending_messages: Arc, + pending_events: Arc, +} + +impl LSPS0ClientHandler +where + ES::Target: EntropySource, +{ + /// Returns a new instance of [`LSPS0ClientHandler`]. + pub(crate) fn new( + entropy_source: ES, pending_messages: Arc, pending_events: Arc, + ) -> Self { + Self { entropy_source, pending_messages, pending_events } + } + + /// Calls LSPS0's `list_protocols`. + /// + /// Please refer to the [LSPS0 + /// specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) + /// for more information. + pub fn list_protocols(&self, counterparty_node_id: &PublicKey) { + let msg = LSPS0Message::Request( + utils::generate_request_id(&self.entropy_source), + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + ); + + self.pending_messages.enqueue(counterparty_node_id, msg.into()); + } + + fn handle_response( + &self, response: LSPS0Response, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match response { + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols }) => { + self.pending_events.enqueue(Event::LSPS0Client( + LSPS0ClientEvent::ListProtocolsResponse { + counterparty_node_id: *counterparty_node_id, + protocols, + }, + )); + Ok(()) + }, + LSPS0Response::ListProtocolsError(ResponseError { code, message, data, .. }) => { + Err(LightningError { + err: format!( + "ListProtocols error received. code = {}, message = {}, data = {:?}", + code, message, data + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + }, + } + } +} + +impl ProtocolMessageHandler for LSPS0ClientHandler +where + ES::Target: EntropySource, +{ + type ProtocolMessage = LSPS0Message; + const PROTOCOL_NUMBER: Option = None; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS0Message::Response(_, response) => { + self.handle_response(response, counterparty_node_id) + }, + LSPS0Message::Request(..) => { + debug_assert!( + false, + "Client handler received LSPS0 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS0 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +#[cfg(test)] +mod tests { + + use alloc::string::ToString; + use alloc::sync::Arc; + + use crate::lsps0::ser::{LSPSMessage, RequestId}; + use crate::tests::utils::{self, TestEntropy}; + + use super::*; + + #[test] + fn test_list_protocols() { + let pending_messages = Arc::new(MessageQueue::new()); + let entropy_source = Arc::new(TestEntropy {}); + let event_queue = Arc::new(EventQueue::new()); + + let lsps0_handler = Arc::new(LSPS0ClientHandler::new( + entropy_source, + Arc::clone(&pending_messages), + event_queue, + )); + + let counterparty_node_id = utils::parse_pubkey( + "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", + ) + .unwrap(); + + lsps0_handler.list_protocols(&counterparty_node_id); + let pending_messages = pending_messages.get_and_clear_pending_msgs(); + + assert_eq!(pending_messages.len(), 1); + + let (pubkey, message) = &pending_messages[0]; + + assert_eq!(*pubkey, counterparty_node_id); + assert_eq!( + *message, + LSPSMessage::LSPS0(LSPS0Message::Request( + RequestId("00000000000000000000000000000000".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}) + )) + ); + } +} diff --git a/lightning-liquidity/src/lsps0/event.rs b/lightning-liquidity/src/lsps0/event.rs new file mode 100644 index 00000000000..163114ddb54 --- /dev/null +++ b/lightning-liquidity/src/lsps0/event.rs @@ -0,0 +1,25 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains LSPS0 event types + +use crate::prelude::Vec; +use bitcoin::secp256k1::PublicKey; + +/// An event which an LSPS0 client may want to take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS0ClientEvent { + /// Information from the LSP about the protocols they support. + ListProtocolsResponse { + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// A list of supported protocols. + protocols: Vec, + }, +} diff --git a/lightning-liquidity/src/lsps0/mod.rs b/lightning-liquidity/src/lsps0/mod.rs new file mode 100644 index 00000000000..4211ef5c2d6 --- /dev/null +++ b/lightning-liquidity/src/lsps0/mod.rs @@ -0,0 +1,16 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Types and primitives that implement the LSPS0: Transport Layer specification. + +pub mod client; +pub mod event; +pub mod msgs; +pub mod ser; +pub mod service; diff --git a/lightning-liquidity/src/lsps0/msgs.rs b/lightning-liquidity/src/lsps0/msgs.rs new file mode 100644 index 00000000000..2366aa67cf4 --- /dev/null +++ b/lightning-liquidity/src/lsps0/msgs.rs @@ -0,0 +1,216 @@ +//! Message, request, and other primitive types used to implement LSPS1. + +use crate::lsps0::ser::{LSPSMessage, RequestId, ResponseError}; +use crate::prelude::Vec; + +use serde::{Deserialize, Serialize}; + +use core::convert::TryFrom; + +pub(crate) const LSPS0_LISTPROTOCOLS_METHOD_NAME: &str = "lsps0.list_protocols"; + +/// A `list_protocols` request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] +pub struct ListProtocolsRequest {} + +/// A response to a `list_protocols` request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0#lsps-specification-support-query) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ListProtocolsResponse { + /// A list of supported protocols. + pub protocols: Vec, +} + +/// An LSPS0 protocol request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS0Request { + /// A request calling `list_protocols`. + ListProtocols(ListProtocolsRequest), +} + +impl LSPS0Request { + /// Returns the method name associated with the given request variant. + pub fn method(&self) -> &str { + match self { + LSPS0Request::ListProtocols(_) => LSPS0_LISTPROTOCOLS_METHOD_NAME, + } + } +} + +/// An LSPS0 protocol request. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS0Response { + /// A response to a `list_protocols` request. + ListProtocols(ListProtocolsResponse), + /// An error response to a `list_protocols` request. + ListProtocolsError(ResponseError), +} + +/// An LSPS0 protocol message. +/// +/// Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS0Message { + /// A request variant. + Request(RequestId, LSPS0Request), + /// A response variant. + Response(RequestId, LSPS0Response), +} + +impl TryFrom for LSPS0Message { + type Error = (); + + fn try_from(message: LSPSMessage) -> Result { + match message { + LSPSMessage::Invalid(_) => Err(()), + LSPSMessage::LSPS0(message) => Ok(message), + LSPSMessage::LSPS1(_) => Err(()), + LSPSMessage::LSPS2(_) => Err(()), + } + } +} + +impl From for LSPSMessage { + fn from(message: LSPS0Message) -> Self { + LSPSMessage::LSPS0(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::lsps0::ser::LSPSMethod; + use crate::prelude::{HashMap, ToString}; + + #[test] + fn deserializes_request() { + let json = r#"{ + "jsonrpc": "2.0", + "id": "request:id:xyz123", + "method": "lsps0.list_protocols" + }"#; + + let mut request_id_method_map = HashMap::new(); + + let msg = LSPSMessage::from_str_with_id_map(json, &mut request_id_method_map); + assert!(msg.is_ok()); + let msg = msg.unwrap(); + assert_eq!( + msg, + LSPSMessage::LSPS0(LSPS0Message::Request( + RequestId("request:id:xyz123".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}) + )) + ); + } + + #[test] + fn serializes_request() { + let request = LSPSMessage::LSPS0(LSPS0Message::Request( + RequestId("request:id:xyz123".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + )); + let json = serde_json::to_string(&request).unwrap(); + assert_eq!( + json, + r#"{"jsonrpc":"2.0","id":"request:id:xyz123","method":"lsps0.list_protocols","params":{}}"# + ); + } + + #[test] + fn deserializes_success_response() { + let json = r#"{ + "jsonrpc": "2.0", + "id": "request:id:xyz123", + "result": { + "protocols": [1,2,3] + } + }"#; + let mut request_id_to_method_map = HashMap::new(); + request_id_to_method_map + .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); + + let response = + LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map).unwrap(); + + assert_eq!( + response, + LSPSMessage::LSPS0(LSPS0Message::Response( + RequestId("request:id:xyz123".to_string()), + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![1, 2, 3] }) + )) + ); + } + + #[test] + fn deserializes_error_response() { + let json = r#"{ + "jsonrpc": "2.0", + "id": "request:id:xyz123", + "error": { + "code": -32617, + "message": "Unknown Error" + } + }"#; + let mut request_id_to_method_map = HashMap::new(); + request_id_to_method_map + .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); + + let response = + LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map).unwrap(); + + assert_eq!( + response, + LSPSMessage::LSPS0(LSPS0Message::Response( + RequestId("request:id:xyz123".to_string()), + LSPS0Response::ListProtocolsError(ResponseError { + code: -32617, + message: "Unknown Error".to_string(), + data: None + }) + )) + ); + } + + #[test] + fn deserialize_fails_with_unknown_request_id() { + let json = r#"{ + "jsonrpc": "2.0", + "id": "request:id:xyz124", + "result": { + "protocols": [1,2,3] + } + }"#; + let mut request_id_to_method_map = HashMap::new(); + request_id_to_method_map + .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); + + let response = LSPSMessage::from_str_with_id_map(json, &mut request_id_to_method_map); + assert!(response.is_err()); + } + + #[test] + fn serializes_response() { + let response = LSPSMessage::LSPS0(LSPS0Message::Response( + RequestId("request:id:xyz123".to_string()), + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![1, 2, 3] }), + )); + let json = serde_json::to_string(&response).unwrap(); + assert_eq!( + json, + r#"{"jsonrpc":"2.0","id":"request:id:xyz123","result":{"protocols":[1,2,3]}}"# + ); + } +} diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs new file mode 100644 index 00000000000..e73ac81c965 --- /dev/null +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -0,0 +1,713 @@ +//! Contains basic data types that allow for the (de-)seralization of LSPS messages in the JSON-RPC 2.0 format. +//! +//! Please refer to the [LSPS0 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more information. + +use crate::lsps0::msgs::{ + LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsRequest, + LSPS0_LISTPROTOCOLS_METHOD_NAME, +}; + +use crate::lsps1::msgs::{ + LSPS1Message, LSPS1Request, LSPS1Response, LSPS1_CREATE_ORDER_METHOD_NAME, + LSPS1_GET_INFO_METHOD_NAME, LSPS1_GET_ORDER_METHOD_NAME, +}; +use crate::lsps2::msgs::{ + LSPS2Message, LSPS2Request, LSPS2Response, LSPS2_BUY_METHOD_NAME, LSPS2_GET_INFO_METHOD_NAME, +}; +use crate::prelude::{HashMap, String, ToString}; + +use lightning::ln::msgs::LightningError; +use lightning::ln::wire; +use lightning::util::ser::WithoutLength; + +use bitcoin::secp256k1::PublicKey; + +use core::fmt::{self, Display}; +use core::str::FromStr; + +use serde::de::{self, MapAccess, Visitor}; +use serde::ser::SerializeStruct; +use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::json; + +pub(crate) const LSPS_MESSAGE_SERIALIZED_STRUCT_NAME: &str = "LSPSMessage"; +pub(crate) const JSONRPC_FIELD_KEY: &str = "jsonrpc"; +pub(crate) const JSONRPC_FIELD_VALUE: &str = "2.0"; +pub(crate) const JSONRPC_METHOD_FIELD_KEY: &str = "method"; +pub(crate) const JSONRPC_ID_FIELD_KEY: &str = "id"; +pub(crate) const JSONRPC_PARAMS_FIELD_KEY: &str = "params"; +pub(crate) const JSONRPC_RESULT_FIELD_KEY: &str = "result"; +pub(crate) const JSONRPC_ERROR_FIELD_KEY: &str = "error"; +pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_CODE: i32 = -32700; +pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE: &str = "parse error"; + +pub(crate) const _LSPS0_CLIENT_REJECTED_ERROR_CODE: i32 = 1; + +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub(crate) enum LSPSMethod { + LSPS0ListProtocols, + LSPS1GetInfo, + LSPS1GetOrder, + LSPS1CreateOrder, + LSPS2GetInfo, + LSPS2Buy, +} + +impl FromStr for LSPSMethod { + type Err = &'static str; + fn from_str(s: &str) -> Result { + match s { + LSPS0_LISTPROTOCOLS_METHOD_NAME => Ok(Self::LSPS0ListProtocols), + LSPS1_GET_INFO_METHOD_NAME => Ok(Self::LSPS1GetInfo), + LSPS1_CREATE_ORDER_METHOD_NAME => Ok(Self::LSPS1CreateOrder), + LSPS1_GET_ORDER_METHOD_NAME => Ok(Self::LSPS1GetOrder), + LSPS2_GET_INFO_METHOD_NAME => Ok(Self::LSPS2GetInfo), + LSPS2_BUY_METHOD_NAME => Ok(Self::LSPS2Buy), + _ => Err(&"Unknown method name"), + } + } +} + +impl Display for LSPSMethod { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let s = match self { + Self::LSPS0ListProtocols => LSPS0_LISTPROTOCOLS_METHOD_NAME, + Self::LSPS1GetInfo => LSPS1_GET_INFO_METHOD_NAME, + Self::LSPS1CreateOrder => LSPS1_CREATE_ORDER_METHOD_NAME, + Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME, + Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME, + Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME, + }; + write!(f, "{}", s) + } +} + +impl From<&LSPS0Request> for LSPSMethod { + fn from(value: &LSPS0Request) -> Self { + match value { + LSPS0Request::ListProtocols(_) => Self::LSPS0ListProtocols, + } + } +} + +impl From<&LSPS1Request> for LSPSMethod { + fn from(value: &LSPS1Request) -> Self { + match value { + LSPS1Request::GetInfo(_) => Self::LSPS1GetInfo, + LSPS1Request::CreateOrder(_) => Self::LSPS1CreateOrder, + LSPS1Request::GetOrder(_) => Self::LSPS1GetOrder, + } + } +} + +impl From<&LSPS2Request> for LSPSMethod { + fn from(value: &LSPS2Request) -> Self { + match value { + LSPS2Request::GetInfo(_) => Self::LSPS2GetInfo, + LSPS2Request::Buy(_) => Self::LSPS2Buy, + } + } +} + +impl<'de> Deserialize<'de> for LSPSMethod { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = <&str>::deserialize(deserializer)?; + FromStr::from_str(&s).map_err(de::Error::custom) + } +} + +impl Serialize for LSPSMethod { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_str(&self.to_string()) + } +} + +/// The Lightning message type id for LSPS messages. +pub const LSPS_MESSAGE_TYPE_ID: u16 = 37913; + +/// A trait used to implement a specific LSPS protocol. +/// +/// The messages the protocol uses need to be able to be mapped +/// from and into [`LSPSMessage`]. +pub(crate) trait ProtocolMessageHandler { + type ProtocolMessage: TryFrom + Into; + const PROTOCOL_NUMBER: Option; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError>; +} + +/// Lightning message type used by LSPS protocols. +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct RawLSPSMessage { + /// The raw string payload that holds the actual message. + pub payload: String, +} + +// We encode `RawLSPSMessage`'s payload without a length prefix as LSPS0 expects it to be the +// remainder of the object. +impl lightning::util::ser::Writeable for RawLSPSMessage { + fn write( + &self, w: &mut W, + ) -> Result<(), lightning::io::Error> { + WithoutLength(&self.payload).write(w)?; + Ok(()) + } +} + +impl lightning::util::ser::Readable for RawLSPSMessage { + fn read(r: &mut R) -> Result { + let payload_without_length = WithoutLength::read(r)?; + Ok(Self { payload: payload_without_length.0 }) + } +} + +impl wire::Type for RawLSPSMessage { + fn type_id(&self) -> u16 { + LSPS_MESSAGE_TYPE_ID + } +} + +/// A JSON-RPC request's `id`. +/// +/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#request_object) for +/// more information. +#[derive(Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)] +#[serde(transparent)] +pub struct RequestId(pub String); + +/// An error returned in response to an JSON-RPC request. +/// +/// Please refer to the [JSON-RPC 2.0 specification](https://www.jsonrpc.org/specification#error_object) for +/// more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ResponseError { + /// A number that indicates the error type that occurred. + pub code: i32, + /// A string providing a short description of the error. + pub message: String, + /// A primitive or structured value that contains additional information about the error. + pub data: Option, +} + +/// A (de-)serializable LSPS message allowing to be sent over the wire. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPSMessage { + /// An invalid variant. + Invalid(ResponseError), + /// An LSPS0 message. + LSPS0(LSPS0Message), + /// An LSPS1 message. + LSPS1(LSPS1Message), + /// An LSPS2 message. + LSPS2(LSPS2Message), +} + +impl LSPSMessage { + /// A constructor returning an `LSPSMessage` from a raw JSON string. + /// + /// The given `request_id_to_method` associates request ids with method names, as response objects + /// don't carry the latter. + pub(crate) fn from_str_with_id_map( + json_str: &str, request_id_to_method_map: &mut HashMap, + ) -> Result { + let deserializer = &mut serde_json::Deserializer::from_str(json_str); + let visitor = LSPSMessageVisitor { request_id_to_method_map }; + deserializer.deserialize_any(visitor) + } + + /// Returns the request id and the method. + pub(crate) fn get_request_id_and_method(&self) -> Option<(RequestId, LSPSMethod)> { + match self { + LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { + Some((RequestId(request_id.0.clone()), request.into())) + }, + LSPSMessage::LSPS1(LSPS1Message::Request(request_id, request)) => { + Some((RequestId(request_id.0.clone()), request.into())) + }, + LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { + Some((RequestId(request_id.0.clone()), request.into())) + }, + _ => None, + } + } +} + +impl Serialize for LSPSMessage { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let mut jsonrpc_object = + serializer.serialize_struct(LSPS_MESSAGE_SERIALIZED_STRUCT_NAME, 3)?; + + jsonrpc_object.serialize_field(JSONRPC_FIELD_KEY, JSONRPC_FIELD_VALUE)?; + + match self { + LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object + .serialize_field(JSONRPC_METHOD_FIELD_KEY, &LSPSMethod::from(request))?; + + match request { + LSPS0Request::ListProtocols(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + }; + }, + LSPSMessage::LSPS0(LSPS0Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + LSPS0Response::ListProtocols(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)?; + }, + LSPS0Response::ListProtocolsError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)?; + }, + } + }, + LSPSMessage::LSPS1(LSPS1Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object + .serialize_field(JSONRPC_METHOD_FIELD_KEY, &LSPSMethod::from(request))?; + + match request { + LSPS1Request::GetInfo(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + LSPS1Request::CreateOrder(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + LSPS1Request::GetOrder(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + } + }, + LSPSMessage::LSPS1(LSPS1Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + LSPS1Response::GetInfo(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + LSPS1Response::GetInfoError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + }, + LSPS1Response::CreateOrder(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + LSPS1Response::CreateOrderError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + }, + LSPS1Response::GetOrder(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + LSPS1Response::GetOrderError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + }, + } + }, + LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object + .serialize_field(JSONRPC_METHOD_FIELD_KEY, &LSPSMethod::from(request))?; + + match request { + LSPS2Request::GetInfo(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + LSPS2Request::Buy(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + }, + } + }, + LSPSMessage::LSPS2(LSPS2Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + LSPS2Response::GetInfo(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + LSPS2Response::GetInfoError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + }, + LSPS2Response::Buy(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + }, + LSPS2Response::BuyError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + }, + } + }, + LSPSMessage::Invalid(error) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &serde_json::Value::Null)?; + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, &error)?; + }, + } + + jsonrpc_object.end() + } +} + +struct LSPSMessageVisitor<'a> { + request_id_to_method_map: &'a mut HashMap, +} + +impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { + type Value = LSPSMessage; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("JSON-RPC object") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut id: Option = None; + let mut method: Option = None; + let mut params = None; + let mut result = None; + let mut error: Option = None; + + while let Some(key) = map.next_key()? { + match key { + "id" => { + id = map.next_value()?; + }, + "method" => { + method = Some(map.next_value()?); + }, + "params" => { + params = Some(map.next_value()?); + }, + "result" => { + result = Some(map.next_value()?); + }, + "error" => { + error = Some(map.next_value()?); + }, + _ => { + let _: serde_json::Value = map.next_value()?; + }, + } + } + + let id = match id { + Some(id) => id, + None => { + if let Some(method) = method { + return Err(de::Error::custom(format!( + "Received unknown notification: {}", + method + ))); + } else { + if let Some(error) = error { + if error.code == JSONRPC_INVALID_MESSAGE_ERROR_CODE { + return Ok(LSPSMessage::Invalid(error)); + } + } + + return Err(de::Error::custom("Received unknown error message")); + } + }, + }; + + match method { + Some(method) => match method { + LSPSMethod::LSPS0ListProtocols => Ok(LSPSMessage::LSPS0(LSPS0Message::Request( + id, + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + ))), + LSPSMethod::LSPS1GetInfo => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Request( + id, + LSPS1Request::GetInfo(request), + ))) + }, + LSPSMethod::LSPS1CreateOrder => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Request( + id, + LSPS1Request::CreateOrder(request), + ))) + }, + LSPSMethod::LSPS1GetOrder => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Request( + id, + LSPS1Request::GetOrder(request), + ))) + }, + LSPSMethod::LSPS2GetInfo => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( + id, + LSPS2Request::GetInfo(request), + ))) + }, + LSPSMethod::LSPS2Buy => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Request(id, LSPS2Request::Buy(request)))) + }, + }, + None => match self.request_id_to_method_map.remove(&id) { + Some(method) => match method { + LSPSMethod::LSPS0ListProtocols => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS0(LSPS0Message::Response( + id, + LSPS0Response::ListProtocolsError(error), + ))) + } else if let Some(result) = result { + let list_protocols_response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS0(LSPS0Message::Response( + id, + LSPS0Response::ListProtocols(list_protocols_response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + LSPSMethod::LSPS1GetInfo => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::GetInfoError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::GetInfo(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + LSPSMethod::LSPS1CreateOrder => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::CreateOrderError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::CreateOrder(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + LSPSMethod::LSPS1GetOrder => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::GetOrderError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS1(LSPS1Message::Response( + id, + LSPS1Response::GetOrder(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + LSPSMethod::LSPS2GetInfo => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + id, + LSPS2Response::GetInfoError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + id, + LSPS2Response::GetInfo(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + LSPSMethod::LSPS2Buy => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + id, + LSPS2Response::BuyError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( + id, + LSPS2Response::Buy(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + }, + }, + None => Err(de::Error::custom(format!( + "Received response for unknown request id: {}", + id.0 + ))), + }, + } + } +} + +pub(crate) mod string_amount { + use crate::prelude::{String, ToString}; + use core::str::FromStr; + use serde::de::Unexpected; + use serde::{Deserialize, Deserializer, Serializer}; + + pub(crate) fn serialize(x: &u64, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&x.to_string()) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let buf = String::deserialize(deserializer)?; + + u64::from_str(&buf).map_err(|_| { + serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid u64 amount string") + }) + } +} + +pub(crate) mod string_amount_option { + use crate::prelude::{String, ToString}; + use core::str::FromStr; + use serde::de::Unexpected; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub(crate) fn serialize(x: &Option, s: S) -> Result + where + S: Serializer, + { + let v = x.as_ref().map(|v| v.to_string()); + Option::::serialize(&v, s) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + if let Some(buf) = Option::::deserialize(deserializer)? { + let val = u64::from_str(&buf).map_err(|_| { + serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid u64 amount string") + })?; + Ok(Some(val)) + } else { + Ok(None) + } + } +} + +pub(crate) mod unchecked_address { + use crate::prelude::{String, ToString}; + use bitcoin::Address; + use core::str::FromStr; + use serde::de::Unexpected; + use serde::{Deserialize, Deserializer, Serializer}; + + pub(crate) fn serialize(x: &Address, s: S) -> Result + where + S: Serializer, + { + s.serialize_str(&x.to_string()) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let buf = String::deserialize(deserializer)?; + + let parsed_addr = Address::from_str(&buf).map_err(|_| { + serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid address string") + })?; + Ok(parsed_addr.assume_checked()) + } +} + +pub(crate) mod unchecked_address_option { + use crate::prelude::{String, ToString}; + use bitcoin::Address; + use core::str::FromStr; + use serde::de::Unexpected; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + pub(crate) fn serialize(x: &Option
, s: S) -> Result + where + S: Serializer, + { + let v = x.as_ref().map(|v| v.to_string()); + Option::::serialize(&v, s) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + if let Some(buf) = Option::::deserialize(deserializer)? { + let val = Address::from_str(&buf).map_err(|_| { + serde::de::Error::invalid_value(Unexpected::Str(&buf), &"invalid address string") + })?; + Ok(Some(val.assume_checked())) + } else { + Ok(None) + } + } +} + +pub(crate) mod u32_fee_rate { + use bitcoin::FeeRate; + use serde::{Deserialize, Deserializer, Serializer}; + + pub(crate) fn serialize(x: &FeeRate, s: S) -> Result + where + S: Serializer, + { + let fee_rate_sat_kwu = x.to_sat_per_kwu(); + s.serialize_u32(fee_rate_sat_kwu as u32) + } + + pub(crate) fn deserialize<'de, D>(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let fee_rate_sat_kwu = u32::deserialize(deserializer)?; + + Ok(FeeRate::from_sat_per_kwu(fee_rate_sat_kwu as u64)) + } +} diff --git a/lightning-liquidity/src/lsps0/service.rs b/lightning-liquidity/src/lsps0/service.rs new file mode 100644 index 00000000000..bc52fa11dd9 --- /dev/null +++ b/lightning-liquidity/src/lsps0/service.rs @@ -0,0 +1,122 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS0 server-side object, [`LSPS0ServiceHandler`]. +//! +//! Please refer to the [LSPS0 +//! specifcation](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS0) for more +//! information. + +use crate::lsps0::msgs::{LSPS0Message, LSPS0Request, LSPS0Response, ListProtocolsResponse}; +use crate::lsps0::ser::{ProtocolMessageHandler, RequestId}; +use crate::message_queue::MessageQueue; +use crate::prelude::Vec; +use crate::sync::Arc; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +/// The main server-side object allowing to send and receive LSPS0 messages. +pub struct LSPS0ServiceHandler { + pending_messages: Arc, + protocols: Vec, +} + +impl LSPS0ServiceHandler { + /// Returns a new instance of [`LSPS0ServiceHandler`]. + pub(crate) fn new(protocols: Vec, pending_messages: Arc) -> Self { + Self { protocols, pending_messages } + } + + fn handle_request( + &self, request_id: RequestId, request: LSPS0Request, counterparty_node_id: &PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + match request { + LSPS0Request::ListProtocols(_) => { + let msg = LSPS0Message::Response( + request_id, + LSPS0Response::ListProtocols(ListProtocolsResponse { + protocols: self.protocols.clone(), + }), + ); + self.pending_messages.enqueue(counterparty_node_id, msg.into()); + Ok(()) + }, + } + } +} + +impl ProtocolMessageHandler for LSPS0ServiceHandler { + type ProtocolMessage = LSPS0Message; + const PROTOCOL_NUMBER: Option = None; + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS0Message::Request(request_id, request) => { + self.handle_request(request_id, request, counterparty_node_id) + }, + LSPS0Message::Response(..) => { + debug_assert!( + false, + "Service handler received LSPS0 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS0 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +#[cfg(test)] +mod tests { + + use crate::lsps0::msgs::ListProtocolsRequest; + use crate::lsps0::ser::LSPSMessage; + use crate::tests::utils; + use alloc::string::ToString; + use alloc::sync::Arc; + + use super::*; + + #[test] + fn test_handle_list_protocols_request() { + let protocols: Vec = vec![]; + let pending_messages = Arc::new(MessageQueue::new()); + + let lsps0_handler = Arc::new(LSPS0ServiceHandler::new(protocols, pending_messages.clone())); + + let list_protocols_request = LSPS0Message::Request( + RequestId("xyz123".to_string()), + LSPS0Request::ListProtocols(ListProtocolsRequest {}), + ); + let counterparty_node_id = utils::parse_pubkey( + "027100442c3b79f606f80f322d98d499eefcb060599efc5d4ecb00209c2cb54190", + ) + .unwrap(); + + lsps0_handler.handle_message(list_protocols_request, &counterparty_node_id).unwrap(); + let pending_messages = pending_messages.get_and_clear_pending_msgs(); + + assert_eq!(pending_messages.len(), 1); + + let (pubkey, message) = &pending_messages[0]; + + assert_eq!(*pubkey, counterparty_node_id); + assert_eq!( + *message, + LSPSMessage::LSPS0(LSPS0Message::Response( + RequestId("xyz123".to_string()), + LSPS0Response::ListProtocols(ListProtocolsResponse { protocols: vec![] }) + )) + ); + } +} diff --git a/lightning-liquidity/src/lsps1/client.rs b/lightning-liquidity/src/lsps1/client.rs new file mode 100644 index 00000000000..a83c2ae87b0 --- /dev/null +++ b/lightning-liquidity/src/lsps1/client.rs @@ -0,0 +1,471 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS1 client object, [`LSPS1ClientHandler`]. + +use super::event::LSPS1ClientEvent; +use super::msgs::{ + CreateOrderRequest, CreateOrderResponse, GetInfoRequest, GetInfoResponse, GetOrderRequest, + LSPS1Message, LSPS1Request, LSPS1Response, OrderId, OrderParameters, +}; +use crate::message_queue::MessageQueue; + +use crate::events::{Event, EventQueue}; +use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; +use crate::prelude::{HashMap, HashSet}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; +use bitcoin::Address; + +use core::ops::Deref; + +/// Client-side configuration options for LSPS1 channel requests. +#[derive(Clone, Debug)] +pub struct LSPS1ClientConfig { + /// The maximally allowed channel fees. + pub max_channel_fees_msat: Option, +} + +#[derive(Default)] +struct PeerState { + pending_get_info_requests: HashSet, + pending_create_order_requests: HashSet, + pending_get_order_requests: HashSet, +} + +/// The main object allowing to send and receive LSPS1 messages. +pub struct LSPS1ClientHandler +where + ES::Target: EntropySource, +{ + entropy_source: ES, + pending_messages: Arc, + pending_events: Arc, + per_peer_state: RwLock>>, + _config: LSPS1ClientConfig, +} + +impl LSPS1ClientHandler +where + ES::Target: EntropySource, +{ + /// Constructs an `LSPS1ClientHandler`. + pub(crate) fn new( + entropy_source: ES, pending_messages: Arc, pending_events: Arc, + config: LSPS1ClientConfig, + ) -> Self { + Self { + entropy_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + _config: config, + } + } + + /// Request the supported options from the LSP. + /// + /// The user will receive the LSP's response via an [`SupportedOptionsReady`] event. + /// + /// `counterparty_node_id` is the `node_id` of the LSP you would like to use. + /// + /// Returns the used [`RequestId`], which will be returned via [`SupportedOptionsReady`]. + /// + /// [`SupportedOptionsReady`]: crate::lsps1::event::LSPS1ClientEvent::SupportedOptionsReady + pub fn request_supported_options(&self, counterparty_node_id: PublicKey) -> RequestId { + let request_id = crate::utils::generate_request_id(&self.entropy_source); + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(counterparty_node_id) + .or_insert(Mutex::new(PeerState::default())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.pending_get_info_requests.insert(request_id.clone()); + } + + let request = LSPS1Request::GetInfo(GetInfoRequest {}); + let msg = LSPS1Message::Request(request_id.clone(), request).into(); + self.pending_messages.enqueue(&counterparty_node_id, msg); + request_id + } + + fn handle_get_info_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.write().unwrap(); + + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get_info response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client( + LSPS1ClientEvent::SupportedOptionsReady { + counterparty_node_id: *counterparty_node_id, + supported_options: result.options, + request_id, + }, + )); + Ok(()) + }, + None => Err(LightningError { + err: format!( + "Received get_info response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }), + } + } + + fn handle_get_info_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get_info error for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client( + LSPS1ClientEvent::SupportedOptionsRequestFailed { + request_id: request_id.clone(), + counterparty_node_id: *counterparty_node_id, + error: error.clone(), + }, + )); + + Err(LightningError { + err: format!( + "Received get_info error response for request {:?}: {:?}", + request_id, error + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }) + }, + None => { + return Err(LightningError { + err: format!( + "Received get_info error response from an unknown counterparty ({:?})", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + }, + } + } + + /// Places an order with the connected LSP given its `counterparty_node_id`. + /// + /// The client agrees to paying channel fees according to the provided parameters. + pub fn create_order( + &self, counterparty_node_id: &PublicKey, order: OrderParameters, + refund_onchain_address: Option
, + ) -> RequestId { + let (request_id, request_msg) = { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::default())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + let request_id = crate::utils::generate_request_id(&self.entropy_source); + let request = + LSPS1Request::CreateOrder(CreateOrderRequest { order, refund_onchain_address }); + let msg = LSPS1Message::Request(request_id.clone(), request).into(); + peer_state_lock.pending_create_order_requests.insert(request_id.clone()); + + (request_id, Some(msg)) + }; + + if let Some(msg) = request_msg { + self.pending_messages.enqueue(&counterparty_node_id, msg); + } + + request_id + } + + fn handle_create_order_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + response: CreateOrderResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_create_order_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received create_order response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client(LSPS1ClientEvent::OrderCreated { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: response.order_id, + order: response.order, + payment: response.payment, + channel: response.channel, + })); + }, + None => { + return Err(LightningError { + err: format!( + "Received create_order response from unknown peer: {}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }) + }, + } + + Ok(()) + } + + fn handle_create_order_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_create_order_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received create order error for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client( + LSPS1ClientEvent::OrderRequestFailed { + request_id: request_id.clone(), + counterparty_node_id: *counterparty_node_id, + error: error.clone(), + }, + )); + + Err(LightningError { + err: format!( + "Received create_order error response for request {:?}: {:?}", + request_id, error + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }) + }, + None => { + return Err(LightningError { + err: format!( + "Received error response for a create order request from an unknown counterparty ({:?})", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + }, + } + } + + /// Queries the status of a pending payment, i.e., whether a payment has been received by the LSP. + /// + /// Upon success an [`LSPS1ClientEvent::OrderStatus`] event will be emitted. + /// + /// [`LSPS1ClientEvent::OrderStatus`]: crate::lsps1::event::LSPS1ClientEvent::OrderStatus + pub fn check_order_status( + &self, counterparty_node_id: &PublicKey, order_id: OrderId, + ) -> RequestId { + let (request_id, request_msg) = { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::default())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + let request_id = crate::utils::generate_request_id(&self.entropy_source); + peer_state_lock.pending_get_order_requests.insert(request_id.clone()); + + let request = LSPS1Request::GetOrder(GetOrderRequest { order_id: order_id.clone() }); + let msg = LSPS1Message::Request(request_id.clone(), request).into(); + + (request_id, Some(msg)) + }; + + if let Some(msg) = request_msg { + self.pending_messages.enqueue(&counterparty_node_id, msg); + } + + request_id + } + + fn handle_get_order_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + response: CreateOrderResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_get_order_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get_order response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client(LSPS1ClientEvent::OrderStatus { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: response.order_id, + order: response.order, + payment: response.payment, + channel: response.channel, + })); + }, + None => { + return Err(LightningError { + err: format!( + "Received get_order response from unknown peer: {}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }) + }, + } + + Ok(()) + } + + fn handle_get_order_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if !peer_state_lock.pending_get_order_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get order error for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + } + + self.pending_events.enqueue(Event::LSPS1Client( + LSPS1ClientEvent::OrderRequestFailed { + request_id: request_id.clone(), + counterparty_node_id: *counterparty_node_id, + error: error.clone(), + }, + )); + + Err(LightningError { + err: format!( + "Received get_order error response for request {:?}: {:?}", + request_id, error + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }) + }, + None => { + return Err(LightningError { + err: format!( + "Received error response for a get order request from an unknown counterparty ({:?})", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Debug), + }); + }, + } + } +} + +impl ProtocolMessageHandler for LSPS1ClientHandler +where + ES::Target: EntropySource, +{ + type ProtocolMessage = LSPS1Message; + const PROTOCOL_NUMBER: Option = Some(1); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS1Message::Response(request_id, response) => match response { + LSPS1Response::GetInfo(params) => { + self.handle_get_info_response(request_id, counterparty_node_id, params) + }, + LSPS1Response::GetInfoError(error) => { + self.handle_get_info_error(request_id, counterparty_node_id, error) + }, + LSPS1Response::CreateOrder(params) => { + self.handle_create_order_response(request_id, counterparty_node_id, params) + }, + LSPS1Response::CreateOrderError(error) => { + self.handle_create_order_error(request_id, counterparty_node_id, error) + }, + LSPS1Response::GetOrder(params) => { + self.handle_get_order_response(request_id, counterparty_node_id, params) + }, + LSPS1Response::GetOrderError(error) => { + self.handle_get_order_error(request_id, counterparty_node_id, error) + }, + }, + _ => { + debug_assert!( + false, + "Client handler received LSPS1 request message. This should never happen." + ); + Err(LightningError { + err: format!( + "Client handler received LSPS1 request message from node {:?}. This should never happen.", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Error), + }) + }, + } + } +} diff --git a/lightning-liquidity/src/lsps1/event.rs b/lightning-liquidity/src/lsps1/event.rs new file mode 100644 index 00000000000..ff4961d49b8 --- /dev/null +++ b/lightning-liquidity/src/lsps1/event.rs @@ -0,0 +1,183 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains LSPS1 event types + +use super::msgs::OrderId; +use super::msgs::{ChannelInfo, LSPS1Options, OrderParameters, PaymentInfo}; + +use crate::lsps0::ser::{RequestId, ResponseError}; + +use bitcoin::secp256k1::PublicKey; + +/// An event which an LSPS1 client should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1ClientEvent { + /// A request previously issued via [`LSPS1ClientHandler::request_supported_options`] + /// succeeded as the LSP returned the options it supports. + /// + /// You must check whether LSP supports the parameters the client wants and then call + /// [`LSPS1ClientHandler::create_order`] to place an order. + /// + /// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options + /// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order + SupportedOptionsReady { + /// The identifier of the issued LSPS1 `get_info` request, as returned by + /// [`LSPS1ClientHandler::request_supported_options`] + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options + request_id: RequestId, + /// The node id of the LSP that provided this response. + counterparty_node_id: PublicKey, + /// All options supported by the LSP. + supported_options: LSPS1Options, + }, + /// A request previously issued via [`LSPS1ClientHandler::request_supported_options`] + /// failed as the LSP returned an error response. + /// + /// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options + SupportedOptionsRequestFailed { + /// The identifier of the issued LSPS1 `get_info` request, as returned by + /// [`LSPS1ClientHandler::request_supported_options`] + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS1ClientHandler::request_supported_options`]: crate::lsps1::client::LSPS1ClientHandler::request_supported_options + request_id: RequestId, + /// The node id of the LSP that provided this response. + counterparty_node_id: PublicKey, + /// The error that was returned. + error: ResponseError, + }, + /// Confirmation from the LSP about the order created by the client. + /// + /// When the payment is confirmed, the LSP will open a channel to you + /// with the below agreed upon parameters. + /// + /// You must pay the invoice or onchain address if you want to continue and then + /// call [`LSPS1ClientHandler::check_order_status`] with the order id + /// to get information from LSP about progress of the order. + /// + /// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status + OrderCreated { + /// The identifier of the issued LSPS1 `create_order` request, as returned by + /// [`LSPS1ClientHandler::create_order`] + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order + request_id: RequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The id of the channel order. + order_id: OrderId, + /// The order created by client and approved by LSP. + order: OrderParameters, + /// The details regarding payment of the order + payment: PaymentInfo, + /// The details regarding state of the channel ordered. + channel: Option, + }, + /// Information from the LSP about the status of a previously created order. + /// + /// Will be emitted in response to calling [`LSPS1ClientHandler::check_order_status`]. + /// + /// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status + OrderStatus { + /// The identifier of the issued LSPS1 `get_order` request, as returned by + /// [`LSPS1ClientHandler::check_order_status`] + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status + request_id: RequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The id of the channel order. + order_id: OrderId, + /// The order created by client and approved by LSP. + order: OrderParameters, + /// The details regarding payment of the order + payment: PaymentInfo, + /// The details regarding state of the channel ordered. + channel: Option, + }, + /// A request previously issued via [`LSPS1ClientHandler::create_order`] or [`LSPS1ClientHandler::check_order_status`]. + /// failed as the LSP returned an error response. + /// + /// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order + /// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status + OrderRequestFailed { + /// The identifier of the issued LSPS1 `create_order` or `get_order` request, as returned by + /// [`LSPS1ClientHandler::create_order`] or [`LSPS1ClientHandler::check_order_status`]. + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS1ClientHandler::create_order`]: crate::lsps1::client::LSPS1ClientHandler::create_order + /// [`LSPS1ClientHandler::check_order_status`]: crate::lsps1::client::LSPS1ClientHandler::check_order_status + request_id: RequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The error that was returned. + error: ResponseError, + }, +} + +/// An event which an LSPS1 server should take some action in response to. +#[cfg(lsps1_service)] +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1ServiceEvent { + /// A client has selected the parameters to use from the supported options of the LSP + /// and would like to open a channel with the given payment parameters. + /// + /// You must call [`LSPS1ServiceHandler::send_payment_details`] to + /// send order parameters including the details regarding the + /// payment and order id for this order for the client. + /// + /// [`LSPS1ServiceHandler::send_payment_details`]: crate::lsps1::service::LSPS1ServiceHandler::send_payment_details + RequestForPaymentDetails { + /// An identifier that must be passed to [`LSPS1ServiceHandler::send_payment_details`]. + /// + /// [`LSPS1ServiceHandler::send_payment_details`]: crate::lsps1::service::LSPS1ServiceHandler::send_payment_details + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// The order requested by the client. + order: OrderParameters, + }, + /// A request from client to check the status of the payment. + /// + /// An event to poll for checking payment status either onchain or lightning. + /// + /// You must call [`LSPS1ServiceHandler::update_order_status`] to update the client + /// regarding the status of the payment and order. + /// + /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status + CheckPaymentConfirmation { + /// An identifier that must be passed to [`LSPS1ServiceHandler::update_order_status`]. + /// + /// [`LSPS1ServiceHandler::update_order_status`]: crate::lsps1::service::LSPS1ServiceHandler::update_order_status + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// The order id of order with pending payment. + order_id: OrderId, + }, + /// If error is encountered, refund the amount if paid by the client. + Refund { + /// An identifier. + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// The order id of the refunded order. + order_id: OrderId, + }, +} diff --git a/lightning-liquidity/src/lsps1/mod.rs b/lightning-liquidity/src/lsps1/mod.rs new file mode 100644 index 00000000000..d04a26b29c1 --- /dev/null +++ b/lightning-liquidity/src/lsps1/mod.rs @@ -0,0 +1,16 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Types and primitives that implement the LSPS1: Channel Request specification. + +pub mod client; +pub mod event; +pub mod msgs; +#[cfg(lsps1_service)] +pub mod service; diff --git a/lightning-liquidity/src/lsps1/msgs.rs b/lightning-liquidity/src/lsps1/msgs.rs new file mode 100644 index 00000000000..42f10c04772 --- /dev/null +++ b/lightning-liquidity/src/lsps1/msgs.rs @@ -0,0 +1,473 @@ +//! Message, request, and other primitive types used to implement LSPS1. + +use crate::lsps0::ser::{ + string_amount, u32_fee_rate, unchecked_address, unchecked_address_option, LSPSMessage, + RequestId, ResponseError, +}; + +use crate::prelude::String; + +use bitcoin::{Address, FeeRate, OutPoint}; + +use lightning_invoice::Bolt11Invoice; + +use serde::{Deserialize, Serialize}; + +use chrono::Utc; + +use core::convert::TryFrom; + +pub(crate) const LSPS1_GET_INFO_METHOD_NAME: &str = "lsps1.get_info"; +pub(crate) const LSPS1_CREATE_ORDER_METHOD_NAME: &str = "lsps1.create_order"; +pub(crate) const LSPS1_GET_ORDER_METHOD_NAME: &str = "lsps1.get_order"; + +pub(crate) const _LSPS1_CREATE_ORDER_REQUEST_INVALID_PARAMS_ERROR_CODE: i32 = -32602; +#[cfg(lsps1_service)] +pub(crate) const LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE: i32 = 100; + +/// The identifier of an order. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Hash)] +pub struct OrderId(pub String); + +/// A request made to an LSP to retrieve the supported options. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#1-lsps1info) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(default)] +pub struct GetInfoRequest {} + +/// An object representing the supported protocol options. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct LSPS1Options { + /// The smallest number of confirmations needed for the LSP to accept a channel as confirmed. + pub min_required_channel_confirmations: u16, + /// The smallest number of blocks in which the LSP can confirm the funding transaction. + pub min_funding_confirms_within_blocks: u16, + /// Indicates if the LSP supports zero reserve. + pub supports_zero_channel_reserve: bool, + /// The maximum number of blocks a channel can be leased for. + pub max_channel_expiry_blocks: u32, + /// The minimum number of satoshi that the client MUST request. + #[serde(with = "string_amount")] + pub min_initial_client_balance_sat: u64, + /// The maximum number of satoshi that the client MUST request. + #[serde(with = "string_amount")] + pub max_initial_client_balance_sat: u64, + /// The minimum number of satoshi that the LSP will provide to the channel. + #[serde(with = "string_amount")] + pub min_initial_lsp_balance_sat: u64, + /// The maximum number of satoshi that the LSP will provide to the channel. + #[serde(with = "string_amount")] + pub max_initial_lsp_balance_sat: u64, + /// The minimal channel size. + #[serde(with = "string_amount")] + pub min_channel_balance_sat: u64, + /// The maximal channel size. + #[serde(with = "string_amount")] + pub max_channel_balance_sat: u64, +} + +/// A response to a [`GetInfoRequest`]. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetInfoResponse { + /// All options supported by the LSP. + #[serde(flatten)] + pub options: LSPS1Options, +} + +/// A request made to an LSP to create an order. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#2-lsps1create_order) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct CreateOrderRequest { + /// The order made. + #[serde(flatten)] + pub order: OrderParameters, + /// The address where the LSP will send the funds if the order fails. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "unchecked_address_option")] + pub refund_onchain_address: Option
, +} + +/// An object representing an LSPS1 channel order. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct OrderParameters { + /// Indicates how many satoshi the LSP will provide on their side. + #[serde(with = "string_amount")] + pub lsp_balance_sat: u64, + /// Indicates how many satoshi the client will provide on their side. + /// + /// The client sends these funds to the LSP, who will push them back to the client upon opening + /// the channel. + #[serde(with = "string_amount")] + pub client_balance_sat: u64, + /// The number of confirmations the funding tx must have before the LSP sends `channel_ready`. + pub required_channel_confirmations: u16, + /// The maximum number of blocks the client wants to wait until the funding transaction is confirmed. + pub funding_confirms_within_blocks: u16, + /// Indicates how long the channel is leased for in block time. + pub channel_expiry_blocks: u32, + /// May contain arbitrary associated data like a coupon code or a authentication token. + pub token: Option, + /// Indicates if the channel should be announced to the network. + pub announce_channel: bool, +} + +/// A response to a [`CreateOrderRequest`]. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct CreateOrderResponse { + /// The id of the channel order. + pub order_id: OrderId, + /// The parameters of channel order. + #[serde(flatten)] + pub order: OrderParameters, + /// The datetime when the order was created + pub created_at: chrono::DateTime, + /// The current state of the order. + pub order_state: OrderState, + /// Contains details about how to pay for the order. + pub payment: PaymentInfo, + /// Contains information about the channel state. + pub channel: Option, +} + +/// An object representing the state of an order. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum OrderState { + /// The order has been created. + Created, + /// The LSP has opened the channel and published the funding transaction. + Completed, + /// The order failed. + Failed, +} + +/// Details regarding how to pay for an order. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct PaymentInfo { + /// A Lightning payment using BOLT 11. + pub bolt11: Option, + /// An onchain payment. + pub onchain: Option, +} + +/// A Lightning payment using BOLT 11. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct Bolt11PaymentInfo { + /// Indicates the current state of the payment. + pub state: PaymentState, + /// The datetime when the payment option expires. + pub expires_at: chrono::DateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + #[serde(with = "string_amount")] + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + #[serde(with = "string_amount")] + pub order_total_sat: u64, + /// A BOLT11 invoice the client can pay to have to channel opened. + pub invoice: Bolt11Invoice, +} + +/// An onchain payment. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct OnchainPaymentInfo { + /// Indicates the current state of the payment. + pub state: PaymentState, + /// The datetime when the payment option expires. + pub expires_at: chrono::DateTime, + /// The total fee the LSP will charge to open this channel in satoshi. + #[serde(with = "string_amount")] + pub fee_total_sat: u64, + /// The amount the client needs to pay to have the requested channel openend. + #[serde(with = "string_amount")] + pub order_total_sat: u64, + /// An on-chain address the client can send [`Self::order_total_sat`] to to have the channel + /// opened. + #[serde(with = "unchecked_address")] + pub address: Address, + /// The minimum number of block confirmations that are required for the on-chain payment to be + /// considered confirmed. + pub min_onchain_payment_confirmations: Option, + /// The minimum fee rate for the on-chain payment in case the client wants the payment to be + /// confirmed without a confirmation. + #[serde(with = "u32_fee_rate")] + pub min_fee_for_0conf: FeeRate, + /// The address where the LSP will send the funds if the order fails. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "unchecked_address_option")] + pub refund_onchain_address: Option
, +} + +/// The state of a payment. +/// +/// *Note*: Previously, the spec also knew a `CANCELLED` state for BOLT11 payments, which has since +/// been deprecated and `REFUNDED` should be used instead. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum PaymentState { + /// A payment is expected. + ExpectPayment, + /// A sufficient payment has been received. + Paid, + /// The payment has been refunded. + #[serde(alias = "CANCELLED")] + Refunded, +} + +/// Details regarding a detected on-chain payment. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct OnchainPayment { + /// The outpoint of the payment. + pub outpoint: String, + /// The amount of satoshi paid. + #[serde(with = "string_amount")] + pub sat: u64, + /// Indicates if the LSP regards the transaction as sufficiently confirmed. + pub confirmed: bool, +} + +/// Details regarding the state of an ordered channel. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct ChannelInfo { + /// The datetime when the funding transaction has been published. + pub funded_at: chrono::DateTime, + /// The outpoint of the funding transaction. + pub funding_outpoint: OutPoint, + /// The earliest datetime when the channel may be closed by the LSP. + pub expires_at: chrono::DateTime, +} + +/// A request made to an LSP to retrieve information about an previously made order. +/// +/// Please refer to the [LSPS1 specification](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS1#21-lsps1get_order) +/// for more information. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetOrderRequest { + /// The id of the order. + pub order_id: OrderId, +} + +/// An enum that captures all the valid JSON-RPC requests in the LSPS1 protocol. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1Request { + /// A request to learn about the options supported by the LSP. + GetInfo(GetInfoRequest), + /// A request to create a channel order. + CreateOrder(CreateOrderRequest), + /// A request to query a previously created channel order. + GetOrder(GetOrderRequest), +} + +/// An enum that captures all the valid JSON-RPC responses in the LSPS1 protocol. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1Response { + /// A successful response to a [`GetInfoRequest`]. + GetInfo(GetInfoResponse), + /// An error response to a [`GetInfoRequest`]. + GetInfoError(ResponseError), + /// A successful response to a [`CreateOrderRequest`]. + CreateOrder(CreateOrderResponse), + /// An error response to a [`CreateOrderRequest`]. + CreateOrderError(ResponseError), + /// A successful response to a [`GetOrderRequest`]. + GetOrder(CreateOrderResponse), + /// An error response to a [`GetOrderRequest`]. + GetOrderError(ResponseError), +} + +/// An enum that captures all valid JSON-RPC messages in the LSPS1 protocol. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS1Message { + /// An LSPS1 JSON-RPC request. + Request(RequestId, LSPS1Request), + /// An LSPS1 JSON-RPC response. + Response(RequestId, LSPS1Response), +} + +impl TryFrom for LSPS1Message { + type Error = (); + + fn try_from(message: LSPSMessage) -> Result { + if let LSPSMessage::LSPS1(message) = message { + return Ok(message); + } + + Err(()) + } +} + +impl From for LSPSMessage { + fn from(message: LSPS1Message) -> Self { + LSPSMessage::LSPS1(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::alloc::string::ToString; + + #[test] + fn options_supported_serialization() { + let min_required_channel_confirmations = 0; + let min_funding_confirms_within_blocks = 6; + let supports_zero_channel_reserve = true; + let max_channel_expiry_blocks = 144; + let min_initial_client_balance_sat = 10_000_000; + let max_initial_client_balance_sat = 100_000_000; + let min_initial_lsp_balance_sat = 100_000; + let max_initial_lsp_balance_sat = 100_000_000; + let min_channel_balance_sat = 100_000; + let max_channel_balance_sat = 100_000_000; + + let options_supported = LSPS1Options { + min_required_channel_confirmations, + min_funding_confirms_within_blocks, + supports_zero_channel_reserve, + max_channel_expiry_blocks, + min_initial_client_balance_sat, + max_initial_client_balance_sat, + min_initial_lsp_balance_sat, + max_initial_lsp_balance_sat, + min_channel_balance_sat, + max_channel_balance_sat, + }; + + let json_str = r#"{"max_channel_balance_sat":"100000000","max_channel_expiry_blocks":144,"max_initial_client_balance_sat":"100000000","max_initial_lsp_balance_sat":"100000000","min_channel_balance_sat":"100000","min_funding_confirms_within_blocks":6,"min_initial_client_balance_sat":"10000000","min_initial_lsp_balance_sat":"100000","min_required_channel_confirmations":0,"supports_zero_channel_reserve":true}"#; + + assert_eq!(json_str, serde_json::json!(options_supported).to_string()); + assert_eq!(options_supported, serde_json::from_str(json_str).unwrap()); + } + + #[test] + fn parse_spec_test_vectors() { + // Here, we simply assert that we're able to parse all examples given in LSPS1. + let json_str = r#"{}"#; + let _get_info_request: GetInfoRequest = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "min_required_channel_confirmations": 0, + "min_funding_confirms_within_blocks" : 6, + "supports_zero_channel_reserve": true, + "max_channel_expiry_blocks": 20160, + "min_initial_client_balance_sat": "20000", + "max_initial_client_balance_sat": "100000000", + "min_initial_lsp_balance_sat": "0", + "max_initial_lsp_balance_sat": "100000000", + "min_channel_balance_sat": "50000", + "max_channel_balance_sat": "100000000" + }"#; + let _get_info_response: GetInfoResponse = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "lsp_balance_sat": "5000000", + "client_balance_sat": "2000000", + "required_channel_confirmations" : 0, + "funding_confirms_within_blocks": 6, + "channel_expiry_blocks": 144, + "token": "", + "refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h", + "announce_channel": true + }"#; + let _create_order_request: CreateOrderRequest = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "state" : "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "8888", + "order_total_sat": "200888", + "invoice": "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05" + }"#; + let _bolt11_payment: Bolt11PaymentInfo = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "state": "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + }"#; + let _onchain_payment: OnchainPaymentInfo = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "bolt11": { + "state" : "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "8888", + "order_total_sat": "200888", + "invoice": "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05" + }, + "onchain": { + "state": "EXPECT_PAYMENT", + "expires_at": "2025-01-01T00:00:00Z", + "fee_total_sat": "9999", + "order_total_sat": "200999", + "address": "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_onchain_payment_confirmations": 1, + "min_fee_for_0conf": 253 + } + }"#; + let _payment: PaymentInfo = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c", + "lsp_balance_sat": "5000000", + "client_balance_sat": "2000000", + "required_channel_confirmations" : 0, + "funding_confirms_within_blocks": 1, + "channel_expiry_blocks": 12, + "token": "", + "created_at": "2012-04-23T18:25:43.511Z", + "announce_channel": true, + "order_state": "CREATED", + "payment": { + "bolt11": { + "state": "EXPECT_PAYMENT", + "expires_at": "2015-01-25T19:29:44.612Z", + "fee_total_sat": "8888", + "order_total_sat": "2008888", + "invoice" : "lnbc252u1p3aht9ysp580g4633gd2x9lc5al0wd8wx0mpn9748jeyz46kqjrpxn52uhfpjqpp5qgf67tcqmuqehzgjm8mzya90h73deafvr4m5705l5u5l4r05l8cqdpud3h8ymm4w3jhytnpwpczqmt0de6xsmre2pkxzm3qydmkzdjrdev9s7zhgfaqxqyjw5qcqpjrzjqt6xptnd85lpqnu2lefq4cx070v5cdwzh2xlvmdgnu7gqp4zvkus5zapryqqx9qqqyqqqqqqqqqqqcsq9q9qyysgqen77vu8xqjelum24hgjpgfdgfgx4q0nehhalcmuggt32japhjuksq9jv6eksjfnppm4hrzsgyxt8y8xacxut9qv3fpyetz8t7tsymygq8yzn05" + }, + "onchain": { + "state": "EXPECT_PAYMENT", + "expires_at": "2015-01-25T19:29:44.612Z", + "fee_total_sat": "9999", + "order_total_sat": "2009999", + "address" : "bc1p5uvtaxzkjwvey2tfy49k5vtqfpjmrgm09cvs88ezyy8h2zv7jhas9tu4yr", + "min_fee_for_0conf": 253, + "min_onchain_payment_confirmations": 0, + "refund_onchain_address": "bc1qvmsy0f3yyes6z9jvddk8xqwznndmdwapvrc0xrmhd3vqj5rhdrrq6hz49h" + } + }, + "channel": null + }"#; + let _create_order_response: CreateOrderResponse = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "order_id": "bb4b5d0a-8334-49d8-9463-90a6d413af7c" + }"#; + let _get_order_request: GetOrderRequest = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "funded_at": "2012-04-23T18:25:43.511Z", + "funding_outpoint": "0301e0480b374b32851a9462db29dc19fe830a7f7d7a88b81612b9d42099c0ae:0", + "expires_at": "2012-04-23T18:25:43.511Z" + }"#; + let _channel: ChannelInfo = serde_json::from_str(json_str).unwrap(); + + let json_str = r#""CANCELLED""#; + let payment_state: PaymentState = serde_json::from_str(json_str).unwrap(); + assert_eq!(payment_state, PaymentState::Refunded); + + let json_str = r#""REFUNDED""#; + let payment_state: PaymentState = serde_json::from_str(json_str).unwrap(); + assert_eq!(payment_state, PaymentState::Refunded); + } +} diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs new file mode 100644 index 00000000000..e4e6508a281 --- /dev/null +++ b/lightning-liquidity/src/lsps1/service.rs @@ -0,0 +1,459 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS1 server object, [`LSPS1ServiceHandler`]. + +use super::event::LSPS1ServiceEvent; +use super::msgs::{ + ChannelInfo, CreateOrderRequest, CreateOrderResponse, GetInfoResponse, GetOrderRequest, + LSPS1Message, LSPS1Options, LSPS1Request, LSPS1Response, OrderId, OrderParameters, OrderState, + PaymentInfo, LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, +}; +use super::utils::is_valid; +use crate::message_queue::MessageQueue; + +use crate::events::{Event, EventQueue}; +use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; +use crate::prelude::{HashMap, String, ToString}; +use crate::sync::{Arc, Mutex, RwLock}; +use crate::utils; + +use lightning::chain::Filter; +use lightning::ln::channelmanager::AChannelManager; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use chrono::Utc; +use core::ops::Deref; + +/// Server-side configuration options for LSPS1 channel requests. +#[derive(Clone, Debug)] +pub struct LSPS1ServiceConfig { + /// A token to be send with each channel request. + pub token: Option, + /// The options supported by the LSP. + pub supported_options: Option, +} + +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +#[derive(PartialEq, Debug)] +enum OutboundRequestState { + OrderCreated { order_id: OrderId }, + WaitingPayment { order_id: OrderId }, + Ready, +} + +impl OutboundRequestState { + fn awaiting_payment(&self) -> Result { + match self { + OutboundRequestState::OrderCreated { order_id } => { + Ok(OutboundRequestState::WaitingPayment { order_id: order_id.clone() }) + }, + state => Err(ChannelStateError(format!("TODO. JIT Channel was in state: {:?}", state))), + } + } +} + +struct OutboundLSPS1Config { + order: OrderParameters, + created_at: chrono::DateTime, + payment: PaymentInfo, +} + +struct OutboundCRChannel { + state: OutboundRequestState, + config: OutboundLSPS1Config, +} + +impl OutboundCRChannel { + fn new( + order: OrderParameters, created_at: chrono::DateTime, order_id: OrderId, + payment: PaymentInfo, + ) -> Self { + Self { + state: OutboundRequestState::OrderCreated { order_id }, + config: OutboundLSPS1Config { order, created_at, payment }, + } + } + fn awaiting_payment(&mut self) -> Result<(), LightningError> { + self.state = self.state.awaiting_payment()?; + Ok(()) + } + + fn check_order_validity(&self, supported_options: &LSPS1Options) -> bool { + let order = &self.config.order; + + is_valid(order, supported_options) + } +} + +#[derive(Default)] +struct PeerState { + outbound_channels_by_order_id: HashMap, + request_to_cid: HashMap, + pending_requests: HashMap, +} + +impl PeerState { + fn insert_outbound_channel(&mut self, order_id: OrderId, channel: OutboundCRChannel) { + self.outbound_channels_by_order_id.insert(order_id, channel); + } + + fn insert_request(&mut self, request_id: RequestId, channel_id: u128) { + self.request_to_cid.insert(request_id, channel_id); + } + + fn remove_outbound_channel(&mut self, order_id: OrderId) { + self.outbound_channels_by_order_id.remove(&order_id); + } +} + +/// The main object allowing to send and receive LSPS1 messages. +pub struct LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + entropy_source: ES, + channel_manager: CM, + chain_source: Option, + pending_messages: Arc, + pending_events: Arc, + per_peer_state: RwLock>>, + config: LSPS1ServiceConfig, +} + +impl LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, + ES::Target: EntropySource, +{ + /// Constructs a `LSPS1ServiceHandler`. + pub(crate) fn new( + entropy_source: ES, pending_messages: Arc, pending_events: Arc, + channel_manager: CM, chain_source: Option, config: LSPS1ServiceConfig, + ) -> Self { + Self { + entropy_source, + channel_manager, + chain_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + config, + } + } + + fn handle_get_info_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + let response = LSPS1Response::GetInfo(GetInfoResponse { + options: self + .config + .supported_options + .clone() + .ok_or(LightningError { + err: format!("Configuration for LSP server not set."), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + .unwrap(), + }); + + let msg = LSPS1Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + Ok(()) + } + + fn handle_create_order_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: CreateOrderRequest, + ) -> Result<(), LightningError> { + if !is_valid(¶ms.order, &self.config.supported_options.as_ref().unwrap()) { + let response = LSPS1Response::CreateOrderError(ResponseError { + code: LSPS1_CREATE_ORDER_REQUEST_ORDER_MISMATCH_ERROR_CODE, + message: format!("Order does not match options supported by LSP server"), + data: Some(format!( + "Supported options are {:?}", + &self.config.supported_options.as_ref().unwrap() + )), + }); + let msg = LSPS1Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: format!( + "Client order does not match any supported options: {:?}", + params.order + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + + let inner_state_lock = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::default())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS1Request::CreateOrder(params.clone())); + } + + self.pending_events.enqueue(Event::LSPS1Service( + LSPS1ServiceEvent::RequestForPaymentDetails { + request_id, + counterparty_node_id: *counterparty_node_id, + order: params.order, + }, + )); + + Ok(()) + } + + /// Used by LSP to send response containing details regarding the channel fees and payment information. + /// + /// Should be called in response to receiving a [`LSPS1ServiceEvent::RequestForPaymentDetails`] event. + /// + /// [`LSPS1ServiceEvent::RequestForPaymentDetails`]: crate::lsps1::event::LSPS1ServiceEvent::RequestForPaymentDetails + pub fn send_payment_details( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, payment: PaymentInfo, + created_at: chrono::DateTime, + ) -> Result<(), APIError> { + let (result, response) = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + match peer_state_lock.pending_requests.remove(&request_id) { + Some(LSPS1Request::CreateOrder(params)) => { + let order_id = self.generate_order_id(); + let channel = OutboundCRChannel::new( + params.order.clone(), + created_at.clone(), + order_id.clone(), + payment.clone(), + ); + + peer_state_lock.insert_outbound_channel(order_id.clone(), channel); + + let response = LSPS1Response::CreateOrder(CreateOrderResponse { + order: params.order, + order_id, + order_state: OrderState::Created, + created_at, + payment, + channel: None, + }); + + (Ok(()), Some(response)) + }, + + _ => ( + Err(APIError::APIMisuseError { + err: format!( + "No pending buy request for request_id: {:?}", + request_id + ), + }), + None, + ), + } + }, + None => ( + Err(APIError::APIMisuseError { + err: format!( + "No state for the counterparty exists: {:?}", + counterparty_node_id + ), + }), + None, + ), + } + }; + + if let Some(response) = response { + let msg = LSPS1Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result + } + + fn handle_get_order_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetOrderRequest, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + let outbound_channel = peer_state_lock + .outbound_channels_by_order_id + .get_mut(¶ms.order_id) + .ok_or(LightningError { + err: format!( + "Received get order request for unknown order id {:?}", + params.order_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Err(e) = outbound_channel.awaiting_payment() { + peer_state_lock.outbound_channels_by_order_id.remove(¶ms.order_id); + self.pending_events.enqueue(Event::LSPS1Service(LSPS1ServiceEvent::Refund { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: params.order_id, + })); + return Err(e); + } + + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS1Request::GetOrder(params.clone())); + + self.pending_events.enqueue(Event::LSPS1Service( + LSPS1ServiceEvent::CheckPaymentConfirmation { + request_id, + counterparty_node_id: *counterparty_node_id, + order_id: params.order_id, + }, + )); + }, + None => { + return Err(LightningError { + err: format!("Received error response for a create order request from an unknown counterparty ({:?})", counterparty_node_id), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + }, + } + + Ok(()) + } + + /// Used by LSP to give details to client regarding the status of channel opening. + /// Called to respond to client's GetOrder request. + /// The LSP continously polls for checking payment confirmation on-chain or lighting + /// and then responds to client request. + /// + /// Should be called in response to receiving a [`LSPS1ServiceEvent::CheckPaymentConfirmation`] event. + /// + /// [`LSPS1ServiceEvent::CheckPaymentConfirmation`]: crate::lsps1::event::LSPS1ServiceEvent::CheckPaymentConfirmation + pub fn update_order_status( + &self, request_id: RequestId, counterparty_node_id: PublicKey, order_id: OrderId, + order_state: OrderState, channel: Option, + ) -> Result<(), APIError> { + let (result, response) = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + if let Some(outbound_channel) = + peer_state_lock.outbound_channels_by_order_id.get_mut(&order_id) + { + let config = &outbound_channel.config; + + let response = LSPS1Response::GetOrder(CreateOrderResponse { + order_id, + order: config.order.clone(), + order_state, + created_at: config.created_at, + payment: config.payment.clone(), + channel, + }); + (Ok(()), Some(response)) + } else { + ( + Err(APIError::APIMisuseError { + err: format!("Channel with order_id {} not found", order_id.0), + }), + None, + ) + } + }, + None => ( + Err(APIError::APIMisuseError { + err: format!( + "No existing state with counterparty {}", + counterparty_node_id + ), + }), + None, + ), + } + }; + + if let Some(response) = response { + let msg = LSPS1Message::Response(request_id, response).into(); + self.pending_messages.enqueue(&counterparty_node_id, msg); + } + + result + } + + fn generate_order_id(&self) -> OrderId { + let bytes = self.entropy_source.get_secure_random_bytes(); + OrderId(utils::hex_str(&bytes[0..16])) + } +} + +impl ProtocolMessageHandler + for LSPS1ServiceHandler +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + type ProtocolMessage = LSPS1Message; + const PROTOCOL_NUMBER: Option = Some(1); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS1Message::Request(request_id, request) => match request { + LSPS1Request::GetInfo(_) => { + self.handle_get_info_request(request_id, counterparty_node_id) + }, + LSPS1Request::CreateOrder(params) => { + self.handle_create_order_request(request_id, counterparty_node_id, params) + }, + LSPS1Request::GetOrder(params) => { + self.handle_get_order_request(request_id, counterparty_node_id, params) + }, + }, + _ => { + debug_assert!( + false, + "Service handler received LSPS1 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS1 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} diff --git a/lightning-liquidity/src/lsps2/client.rs b/lightning-liquidity/src/lsps2/client.rs new file mode 100644 index 00000000000..eccabfcbb64 --- /dev/null +++ b/lightning-liquidity/src/lsps2/client.rs @@ -0,0 +1,359 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS2 client object, [`LSPS2ClientHandler`]. + +use crate::events::{Event, EventQueue}; +use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; +use crate::lsps2::event::LSPS2ClientEvent; +use crate::message_queue::MessageQueue; +use crate::prelude::{HashMap, HashSet, String, ToString}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::sign::EntropySource; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; + +use bitcoin::secp256k1::PublicKey; + +use core::default::Default; +use core::ops::Deref; + +use crate::lsps2::msgs::{ + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, LSPS2Message, LSPS2Request, + LSPS2Response, OpeningFeeParams, +}; + +/// Client-side configuration options for JIT channels. +#[derive(Clone, Debug, Copy, Default)] +pub struct LSPS2ClientConfig {} + +struct InboundJITChannel { + payment_size_msat: Option, +} + +impl InboundJITChannel { + fn new(payment_size_msat: Option) -> Self { + Self { payment_size_msat } + } +} + +struct PeerState { + pending_get_info_requests: HashSet, + pending_buy_requests: HashMap, +} + +impl PeerState { + fn new() -> Self { + let pending_get_info_requests = HashSet::new(); + let pending_buy_requests = HashMap::new(); + Self { pending_get_info_requests, pending_buy_requests } + } +} + +/// The main object allowing to send and receive LSPS2 messages. +/// +/// Note that currently only the 'client-trusts-LSP' trust model is supported, i.e., we don't +/// provide any additional API guidance to allow withholding the preimage until the channel is +/// opened. Please refer to the [`LSPS2 specification`] for more information. +/// +/// [`LSPS2 specification`]: https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#trust-models +pub struct LSPS2ClientHandler +where + ES::Target: EntropySource, +{ + entropy_source: ES, + pending_messages: Arc, + pending_events: Arc, + per_peer_state: RwLock>>, + _config: LSPS2ClientConfig, +} + +impl LSPS2ClientHandler +where + ES::Target: EntropySource, +{ + /// Constructs an `LSPS2ClientHandler`. + pub(crate) fn new( + entropy_source: ES, pending_messages: Arc, pending_events: Arc, + _config: LSPS2ClientConfig, + ) -> Self { + Self { + entropy_source, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + _config, + } + } + + /// Request the channel opening parameters from the LSP. + /// + /// This initiates the JIT-channel flow that, at the end of it, will have the LSP + /// open a channel with sufficient inbound liquidity to be able to receive the payment. + /// + /// The user will receive the LSP's response via an [`OpeningParametersReady`] event. + /// + /// `counterparty_node_id` is the `node_id` of the LSP you would like to use. + /// + /// `token` is an optional `String` that will be provided to the LSP. + /// It can be used by the LSP as an API key, coupon code, or some other way to identify a user. + /// + /// Returns the used [`RequestId`], which will be returned via [`OpeningParametersReady`]. + /// + /// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady + pub fn request_opening_params( + &self, counterparty_node_id: PublicKey, token: Option, + ) -> RequestId { + let request_id = crate::utils::generate_request_id(&self.entropy_source); + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.pending_get_info_requests.insert(request_id.clone()); + } + + let request = LSPS2Request::GetInfo(GetInfoRequest { token }); + let msg = LSPS2Message::Request(request_id.clone(), request).into(); + self.pending_messages.enqueue(&counterparty_node_id, msg); + + request_id + } + + /// Confirms a set of chosen channel opening parameters to use for the JIT channel and + /// requests the necessary invoice generation parameters from the LSP. + /// + /// Should be called in response to receiving a [`OpeningParametersReady`] event. + /// + /// The user will receive the LSP's response via an [`InvoiceParametersReady`] event. + /// + /// If `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount + /// and MPP can be used to pay it. + /// + /// If `payment_size_msat` is [`Option::None`] then the invoice can be for an arbitrary amount + /// but MPP can no longer be used to pay it. + /// + /// The client agrees to paying an opening fee equal to + /// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`. + /// + /// [`OpeningParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::OpeningParametersReady + /// [`InvoiceParametersReady`]: crate::lsps2::event::LSPS2ClientEvent::InvoiceParametersReady + pub fn select_opening_params( + &self, counterparty_node_id: PublicKey, payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + ) -> Result { + let request_id = crate::utils::generate_request_id(&self.entropy_source); + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + + let jit_channel = InboundJITChannel::new(payment_size_msat); + if peer_state_lock + .pending_buy_requests + .insert(request_id.clone(), jit_channel) + .is_some() + { + return Err(APIError::APIMisuseError { + err: "Failed due to duplicate request_id. This should never happen!" + .to_string(), + }); + } + } + + let request = LSPS2Request::Buy(BuyRequest { opening_fee_params, payment_size_msat }); + let msg = LSPS2Message::Request(request_id.clone(), request).into(); + self.pending_messages.enqueue(&counterparty_node_id, msg); + + Ok(request_id) + } + + fn handle_get_info_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetInfoResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + if !peer_state.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get_info response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + self.pending_events.enqueue(Event::LSPS2Client( + LSPS2ClientEvent::OpeningParametersReady { + request_id, + counterparty_node_id: *counterparty_node_id, + opening_fee_params_menu: result.opening_fee_params_menu, + }, + )); + }, + None => { + return Err(LightningError { + err: format!( + "Received get_info response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + }, + } + + Ok(()) + } + + fn handle_get_info_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + if !peer_state.pending_get_info_requests.remove(&request_id) { + return Err(LightningError { + err: format!( + "Received get_info error for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + Ok(()) + }, + None => { + return Err(LightningError { err: format!("Received error response for a get_info request from an unknown counterparty ({:?})",counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + } + + fn handle_buy_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: BuyResponse, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + let jit_channel = + peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError { + err: format!( + "Received buy response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Ok(intercept_scid) = result.jit_channel_scid.to_scid() { + self.pending_events.enqueue(Event::LSPS2Client( + LSPS2ClientEvent::InvoiceParametersReady { + request_id, + counterparty_node_id: *counterparty_node_id, + intercept_scid, + cltv_expiry_delta: result.lsp_cltv_expiry_delta, + payment_size_msat: jit_channel.payment_size_msat, + }, + )); + } else { + return Err(LightningError { + err: format!( + "Received buy response with an invalid intercept scid {:?}", + result.jit_channel_scid + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + }, + None => { + return Err(LightningError { + err: format!( + "Received buy response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + }, + } + Ok(()) + } + + fn handle_buy_error( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, _error: ResponseError, + ) -> Result<(), LightningError> { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + peer_state.pending_buy_requests.remove(&request_id).ok_or(LightningError { + err: format!("Received buy error for an unknown request: {:?}", request_id), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + Ok(()) + }, + None => { + return Err(LightningError { err: format!("Received error response for a buy request from an unknown counterparty ({:?})", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + } +} + +impl ProtocolMessageHandler for LSPS2ClientHandler +where + ES::Target: EntropySource, +{ + type ProtocolMessage = LSPS2Message; + const PROTOCOL_NUMBER: Option = Some(2); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS2Message::Response(request_id, response) => match response { + LSPS2Response::GetInfo(result) => { + self.handle_get_info_response(request_id, counterparty_node_id, result) + }, + LSPS2Response::GetInfoError(error) => { + self.handle_get_info_error(request_id, counterparty_node_id, error) + }, + LSPS2Response::Buy(result) => { + self.handle_buy_response(request_id, counterparty_node_id, result) + }, + LSPS2Response::BuyError(error) => { + self.handle_buy_error(request_id, counterparty_node_id, error) + }, + }, + _ => { + debug_assert!( + false, + "Client handler received LSPS2 request message. This should never happen." + ); + Err(LightningError { err: format!("Client handler received LSPS2 request message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +#[cfg(test)] +mod tests {} diff --git a/lightning-liquidity/src/lsps2/event.rs b/lightning-liquidity/src/lsps2/event.rs new file mode 100644 index 00000000000..1e28b259757 --- /dev/null +++ b/lightning-liquidity/src/lsps2/event.rs @@ -0,0 +1,125 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains LSPS2 event types + +use super::msgs::OpeningFeeParams; +use crate::lsps0::ser::RequestId; +use crate::prelude::{String, Vec}; + +use bitcoin::secp256k1::PublicKey; + +/// An event which an LSPS2 client should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS2ClientEvent { + /// Information from the LSP about their current fee rates and channel parameters. + /// + /// You must call [`LSPS2ClientHandler::select_opening_params`] with the fee parameter + /// you want to use if you wish to proceed opening a channel. + /// + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + OpeningParametersReady { + /// The identifier of the issued LSPS2 `get_info` request, as returned by + /// [`LSPS2ClientHandler::request_opening_params`] + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS2ClientHandler::request_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::request_opening_params + request_id: RequestId, + /// The node id of the LSP that provided this response. + counterparty_node_id: PublicKey, + /// The menu of fee parameters the LSP is offering at this time. + /// You must select one of these if you wish to proceed. + opening_fee_params_menu: Vec, + }, + /// Provides the necessary information to generate a payable invoice that then may be given to + /// the payer. + /// + /// When the invoice is paid, the LSP will open a channel with the previously agreed upon + /// parameters to you. + InvoiceParametersReady { + /// The identifier of the issued LSPS2 `buy` request, as returned by + /// [`LSPS2ClientHandler::select_opening_params`]. + /// + /// This can be used to track which request this event corresponds to. + /// + /// [`LSPS2ClientHandler::select_opening_params`]: crate::lsps2::client::LSPS2ClientHandler::select_opening_params + request_id: RequestId, + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The intercept short channel id to use in the route hint. + intercept_scid: u64, + /// The `cltv_expiry_delta` to use in the route hint. + cltv_expiry_delta: u32, + /// The initial payment size you specified. + payment_size_msat: Option, + }, +} + +/// An event which an LSPS2 server should take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum LSPS2ServiceEvent { + /// A request from a client for information about JIT Channel parameters. + /// + /// You must calculate the parameters for this client and pass them to + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]. + /// + /// If an unrecognized or stale token is provided you can use + /// `[LSPS2ServiceHandler::invalid_token_provided`] to error the request. + /// + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated + /// [`LSPS2ServiceHandler::invalid_token_provided`]: crate::lsps2::service::LSPS2ServiceHandler::invalid_token_provided + GetInfo { + /// An identifier that must be passed to [`LSPS2ServiceHandler::opening_fee_params_generated`]. + /// + /// [`LSPS2ServiceHandler::opening_fee_params_generated`]: crate::lsps2::service::LSPS2ServiceHandler::opening_fee_params_generated + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// An optional token that can be used as an API key, coupon code, etc. + token: Option, + }, + /// A client has selected a opening fee parameter to use and would like to + /// purchase a channel with an optional initial payment size. + /// + /// If `payment_size_msat` is [`Option::Some`] then the payer is allowed to use MPP. + /// If `payment_size_msat` is [`Option::None`] then the payer cannot use MPP. + /// + /// You must generate an intercept scid and `cltv_expiry_delta` for them to use + /// and call [`LSPS2ServiceHandler::invoice_parameters_generated`]. + /// + /// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated + BuyRequest { + /// An identifier that must be passed into [`LSPS2ServiceHandler::invoice_parameters_generated`]. + /// + /// [`LSPS2ServiceHandler::invoice_parameters_generated`]: crate::lsps2::service::LSPS2ServiceHandler::invoice_parameters_generated + request_id: RequestId, + /// The client node id that is making this request. + counterparty_node_id: PublicKey, + /// The channel parameters they have selected. + opening_fee_params: OpeningFeeParams, + /// The size of the initial payment they would like to receive. + payment_size_msat: Option, + }, + /// You should open a channel using [`ChannelManager::create_channel`]. + /// + /// [`ChannelManager::create_channel`]: lightning::ln::channelmanager::ChannelManager::create_channel + OpenChannel { + /// The node to open channel with. + their_network_key: PublicKey, + /// The amount to forward after fees. + amt_to_forward_msat: u64, + /// The fee earned for opening the channel. + opening_fee_msat: u64, + /// A user specified id used to track channel open. + user_channel_id: u128, + /// The intercept short channel id to use in the route hint. + intercept_scid: u64, + }, +} diff --git a/lightning-liquidity/src/lsps2/mod.rs b/lightning-liquidity/src/lsps2/mod.rs new file mode 100644 index 00000000000..0a29ac636d6 --- /dev/null +++ b/lightning-liquidity/src/lsps2/mod.rs @@ -0,0 +1,17 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Implementation of LSPS2: JIT Channel Negotiation specification. + +pub mod client; +pub mod event; +pub mod msgs; +pub(crate) mod payment_queue; +pub mod service; +pub mod utils; diff --git a/lightning-liquidity/src/lsps2/msgs.rs b/lightning-liquidity/src/lsps2/msgs.rs new file mode 100644 index 00000000000..f7c0df9db06 --- /dev/null +++ b/lightning-liquidity/src/lsps2/msgs.rs @@ -0,0 +1,434 @@ +//! Message, request, and other primitive types used to implement LSPS2. + +use core::convert::TryFrom; + +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; +use chrono::Utc; +use serde::{Deserialize, Serialize}; + +use lightning::util::scid_utils; + +use crate::lsps0::ser::{ + string_amount, string_amount_option, LSPSMessage, RequestId, ResponseError, +}; +use crate::prelude::{String, Vec}; +use crate::utils; + +pub(crate) const LSPS2_GET_INFO_METHOD_NAME: &str = "lsps2.get_info"; +pub(crate) const LSPS2_BUY_METHOD_NAME: &str = "lsps2.buy"; + +pub(crate) const LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE: i32 = 200; + +pub(crate) const LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE: i32 = 201; +pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE: i32 = 202; +pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE: i32 = 203; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// A request made to an LSP to learn their current channel fees and parameters. +pub struct GetInfoRequest { + /// An optional token to provide to the LSP. + pub token: Option, +} + +/// Fees and parameters for a JIT Channel without the promise. +/// +/// The promise will be calculated automatically for the LSP and this type converted +/// into an [`OpeningFeeParams`] for transit over the wire. +pub struct RawOpeningFeeParams { + /// The minimum fee required for the channel open. + pub min_fee_msat: u64, + /// A fee proportional to the size of the initial payment. + pub proportional: u32, + /// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid. + pub valid_until: chrono::DateTime, + /// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing. + pub min_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that the LSP will accept when opening a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size that the LSP will accept when opening a channel. + pub max_payment_size_msat: u64, +} + +impl RawOpeningFeeParams { + pub(crate) fn into_opening_fee_params(self, promise_secret: &[u8; 32]) -> OpeningFeeParams { + let mut hmac = HmacEngine::::new(promise_secret); + hmac.input(&self.min_fee_msat.to_be_bytes()); + hmac.input(&self.proportional.to_be_bytes()); + hmac.input(self.valid_until.to_rfc3339().as_bytes()); + hmac.input(&self.min_lifetime.to_be_bytes()); + hmac.input(&self.max_client_to_self_delay.to_be_bytes()); + hmac.input(&self.min_payment_size_msat.to_be_bytes()); + hmac.input(&self.max_payment_size_msat.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).to_byte_array(); + let promise = utils::hex_str(&promise_bytes[..]); + OpeningFeeParams { + min_fee_msat: self.min_fee_msat, + proportional: self.proportional, + valid_until: self.valid_until.clone(), + min_lifetime: self.min_lifetime, + max_client_to_self_delay: self.max_client_to_self_delay, + min_payment_size_msat: self.min_payment_size_msat, + max_payment_size_msat: self.max_payment_size_msat, + promise, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// Fees and parameters for a JIT Channel including the promise. +/// +/// The promise is an HMAC calculated using a secret known to the LSP and the rest of the fields as input. +/// It exists so the LSP can verify the authenticity of a client provided OpeningFeeParams by recalculating +/// the promise using the secret. Once verified they can be confident it was not modified by the client. +pub struct OpeningFeeParams { + /// The minimum fee required for the channel open. + #[serde(with = "string_amount")] + pub min_fee_msat: u64, + /// A fee proportional to the size of the initial payment. + pub proportional: u32, + /// An [`ISO8601`](https://www.iso.org/iso-8601-date-and-time-format.html) formatted date for which these params are valid. + pub valid_until: chrono::DateTime, + /// The number of blocks after confirmation that the LSP promises it will keep the channel alive without closing. + pub min_lifetime: u32, + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. + pub max_client_to_self_delay: u32, + /// The minimum payment size that the LSP will accept when opening a channel. + #[serde(with = "string_amount")] + pub min_payment_size_msat: u64, + /// The maximum payment size that the LSP will accept when opening a channel. + #[serde(with = "string_amount")] + pub max_payment_size_msat: u64, + /// The HMAC used to verify the authenticity of these parameters. + pub promise: String, +} + +/// A response to a [`GetInfoRequest`] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetInfoResponse { + /// A set of opening fee parameters. + pub opening_fee_params_menu: Vec, +} + +/// A request to buy a JIT channel. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuyRequest { + /// The fee parameters you would like to use. + pub opening_fee_params: OpeningFeeParams, + /// The size of the initial payment you expect to receive. + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + #[serde(with = "string_amount_option")] + pub payment_size_msat: Option, +} + +/// A newtype that holds a `short_channel_id` in human readable format of BBBxTTTx000. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct InterceptScid(String); + +impl From for InterceptScid { + fn from(scid: u64) -> Self { + let block = scid_utils::block_from_scid(scid); + let tx_index = scid_utils::tx_index_from_scid(scid); + let vout = scid_utils::vout_from_scid(scid); + + Self(format!("{}x{}x{}", block, tx_index, vout)) + } +} + +impl InterceptScid { + /// Try to convert a [`InterceptScid`] into a u64 used by LDK. + pub fn to_scid(&self) -> Result { + utils::scid_from_human_readable_string(&self.0) + } +} + +/// A response to a [`BuyRequest`]. +/// +/// Includes information needed to construct an invoice. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuyResponse { + /// The intercept short channel id used by LSP to identify need to open channel. + pub jit_channel_scid: InterceptScid, + /// The locktime expiry delta the lsp requires. + pub lsp_cltv_expiry_delta: u32, + /// A flag that indicates who is trusting who. + #[serde(default)] + pub client_trusts_lsp: bool, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC requests in the LSPS2 protocol. +pub enum LSPS2Request { + /// A request to learn an LSP's channel fees and parameters. + GetInfo(GetInfoRequest), + /// A request to buy a JIT channel from an LSP. + Buy(BuyRequest), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all the valid JSON-RPC responses in the LSPS2 protocol. +pub enum LSPS2Response { + /// A successful response to a [`LSPS2Request::GetInfo`] request. + GetInfo(GetInfoResponse), + /// An error response to a [`LSPS2Request::GetInfo`] request. + GetInfoError(ResponseError), + /// A successful response to a [`LSPS2Request::Buy`] request. + Buy(BuyResponse), + /// An error response to a [`LSPS2Request::Buy`] request. + BuyError(ResponseError), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +/// An enum that captures all valid JSON-RPC messages in the LSPS2 protocol. +pub enum LSPS2Message { + /// An LSPS2 JSON-RPC request. + Request(RequestId, LSPS2Request), + /// An LSPS2 JSON-RPC response. + Response(RequestId, LSPS2Response), +} + +impl TryFrom for LSPS2Message { + type Error = (); + + fn try_from(message: LSPSMessage) -> Result { + if let LSPSMessage::LSPS2(message) = message { + return Ok(message); + } + + Err(()) + } +} + +impl From for LSPSMessage { + fn from(message: LSPS2Message) -> Self { + LSPSMessage::LSPS2(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::alloc::string::ToString; + use crate::lsps2::utils::is_valid_opening_fee_params; + + #[test] + fn into_opening_fee_params_produces_valid_promise() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until: chrono::DateTime = + chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + let min_payment_size_msat = 1; + let max_payment_size_msat = 100_000_000; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.clone().into(), + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let promise_secret = [1u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + + assert_eq!(opening_fee_params.min_fee_msat, min_fee_msat); + assert_eq!(opening_fee_params.proportional, proportional); + assert_eq!(opening_fee_params.valid_until, valid_until); + assert_eq!(opening_fee_params.min_lifetime, min_lifetime); + assert_eq!(opening_fee_params.max_client_to_self_delay, max_client_to_self_delay); + assert_eq!(opening_fee_params.min_payment_size_msat, min_payment_size_msat); + assert_eq!(opening_fee_params.max_payment_size_msat, max_payment_size_msat); + + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } + + #[test] + fn changing_single_field_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + let min_payment_size_msat = 1; + let max_payment_size_msat = 100_000_000; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let promise_secret = [1u8; 32]; + + let mut opening_fee_params = raw.into_opening_fee_params(&promise_secret); + opening_fee_params.min_fee_msat = min_fee_msat + 1; + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } + + #[test] + fn wrong_secret_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + let min_payment_size_msat = 1; + let max_payment_size_msat = 100_000_000; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let promise_secret = [1u8; 32]; + let other_secret = [2u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret)); + } + + #[test] + #[cfg(feature = "std")] + // TODO: We need to find a way to check expiry times in no-std builds. + fn expired_params_produces_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + let min_payment_size_msat = 1; + let max_payment_size_msat = 100_000_000; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let promise_secret = [1u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + } + + #[test] + fn buy_request_serialization() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + let min_payment_size_msat = 1; + let max_payment_size_msat = 100_000_000; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + min_payment_size_msat, + max_payment_size_msat, + }; + + let promise_secret = [1u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + let json_str = r#"{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"}"#; + assert_eq!(json_str, serde_json::json!(opening_fee_params).to_string()); + assert_eq!(opening_fee_params, serde_json::from_str(json_str).unwrap()); + + let payment_size_msat = Some(1234); + let buy_request_fixed = + BuyRequest { opening_fee_params: opening_fee_params.clone(), payment_size_msat }; + let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"},"payment_size_msat":"1234"}"#; + assert_eq!(json_str, serde_json::json!(buy_request_fixed).to_string()); + assert_eq!(buy_request_fixed, serde_json::from_str(json_str).unwrap()); + + let payment_size_msat = None; + let buy_request_variable = BuyRequest { opening_fee_params, payment_size_msat }; + + // Check we skip serialization if payment_size_msat is None. + let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"}}"#; + assert_eq!(json_str, serde_json::json!(buy_request_variable).to_string()); + assert_eq!(buy_request_variable, serde_json::from_str(json_str).unwrap()); + + // Check we still deserialize correctly if payment_size_msat is 'null'. + let json_str = r#"{"opening_fee_params":{"max_client_to_self_delay":128,"max_payment_size_msat":"100000000","min_fee_msat":"100","min_lifetime":144,"min_payment_size_msat":"1","promise":"1134a5c51e3ba2e8f4259610d5e12c1bf4c50ddcd3f8af563e0a00d1fff41dea","proportional":21,"valid_until":"2023-05-20T08:30:45Z"},"payment_size_msat":null}"#; + assert_eq!(buy_request_variable, serde_json::from_str(json_str).unwrap()); + } + + #[test] + fn parse_spec_test_vectors() { + // Here, we simply assert that we're able to parse all examples given in LSPS2. + let json_str = r#"{ + "opening_fee_params_menu": [ + { + "min_fee_msat": "546000", + "proportional": 1200, + "valid_until": "2023-02-23T08:47:30.511Z", + "min_lifetime": 1008, + "max_client_to_self_delay": 2016, + "min_payment_size_msat": "1000", + "max_payment_size_msat": "1000000", + "promise": "abcdefghijklmnopqrstuvwxyz" + }, + { + "min_fee_msat": "1092000", + "proportional": 2400, + "valid_until": "2023-02-27T21:23:57.984Z", + "min_lifetime": 1008, + "max_client_to_self_delay": 2016, + "min_payment_size_msat": "1000", + "max_payment_size_msat": "1000000", + "promise": "abcdefghijklmnopqrstuvwxyz" + } + ] + }"#; + let _get_info_response: GetInfoResponse = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "opening_fee_params": { + "min_fee_msat": "546000", + "proportional": 1200, + "valid_until": "2023-02-23T08:47:30.511Z", + "min_lifetime": 1008, + "max_client_to_self_delay": 2016, + "min_payment_size_msat": "1000", + "max_payment_size_msat": "1000000", + "promise": "abcdefghijklmnopqrstuvwxyz" + }, + "payment_size_msat": "42000" + }"#; + let _buy_request: BuyRequest = serde_json::from_str(json_str).unwrap(); + + let json_str = r#"{ + "jit_channel_scid": "29451x4815x1", + "lsp_cltv_expiry_delta" : 144, + "client_trusts_lsp": false + }"#; + let _buy_response: BuyResponse = serde_json::from_str(json_str).unwrap(); + } +} diff --git a/lightning-liquidity/src/lsps2/payment_queue.rs b/lightning-liquidity/src/lsps2/payment_queue.rs new file mode 100644 index 00000000000..1ad3164df36 --- /dev/null +++ b/lightning-liquidity/src/lsps2/payment_queue.rs @@ -0,0 +1,119 @@ +use crate::prelude::Vec; +use lightning::ln::channelmanager::InterceptId; +use lightning_types::payment::PaymentHash; + +/// Holds payments with the corresponding HTLCs until it is possible to pay the fee. +/// When the fee is successfully paid with a forwarded payment, the queue should be consumed and the +/// remaining payments forwarded. +#[derive(Clone, PartialEq, Eq, Debug)] +pub(crate) struct PaymentQueue { + payments: Vec<(PaymentHash, Vec)>, +} + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) struct InterceptedHTLC { + pub(crate) intercept_id: InterceptId, + pub(crate) expected_outbound_amount_msat: u64, + pub(crate) payment_hash: PaymentHash, +} + +impl PaymentQueue { + pub(crate) fn new() -> PaymentQueue { + PaymentQueue { payments: Vec::new() } + } + + pub(crate) fn add_htlc(&mut self, new_htlc: InterceptedHTLC) -> (u64, usize) { + let payment = self.payments.iter_mut().find(|(p, _)| p == &new_htlc.payment_hash); + if let Some((payment_hash, htlcs)) = payment { + // HTLCs within a payment should have the same payment hash. + debug_assert!(htlcs.iter().all(|htlc| htlc.payment_hash == *payment_hash)); + // The given HTLC should not already be present. + debug_assert!(htlcs.iter().all(|htlc| htlc.intercept_id != new_htlc.intercept_id)); + htlcs.push(new_htlc); + let total_expected_outbound_amount_msat = + htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); + (total_expected_outbound_amount_msat, htlcs.len()) + } else { + let expected_outbound_amount_msat = new_htlc.expected_outbound_amount_msat; + self.payments.push((new_htlc.payment_hash, vec![new_htlc])); + (expected_outbound_amount_msat, 1) + } + } + + pub(crate) fn pop_greater_than_msat( + &mut self, amount_msat: u64, + ) -> Option<(PaymentHash, Vec)> { + let position = self.payments.iter().position(|(_payment_hash, htlcs)| { + htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum::() >= amount_msat + }); + position.map(|position| self.payments.remove(position)) + } + + pub(crate) fn clear(&mut self) -> Vec { + self.payments.drain(..).map(|(_k, v)| v).flatten().collect() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_payment_queue() { + let mut payment_queue = PaymentQueue::new(); + assert_eq!( + payment_queue.add_htlc(InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([100; 32]), + }), + (200_000_000, 1), + ); + assert_eq!(payment_queue.pop_greater_than_msat(500_000_000), None); + + assert_eq!( + payment_queue.add_htlc(InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([101; 32]), + }), + (300_000_000, 1), + ); + assert_eq!(payment_queue.pop_greater_than_msat(500_000_000), None); + + assert_eq!( + payment_queue.add_htlc(InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }), + (500_000_000, 2), + ); + assert_eq!( + payment_queue.pop_greater_than_msat(500_000_000), + Some(( + PaymentHash([100; 32]), + vec![ + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([100; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }, + ] + )) + ); + assert_eq!( + payment_queue.clear(), + vec![InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([101; 32]), + }] + ); + } +} diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs new file mode 100644 index 00000000000..e68b1c49a10 --- /dev/null +++ b/lightning-liquidity/src/lsps2/service.rs @@ -0,0 +1,1603 @@ +// This file is Copyright its original authors, visible in version control +// history. +// +// This file is licensed under the Apache License, Version 2.0 or the MIT license +// , at your option. +// You may not use this file except in accordance with one or both of these +// licenses. + +//! Contains the main LSPS2 server-side object, [`LSPS2ServiceHandler`]. + +use crate::events::{Event, EventQueue}; +use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; +use crate::lsps2::event::LSPS2ServiceEvent; +use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; +use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; +use crate::message_queue::MessageQueue; +use crate::prelude::{HashMap, String, ToString, Vec}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::events::HTLCDestination; +use lightning::ln::channelmanager::{AChannelManager, InterceptId}; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::ln::types::ChannelId; +use lightning::util::errors::APIError; +use lightning::util::logger::Level; + +use lightning_types::payment::PaymentHash; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +use crate::lsps2::msgs::{ + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, LSPS2Message, LSPS2Request, + LSPS2Response, OpeningFeeParams, RawOpeningFeeParams, + LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, +}; + +/// Server-side configuration options for JIT channels. +#[derive(Clone, Debug)] +pub struct LSPS2ServiceConfig { + /// Used to calculate the promise for channel parameters supplied to clients. + /// + /// Note: If this changes then old promises given out will be considered invalid. + pub promise_secret: [u8; 32], +} + +/// Information about the initial payment size and JIT channel opening fee. +/// This will be provided in the `OpenChannel` event. +#[derive(Clone, Debug, PartialEq)] +struct OpenChannelParams { + opening_fee_msat: u64, + amt_to_forward_msat: u64, +} + +/// A payment that will be forwarded while skimming the given JIT channel opening fee. +#[derive(Clone, Debug, PartialEq)] +struct FeePayment { + htlcs: Vec, + opening_fee_msat: u64, +} + +#[derive(Debug)] +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +/// Possible actions that need to be taken when an HTLC is intercepted. +#[derive(Debug, PartialEq)] +enum HTLCInterceptedAction { + /// The opening of the JIT channel. + OpenChannel(OpenChannelParams), + /// The forwarding of the intercepted HTLC. + ForwardHTLC(ChannelId), + ForwardPayment(ChannelId, FeePayment), +} + +/// The forwarding of a payment while skimming the JIT channel opening fee. +#[derive(Debug, PartialEq)] +struct ForwardPaymentAction(ChannelId, FeePayment); + +/// The forwarding of previously intercepted HTLCs without skimming any further fees. +#[derive(Debug, PartialEq)] +struct ForwardHTLCsAction(ChannelId, Vec); + +/// The different states a requested JIT channel can be in. +#[derive(Debug)] +enum OutboundJITChannelState { + /// The JIT channel SCID was created after a buy request, and we are awaiting an initial payment + /// of sufficient size to open the channel. + PendingInitialPayment { payment_queue: Arc> }, + /// An initial payment of sufficient size was intercepted to the JIT channel SCID, triggering the + /// opening of the channel. We are awaiting the completion of the channel establishment. + PendingChannelOpen { payment_queue: Arc>, opening_fee_msat: u64 }, + /// The channel is open and a payment was forwarded while skimming the JIT channel fee. + /// No further payments can be forwarded until the pending payment succeeds or fails, as we need + /// to know whether the JIT channel fee needs to be skimmed from a next payment or not. + PendingPaymentForward { + payment_queue: Arc>, + opening_fee_msat: u64, + channel_id: ChannelId, + }, + /// The channel is open, no payment is currently being forwarded, and the JIT channel fee still + /// needs to be paid. This state can occur when the initial payment fails, e.g. due to a + /// prepayment probe. We are awaiting a next payment of sufficient size to forward and skim the + /// JIT channel fee. + PendingPayment { + payment_queue: Arc>, + opening_fee_msat: u64, + channel_id: ChannelId, + }, + /// The channel is open and a payment was successfully forwarded while skimming the JIT channel + /// fee. Any subsequent HTLCs can be forwarded without additional logic. + PaymentForwarded { channel_id: ChannelId }, +} + +impl OutboundJITChannelState { + fn new() -> Self { + OutboundJITChannelState::PendingInitialPayment { + payment_queue: Arc::new(Mutex::new(PaymentQueue::new())), + } + } + + fn htlc_intercepted( + &mut self, opening_fee_params: &OpeningFeeParams, payment_size_msat: &Option, + htlc: InterceptedHTLC, + ) -> Result<(Self, Option), ChannelStateError> { + match self { + OutboundJITChannelState::PendingInitialPayment { payment_queue } => { + let (total_expected_outbound_amount_msat, num_htlcs) = + payment_queue.lock().unwrap().add_htlc(htlc); + + let (expected_payment_size_msat, mpp_mode) = + if let Some(payment_size_msat) = payment_size_msat { + (*payment_size_msat, true) + } else { + debug_assert_eq!(num_htlcs, 1); + if num_htlcs != 1 { + return Err(ChannelStateError( + "Paying via multiple HTLCs is disallowed in \"no-MPP+var-invoice\" mode.".to_string() + )); + } + (total_expected_outbound_amount_msat, false) + }; + + if expected_payment_size_msat < opening_fee_params.min_payment_size_msat + || expected_payment_size_msat > opening_fee_params.max_payment_size_msat + { + return Err(ChannelStateError( + format!("Payment size violates our limits: expected_payment_size_msat = {}, min_payment_size_msat = {}, max_payment_size_msat = {}", + expected_payment_size_msat, + opening_fee_params.min_payment_size_msat, + opening_fee_params.max_payment_size_msat + ))); + } + + let opening_fee_msat = compute_opening_fee( + expected_payment_size_msat, + opening_fee_params.min_fee_msat, + opening_fee_params.proportional.into(), + ).ok_or(ChannelStateError( + format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_payment_size_msat = {}", + opening_fee_params.min_fee_msat, + opening_fee_params.proportional, + expected_payment_size_msat + ) + ))?; + + let amt_to_forward_msat = + expected_payment_size_msat.saturating_sub(opening_fee_msat); + + // Go ahead and open the channel if we intercepted sufficient HTLCs. + if total_expected_outbound_amount_msat >= expected_payment_size_msat + && amt_to_forward_msat > 0 + { + let pending_channel_open = OutboundJITChannelState::PendingChannelOpen { + payment_queue: Arc::clone(&payment_queue), + opening_fee_msat, + }; + let open_channel = HTLCInterceptedAction::OpenChannel(OpenChannelParams { + opening_fee_msat, + amt_to_forward_msat, + }); + Ok((pending_channel_open, Some(open_channel))) + } else { + if mpp_mode { + let pending_initial_payment = + OutboundJITChannelState::PendingInitialPayment { + payment_queue: Arc::clone(&payment_queue), + }; + Ok((pending_initial_payment, None)) + } else { + Err(ChannelStateError( + "Intercepted HTLC is too small to pay opening fee".to_string(), + )) + } + } + }, + OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + payment_queue_lock.add_htlc(htlc); + let pending_channel_open = OutboundJITChannelState::PendingChannelOpen { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + }; + Ok((pending_channel_open, None)) + }, + OutboundJITChannelState::PendingPaymentForward { + payment_queue, + opening_fee_msat, + channel_id, + } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + payment_queue_lock.add_htlc(htlc); + let pending_payment_forward = OutboundJITChannelState::PendingPaymentForward { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + Ok((pending_payment_forward, None)) + }, + OutboundJITChannelState::PendingPayment { + payment_queue, + opening_fee_msat, + channel_id, + } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + payment_queue_lock.add_htlc(htlc); + if let Some((_payment_hash, htlcs)) = + payment_queue_lock.pop_greater_than_msat(*opening_fee_msat) + { + let pending_payment_forward = OutboundJITChannelState::PendingPaymentForward { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + let forward_payment = HTLCInterceptedAction::ForwardPayment( + *channel_id, + FeePayment { htlcs, opening_fee_msat: *opening_fee_msat }, + ); + Ok((pending_payment_forward, Some(forward_payment))) + } else { + let pending_payment = OutboundJITChannelState::PendingPayment { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + Ok((pending_payment, None)) + } + }, + OutboundJITChannelState::PaymentForwarded { channel_id } => { + let payment_forwarded = + OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + let forward = HTLCInterceptedAction::ForwardHTLC(*channel_id); + Ok((payment_forwarded, Some(forward))) + }, + } + } + + fn channel_ready( + &self, channel_id: ChannelId, + ) -> Result<(Self, ForwardPaymentAction), ChannelStateError> { + match self { + OutboundJITChannelState::PendingChannelOpen { payment_queue, opening_fee_msat } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + if let Some((_payment_hash, htlcs)) = + payment_queue_lock.pop_greater_than_msat(*opening_fee_msat) + { + let pending_payment_forward = OutboundJITChannelState::PendingPaymentForward { + payment_queue: Arc::clone(&payment_queue), + opening_fee_msat: *opening_fee_msat, + channel_id, + }; + let forward_payment = ForwardPaymentAction( + channel_id, + FeePayment { opening_fee_msat: *opening_fee_msat, htlcs }, + ); + Ok((pending_payment_forward, forward_payment)) + } else { + Err(ChannelStateError( + "No forwardable payment available when moving to channel ready." + .to_string(), + )) + } + }, + state => Err(ChannelStateError(format!( + "Channel ready received when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn htlc_handling_failed( + &mut self, + ) -> Result<(Self, Option), ChannelStateError> { + match self { + OutboundJITChannelState::PendingPaymentForward { + payment_queue, + opening_fee_msat, + channel_id, + } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + if let Some((_payment_hash, htlcs)) = + payment_queue_lock.pop_greater_than_msat(*opening_fee_msat) + { + let pending_payment_forward = OutboundJITChannelState::PendingPaymentForward { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + let forward_payment = ForwardPaymentAction( + *channel_id, + FeePayment { htlcs, opening_fee_msat: *opening_fee_msat }, + ); + Ok((pending_payment_forward, Some(forward_payment))) + } else { + let pending_payment = OutboundJITChannelState::PendingPayment { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + Ok((pending_payment, None)) + } + }, + OutboundJITChannelState::PendingPayment { + payment_queue, + opening_fee_msat, + channel_id, + } => { + let pending_payment = OutboundJITChannelState::PendingPayment { + payment_queue: payment_queue.clone(), + opening_fee_msat: *opening_fee_msat, + channel_id: *channel_id, + }; + Ok((pending_payment, None)) + }, + OutboundJITChannelState::PaymentForwarded { channel_id } => { + let payment_forwarded = + OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + Ok((payment_forwarded, None)) + }, + state => Err(ChannelStateError(format!( + "HTLC handling failed when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn payment_forwarded( + &mut self, + ) -> Result<(Self, Option), ChannelStateError> { + match self { + OutboundJITChannelState::PendingPaymentForward { + payment_queue, channel_id, .. + } => { + let mut payment_queue_lock = payment_queue.lock().unwrap(); + let payment_forwarded = + OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + let forward_htlcs = ForwardHTLCsAction(*channel_id, payment_queue_lock.clear()); + Ok((payment_forwarded, Some(forward_htlcs))) + }, + OutboundJITChannelState::PaymentForwarded { channel_id } => { + let payment_forwarded = + OutboundJITChannelState::PaymentForwarded { channel_id: *channel_id }; + Ok((payment_forwarded, None)) + }, + state => Err(ChannelStateError(format!( + "Payment forwarded when JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct OutboundJITChannel { + state: OutboundJITChannelState, + user_channel_id: u128, + opening_fee_params: OpeningFeeParams, + payment_size_msat: Option, +} + +impl OutboundJITChannel { + fn new( + payment_size_msat: Option, opening_fee_params: OpeningFeeParams, user_channel_id: u128, + ) -> Self { + Self { + user_channel_id, + state: OutboundJITChannelState::new(), + opening_fee_params, + payment_size_msat, + } + } + + fn htlc_intercepted( + &mut self, htlc: InterceptedHTLC, + ) -> Result, LightningError> { + let (new_state, action) = + self.state.htlc_intercepted(&self.opening_fee_params, &self.payment_size_msat, htlc)?; + self.state = new_state; + Ok(action) + } + + fn htlc_handling_failed(&mut self) -> Result, LightningError> { + let (new_state, action) = self.state.htlc_handling_failed()?; + self.state = new_state; + Ok(action) + } + + fn channel_ready( + &mut self, channel_id: ChannelId, + ) -> Result { + let (new_state, action) = self.state.channel_ready(channel_id)?; + self.state = new_state; + Ok(action) + } + + fn payment_forwarded(&mut self) -> Result, LightningError> { + let (new_state, action) = self.state.payment_forwarded()?; + self.state = new_state; + Ok(action) + } +} + +struct PeerState { + outbound_channels_by_intercept_scid: HashMap, + intercept_scid_by_user_channel_id: HashMap, + intercept_scid_by_channel_id: HashMap, + pending_requests: HashMap, +} + +impl PeerState { + fn new() -> Self { + let outbound_channels_by_intercept_scid = HashMap::new(); + let pending_requests = HashMap::new(); + let intercept_scid_by_user_channel_id = HashMap::new(); + let intercept_scid_by_channel_id = HashMap::new(); + Self { + outbound_channels_by_intercept_scid, + pending_requests, + intercept_scid_by_user_channel_id, + intercept_scid_by_channel_id, + } + } + + fn insert_outbound_channel(&mut self, intercept_scid: u64, channel: OutboundJITChannel) { + self.outbound_channels_by_intercept_scid.insert(intercept_scid, channel); + } +} + +/// The main object allowing to send and receive LSPS2 messages. +pub struct LSPS2ServiceHandler +where + CM::Target: AChannelManager, +{ + channel_manager: CM, + pending_messages: Arc, + pending_events: Arc, + per_peer_state: RwLock>>, + peer_by_intercept_scid: RwLock>, + peer_by_channel_id: RwLock>, + config: LSPS2ServiceConfig, +} + +impl LSPS2ServiceHandler +where + CM::Target: AChannelManager, +{ + /// Constructs a `LSPS2ServiceHandler`. + pub(crate) fn new( + pending_messages: Arc, pending_events: Arc, channel_manager: CM, + config: LSPS2ServiceConfig, + ) -> Self { + Self { + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + peer_by_intercept_scid: RwLock::new(HashMap::new()), + peer_by_channel_id: RwLock::new(HashMap::new()), + channel_manager, + config, + } + } + + /// Used by LSP to inform a client requesting a JIT Channel the token they used is invalid. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event. + /// + /// [`LSPS2ServiceEvent::GetInfo`]: crate::lsps2::event::LSPS2ServiceEvent::GetInfo + pub fn invalid_token_provided( + &self, counterparty_node_id: &PublicKey, request_id: RequestId, + ) -> Result<(), APIError> { + let (result, response) = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + match peer_state.pending_requests.remove(&request_id) { + Some(LSPS2Request::GetInfo(_)) => { + let response = LSPS2Response::GetInfoError(ResponseError { + code: LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, + message: "an unrecognized or stale token was provided".to_string(), + data: None, + }); + (Ok(()), Some(response)) + }, + _ => ( + Err(APIError::APIMisuseError { + err: format!( + "No pending get_info request for request_id: {:?}", + request_id + ), + }), + None, + ), + } + }, + None => ( + Err(APIError::APIMisuseError { + err: format!( + "No state for the counterparty exists: {:?}", + counterparty_node_id + ), + }), + None, + ), + } + }; + + if let Some(response) = response { + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result + } + + /// Used by LSP to provide fee parameters to a client requesting a JIT Channel. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::GetInfo`] event. + /// + /// [`LSPS2ServiceEvent::GetInfo`]: crate::lsps2::event::LSPS2ServiceEvent::GetInfo + pub fn opening_fee_params_generated( + &self, counterparty_node_id: &PublicKey, request_id: RequestId, + opening_fee_params_menu: Vec, + ) -> Result<(), APIError> { + let (result, response) = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + match peer_state.pending_requests.remove(&request_id) { + Some(LSPS2Request::GetInfo(_)) => { + let response = LSPS2Response::GetInfo(GetInfoResponse { + opening_fee_params_menu: opening_fee_params_menu + .into_iter() + .map(|param| { + param.into_opening_fee_params(&self.config.promise_secret) + }) + .collect(), + }); + (Ok(()), Some(response)) + }, + _ => ( + Err(APIError::APIMisuseError { + err: format!( + "No pending get_info request for request_id: {:?}", + request_id + ), + }), + None, + ), + } + }, + None => ( + Err(APIError::APIMisuseError { + err: format!( + "No state for the counterparty exists: {:?}", + counterparty_node_id + ), + }), + None, + ), + } + }; + + if let Some(response) = response { + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result + } + + /// Used by LSP to provide client with the intercept scid and cltv_expiry_delta to use in their invoice. + /// + /// Should be called in response to receiving a [`LSPS2ServiceEvent::BuyRequest`] event. + /// + /// [`LSPS2ServiceEvent::BuyRequest`]: crate::lsps2::event::LSPS2ServiceEvent::BuyRequest + pub fn invoice_parameters_generated( + &self, counterparty_node_id: &PublicKey, request_id: RequestId, intercept_scid: u64, + cltv_expiry_delta: u32, client_trusts_lsp: bool, user_channel_id: u128, + ) -> Result<(), APIError> { + let (result, response) = { + let outer_state_lock = self.per_peer_state.read().unwrap(); + + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + + match peer_state.pending_requests.remove(&request_id) { + Some(LSPS2Request::Buy(buy_request)) => { + { + let mut peer_by_intercept_scid = + self.peer_by_intercept_scid.write().unwrap(); + peer_by_intercept_scid + .insert(intercept_scid, *counterparty_node_id); + } + + let outbound_jit_channel = OutboundJITChannel::new( + buy_request.payment_size_msat, + buy_request.opening_fee_params, + user_channel_id, + ); + + peer_state + .intercept_scid_by_user_channel_id + .insert(user_channel_id, intercept_scid); + peer_state + .insert_outbound_channel(intercept_scid, outbound_jit_channel); + + let response = LSPS2Response::Buy(BuyResponse { + jit_channel_scid: intercept_scid.into(), + lsp_cltv_expiry_delta: cltv_expiry_delta, + client_trusts_lsp, + }); + (Ok(()), Some(response)) + }, + _ => ( + Err(APIError::APIMisuseError { + err: format!( + "No pending buy request for request_id: {:?}", + request_id + ), + }), + None, + ), + } + }, + None => ( + Err(APIError::APIMisuseError { + err: format!( + "No state for the counterparty exists: {:?}", + counterparty_node_id + ), + }), + None, + ), + } + }; + + if let Some(response) = response { + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result + } + + /// Forward [`Event::HTLCIntercepted`] event parameters into this function. + /// + /// Will fail the intercepted HTLC if the intercept scid matches a payment we are expecting + /// but the payment amount is incorrect or the expiry has passed. + /// + /// Will generate a [`LSPS2ServiceEvent::OpenChannel`] event if the intercept scid matches a payment we are expected + /// and the payment amount is correct and the offer has not expired. + /// + /// Will do nothing if the intercept scid does not match any of the ones we gave out. + /// + /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted + /// [`LSPS2ServiceEvent::OpenChannel`]: crate::lsps2::event::LSPS2ServiceEvent::OpenChannel + pub fn htlc_intercepted( + &self, intercept_scid: u64, intercept_id: InterceptId, expected_outbound_amount_msat: u64, + payment_hash: PaymentHash, + ) -> Result<(), APIError> { + let peer_by_intercept_scid = self.peer_by_intercept_scid.read().unwrap(); + if let Some(counterparty_node_id) = peer_by_intercept_scid.get(&intercept_scid) { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + if let Some(jit_channel) = + peer_state.outbound_channels_by_intercept_scid.get_mut(&intercept_scid) + { + let htlc = InterceptedHTLC { + intercept_id, + expected_outbound_amount_msat, + payment_hash, + }; + match jit_channel.htlc_intercepted(htlc) { + Ok(Some(HTLCInterceptedAction::OpenChannel(open_channel_params))) => { + let event = Event::LSPS2Service(LSPS2ServiceEvent::OpenChannel { + their_network_key: counterparty_node_id.clone(), + amt_to_forward_msat: open_channel_params.amt_to_forward_msat, + opening_fee_msat: open_channel_params.opening_fee_msat, + user_channel_id: jit_channel.user_channel_id, + intercept_scid, + }); + self.pending_events.enqueue(event); + }, + Ok(Some(HTLCInterceptedAction::ForwardHTLC(channel_id))) => { + self.channel_manager.get_cm().forward_intercepted_htlc( + intercept_id, + &channel_id, + *counterparty_node_id, + expected_outbound_amount_msat, + )?; + }, + Ok(Some(HTLCInterceptedAction::ForwardPayment( + channel_id, + FeePayment { opening_fee_msat, htlcs }, + ))) => { + let amounts_to_forward_msat = + calculate_amount_to_forward_per_htlc(&htlcs, opening_fee_msat); + + for (intercept_id, amount_to_forward_msat) in + amounts_to_forward_msat + { + self.channel_manager.get_cm().forward_intercepted_htlc( + intercept_id, + &channel_id, + *counterparty_node_id, + amount_to_forward_msat, + )?; + } + }, + Ok(None) => {}, + Err(e) => { + self.channel_manager + .get_cm() + .fail_intercepted_htlc(intercept_id)?; + peer_state + .outbound_channels_by_intercept_scid + .remove(&intercept_scid); + // TODO: cleanup peer_by_intercept_scid + return Err(APIError::APIMisuseError { err: e.err }); + }, + } + } + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty found for scid: {}", intercept_scid), + }); + }, + } + } + + Ok(()) + } + + /// Forward [`Event::HTLCHandlingFailed`] event parameter into this function. + /// + /// Will attempt to forward the next payment in the queue if one is present. + /// Will do nothing if the intercept scid does not match any of the ones we gave out + /// or if the payment queue is empty + /// + /// [`Event::HTLCHandlingFailed`]: lightning::events::Event::HTLCHandlingFailed + pub fn htlc_handling_failed( + &self, failed_next_destination: HTLCDestination, + ) -> Result<(), APIError> { + if let HTLCDestination::NextHopChannel { channel_id, .. } = failed_next_destination { + let peer_by_channel_id = self.peer_by_channel_id.read().unwrap(); + if let Some(counterparty_node_id) = peer_by_channel_id.get(&channel_id) { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + if let Some(intercept_scid) = + peer_state.intercept_scid_by_channel_id.get(&channel_id).copied() + { + if let Some(jit_channel) = peer_state + .outbound_channels_by_intercept_scid + .get_mut(&intercept_scid) + { + match jit_channel.htlc_handling_failed() { + Ok(Some(ForwardPaymentAction( + channel_id, + FeePayment { opening_fee_msat, htlcs }, + ))) => { + let amounts_to_forward_msat = + calculate_amount_to_forward_per_htlc( + &htlcs, + opening_fee_msat, + ); + + for (intercept_id, amount_to_forward_msat) in + amounts_to_forward_msat + { + self.channel_manager + .get_cm() + .forward_intercepted_htlc( + intercept_id, + &channel_id, + *counterparty_node_id, + amount_to_forward_msat, + )?; + } + }, + Ok(None) => {}, + Err(e) => { + return Err(APIError::APIMisuseError { + err: format!("Unable to fail HTLC: {}.", e.err), + }); + }, + } + } + } + }, + None => {}, + } + } + } + + Ok(()) + } + + /// Forward [`Event::PaymentForwarded`] event parameter into this function. + /// + /// Will register the forwarded payment as having paid the JIT channel fee, and forward any held + /// and future HTLCs for the SCID of the initial invoice. In the future, this will verify the + /// `skimmed_fee_msat` in [`Event::PaymentForwarded`]. + /// + /// Note that `next_channel_id` is required to be provided. Therefore, the corresponding + /// [`Event::PaymentForwarded`] events need to be generated and serialized by LDK versions + /// greater or equal to 0.0.107. + /// + /// [`Event::PaymentForwarded`]: lightning::events::Event::PaymentForwarded + pub fn payment_forwarded(&self, next_channel_id: ChannelId) -> Result<(), APIError> { + if let Some(counterparty_node_id) = + self.peer_by_channel_id.read().unwrap().get(&next_channel_id) + { + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(&counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + if let Some(intercept_scid) = + peer_state.intercept_scid_by_channel_id.get(&next_channel_id).copied() + { + if let Some(jit_channel) = + peer_state.outbound_channels_by_intercept_scid.get_mut(&intercept_scid) + { + match jit_channel.payment_forwarded() { + Ok(Some(ForwardHTLCsAction(channel_id, htlcs))) => { + for htlc in htlcs { + self.channel_manager.get_cm().forward_intercepted_htlc( + htlc.intercept_id, + &channel_id, + *counterparty_node_id, + htlc.expected_outbound_amount_msat, + )?; + } + }, + Ok(None) => {}, + Err(e) => { + return Err(APIError::APIMisuseError { + err: format!( + "Forwarded payment was not applicable for JIT channel: {}", + e.err + ), + }) + }, + } + } + } else { + return Err(APIError::APIMisuseError { + err: format!("No state for for channel id: {}", next_channel_id), + }); + } + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + }); + }, + } + } + + Ok(()) + } + + /// Forward [`Event::ChannelReady`] event parameters into this function. + /// + /// Will forward the intercepted HTLC if it matches a channel + /// we need to forward a payment over otherwise it will be ignored. + /// + /// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady + pub fn channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) -> Result<(), APIError> { + { + let mut peer_by_channel_id = self.peer_by_channel_id.write().unwrap(); + peer_by_channel_id.insert(*channel_id, *counterparty_node_id); + } + let outer_state_lock = self.per_peer_state.read().unwrap(); + match outer_state_lock.get(counterparty_node_id) { + Some(inner_state_lock) => { + let mut peer_state = inner_state_lock.lock().unwrap(); + if let Some(intercept_scid) = + peer_state.intercept_scid_by_user_channel_id.get(&user_channel_id).copied() + { + peer_state.intercept_scid_by_channel_id.insert(*channel_id, intercept_scid); + if let Some(jit_channel) = + peer_state.outbound_channels_by_intercept_scid.get_mut(&intercept_scid) + { + match jit_channel.channel_ready(*channel_id) { + Ok(ForwardPaymentAction( + channel_id, + FeePayment { opening_fee_msat, htlcs }, + )) => { + let amounts_to_forward_msat = + calculate_amount_to_forward_per_htlc(&htlcs, opening_fee_msat); + + for (intercept_id, amount_to_forward_msat) in + amounts_to_forward_msat + { + self.channel_manager.get_cm().forward_intercepted_htlc( + intercept_id, + &channel_id, + *counterparty_node_id, + amount_to_forward_msat, + )?; + } + }, + Err(e) => { + return Err(APIError::APIMisuseError { + err: format!( + "Failed to transition to channel ready: {}", + e.err + ), + }) + }, + } + } else { + return Err(APIError::APIMisuseError { + err: format!( + "Could not find a channel with user_channel_id {}", + user_channel_id + ), + }); + } + } else { + return Err(APIError::APIMisuseError { + err: format!( + "Could not find a channel with that user_channel_id {}", + user_channel_id + ), + }); + } + }, + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + }); + }, + } + + Ok(()) + } + + fn handle_get_info_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetInfoRequest, + ) -> Result<(), LightningError> { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock: &mut Mutex = + outer_state_lock.entry(*counterparty_node_id).or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); + + let event = Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id: *counterparty_node_id, + token: params.token, + }); + self.pending_events.enqueue(event); + Ok(()) + } + + fn handle_buy_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: BuyRequest, + ) -> Result<(), LightningError> { + if let Some(payment_size_msat) = params.payment_size_msat { + if payment_size_msat < params.opening_fee_params.min_payment_size_msat { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + message: "payment size is below our minimum supported payment size".to_string(), + data: None, + }); + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + + return Err(LightningError { + err: "payment size is below our minimum supported payment size".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + if payment_size_msat > params.opening_fee_params.max_payment_size_msat { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + message: "payment size is above our maximum supported payment size".to_string(), + data: None, + }); + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: "payment size is above our maximum supported payment size".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + match compute_opening_fee( + payment_size_msat, + params.opening_fee_params.min_fee_msat, + params.opening_fee_params.proportional.into(), + ) { + Some(opening_fee) => { + if opening_fee >= payment_size_msat { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, + message: "payment size is too small to cover the opening fee" + .to_string(), + data: None, + }); + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: "payment size is too small to cover the opening fee".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + }, + None => { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + message: "overflow error when calculating opening_fee".to_string(), + data: None, + }); + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: "overflow error when calculating opening_fee".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + }, + } + } + + // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. + + if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.config.promise_secret) { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + message: "valid_until is already past OR the promise did not match the provided parameters".to_string(), + data: None, + }); + let msg = LSPS2Message::Response(request_id, response).into(); + self.pending_messages.enqueue(counterparty_node_id, msg); + return Err(LightningError { + err: "invalid opening fee parameters were supplied by client".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); + } + + let event = Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + }); + self.pending_events.enqueue(event); + + Ok(()) + } +} + +impl ProtocolMessageHandler for LSPS2ServiceHandler +where + CM::Target: AChannelManager, +{ + type ProtocolMessage = LSPS2Message; + const PROTOCOL_NUMBER: Option = Some(2); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + LSPS2Message::Request(request_id, request) => match request { + LSPS2Request::GetInfo(params) => { + self.handle_get_info_request(request_id, counterparty_node_id, params) + }, + LSPS2Request::Buy(params) => { + self.handle_buy_request(request_id, counterparty_node_id, params) + }, + }, + _ => { + debug_assert!( + false, + "Service handler received LSPS2 response message. This should never happen." + ); + Err(LightningError { err: format!("Service handler received LSPS2 response message from node {:?}. This should never happen.", counterparty_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}) + }, + } + } +} + +fn calculate_amount_to_forward_per_htlc( + htlcs: &[InterceptedHTLC], total_fee_msat: u64, +) -> Vec<(InterceptId, u64)> { + // TODO: we should eventually make sure the HTLCs are all above ChannelDetails::next_outbound_minimum_msat + let total_expected_outbound_msat: u64 = + htlcs.iter().map(|htlc| htlc.expected_outbound_amount_msat).sum(); + if total_fee_msat > total_expected_outbound_msat { + debug_assert!(false, "Fee is larger than the total expected outbound amount."); + return Vec::new(); + } + + let mut fee_remaining_msat = total_fee_msat; + let mut per_htlc_forwards = vec![]; + for (index, htlc) in htlcs.iter().enumerate() { + let proportional_fee_amt_msat = (total_fee_msat as u128 + * htlc.expected_outbound_amount_msat as u128 + / total_expected_outbound_msat as u128) as u64; + + let mut actual_fee_amt_msat = core::cmp::min(fee_remaining_msat, proportional_fee_amt_msat); + actual_fee_amt_msat = + core::cmp::min(actual_fee_amt_msat, htlc.expected_outbound_amount_msat); + fee_remaining_msat -= actual_fee_amt_msat; + + if index == htlcs.len() - 1 { + actual_fee_amt_msat += fee_remaining_msat; + } + + let amount_to_forward_msat = + htlc.expected_outbound_amount_msat.saturating_sub(actual_fee_amt_msat); + + per_htlc_forwards.push((htlc.intercept_id, amount_to_forward_msat)) + } + per_htlc_forwards +} + +#[cfg(test)] +mod tests { + + use super::*; + use chrono::TimeZone; + use chrono::Utc; + use proptest::prelude::*; + + const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; + + fn arb_forward_amounts() -> impl Strategy { + (1u64..MAX_VALUE_MSAT, 1u64..MAX_VALUE_MSAT, 1u64..MAX_VALUE_MSAT, 1u64..MAX_VALUE_MSAT) + .prop_map(|(a, b, c, d)| { + (a, b, c, core::cmp::min(d, a.saturating_add(b).saturating_add(c))) + }) + } + + proptest! { + #[test] + fn proptest_calculate_amount_to_forward((o_0, o_1, o_2, total_fee_msat) in arb_forward_amounts()) { + let htlcs = vec![ + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: o_0, + payment_hash: PaymentHash([0; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: o_1, + payment_hash: PaymentHash([0; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: o_2, + payment_hash: PaymentHash([0; 32]), + }, + ]; + + let result = calculate_amount_to_forward_per_htlc(&htlcs, total_fee_msat); + let total_received_msat = o_0 + o_1 + o_2; + + if total_received_msat < total_fee_msat { + assert_eq!(result.len(), 0); + } else { + assert_ne!(result.len(), 0); + assert_eq!(result[0].0, htlcs[0].intercept_id); + assert_eq!(result[1].0, htlcs[1].intercept_id); + assert_eq!(result[2].0, htlcs[2].intercept_id); + assert!(result[0].1 <= o_0); + assert!(result[1].1 <= o_1); + assert!(result[2].1 <= o_2); + + let result_sum = result.iter().map(|(_, f)| f).sum::(); + assert_eq!(total_received_msat - result_sum, total_fee_msat); + let five_pct = result_sum as f32 * 0.05; + let fair_share_0 = (o_0 as f32 / total_received_msat as f32) * result_sum as f32; + assert!(result[0].1 as f32 <= fair_share_0 + five_pct); + let fair_share_1 = (o_1 as f32 / total_received_msat as f32) * result_sum as f32; + assert!(result[1].1 as f32 <= fair_share_1 + five_pct); + let fair_share_2 = (o_2 as f32 / total_received_msat as f32) * result_sum as f32; + assert!(result[2].1 as f32 <= fair_share_2 + five_pct); + } + } + } + + #[test] + fn test_calculate_amount_to_forward() { + let htlcs = vec![ + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 2, + payment_hash: PaymentHash([0; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 6, + payment_hash: PaymentHash([0; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 2, + payment_hash: PaymentHash([0; 32]), + }, + ]; + let result = calculate_amount_to_forward_per_htlc(&htlcs, 5); + assert_eq!( + result, + vec![ + (htlcs[0].intercept_id, 1), + (htlcs[1].intercept_id, 3), + (htlcs[2].intercept_id, 1), + ] + ); + } + + #[test] + fn test_jit_channel_state_mpp() { + let payment_size_msat = Some(500_000_000); + let opening_fee_params = OpeningFeeParams { + min_fee_msat: 10_000_000, + proportional: 10_000, + valid_until: Utc.timestamp_opt(3000, 0).unwrap(), + min_lifetime: 4032, + max_client_to_self_delay: 2016, + min_payment_size_msat: 10_000_000, + max_payment_size_msat: 1_000_000_000, + promise: "ignore".to_string(), + }; + let mut state = OutboundJITChannelState::new(); + // Intercepts the first HTLC of a multipart payment A. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([100; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingInitialPayment { .. })); + assert!(action.is_none()); + state = new_state; + } + // Intercepts the first HTLC of a different multipart payment B. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 1_000_000, + payment_hash: PaymentHash([101; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingInitialPayment { .. })); + assert!(action.is_none()); + state = new_state; + } + // Intercepts the second HTLC of multipart payment A, completing the expected payment and + // opening the channel. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingChannelOpen { .. })); + assert!(matches!(action, Some(HTLCInterceptedAction::OpenChannel(_)))); + state = new_state; + } + // Channel opens, becomes ready, and multipart payment A gets forwarded. + { + let (new_state, ForwardPaymentAction(channel_id, payment)) = + state.channel_ready(ChannelId([200; 32])).unwrap(); + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!(payment.opening_fee_msat, 10_000_000); + assert_eq!( + payment.htlcs, + vec![ + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([100; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 300_000_000, + payment_hash: PaymentHash([100; 32]), + }, + ] + ); + state = new_state; + } + // Intercepts the first HTLC of a different payment C. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([3; 32]), + expected_outbound_amount_msat: 2_000_000, + payment_hash: PaymentHash([102; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingPaymentForward { .. })); + assert!(action.is_none()); + state = new_state; + } + // Payment A fails. + { + let (new_state, action) = state.htlc_handling_failed().unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingPayment { .. })); + // No payments have received sufficient HTLCs yet. + assert!(action.is_none()); + state = new_state; + } + // Additional HTLC of payment B arrives, completing the expectd payment. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([4; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([101; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingPaymentForward { .. })); + match action { + Some(HTLCInterceptedAction::ForwardPayment(channel_id, payment)) => { + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!(payment.opening_fee_msat, 10_000_000); + assert_eq!( + payment.htlcs, + vec![ + InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 1_000_000, + payment_hash: PaymentHash([101; 32]), + }, + InterceptedHTLC { + intercept_id: InterceptId([4; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([101; 32]), + }, + ] + ); + }, + _ => panic!("Unexpected action when intercepted HTLC."), + } + state = new_state; + } + // Payment completes, queued payments get forwarded. + { + let (new_state, action) = state.payment_forwarded().unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PaymentForwarded { .. })); + match action { + Some(ForwardHTLCsAction(channel_id, htlcs)) => { + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!( + htlcs, + vec![InterceptedHTLC { + intercept_id: InterceptId([3; 32]), + expected_outbound_amount_msat: 2_000_000, + payment_hash: PaymentHash([102; 32]), + }] + ); + }, + _ => panic!("Unexpected action when forwarded payment."), + } + state = new_state; + } + // Any new HTLC gets automatically forwarded. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([5; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([103; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PaymentForwarded { .. })); + assert!( + matches!(action, Some(HTLCInterceptedAction::ForwardHTLC(channel_id)) if channel_id == ChannelId([200; 32])) + ); + } + } + + #[test] + fn test_jit_channel_state_no_mpp() { + let payment_size_msat = None; + let opening_fee_params = OpeningFeeParams { + min_fee_msat: 10_000_000, + proportional: 10_000, + valid_until: Utc.timestamp_opt(3000, 0).unwrap(), + min_lifetime: 4032, + max_client_to_self_delay: 2016, + min_payment_size_msat: 10_000_000, + max_payment_size_msat: 1_000_000_000, + promise: "ignore".to_string(), + }; + let mut state = OutboundJITChannelState::new(); + // Intercepts payment A, opening the channel. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([100; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingChannelOpen { .. })); + assert!(matches!(action, Some(HTLCInterceptedAction::OpenChannel(_)))); + state = new_state; + } + // Intercepts payment B. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 600_000_000, + payment_hash: PaymentHash([101; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingChannelOpen { .. })); + assert!(action.is_none()); + state = new_state; + } + // Channel opens, becomes ready, and payment A gets forwarded. + { + let (new_state, ForwardPaymentAction(channel_id, payment)) = + state.channel_ready(ChannelId([200; 32])).unwrap(); + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!(payment.opening_fee_msat, 10_000_000); + assert_eq!( + payment.htlcs, + vec![InterceptedHTLC { + intercept_id: InterceptId([0; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([100; 32]), + },] + ); + state = new_state; + } + // Intercepts payment C. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([102; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingPaymentForward { .. })); + assert!(action.is_none()); + state = new_state; + } + // Payment A fails, and payment B is forwarded. + { + let (new_state, action) = state.htlc_handling_failed().unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PendingPaymentForward { .. })); + match action { + Some(ForwardPaymentAction(channel_id, payment)) => { + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!( + payment.htlcs, + vec![InterceptedHTLC { + intercept_id: InterceptId([1; 32]), + expected_outbound_amount_msat: 600_000_000, + payment_hash: PaymentHash([101; 32]), + },] + ); + }, + _ => panic!("Unexpected action when HTLC handling failed."), + } + state = new_state; + } + // Payment completes, queued payments get forwarded. + { + let (new_state, action) = state.payment_forwarded().unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PaymentForwarded { .. })); + match action { + Some(ForwardHTLCsAction(channel_id, htlcs)) => { + assert_eq!(channel_id, ChannelId([200; 32])); + assert_eq!( + htlcs, + vec![InterceptedHTLC { + intercept_id: InterceptId([2; 32]), + expected_outbound_amount_msat: 500_000_000, + payment_hash: PaymentHash([102; 32]), + }] + ); + }, + _ => panic!("Unexpected action when forwarded payment."), + } + state = new_state; + } + // Any new HTLC gets automatically forwarded. + { + let (new_state, action) = state + .htlc_intercepted( + &opening_fee_params, + &payment_size_msat, + InterceptedHTLC { + intercept_id: InterceptId([3; 32]), + expected_outbound_amount_msat: 200_000_000, + payment_hash: PaymentHash([103; 32]), + }, + ) + .unwrap(); + assert!(matches!(new_state, OutboundJITChannelState::PaymentForwarded { .. })); + assert!( + matches!(action, Some(HTLCInterceptedAction::ForwardHTLC(channel_id)) if channel_id == ChannelId([200; 32])) + ); + } + } +} diff --git a/lightning-liquidity/src/lsps2/utils.rs b/lightning-liquidity/src/lsps2/utils.rs new file mode 100644 index 00000000000..a48456ae769 --- /dev/null +++ b/lightning-liquidity/src/lsps2/utils.rs @@ -0,0 +1,86 @@ +//! Utilities for implementing the LSPS2 standard. + +use crate::lsps2::msgs::OpeningFeeParams; +use crate::utils; + +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; + +#[cfg(feature = "std")] +use std::time::{SystemTime, UNIX_EPOCH}; + +/// Determines if the given parameters are valid given the secret used to generate the promise. +pub fn is_valid_opening_fee_params( + fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], +) -> bool { + #[cfg(feature = "std")] + { + // TODO: We need to find a way to check expiry times in no-std builds. + let seconds_since_epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock to be ahead of the unix epoch") + .as_secs(); + let valid_until_seconds_since_epoch = fee_params + .valid_until + .timestamp() + .try_into() + .expect("expiration to be ahead of unix epoch"); + if seconds_since_epoch > valid_until_seconds_since_epoch { + return false; + } + } + + let mut hmac = HmacEngine::::new(promise_secret); + hmac.input(&fee_params.min_fee_msat.to_be_bytes()); + hmac.input(&fee_params.proportional.to_be_bytes()); + hmac.input(fee_params.valid_until.to_rfc3339().as_bytes()); + hmac.input(&fee_params.min_lifetime.to_be_bytes()); + hmac.input(&fee_params.max_client_to_self_delay.to_be_bytes()); + hmac.input(&fee_params.min_payment_size_msat.to_be_bytes()); + hmac.input(&fee_params.max_payment_size_msat.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).to_byte_array(); + let promise = utils::hex_str(&promise_bytes[..]); + promise == fee_params.promise +} + +/// Computes the opening fee given a payment size and the fee parameters. +/// +/// Returns [`Option::None`] when the computation overflows. +/// +/// See the [`specification`](https://github.com/BitcoinAndLightningLayerSpecs/lsp/tree/main/LSPS2#computing-the-opening_fee) for more details. +pub fn compute_opening_fee( + payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u64, +) -> Option { + payment_size_msat + .checked_mul(opening_fee_proportional) + .and_then(|f| f.checked_add(999999)) + .and_then(|f| f.checked_div(1000000)) + .map(|f| core::cmp::max(f, opening_fee_min_fee_msat)) +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + + const MAX_VALUE_MSAT: u64 = 21_000_000_0000_0000_000; + + fn arb_opening_fee_params() -> impl Strategy { + (0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT, 0u64..MAX_VALUE_MSAT) + } + + proptest! { + #[test] + fn test_compute_opening_fee((payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) in arb_opening_fee_params()) { + if let Some(res) = compute_opening_fee(payment_size_msat, opening_fee_min_fee_msat, opening_fee_proportional) { + assert!(res >= opening_fee_min_fee_msat); + assert_eq!(res as f32, (payment_size_msat as f32 * opening_fee_proportional as f32)); + } else { + // Check we actually overflowed. + let max_value = u64::MAX as u128; + assert!((payment_size_msat as u128 * opening_fee_proportional as u128) > max_value); + } + } + } +} diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs new file mode 100644 index 00000000000..8b6098a253b --- /dev/null +++ b/lightning-liquidity/src/manager.rs @@ -0,0 +1,665 @@ +use crate::events::{Event, EventQueue}; +use crate::lsps0::client::LSPS0ClientHandler; +use crate::lsps0::msgs::LSPS0Message; +use crate::lsps0::ser::{ + LSPSMessage, LSPSMethod, ProtocolMessageHandler, RawLSPSMessage, RequestId, ResponseError, + JSONRPC_INVALID_MESSAGE_ERROR_CODE, JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE, + LSPS_MESSAGE_TYPE_ID, +}; +use crate::lsps0::service::LSPS0ServiceHandler; +use crate::message_queue::MessageQueue; + +use crate::lsps1::client::{LSPS1ClientConfig, LSPS1ClientHandler}; +use crate::lsps1::msgs::LSPS1Message; +#[cfg(lsps1_service)] +use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; + +use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; +use crate::lsps2::msgs::LSPS2Message; +use crate::lsps2::service::{LSPS2ServiceConfig, LSPS2ServiceHandler}; +use crate::prelude::{HashMap, HashSet, ToString, Vec}; +use crate::sync::{Arc, Mutex, RwLock}; + +use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; +use lightning::ln::channelmanager::{AChannelManager, ChainParameters}; +use lightning::ln::msgs::{ErrorAction, LightningError}; +use lightning::ln::peer_handler::CustomMessageHandler; +use lightning::ln::wire::CustomMessageReader; +use lightning::sign::EntropySource; +use lightning::util::logger::Level; +use lightning::util::ser::Readable; + +use lightning_types::features::{InitFeatures, NodeFeatures}; + +use bitcoin::secp256k1::PublicKey; + +use core::ops::Deref; + +const LSPS_FEATURE_BIT: usize = 729; + +/// A server-side configuration for [`LiquidityManager`]. +/// +/// Allows end-users to configure options when using the [`LiquidityManager`] +/// to provide liquidity services to clients. +pub struct LiquidityServiceConfig { + /// Optional server-side configuration for LSPS1 channel requests. + #[cfg(lsps1_service)] + pub lsps1_service_config: Option, + /// Optional server-side configuration for JIT channels + /// should you want to support them. + pub lsps2_service_config: Option, + /// Controls whether the liquidity service should be advertised via setting the feature bit in + /// node announcment and the init message. + pub advertise_service: bool, +} + +/// A client-side configuration for [`LiquidityManager`]. +/// +/// Allows end-user to configure options when using the [`LiquidityManager`] +/// to access liquidity services from a provider. +pub struct LiquidityClientConfig { + /// Optional client-side configuration for LSPS1 channel requests. + pub lsps1_client_config: Option, + /// Optional client-side configuration for JIT channels. + pub lsps2_client_config: Option, +} + +/// The main interface into LSP functionality. +/// +/// Should be used as a [`CustomMessageHandler`] for your [`PeerManager`]'s [`MessageHandler`]. +/// +/// Users should provide a callback to process queued messages via +/// [`LiquidityManager::set_process_msgs_callback`] post construction. This allows the +/// [`LiquidityManager`] to wake the [`PeerManager`] when there are pending messages to be sent. +/// +/// Users need to continually poll [`LiquidityManager::get_and_clear_pending_events`] in order to surface +/// [`Event`]'s that likely need to be handled. +/// +/// If configured, users must forward the [`Event::HTLCIntercepted`] event parameters to [`LSPS2ServiceHandler::htlc_intercepted`] +/// and the [`Event::ChannelReady`] event parameters to [`LSPS2ServiceHandler::channel_ready`]. +/// +/// [`PeerManager`]: lightning::ln::peer_handler::PeerManager +/// [`MessageHandler`]: lightning::ln::peer_handler::MessageHandler +/// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted +/// [`Event::ChannelReady`]: lightning::events::Event::ChannelReady +pub struct LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + pending_messages: Arc, + pending_events: Arc, + request_id_to_method_map: Mutex>, + // We ignore peers if they send us bogus data. + ignored_peers: RwLock>, + lsps0_client_handler: LSPS0ClientHandler, + lsps0_service_handler: Option, + #[cfg(lsps1_service)] + lsps1_service_handler: Option>, + lsps1_client_handler: Option>, + lsps2_service_handler: Option>, + lsps2_client_handler: Option>, + service_config: Option, + _client_config: Option, + best_block: Option>, + _chain_source: Option, +} + +impl LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + /// Constructor for the [`LiquidityManager`]. + /// + /// Sets up the required protocol message handlers based on the given + /// [`LiquidityClientConfig`] and [`LiquidityServiceConfig`]. + pub fn new( + entropy_source: ES, channel_manager: CM, chain_source: Option, + chain_params: Option, service_config: Option, + client_config: Option, + ) -> Self +where { + let pending_messages = Arc::new(MessageQueue::new()); + let pending_events = Arc::new(EventQueue::new()); + let ignored_peers = RwLock::new(HashSet::new()); + + let mut supported_protocols = Vec::new(); + + let lsps2_client_handler = client_config.as_ref().and_then(|config| { + config.lsps2_client_config.map(|config| { + LSPS2ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + config.clone(), + ) + }) + }); + let lsps2_service_handler = service_config.as_ref().and_then(|config| { + config.lsps2_service_config.as_ref().map(|config| { + if let Some(number) = + as ProtocolMessageHandler>::PROTOCOL_NUMBER + { + supported_protocols.push(number); + } + LSPS2ServiceHandler::new( + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + config.clone(), + ) + }) + }); + + let lsps1_client_handler = client_config.as_ref().and_then(|config| { + config.lsps1_client_config.as_ref().map(|config| { + LSPS1ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + config.clone(), + ) + }) + }); + + #[cfg(lsps1_service)] + let lsps1_service_handler = service_config.as_ref().and_then(|config| { + if let Some(number) = + as ProtocolMessageHandler>::PROTOCOL_NUMBER + { + supported_protocols.push(number); + } + config.lsps1_service_config.as_ref().map(|config| { + LSPS1ServiceHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + channel_manager.clone(), + chain_source.clone(), + config.clone(), + ) + }) + }); + + let lsps0_client_handler = LSPS0ClientHandler::new( + entropy_source.clone(), + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + ); + + let lsps0_service_handler = if service_config.is_some() { + Some(LSPS0ServiceHandler::new(vec![], Arc::clone(&pending_messages))) + } else { + None + }; + + Self { + pending_messages, + pending_events, + request_id_to_method_map: Mutex::new(HashMap::new()), + ignored_peers, + lsps0_client_handler, + lsps0_service_handler, + lsps1_client_handler, + #[cfg(lsps1_service)] + lsps1_service_handler, + lsps2_client_handler, + lsps2_service_handler, + service_config, + _client_config: client_config, + best_block: chain_params.map(|chain_params| RwLock::new(chain_params.best_block)), + _chain_source: chain_source, + } + } + + /// Returns a reference to the LSPS0 client-side handler. + pub fn lsps0_client_handler(&self) -> &LSPS0ClientHandler { + &self.lsps0_client_handler + } + + /// Returns a reference to the LSPS0 server-side handler. + pub fn lsps0_service_handler(&self) -> Option<&LSPS0ServiceHandler> { + self.lsps0_service_handler.as_ref() + } + + /// Returns a reference to the LSPS1 client-side handler. + /// + /// The returned hendler allows to initiate the LSPS1 client-side flow, i.e., allows to request + /// channels from the configured LSP. + pub fn lsps1_client_handler(&self) -> Option<&LSPS1ClientHandler> { + self.lsps1_client_handler.as_ref() + } + + /// Returns a reference to the LSPS1 server-side handler. + #[cfg(lsps1_service)] + pub fn lsps1_service_handler(&self) -> Option<&LSPS1ServiceHandler> { + self.lsps1_service_handler.as_ref() + } + + /// Returns a reference to the LSPS2 client-side handler. + /// + /// The returned hendler allows to initiate the LSPS2 client-side flow. That is, it allows to + /// retrieve all necessary data to create 'just-in-time' invoices that, when paid, will have + /// the configured LSP open a 'just-in-time' channel. + pub fn lsps2_client_handler(&self) -> Option<&LSPS2ClientHandler> { + self.lsps2_client_handler.as_ref() + } + + /// Returns a reference to the LSPS2 server-side handler. + /// + /// The returned hendler allows to initiate the LSPS2 service-side flow. + pub fn lsps2_service_handler(&self) -> Option<&LSPS2ServiceHandler> { + self.lsps2_service_handler.as_ref() + } + + /// Allows to set a callback that will be called after new messages are pushed to the message + /// queue. + /// + /// Usually, you'll want to use this to call [`PeerManager::process_events`] to clear the + /// message queue. For example: + /// + /// ``` + /// # use lightning::io; + /// # use lightning_liquidity::LiquidityManager; + /// # use std::sync::{Arc, RwLock}; + /// # use std::sync::atomic::{AtomicBool, Ordering}; + /// # use std::time::SystemTime; + /// # struct MyStore {} + /// # impl lightning::util::persist::KVStore for MyStore { + /// # fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> io::Result> { Ok(Vec::new()) } + /// # fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8]) -> io::Result<()> { Ok(()) } + /// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) } + /// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { Ok(Vec::new()) } + /// # } + /// # struct MyEntropySource {} + /// # impl lightning::sign::EntropySource for MyEntropySource { + /// # fn get_secure_random_bytes(&self) -> [u8; 32] { [0u8; 32] } + /// # } + /// # struct MyEventHandler {} + /// # impl MyEventHandler { + /// # async fn handle_event(&self, _: lightning::events::Event) {} + /// # } + /// # #[derive(Eq, PartialEq, Clone, Hash)] + /// # struct MySocketDescriptor {} + /// # impl lightning::ln::peer_handler::SocketDescriptor for MySocketDescriptor { + /// # fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { 0 } + /// # fn disconnect_socket(&mut self) {} + /// # } + /// # type MyBroadcaster = dyn lightning::chain::chaininterface::BroadcasterInterface + Send + Sync; + /// # type MyFeeEstimator = dyn lightning::chain::chaininterface::FeeEstimator + Send + Sync; + /// # type MyNodeSigner = dyn lightning::sign::NodeSigner + Send + Sync; + /// # type MyUtxoLookup = dyn lightning::routing::utxo::UtxoLookup + Send + Sync; + /// # type MyFilter = dyn lightning::chain::Filter + Send + Sync; + /// # type MyLogger = dyn lightning::util::logger::Logger + Send + Sync; + /// # type MyChainMonitor = lightning::chain::chainmonitor::ChainMonitor, Arc, Arc, Arc, Arc>; + /// # type MyPeerManager = lightning::ln::peer_handler::SimpleArcPeerManager, MyLogger>; + /// # type MyNetworkGraph = lightning::routing::gossip::NetworkGraph>; + /// # type MyGossipSync = lightning::routing::gossip::P2PGossipSync, Arc, Arc>; + /// # type MyChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager; + /// # type MyScorer = RwLock, Arc>>; + /// # type MyLiquidityManager = LiquidityManager, Arc, Arc>; + /// # fn setup_background_processing(my_persister: Arc, my_event_handler: Arc, my_chain_monitor: Arc, my_channel_manager: Arc, my_logger: Arc, my_peer_manager: Arc, my_liquidity_manager: Arc) { + /// let process_msgs_pm = Arc::clone(&my_peer_manager); + /// let process_msgs_callback = move || process_msgs_pm.process_events(); + /// + /// my_liquidity_manager.set_process_msgs_callback(process_msgs_callback); + /// # } + /// ``` + /// + /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events + #[cfg(feature = "std")] + pub fn set_process_msgs_callback(&self, callback: impl Fn() + Send + Sync + 'static) { + self.pending_messages.set_process_msgs_callback(callback) + } + + /// Allows to set a callback that will be called after new messages are pushed to the message + /// queue. + /// + /// Usually, you'll want to use this to call [`PeerManager::process_events`] to clear the + /// message queue. For example: + /// + /// ``` + /// # use lightning::io; + /// # use lightning_liquidity::LiquidityManager; + /// # use std::sync::{Arc, RwLock}; + /// # use std::sync::atomic::{AtomicBool, Ordering}; + /// # use std::time::SystemTime; + /// # struct MyStore {} + /// # impl lightning::util::persist::KVStore for MyStore { + /// # fn read(&self, primary_namespace: &str, secondary_namespace: &str, key: &str) -> io::Result> { Ok(Vec::new()) } + /// # fn write(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8]) -> io::Result<()> { Ok(()) } + /// # fn remove(&self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool) -> io::Result<()> { Ok(()) } + /// # fn list(&self, primary_namespace: &str, secondary_namespace: &str) -> io::Result> { Ok(Vec::new()) } + /// # } + /// # struct MyEntropySource {} + /// # impl lightning::sign::EntropySource for MyEntropySource { + /// # fn get_secure_random_bytes(&self) -> [u8; 32] { [0u8; 32] } + /// # } + /// # struct MyEventHandler {} + /// # impl MyEventHandler { + /// # async fn handle_event(&self, _: lightning::events::Event) {} + /// # } + /// # #[derive(Eq, PartialEq, Clone, Hash)] + /// # struct MySocketDescriptor {} + /// # impl lightning::ln::peer_handler::SocketDescriptor for MySocketDescriptor { + /// # fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { 0 } + /// # fn disconnect_socket(&mut self) {} + /// # } + /// # type MyBroadcaster = dyn lightning::chain::chaininterface::BroadcasterInterface; + /// # type MyFeeEstimator = dyn lightning::chain::chaininterface::FeeEstimator; + /// # type MyNodeSigner = dyn lightning::sign::NodeSigner; + /// # type MyUtxoLookup = dyn lightning::routing::utxo::UtxoLookup; + /// # type MyFilter = dyn lightning::chain::Filter; + /// # type MyLogger = dyn lightning::util::logger::Logger; + /// # type MyChainMonitor = lightning::chain::chainmonitor::ChainMonitor, Arc, Arc, Arc, Arc>; + /// # type MyPeerManager = lightning::ln::peer_handler::SimpleArcPeerManager, MyLogger>; + /// # type MyNetworkGraph = lightning::routing::gossip::NetworkGraph>; + /// # type MyGossipSync = lightning::routing::gossip::P2PGossipSync, Arc, Arc>; + /// # type MyChannelManager = lightning::ln::channelmanager::SimpleArcChannelManager; + /// # type MyScorer = RwLock, Arc>>; + /// # type MyLiquidityManager = LiquidityManager, Arc, Arc>; + /// # fn setup_background_processing(my_persister: Arc, my_event_handler: Arc, my_chain_monitor: Arc, my_channel_manager: Arc, my_logger: Arc, my_peer_manager: Arc, my_liquidity_manager: Arc) { + /// let process_msgs_pm = Arc::clone(&my_peer_manager); + /// let process_msgs_callback = move || process_msgs_pm.process_events(); + /// + /// my_liquidity_manager.set_process_msgs_callback(process_msgs_callback); + /// # } + /// ``` + /// + /// [`PeerManager::process_events`]: lightning::ln::peer_handler::PeerManager::process_events + #[cfg(not(feature = "std"))] + pub fn set_process_msgs_callback(&self, callback: impl Fn() + 'static) { + self.pending_messages.set_process_msgs_callback(callback) + } + + /// Blocks the current thread until next event is ready and returns it. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + #[cfg(feature = "std")] + pub fn wait_next_event(&self) -> Event { + self.pending_events.wait_next_event() + } + + /// Returns `Some` if an event is ready. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + pub fn next_event(&self) -> Option { + self.pending_events.next_event() + } + + /// Asynchronously polls the event queue and returns once the next event is ready. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + pub async fn next_event_async(&self) -> Event { + self.pending_events.next_event_async().await + } + + /// Returns and clears all events without blocking. + /// + /// Typically you would spawn a thread or task that calls this in a loop. + pub fn get_and_clear_pending_events(&self) -> Vec { + self.pending_events.get_and_clear_pending_events() + } + + fn handle_lsps_message( + &self, msg: LSPSMessage, sender_node_id: &PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + match msg { + LSPSMessage::Invalid(_error) => { + return Err(LightningError { err: format!("{} did not understand a message we previously sent, maybe they don't support a protocol we are trying to use?", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Error)}); + }, + LSPSMessage::LSPS0(msg @ LSPS0Message::Response(..)) => { + self.lsps0_client_handler.handle_message(msg, sender_node_id)?; + }, + LSPSMessage::LSPS0(msg @ LSPS0Message::Request(..)) => { + match &self.lsps0_service_handler { + Some(lsps0_service_handler) => { + lsps0_service_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS0 request message without LSPS0 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, + LSPSMessage::LSPS1(msg @ LSPS1Message::Response(..)) => { + match &self.lsps1_client_handler { + Some(lsps1_client_handler) => { + lsps1_client_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS1 response message without LSPS1 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, + LSPSMessage::LSPS1(_msg @ LSPS1Message::Request(..)) => { + #[cfg(lsps1_service)] + match &self.lsps1_service_handler { + Some(lsps1_service_handler) => { + lsps1_service_handler.handle_message(_msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + #[cfg(not(lsps1_service))] + return Err(LightningError { err: format!("Received LSPS1 request message without LSPS1 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + LSPSMessage::LSPS2(msg @ LSPS2Message::Response(..)) => { + match &self.lsps2_client_handler { + Some(lsps2_client_handler) => { + lsps2_client_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS2 response message without LSPS2 client handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, + LSPSMessage::LSPS2(msg @ LSPS2Message::Request(..)) => { + match &self.lsps2_service_handler { + Some(lsps2_service_handler) => { + lsps2_service_handler.handle_message(msg, sender_node_id)?; + }, + None => { + return Err(LightningError { err: format!("Received LSPS2 request message without LSPS2 service handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + }, + } + }, + } + Ok(()) + } +} + +impl CustomMessageReader + for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + type CustomMessage = RawLSPSMessage; + + fn read( + &self, message_type: u16, buffer: &mut RD, + ) -> Result, lightning::ln::msgs::DecodeError> { + match message_type { + LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)), + _ => Ok(None), + } + } +} + +impl CustomMessageHandler + for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + fn handle_custom_message( + &self, msg: Self::CustomMessage, sender_node_id: PublicKey, + ) -> Result<(), lightning::ln::msgs::LightningError> { + { + if self.ignored_peers.read().unwrap().contains(&sender_node_id) { + let err = format!("Ignoring message from peer {}.", sender_node_id); + return Err(LightningError { + err, + action: ErrorAction::IgnoreAndLog(Level::Trace), + }); + } + } + + let message = { + { + let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); + LSPSMessage::from_str_with_id_map(&msg.payload, &mut request_id_to_method_map) + } + .map_err(|_| { + let error = ResponseError { + code: JSONRPC_INVALID_MESSAGE_ERROR_CODE, + message: JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE.to_string(), + data: None, + }; + + self.pending_messages.enqueue(&sender_node_id, LSPSMessage::Invalid(error)); + self.ignored_peers.write().unwrap().insert(sender_node_id); + let err = format!( + "Failed to deserialize invalid LSPS message. Ignoring peer {} from now on.", + sender_node_id + ); + LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Info) } + })? + }; + + self.handle_lsps_message(message, &sender_node_id) + } + + fn get_and_clear_pending_msg(&self) -> Vec<(PublicKey, Self::CustomMessage)> { + let pending_messages = self.pending_messages.get_and_clear_pending_msgs(); + + let mut request_ids_and_methods = pending_messages + .iter() + .filter_map(|(_, msg)| msg.get_request_id_and_method()) + .peekable(); + + if request_ids_and_methods.peek().is_some() { + let mut request_id_to_method_map_lock = self.request_id_to_method_map.lock().unwrap(); + for (request_id, method) in request_ids_and_methods { + request_id_to_method_map_lock.insert(request_id, method); + } + } + + pending_messages + .into_iter() + .filter_map(|(public_key, msg)| { + serde_json::to_string(&msg) + .ok() + .map(|payload| (public_key, RawLSPSMessage { payload })) + }) + .collect() + } + + fn provided_node_features(&self) -> NodeFeatures { + let mut features = NodeFeatures::empty(); + + let advertise_service = self.service_config.as_ref().map_or(false, |c| c.advertise_service); + + if advertise_service { + features + .set_optional_custom_bit(LSPS_FEATURE_BIT) + .expect("Failed to set LSPS feature bit"); + } + + features + } + + fn provided_init_features(&self, _their_node_id: PublicKey) -> InitFeatures { + let mut features = InitFeatures::empty(); + + let advertise_service = self.service_config.as_ref().map_or(false, |c| c.advertise_service); + if advertise_service { + features + .set_optional_custom_bit(LSPS_FEATURE_BIT) + .expect("Failed to set LSPS feature bit"); + } + + features + } + + fn peer_disconnected(&self, _: bitcoin::secp256k1::PublicKey) {} + fn peer_connected( + &self, _: bitcoin::secp256k1::PublicKey, _: &lightning::ln::msgs::Init, _: bool, + ) -> Result<(), ()> { + Ok(()) + } +} + +impl Listen for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + fn filtered_block_connected( + &self, header: &bitcoin::block::Header, txdata: &chain::transaction::TransactionData, + height: u32, + ) { + if let Some(best_block) = &self.best_block { + let best_block = best_block.read().unwrap(); + assert_eq!(best_block.block_hash, header.prev_blockhash, + "Blocks must be connected in chain-order - the connected header must build on the last connected header"); + assert_eq!(best_block.height, height - 1, + "Blocks must be connected in chain-order - the connected block height must be one greater than the previous height"); + } + + self.transactions_confirmed(header, txdata, height); + self.best_block_updated(header, height); + } + + fn block_disconnected(&self, header: &bitcoin::block::Header, height: u32) { + let new_height = height - 1; + if let Some(best_block) = &self.best_block { + let mut best_block = best_block.write().unwrap(); + assert_eq!(best_block.block_hash, header.block_hash(), + "Blocks must be disconnected in chain-order - the disconnected header must be the last connected header"); + assert_eq!(best_block.height, height, + "Blocks must be disconnected in chain-order - the disconnected block must have the correct height"); + *best_block = BestBlock::new(header.prev_blockhash, new_height) + } + + // TODO: Call block_disconnected on all sub-modules that require it, e.g., LSPS1MessageHandler. + // Internally this should call transaction_unconfirmed for all transactions that were + // confirmed at a height <= the one we now disconnected. + } +} + +impl Confirm for LiquidityManager +where + ES::Target: EntropySource, + CM::Target: AChannelManager, + C::Target: Filter, +{ + fn transactions_confirmed( + &self, _header: &bitcoin::block::Header, _txdata: &chain::transaction::TransactionData, + _height: u32, + ) { + // TODO: Call transactions_confirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. + } + + fn transaction_unconfirmed(&self, _txid: &bitcoin::Txid) { + // TODO: Call transaction_unconfirmed on all sub-modules that require it, e.g., LSPS1MessageHandler. + // Internally this should call transaction_unconfirmed for all transactions that were + // confirmed at a height <= the one we now unconfirmed. + } + + fn best_block_updated(&self, _header: &bitcoin::block::Header, _height: u32) { + // TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler. + } + + fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option)> { + // TODO: Collect relevant txids from all sub-modules that, e.g., LSPS1MessageHandler. + Vec::new() + } +} diff --git a/lightning-liquidity/src/message_queue.rs b/lightning-liquidity/src/message_queue.rs new file mode 100644 index 00000000000..89dab8a318e --- /dev/null +++ b/lightning-liquidity/src/message_queue.rs @@ -0,0 +1,51 @@ +//! Holds types and traits used to implement message queues for [`LSPSMessage`]s. + +use crate::lsps0::ser::LSPSMessage; +use crate::prelude::{Box, Vec, VecDeque}; +use crate::sync::{Mutex, RwLock}; + +use bitcoin::secp256k1::PublicKey; + +/// The default [`MessageQueue`] Implementation used by [`LiquidityManager`]. +/// +/// [`LiquidityManager`]: crate::LiquidityManager +pub struct MessageQueue { + queue: Mutex>, + #[cfg(feature = "std")] + process_msgs_callback: RwLock>>, + #[cfg(not(feature = "std"))] + process_msgs_callback: RwLock>>, +} + +impl MessageQueue { + pub(crate) fn new() -> Self { + let queue = Mutex::new(VecDeque::new()); + let process_msgs_callback = RwLock::new(None); + Self { queue, process_msgs_callback } + } + + #[cfg(feature = "std")] + pub(crate) fn set_process_msgs_callback(&self, callback: impl Fn() + Send + Sync + 'static) { + *self.process_msgs_callback.write().unwrap() = Some(Box::new(callback)); + } + + #[cfg(not(feature = "std"))] + pub(crate) fn set_process_msgs_callback(&self, callback: impl Fn() + 'static) { + *self.process_msgs_callback.write().unwrap() = Some(Box::new(callback)); + } + + pub(crate) fn get_and_clear_pending_msgs(&self) -> Vec<(PublicKey, LSPSMessage)> { + self.queue.lock().unwrap().drain(..).collect() + } + + pub(crate) fn enqueue(&self, counterparty_node_id: &PublicKey, msg: LSPSMessage) { + { + let mut queue = self.queue.lock().unwrap(); + queue.push_back((*counterparty_node_id, msg)); + } + + if let Some(process_msgs_callback) = self.process_msgs_callback.read().unwrap().as_ref() { + (process_msgs_callback)() + } + } +} diff --git a/lightning-liquidity/src/sync/mod.rs b/lightning-liquidity/src/sync/mod.rs new file mode 100644 index 00000000000..e20f15853ef --- /dev/null +++ b/lightning-liquidity/src/sync/mod.rs @@ -0,0 +1,9 @@ +#![allow(unused_imports)] + +#[cfg(feature = "std")] +pub use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; + +#[cfg(not(feature = "std"))] +mod nostd_sync; +#[cfg(not(feature = "std"))] +pub use nostd_sync::*; diff --git a/lightning-liquidity/src/sync/nostd_sync.rs b/lightning-liquidity/src/sync/nostd_sync.rs new file mode 100644 index 00000000000..c5dc277b57f --- /dev/null +++ b/lightning-liquidity/src/sync/nostd_sync.rs @@ -0,0 +1,111 @@ +#![allow(dead_code)] +//! This file was copied from `rust-lightning`. +pub use ::alloc::sync::Arc; +use core::cell::{Ref, RefCell, RefMut}; +use core::fmt; +use core::ops::{Deref, DerefMut}; + +pub type LockResult = Result; + +pub struct Mutex { + inner: RefCell, +} + +impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let t = self.lock().unwrap(); + fmt::Debug::fmt(t.deref(), f) + } +} + +#[must_use = "if unused the Mutex will immediately unlock"] +pub struct MutexGuard<'a, T: ?Sized + 'a> { + lock: RefMut<'a, T>, +} + +impl Deref for MutexGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + &self.lock.deref() + } +} + +impl DerefMut for MutexGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + self.lock.deref_mut() + } +} + +impl Mutex { + pub fn new(inner: T) -> Mutex { + Mutex { inner: RefCell::new(inner) } + } + + pub fn lock<'a>(&'a self) -> LockResult> { + Ok(MutexGuard { lock: self.inner.borrow_mut() }) + } + + pub fn try_lock<'a>(&'a self) -> LockResult> { + Ok(MutexGuard { lock: self.inner.borrow_mut() }) + } + + pub fn into_inner(self) -> LockResult { + Ok(self.inner.into_inner()) + } +} + +pub struct RwLock { + inner: RefCell, +} + +pub struct RwLockReadGuard<'a, T: ?Sized + 'a> { + lock: Ref<'a, T>, +} + +pub struct RwLockWriteGuard<'a, T: ?Sized + 'a> { + lock: RefMut<'a, T>, +} + +impl Deref for RwLockReadGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + &self.lock.deref() + } +} + +impl Deref for RwLockWriteGuard<'_, T> { + type Target = T; + + fn deref(&self) -> &T { + &self.lock.deref() + } +} + +impl DerefMut for RwLockWriteGuard<'_, T> { + fn deref_mut(&mut self) -> &mut T { + self.lock.deref_mut() + } +} + +impl RwLock { + pub fn new(inner: T) -> RwLock { + RwLock { inner: RefCell::new(inner) } + } + + pub fn read<'a>(&'a self) -> LockResult> { + Ok(RwLockReadGuard { lock: self.inner.borrow() }) + } + + pub fn write<'a>(&'a self) -> LockResult> { + Ok(RwLockWriteGuard { lock: self.inner.borrow_mut() }) + } + + pub fn try_write<'a>(&'a self) -> LockResult> { + match self.inner.try_borrow_mut() { + Ok(lock) => Ok(RwLockWriteGuard { lock }), + Err(_) => Err(()), + } + } +} diff --git a/lightning-liquidity/src/tests/mod.rs b/lightning-liquidity/src/tests/mod.rs new file mode 100644 index 00000000000..b5614dd8233 --- /dev/null +++ b/lightning-liquidity/src/tests/mod.rs @@ -0,0 +1 @@ +pub mod utils; diff --git a/lightning-liquidity/src/tests/utils.rs b/lightning-liquidity/src/tests/utils.rs new file mode 100644 index 00000000000..af5e55ae26b --- /dev/null +++ b/lightning-liquidity/src/tests/utils.rs @@ -0,0 +1,58 @@ +use crate::prelude::Vec; +use bitcoin::secp256k1::PublicKey; +use lightning::io; +use lightning::sign::EntropySource; + +pub struct TestEntropy {} +impl EntropySource for TestEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [0; 32] + } +} + +pub fn to_vec(hex: &str) -> Option> { + let mut out = Vec::with_capacity(hex.len() / 2); + + let mut b = 0; + for (idx, c) in hex.as_bytes().iter().enumerate() { + b <<= 4; + match *c { + b'A'..=b'F' => b |= c - b'A' + 10, + b'a'..=b'f' => b |= c - b'a' + 10, + b'0'..=b'9' => b |= c - b'0', + _ => return None, + } + if (idx & 1) == 1 { + out.push(b); + b = 0; + } + } + + Some(out) +} + +pub fn to_compressed_pubkey(hex: &str) -> Option { + if hex.len() != 33 * 2 { + return None; + } + let data = match to_vec(&hex[0..33 * 2]) { + Some(bytes) => bytes, + None => return None, + }; + match PublicKey::from_slice(&data) { + Ok(pk) => Some(pk), + Err(_) => None, + } +} + +pub fn parse_pubkey(pubkey_str: &str) -> Result { + let pubkey = to_compressed_pubkey(pubkey_str); + if pubkey.is_none() { + return Err(io::Error::new( + io::ErrorKind::Other, + "ERROR: unable to parse given pubkey for node", + )); + } + + Ok(pubkey.unwrap()) +} diff --git a/lightning-liquidity/src/utils.rs b/lightning-liquidity/src/utils.rs new file mode 100644 index 00000000000..e355c72eb65 --- /dev/null +++ b/lightning-liquidity/src/utils.rs @@ -0,0 +1,53 @@ +use core::{fmt::Write, ops::Deref}; +use lightning::sign::EntropySource; + +use crate::lsps0::ser::RequestId; +use crate::prelude::String; + +pub fn scid_from_human_readable_string(human_readable_scid: &str) -> Result { + let mut parts = human_readable_scid.split('x'); + + let block: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + let tx_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + let vout_index: u64 = parts.next().ok_or(())?.parse().map_err(|_e| ())?; + + Ok((block << 40) | (tx_index << 16) | vout_index) +} + +pub(crate) fn generate_request_id(entropy_source: &ES) -> RequestId +where + ES::Target: EntropySource, +{ + let bytes = entropy_source.get_secure_random_bytes(); + RequestId(hex_str(&bytes[0..16])) +} + +#[inline] +pub fn hex_str(value: &[u8]) -> String { + let mut res = String::with_capacity(2 * value.len()); + for v in value { + write!(&mut res, "{:02x}", v).expect("Unable to write"); + } + res +} + +#[cfg(test)] +mod tests { + use super::*; + use lightning::util::scid_utils::{block_from_scid, tx_index_from_scid, vout_from_scid}; + + #[test] + fn parses_human_readable_scid_correctly() { + let block = 140; + let tx_index = 123; + let vout = 22; + + let human_readable_scid = format!("{}x{}x{}", block, tx_index, vout); + + let scid = scid_from_human_readable_string(&human_readable_scid).unwrap(); + + assert_eq!(block_from_scid(scid), block); + assert_eq!(tx_index_from_scid(scid), tx_index); + assert_eq!(vout_from_scid(scid), vout); + } +} diff --git a/lightning-liquidity/tests/common/mod.rs b/lightning-liquidity/tests/common/mod.rs new file mode 100644 index 00000000000..8b8507a9f14 --- /dev/null +++ b/lightning-liquidity/tests/common/mod.rs @@ -0,0 +1,685 @@ +#![cfg(test)] +// TODO: remove these flags and unused code once we know what we'll need. +#![allow(dead_code)] +#![allow(unused_imports)] +#![allow(unused_macros)] + +use lightning::chain::Filter; +use lightning::sign::EntropySource; + +use bitcoin::blockdata::constants::{genesis_block, ChainHash}; +use bitcoin::blockdata::transaction::Transaction; +use bitcoin::Network; +use lightning::chain::channelmonitor::ANTI_REORG_DELAY; +use lightning::chain::{chainmonitor, BestBlock, Confirm}; +use lightning::ln::channelmanager; +use lightning::ln::channelmanager::ChainParameters; +use lightning::ln::functional_test_utils::*; +use lightning::ln::msgs::{ChannelMessageHandler, Init}; +use lightning::ln::peer_handler::{ + IgnoringMessageHandler, MessageHandler, PeerManager, SocketDescriptor, +}; + +use lightning::onion_message::messenger::DefaultMessageRouter; +use lightning::routing::gossip::{NetworkGraph, P2PGossipSync}; +use lightning::routing::router::{CandidateRouteHop, DefaultRouter, Path}; +use lightning::routing::scoring::{ChannelUsage, ScoreLookUp, ScoreUpdate}; +use lightning::sign::{InMemorySigner, KeysManager}; +use lightning::util::config::UserConfig; +use lightning::util::persist::{ + KVStore, CHANNEL_MANAGER_PERSISTENCE_KEY, CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE, + CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_KEY, + NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE, NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE, + SCORER_PERSISTENCE_KEY, SCORER_PERSISTENCE_PRIMARY_NAMESPACE, + SCORER_PERSISTENCE_SECONDARY_NAMESPACE, +}; +use lightning::util::test_utils; +use lightning_liquidity::{LiquidityClientConfig, LiquidityManager, LiquidityServiceConfig}; +use lightning_persister::fs_store::FilesystemStore; + +use std::collections::{HashMap, VecDeque}; +use std::path::PathBuf; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::mpsc::SyncSender; +use std::sync::{Arc, Mutex}; +use std::time::Duration; +use std::{env, fs}; + +pub(crate) struct TestEntropy {} +impl EntropySource for TestEntropy { + fn get_secure_random_bytes(&self) -> [u8; 32] { + [0; 32] + } +} + +#[derive(Clone, Hash, PartialEq, Eq)] +pub(crate) struct TestDescriptor {} +impl SocketDescriptor for TestDescriptor { + fn send_data(&mut self, _data: &[u8], _resume_read: bool) -> usize { + 0 + } + + fn disconnect_socket(&mut self) {} +} + +#[cfg(c_bindings)] +type LockingWrapper = lightning::routing::scoring::MultiThreadedLockableScore; +#[cfg(not(c_bindings))] +type LockingWrapper = std::sync::Mutex; + +type ChannelManager = channelmanager::ChannelManager< + Arc, + Arc, + Arc, + Arc, + Arc, + Arc, + Arc< + DefaultRouter< + Arc>>, + Arc, + Arc, + Arc>, + (), + TestScorer, + >, + >, + Arc< + DefaultMessageRouter< + Arc>>, + Arc, + Arc, + >, + >, + Arc, +>; + +type ChainMonitor = chainmonitor::ChainMonitor< + InMemorySigner, + Arc, + Arc, + Arc, + Arc, + Arc, +>; + +type PGS = Arc< + P2PGossipSync< + Arc>>, + Arc, + Arc, + >, +>; + +pub(crate) struct Node { + pub(crate) channel_manager: Arc, + pub(crate) keys_manager: Arc, + pub(crate) p2p_gossip_sync: PGS, + pub(crate) peer_manager: Arc< + PeerManager< + TestDescriptor, + Arc, + Arc, + IgnoringMessageHandler, + Arc, + Arc< + LiquidityManager< + Arc, + Arc, + Arc, + >, + >, + Arc, + >, + >, + pub(crate) liquidity_manager: + Arc, Arc, Arc>>, + pub(crate) check_msgs_processed: Arc, + pub(crate) chain_monitor: Arc, + pub(crate) kv_store: Arc, + pub(crate) tx_broadcaster: Arc, + pub(crate) network_graph: Arc>>, + pub(crate) logger: Arc, + pub(crate) best_block: BestBlock, + pub(crate) scorer: Arc>, +} + +impl Drop for Node { + fn drop(&mut self) { + let data_dir = self.kv_store.get_data_dir(); + match fs::remove_dir_all(data_dir.clone()) { + Err(e) => { + println!("Failed to remove test store directory {}: {}", data_dir.display(), e) + }, + _ => {}, + } + } +} + +struct Persister { + graph_error: Option<(lightning::io::ErrorKind, &'static str)>, + graph_persistence_notifier: Option>, + manager_error: Option<(lightning::io::ErrorKind, &'static str)>, + scorer_error: Option<(lightning::io::ErrorKind, &'static str)>, + kv_store: FilesystemStore, +} + +impl Persister { + fn new(data_dir: PathBuf) -> Self { + let kv_store = FilesystemStore::new(data_dir); + Self { + graph_error: None, + graph_persistence_notifier: None, + manager_error: None, + scorer_error: None, + kv_store, + } + } + + fn with_graph_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self { + Self { graph_error: Some((error, message)), ..self } + } + + fn with_graph_persistence_notifier(self, sender: SyncSender<()>) -> Self { + Self { graph_persistence_notifier: Some(sender), ..self } + } + + fn with_manager_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self { + Self { manager_error: Some((error, message)), ..self } + } + + fn with_scorer_error(self, error: lightning::io::ErrorKind, message: &'static str) -> Self { + Self { scorer_error: Some((error, message)), ..self } + } +} + +impl KVStore for Persister { + fn read( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, + ) -> lightning::io::Result> { + self.kv_store.read(primary_namespace, secondary_namespace, key) + } + + fn write( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, buf: &[u8], + ) -> lightning::io::Result<()> { + if primary_namespace == CHANNEL_MANAGER_PERSISTENCE_PRIMARY_NAMESPACE + && secondary_namespace == CHANNEL_MANAGER_PERSISTENCE_SECONDARY_NAMESPACE + && key == CHANNEL_MANAGER_PERSISTENCE_KEY + { + if let Some((error, message)) = self.manager_error { + return Err(lightning::io::Error::new(error, message)); + } + } + + if primary_namespace == NETWORK_GRAPH_PERSISTENCE_PRIMARY_NAMESPACE + && secondary_namespace == NETWORK_GRAPH_PERSISTENCE_SECONDARY_NAMESPACE + && key == NETWORK_GRAPH_PERSISTENCE_KEY + { + if let Some(sender) = &self.graph_persistence_notifier { + match sender.send(()) { + Ok(()) => {}, + Err(std::sync::mpsc::SendError(())) => { + println!("Persister failed to notify as receiver went away.") + }, + } + }; + + if let Some((error, message)) = self.graph_error { + return Err(lightning::io::Error::new(error, message)); + } + } + + if primary_namespace == SCORER_PERSISTENCE_PRIMARY_NAMESPACE + && secondary_namespace == SCORER_PERSISTENCE_SECONDARY_NAMESPACE + && key == SCORER_PERSISTENCE_KEY + { + if let Some((error, message)) = self.scorer_error { + return Err(lightning::io::Error::new(error, message)); + } + } + + self.kv_store.write(primary_namespace, secondary_namespace, key, buf) + } + + fn remove( + &self, primary_namespace: &str, secondary_namespace: &str, key: &str, lazy: bool, + ) -> lightning::io::Result<()> { + self.kv_store.remove(primary_namespace, secondary_namespace, key, lazy) + } + + fn list( + &self, primary_namespace: &str, secondary_namespace: &str, + ) -> lightning::io::Result> { + self.kv_store.list(primary_namespace, secondary_namespace) + } +} + +pub(crate) struct TestScorer { + event_expectations: Option>, +} + +#[derive(Debug)] +pub(crate) enum TestResult { + PaymentFailure { path: Path, short_channel_id: u64 }, + PaymentSuccess { path: Path }, + ProbeFailure { path: Path }, + ProbeSuccess { path: Path }, +} + +impl TestScorer { + fn new() -> Self { + Self { event_expectations: None } + } + + fn expect(&mut self, expectation: TestResult) { + self.event_expectations.get_or_insert_with(VecDeque::new).push_back(expectation); + } +} + +impl lightning::util::ser::Writeable for TestScorer { + fn write( + &self, _: &mut W, + ) -> Result<(), lightning::io::Error> { + Ok(()) + } +} + +impl ScoreLookUp for TestScorer { + type ScoreParams = (); + fn channel_penalty_msat( + &self, _candidate: &CandidateRouteHop, _usage: ChannelUsage, + _score_params: &Self::ScoreParams, + ) -> u64 { + unimplemented!(); + } +} + +impl ScoreUpdate for TestScorer { + fn payment_path_failed( + &mut self, actual_path: &Path, actual_short_channel_id: u64, _: Duration, + ) { + if let Some(expectations) = &mut self.event_expectations { + match expectations.pop_front().unwrap() { + TestResult::PaymentFailure { path, short_channel_id } => { + assert_eq!(actual_path, &path); + assert_eq!(actual_short_channel_id, short_channel_id); + }, + TestResult::PaymentSuccess { path } => { + panic!("Unexpected successful payment path: {:?}", path) + }, + TestResult::ProbeFailure { path } => { + panic!("Unexpected probe failure: {:?}", path) + }, + TestResult::ProbeSuccess { path } => { + panic!("Unexpected probe success: {:?}", path) + }, + } + } + } + + fn payment_path_successful(&mut self, actual_path: &Path, _: Duration) { + if let Some(expectations) = &mut self.event_expectations { + match expectations.pop_front().unwrap() { + TestResult::PaymentFailure { path, .. } => { + panic!("Unexpected payment path failure: {:?}", path) + }, + TestResult::PaymentSuccess { path } => { + assert_eq!(actual_path, &path); + }, + TestResult::ProbeFailure { path } => { + panic!("Unexpected probe failure: {:?}", path) + }, + TestResult::ProbeSuccess { path } => { + panic!("Unexpected probe success: {:?}", path) + }, + } + } + } + + fn probe_failed(&mut self, actual_path: &Path, _: u64, _: Duration) { + if let Some(expectations) = &mut self.event_expectations { + match expectations.pop_front().unwrap() { + TestResult::PaymentFailure { path, .. } => { + panic!("Unexpected payment path failure: {:?}", path) + }, + TestResult::PaymentSuccess { path } => { + panic!("Unexpected payment path success: {:?}", path) + }, + TestResult::ProbeFailure { path } => { + assert_eq!(actual_path, &path); + }, + TestResult::ProbeSuccess { path } => { + panic!("Unexpected probe success: {:?}", path) + }, + } + } + } + fn probe_successful(&mut self, actual_path: &Path, _: Duration) { + if let Some(expectations) = &mut self.event_expectations { + match expectations.pop_front().unwrap() { + TestResult::PaymentFailure { path, .. } => { + panic!("Unexpected payment path failure: {:?}", path) + }, + TestResult::PaymentSuccess { path } => { + panic!("Unexpected payment path success: {:?}", path) + }, + TestResult::ProbeFailure { path } => { + panic!("Unexpected probe failure: {:?}", path) + }, + TestResult::ProbeSuccess { path } => { + assert_eq!(actual_path, &path); + }, + } + } + } + fn time_passed(&mut self, _: Duration) {} +} + +#[cfg(c_bindings)] +impl lightning::routing::scoring::Score for TestScorer {} + +impl Drop for TestScorer { + fn drop(&mut self) { + if std::thread::panicking() { + return; + } + + if let Some(event_expectations) = &self.event_expectations { + if !event_expectations.is_empty() { + panic!("Unsatisfied event expectations: {:?}", event_expectations); + } + } + } +} + +fn get_full_filepath(filepath: String, filename: String) -> String { + let mut path = PathBuf::from(filepath); + path.push(filename); + path.to_str().unwrap().to_string() +} + +pub(crate) fn create_liquidity_node( + i: usize, persist_dir: &str, network: Network, service_config: Option, + client_config: Option, +) -> Node { + let tx_broadcaster = Arc::new(test_utils::TestBroadcaster::new(network)); + let fee_estimator = Arc::new(test_utils::TestFeeEstimator::new(253)); + let logger = Arc::new(test_utils::TestLogger::with_id(format!("node {}", i))); + let genesis_block = genesis_block(network); + let network_graph = Arc::new(NetworkGraph::new(network, logger.clone())); + let scorer = Arc::new(LockingWrapper::new(TestScorer::new())); + let now = Duration::from_secs(genesis_block.header.time as u64); + let seed = [i as u8; 32]; + let keys_manager = Arc::new(KeysManager::new(&seed, now.as_secs(), now.subsec_nanos())); + let router = Arc::new(DefaultRouter::new( + Arc::clone(&network_graph), + logger.clone(), + keys_manager.clone(), + scorer.clone(), + Default::default(), + )); + let msg_router = + Arc::new(DefaultMessageRouter::new(Arc::clone(&network_graph), Arc::clone(&keys_manager))); + let chain_source = Arc::new(test_utils::TestChainSource::new(Network::Bitcoin)); + let kv_store = + Arc::new(FilesystemStore::new(format!("{}_persister_{}", &persist_dir, i).into())); + let chain_monitor = Arc::new(chainmonitor::ChainMonitor::new( + Some(chain_source.clone()), + tx_broadcaster.clone(), + logger.clone(), + fee_estimator.clone(), + kv_store.clone(), + )); + let best_block = BestBlock::from_network(network); + let chain_params = ChainParameters { network, best_block }; + let channel_manager = Arc::new(ChannelManager::new( + fee_estimator.clone(), + chain_monitor.clone(), + tx_broadcaster.clone(), + router.clone(), + msg_router.clone(), + logger.clone(), + keys_manager.clone(), + keys_manager.clone(), + keys_manager.clone(), + UserConfig::default(), + chain_params, + genesis_block.header.time, + )); + let p2p_gossip_sync = Arc::new(P2PGossipSync::new( + network_graph.clone(), + Some(chain_source.clone()), + logger.clone(), + )); + + let liquidity_manager = Arc::new(LiquidityManager::new( + Arc::clone(&keys_manager), + Arc::clone(&channel_manager), + None::>, + Some(chain_params), + service_config, + client_config, + )); + let msg_handler = MessageHandler { + chan_handler: Arc::new(test_utils::TestChannelMessageHandler::new( + ChainHash::using_genesis_block(Network::Testnet), + )), + route_handler: Arc::new(test_utils::TestRoutingMessageHandler::new()), + onion_message_handler: IgnoringMessageHandler {}, + custom_message_handler: Arc::clone(&liquidity_manager), + }; + let peer_manager = + Arc::new(PeerManager::new(msg_handler, 0, &seed, logger.clone(), keys_manager.clone())); + + // Rather than registering PeerManager's process_events, we handle messages manually and use a + // bool to check whether PeerManager would have been called as expected. + let check_msgs_processed = Arc::new(AtomicBool::new(false)); + + let process_msgs_flag = Arc::clone(&check_msgs_processed); + let process_msgs_callback = move || process_msgs_flag.store(true, Ordering::Release); + liquidity_manager.set_process_msgs_callback(process_msgs_callback); + + Node { + channel_manager, + keys_manager, + p2p_gossip_sync, + peer_manager, + liquidity_manager, + check_msgs_processed, + chain_monitor, + kv_store, + tx_broadcaster, + network_graph, + logger, + best_block, + scorer, + } +} + +pub(crate) fn create_service_and_client_nodes( + persist_dir: &str, service_config: LiquidityServiceConfig, client_config: LiquidityClientConfig, +) -> (Node, Node) { + let persist_temp_path = env::temp_dir().join(persist_dir); + let persist_dir = persist_temp_path.to_string_lossy().to_string(); + let network = Network::Bitcoin; + + let service_node = create_liquidity_node(1, &persist_dir, network, Some(service_config), None); + let client_node = create_liquidity_node(2, &persist_dir, network, None, Some(client_config)); + + service_node + .channel_manager + .peer_connected( + client_node.channel_manager.get_our_node_id(), + &Init { + features: client_node.channel_manager.init_features(), + networks: None, + remote_network_address: None, + }, + true, + ) + .unwrap(); + client_node + .channel_manager + .peer_connected( + service_node.channel_manager.get_our_node_id(), + &Init { + features: service_node.channel_manager.init_features(), + networks: None, + remote_network_address: None, + }, + true, + ) + .unwrap(); + + (service_node, client_node) +} + +macro_rules! open_channel { + ($node_a: expr, $node_b: expr, $channel_value: expr) => {{ + begin_open_channel!($node_a, $node_b, $channel_value); + let events = $node_a.node.get_and_clear_pending_events(); + assert_eq!(events.len(), 1); + let (temporary_channel_id, tx) = + handle_funding_generation_ready!(events[0], $channel_value); + $node_a + .node + .funding_transaction_generated( + &temporary_channel_id, + &$node_b.node.get_our_node_id(), + tx.clone(), + ) + .unwrap(); + $node_b.node.handle_funding_created( + &$node_a.node.get_our_node_id(), + &get_event_msg!( + $node_a, + MessageSendEvent::SendFundingCreated, + $node_b.node.get_our_node_id() + ), + ); + get_event!($node_b, Event::ChannelPending); + $node_a.node.handle_funding_signed( + &$node_b.node.get_our_node_id(), + &get_event_msg!( + $node_b, + MessageSendEvent::SendFundingSigned, + $node_a.node.get_our_node_id() + ), + ); + get_event!($node_a, Event::ChannelPending); + tx + }}; +} + +pub(crate) use open_channel; + +macro_rules! begin_open_channel { + ($node_a: expr, $node_b: expr, $channel_value: expr) => {{ + $node_a + .node + .create_channel($node_b.node.get_our_node_id(), $channel_value, 100, 42, None, None) + .unwrap(); + $node_b.node.handle_open_channel( + &$node_a.node.get_our_node_id(), + &get_event_msg!( + $node_a, + MessageSendEvent::SendOpenChannel, + $node_b.node.get_our_node_id() + ), + ); + $node_a.node.handle_accept_channel( + &$node_b.node.get_our_node_id(), + &get_event_msg!( + $node_b, + MessageSendEvent::SendAcceptChannel, + $node_a.node.get_our_node_id() + ), + ); + }}; +} + +pub(crate) use begin_open_channel; + +macro_rules! handle_funding_generation_ready { + ($event: expr, $channel_value: expr) => {{ + match $event { + Event::FundingGenerationReady { + temporary_channel_id, + channel_value_satoshis, + ref output_script, + user_channel_id, + .. + } => { + assert_eq!(channel_value_satoshis, $channel_value); + assert_eq!(user_channel_id, 42); + + let tx = Transaction { + version: 1 as i32, + lock_time: LockTime::ZERO, + input: Vec::new(), + output: vec![TxOut { + value: channel_value_satoshis, + script_pubkey: output_script.clone(), + }], + }; + (temporary_channel_id, tx) + }, + _ => panic!("Unexpected event"), + } + }}; +} + +pub(crate) use handle_funding_generation_ready; + +macro_rules! get_lsps_message { + ($node: expr, $expected_target_node_id: expr) => {{ + use std::sync::atomic::Ordering; + assert!($node.check_msgs_processed.swap(false, Ordering::AcqRel)); + let msgs = $node.liquidity_manager.get_and_clear_pending_msg(); + assert_eq!(msgs.len(), 1); + let (target_node_id, message) = msgs.into_iter().next().unwrap(); + assert_eq!(target_node_id, $expected_target_node_id); + message + }}; +} + +pub(crate) use get_lsps_message; + +fn confirm_transaction_depth(node: &mut Node, tx: &Transaction, depth: u32) { + for i in 1..=depth { + let prev_blockhash = node.best_block.block_hash; + let height = node.best_block.height + 1; + let header = create_dummy_header(prev_blockhash, height); + let txdata = vec![(0, tx)]; + node.best_block = BestBlock::new(header.block_hash(), height); + match i { + 1 => { + node.channel_manager.transactions_confirmed(&header, &txdata, height); + node.chain_monitor.transactions_confirmed(&header, &txdata, height); + }, + x if x == depth => { + node.channel_manager.best_block_updated(&header, height); + node.chain_monitor.best_block_updated(&header, height); + }, + _ => {}, + } + } +} + +fn confirm_transaction(node: &mut Node, tx: &Transaction) { + confirm_transaction_depth(node, tx, ANTI_REORG_DELAY); +} + +fn advance_chain(node: &mut Node, num_blocks: u32) { + for i in 1..=num_blocks { + let prev_blockhash = node.best_block.block_hash; + let height = node.best_block.height + 1; + let header = create_dummy_header(prev_blockhash, height); + node.best_block = BestBlock::new(header.block_hash(), height); + if i == num_blocks { + node.channel_manager.best_block_updated(&header, height); + node.chain_monitor.best_block_updated(&header, height); + } + } +} diff --git a/lightning-liquidity/tests/lsps2_integration_tests.rs b/lightning-liquidity/tests/lsps2_integration_tests.rs new file mode 100644 index 00000000000..92e172606ab --- /dev/null +++ b/lightning-liquidity/tests/lsps2_integration_tests.rs @@ -0,0 +1,241 @@ +#![cfg(all(test, feature = "std"))] + +mod common; + +use common::{create_service_and_client_nodes, get_lsps_message, Node}; + +use lightning_liquidity::events::Event; +use lightning_liquidity::lsps2::client::LSPS2ClientConfig; +use lightning_liquidity::lsps2::event::{LSPS2ClientEvent, LSPS2ServiceEvent}; +use lightning_liquidity::lsps2::msgs::RawOpeningFeeParams; +use lightning_liquidity::lsps2::service::LSPS2ServiceConfig; +use lightning_liquidity::lsps2::utils::is_valid_opening_fee_params; +use lightning_liquidity::{LiquidityClientConfig, LiquidityServiceConfig}; + +use lightning::ln::channelmanager::MIN_FINAL_CLTV_EXPIRY_DELTA; +use lightning::ln::peer_handler::CustomMessageHandler; +use lightning::log_error; +use lightning::routing::router::{RouteHint, RouteHintHop}; +use lightning::util::logger::Logger; + +use lightning_invoice::{Bolt11Invoice, InvoiceBuilder, RoutingFees}; + +use bitcoin::hashes::{sha256, Hash}; +use bitcoin::secp256k1::{PublicKey, Secp256k1}; +use bitcoin::Network; + +use chrono::DateTime; + +use std::time::Duration; + +fn create_jit_invoice( + node: &Node, service_node_id: PublicKey, intercept_scid: u64, cltv_expiry_delta: u32, + payment_size_msat: Option, description: &str, expiry_secs: u32, +) -> Result { + // LSPS2 requires min_final_cltv_expiry_delta to be at least 2 more than usual. + let min_final_cltv_expiry_delta = MIN_FINAL_CLTV_EXPIRY_DELTA + 2; + let (payment_hash, payment_secret) = node + .channel_manager + .create_inbound_payment(None, expiry_secs, Some(min_final_cltv_expiry_delta)) + .map_err(|e| { + log_error!(node.logger, "Failed to register inbound payment: {:?}", e); + () + })?; + + let route_hint = RouteHint(vec![RouteHintHop { + src_node_id: service_node_id, + short_channel_id: intercept_scid, + fees: RoutingFees { base_msat: 0, proportional_millionths: 0 }, + cltv_expiry_delta: cltv_expiry_delta as u16, + htlc_minimum_msat: None, + htlc_maximum_msat: None, + }]); + + let payment_hash = sha256::Hash::from_slice(&payment_hash.0).map_err(|e| { + log_error!(node.logger, "Invalid payment hash: {:?}", e); + () + })?; + + let currency = Network::Bitcoin.into(); + let mut invoice_builder = InvoiceBuilder::new(currency) + .description(description.to_string()) + .payment_hash(payment_hash) + .payment_secret(payment_secret) + .current_timestamp() + .min_final_cltv_expiry_delta(min_final_cltv_expiry_delta.into()) + .expiry_time(Duration::from_secs(expiry_secs.into())) + .private_route(route_hint); + + if let Some(amount_msat) = payment_size_msat { + invoice_builder = invoice_builder.amount_milli_satoshis(amount_msat).basic_mpp(); + } + + invoice_builder + .build_signed(|hash| { + Secp256k1::new().sign_ecdsa_recoverable(hash, &node.keys_manager.get_node_secret_key()) + }) + .map_err(|e| { + log_error!(node.logger, "Failed to build and sign invoice: {}", e); + () + }) +} + +#[test] +fn invoice_generation_flow() { + let promise_secret = [42; 32]; + let lsps2_service_config = LSPS2ServiceConfig { promise_secret }; + let service_config = LiquidityServiceConfig { + #[cfg(lsps1_service)] + lsps1_service_config: None, + lsps2_service_config: Some(lsps2_service_config), + advertise_service: true, + }; + + let lsps2_client_config = LSPS2ClientConfig::default(); + let client_config = LiquidityClientConfig { + lsps1_client_config: None, + lsps2_client_config: Some(lsps2_client_config), + }; + + let (service_node, client_node) = + create_service_and_client_nodes("invoice_generation_flow", service_config, client_config); + + let service_handler = service_node.liquidity_manager.lsps2_service_handler().unwrap(); + let service_node_id = service_node.channel_manager.get_our_node_id(); + + let client_handler = client_node.liquidity_manager.lsps2_client_handler().unwrap(); + let client_node_id = client_node.channel_manager.get_our_node_id(); + + let get_info_request_id = client_handler.request_opening_params(service_node_id, None); + let get_info_request = get_lsps_message!(client_node, service_node_id); + + service_node.liquidity_manager.handle_custom_message(get_info_request, client_node_id).unwrap(); + + let get_info_event = service_node.liquidity_manager.next_event().unwrap(); + match get_info_event { + Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id, + token, + }) => { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(token, None); + }, + _ => panic!("Unexpected event"), + } + + let raw_opening_params = RawOpeningFeeParams { + min_fee_msat: 100, + proportional: 21, + valid_until: DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into(), + min_lifetime: 144, + max_client_to_self_delay: 128, + min_payment_size_msat: 1, + max_payment_size_msat: 100_000_000, + }; + + service_handler + .opening_fee_params_generated( + &client_node_id, + get_info_request_id.clone(), + vec![raw_opening_params], + ) + .unwrap(); + let get_info_response = get_lsps_message!(service_node, client_node_id); + + client_node + .liquidity_manager + .handle_custom_message(get_info_response, service_node_id) + .unwrap(); + + let opening_params_event = client_node.liquidity_manager.next_event().unwrap(); + let opening_fee_params = match opening_params_event { + Event::LSPS2Client(LSPS2ClientEvent::OpeningParametersReady { + request_id, + counterparty_node_id, + opening_fee_params_menu, + }) => { + assert_eq!(request_id, get_info_request_id); + assert_eq!(counterparty_node_id, service_node_id); + let opening_fee_params = opening_fee_params_menu.first().unwrap().clone(); + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); + opening_fee_params + }, + _ => panic!("Unexpected event"), + }; + + let payment_size_msat = Some(1_000_000); + let buy_request_id = client_handler + .select_opening_params(service_node_id, payment_size_msat, opening_fee_params.clone()) + .unwrap(); + + let buy_request = get_lsps_message!(client_node, service_node_id); + service_node.liquidity_manager.handle_custom_message(buy_request, client_node_id).unwrap(); + + let buy_event = service_node.liquidity_manager.next_event().unwrap(); + match buy_event { + Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id, + opening_fee_params: ofp, + payment_size_msat: psm, + }) => { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, client_node_id); + assert_eq!(opening_fee_params, ofp); + assert_eq!(payment_size_msat, psm); + }, + _ => panic!("Unexpected event"), + } + + let user_channel_id = 42; + let cltv_expiry_delta = 144; + let intercept_scid = service_node.channel_manager.get_intercept_scid(); + let client_trusts_lsp = true; + + service_handler + .invoice_parameters_generated( + &client_node_id, + buy_request_id.clone(), + intercept_scid, + cltv_expiry_delta, + client_trusts_lsp, + user_channel_id, + ) + .unwrap(); + + let buy_response = get_lsps_message!(service_node, client_node_id); + client_node.liquidity_manager.handle_custom_message(buy_response, service_node_id).unwrap(); + + let invoice_params_event = client_node.liquidity_manager.next_event().unwrap(); + match invoice_params_event { + Event::LSPS2Client(LSPS2ClientEvent::InvoiceParametersReady { + request_id, + counterparty_node_id, + intercept_scid: iscid, + cltv_expiry_delta: ced, + payment_size_msat: psm, + }) => { + assert_eq!(request_id, buy_request_id); + assert_eq!(counterparty_node_id, service_node_id); + assert_eq!(intercept_scid, iscid); + assert_eq!(cltv_expiry_delta, ced); + assert_eq!(payment_size_msat, psm); + }, + _ => panic!("Unexpected event"), + }; + + let description = "asdf"; + let expiry_secs = 3600; + let _invoice = create_jit_invoice( + &client_node, + service_node_id, + intercept_scid, + cltv_expiry_delta, + payment_size_msat, + description, + expiry_secs, + ) + .unwrap(); +} From 94c1e37a5907e6ace1193bc1313a5692d44fb408 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 5 Dec 2024 10:01:41 +0100 Subject: [PATCH 02/15] Avoid allocating during `LSPSMethod` serialization --- lightning-liquidity/src/lsps0/ser.rs | 35 ++++++++++++++-------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index e73ac81c965..5cf1fe73770 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -14,7 +14,7 @@ use crate::lsps1::msgs::{ use crate::lsps2::msgs::{ LSPS2Message, LSPS2Request, LSPS2Response, LSPS2_BUY_METHOD_NAME, LSPS2_GET_INFO_METHOD_NAME, }; -use crate::prelude::{HashMap, String, ToString}; +use crate::prelude::{HashMap, String}; use lightning::ln::msgs::LightningError; use lightning::ln::wire; @@ -22,7 +22,7 @@ use lightning::util::ser::WithoutLength; use bitcoin::secp256k1::PublicKey; -use core::fmt::{self, Display}; +use core::fmt; use core::str::FromStr; use serde::de::{self, MapAccess, Visitor}; @@ -53,6 +53,19 @@ pub(crate) enum LSPSMethod { LSPS2Buy, } +impl LSPSMethod { + fn as_static_str(&self) -> &'static str { + match self { + Self::LSPS0ListProtocols => LSPS0_LISTPROTOCOLS_METHOD_NAME, + Self::LSPS1GetInfo => LSPS1_GET_INFO_METHOD_NAME, + Self::LSPS1CreateOrder => LSPS1_CREATE_ORDER_METHOD_NAME, + Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME, + Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME, + Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME, + } + } +} + impl FromStr for LSPSMethod { type Err = &'static str; fn from_str(s: &str) -> Result { @@ -68,20 +81,6 @@ impl FromStr for LSPSMethod { } } -impl Display for LSPSMethod { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let s = match self { - Self::LSPS0ListProtocols => LSPS0_LISTPROTOCOLS_METHOD_NAME, - Self::LSPS1GetInfo => LSPS1_GET_INFO_METHOD_NAME, - Self::LSPS1CreateOrder => LSPS1_CREATE_ORDER_METHOD_NAME, - Self::LSPS1GetOrder => LSPS1_GET_ORDER_METHOD_NAME, - Self::LSPS2GetInfo => LSPS2_GET_INFO_METHOD_NAME, - Self::LSPS2Buy => LSPS2_BUY_METHOD_NAME, - }; - write!(f, "{}", s) - } -} - impl From<&LSPS0Request> for LSPSMethod { fn from(value: &LSPS0Request) -> Self { match value { @@ -124,7 +123,7 @@ impl Serialize for LSPSMethod { where S: serde::Serializer, { - serializer.serialize_str(&self.to_string()) + serializer.serialize_str(&self.as_static_str()) } } @@ -407,7 +406,7 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { if let Some(method) = method { return Err(de::Error::custom(format!( "Received unknown notification: {}", - method + method.as_static_str() ))); } else { if let Some(error) = error { From d8ba98ba2e91c7e9463568052ba883677cbd4ff6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 5 Dec 2024 10:13:22 +0100 Subject: [PATCH 03/15] Use `split_off` instead of collecting in `get_pending_events()` --- lightning-liquidity/src/events.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lightning-liquidity/src/events.rs b/lightning-liquidity/src/events.rs index e71afdca442..8384161d777 100644 --- a/lightning-liquidity/src/events.rs +++ b/lightning-liquidity/src/events.rs @@ -87,7 +87,7 @@ impl EventQueue { } pub fn get_and_clear_pending_events(&self) -> Vec { - self.queue.lock().unwrap().drain(..).collect() + self.queue.lock().unwrap().split_off(0).into() } } From 3a8e1bffccebd72071d6ae084c1a182b76268ad6 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Thu, 5 Dec 2024 13:49:14 +0100 Subject: [PATCH 04/15] Use LDK-internal Hash{Map,Set} types in `lightning-liquidity` --- lightning-liquidity/Cargo.toml | 3 +-- lightning-liquidity/src/lib.rs | 3 ++- lightning-liquidity/src/lsps0/msgs.rs | 12 +++++++----- lightning-liquidity/src/lsps1/client.rs | 14 +++++++------- lightning-liquidity/src/lsps1/service.rs | 4 ++-- lightning-liquidity/src/lsps2/client.rs | 11 +++++------ lightning-liquidity/src/lsps2/service.rs | 18 +++++++++--------- lightning-liquidity/src/manager.rs | 6 +++--- lightning/src/util/hash_tables.rs | 9 ++++++--- 9 files changed, 42 insertions(+), 38 deletions(-) diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index 17eae8762b6..3ee224b3e8b 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -15,7 +15,7 @@ categories = ["cryptography::cryptocurrencies"] [features] default = ["std"] -std = [] +std = ["lightning/std"] [dependencies] lightning = { version = "0.0.124", path = "../lightning", default-features = false } @@ -23,7 +23,6 @@ lightning-types = { version = "0.1", path = "../lightning-types", default-featur lightning-invoice = { version = "0.32.0", path = "../lightning-invoice", default-features = false, features = ["serde"] } bitcoin = { version = "0.32.2", default-features = false, features = ["serde"] } -hashbrown = { version = "0.8" } chrono = { version = "0.4", default-features = false, features = ["serde", "alloc"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } diff --git a/lightning-liquidity/src/lib.rs b/lightning-liquidity/src/lib.rs index f665dcdc2b1..4562eadbf1b 100644 --- a/lightning-liquidity/src/lib.rs +++ b/lightning-liquidity/src/lib.rs @@ -49,10 +49,11 @@ extern crate alloc; mod prelude { #![allow(unused_imports)] pub use alloc::{boxed::Box, collections::VecDeque, string::String, vec, vec::Vec}; - pub use hashbrown::{hash_map, HashMap, HashSet}; pub use alloc::borrow::ToOwned; pub use alloc::string::ToString; + + pub(crate) use lightning::util::hash_tables::*; } pub mod events; diff --git a/lightning-liquidity/src/lsps0/msgs.rs b/lightning-liquidity/src/lsps0/msgs.rs index 2366aa67cf4..79fd46ea676 100644 --- a/lightning-liquidity/src/lsps0/msgs.rs +++ b/lightning-liquidity/src/lsps0/msgs.rs @@ -90,9 +90,11 @@ impl From for LSPSMessage { #[cfg(test)] mod tests { + use lightning::util::hash_tables::new_hash_map; + use super::*; use crate::lsps0::ser::LSPSMethod; - use crate::prelude::{HashMap, ToString}; + use crate::prelude::ToString; #[test] fn deserializes_request() { @@ -102,7 +104,7 @@ mod tests { "method": "lsps0.list_protocols" }"#; - let mut request_id_method_map = HashMap::new(); + let mut request_id_method_map = new_hash_map(); let msg = LSPSMessage::from_str_with_id_map(json, &mut request_id_method_map); assert!(msg.is_ok()); @@ -138,7 +140,7 @@ mod tests { "protocols": [1,2,3] } }"#; - let mut request_id_to_method_map = HashMap::new(); + let mut request_id_to_method_map = new_hash_map(); request_id_to_method_map .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); @@ -164,7 +166,7 @@ mod tests { "message": "Unknown Error" } }"#; - let mut request_id_to_method_map = HashMap::new(); + let mut request_id_to_method_map = new_hash_map(); request_id_to_method_map .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); @@ -193,7 +195,7 @@ mod tests { "protocols": [1,2,3] } }"#; - let mut request_id_to_method_map = HashMap::new(); + let mut request_id_to_method_map = new_hash_map(); request_id_to_method_map .insert(RequestId("request:id:xyz123".to_string()), LSPSMethod::LSPS0ListProtocols); diff --git a/lightning-liquidity/src/lsps1/client.rs b/lightning-liquidity/src/lsps1/client.rs index a83c2ae87b0..75709d512a3 100644 --- a/lightning-liquidity/src/lsps1/client.rs +++ b/lightning-liquidity/src/lsps1/client.rs @@ -18,7 +18,7 @@ use crate::message_queue::MessageQueue; use crate::events::{Event, EventQueue}; use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; -use crate::prelude::{HashMap, HashSet}; +use crate::prelude::{new_hash_map, HashMap, HashSet}; use crate::sync::{Arc, Mutex, RwLock}; use lightning::ln::msgs::{ErrorAction, LightningError}; @@ -69,7 +69,7 @@ where entropy_source, pending_messages, pending_events, - per_peer_state: RwLock::new(HashMap::new()), + per_peer_state: RwLock::new(new_hash_map()), _config: config, } } @@ -142,7 +142,7 @@ where &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, ) -> Result<(), LightningError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); @@ -219,7 +219,7 @@ where response: CreateOrderResponse, ) -> Result<(), LightningError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); @@ -260,7 +260,7 @@ where &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, ) -> Result<(), LightningError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); @@ -338,7 +338,7 @@ where response: CreateOrderResponse, ) -> Result<(), LightningError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); @@ -379,7 +379,7 @@ where &self, request_id: RequestId, counterparty_node_id: &PublicKey, error: ResponseError, ) -> Result<(), LightningError> { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state_lock = inner_state_lock.lock().unwrap(); diff --git a/lightning-liquidity/src/lsps1/service.rs b/lightning-liquidity/src/lsps1/service.rs index e4e6508a281..6520adcf69b 100644 --- a/lightning-liquidity/src/lsps1/service.rs +++ b/lightning-liquidity/src/lsps1/service.rs @@ -20,7 +20,7 @@ use crate::message_queue::MessageQueue; use crate::events::{Event, EventQueue}; use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; -use crate::prelude::{HashMap, String, ToString}; +use crate::prelude::{new_hash_map, HashMap, String, ToString}; use crate::sync::{Arc, Mutex, RwLock}; use crate::utils; @@ -159,7 +159,7 @@ where chain_source, pending_messages, pending_events, - per_peer_state: RwLock::new(HashMap::new()), + per_peer_state: RwLock::new(new_hash_map()), config, } } diff --git a/lightning-liquidity/src/lsps2/client.rs b/lightning-liquidity/src/lsps2/client.rs index eccabfcbb64..10707bc8c5a 100644 --- a/lightning-liquidity/src/lsps2/client.rs +++ b/lightning-liquidity/src/lsps2/client.rs @@ -3,8 +3,7 @@ // // This file is licensed under the Apache License, Version 2.0 or the MIT license -// , at your option. -// You may not use this file except in accordance with one or both of these +// , at your option. You may not use this file except in accordance with one or both of these // licenses. //! Contains the main LSPS2 client object, [`LSPS2ClientHandler`]. @@ -13,7 +12,7 @@ use crate::events::{Event, EventQueue}; use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; use crate::lsps2::event::LSPS2ClientEvent; use crate::message_queue::MessageQueue; -use crate::prelude::{HashMap, HashSet, String, ToString}; +use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet, String, ToString}; use crate::sync::{Arc, Mutex, RwLock}; use lightning::ln::msgs::{ErrorAction, LightningError}; @@ -52,8 +51,8 @@ struct PeerState { impl PeerState { fn new() -> Self { - let pending_get_info_requests = HashSet::new(); - let pending_buy_requests = HashMap::new(); + let pending_get_info_requests = new_hash_set(); + let pending_buy_requests = new_hash_map(); Self { pending_get_info_requests, pending_buy_requests } } } @@ -89,7 +88,7 @@ where entropy_source, pending_messages, pending_events, - per_peer_state: RwLock::new(HashMap::new()), + per_peer_state: RwLock::new(new_hash_map()), _config, } } diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index e68b1c49a10..0fbacfccc00 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -15,7 +15,7 @@ use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; use crate::message_queue::MessageQueue; -use crate::prelude::{HashMap, String, ToString, Vec}; +use crate::prelude::{new_hash_map, HashMap, String, ToString, Vec}; use crate::sync::{Arc, Mutex, RwLock}; use lightning::events::HTLCDestination; @@ -438,10 +438,10 @@ struct PeerState { impl PeerState { fn new() -> Self { - let outbound_channels_by_intercept_scid = HashMap::new(); - let pending_requests = HashMap::new(); - let intercept_scid_by_user_channel_id = HashMap::new(); - let intercept_scid_by_channel_id = HashMap::new(); + let outbound_channels_by_intercept_scid = new_hash_map(); + let pending_requests = new_hash_map(); + let intercept_scid_by_user_channel_id = new_hash_map(); + let intercept_scid_by_channel_id = new_hash_map(); Self { outbound_channels_by_intercept_scid, pending_requests, @@ -481,9 +481,9 @@ where Self { pending_messages, pending_events, - per_peer_state: RwLock::new(HashMap::new()), - peer_by_intercept_scid: RwLock::new(HashMap::new()), - peer_by_channel_id: RwLock::new(HashMap::new()), + per_peer_state: RwLock::new(new_hash_map()), + peer_by_intercept_scid: RwLock::new(new_hash_map()), + peer_by_channel_id: RwLock::new(new_hash_map()), channel_manager, config, } @@ -852,7 +852,7 @@ where self.peer_by_channel_id.read().unwrap().get(&next_channel_id) { let outer_state_lock = self.per_peer_state.read().unwrap(); - match outer_state_lock.get(&counterparty_node_id) { + match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { let mut peer_state = inner_state_lock.lock().unwrap(); if let Some(intercept_scid) = diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 8b6098a253b..023e307644c 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -17,7 +17,7 @@ use crate::lsps1::service::{LSPS1ServiceConfig, LSPS1ServiceHandler}; use crate::lsps2::client::{LSPS2ClientConfig, LSPS2ClientHandler}; use crate::lsps2::msgs::LSPS2Message; use crate::lsps2::service::{LSPS2ServiceConfig, LSPS2ServiceHandler}; -use crate::prelude::{HashMap, HashSet, ToString, Vec}; +use crate::prelude::{new_hash_map, new_hash_set, HashMap, HashSet, ToString, Vec}; use crate::sync::{Arc, Mutex, RwLock}; use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; @@ -124,7 +124,7 @@ where where { let pending_messages = Arc::new(MessageQueue::new()); let pending_events = Arc::new(EventQueue::new()); - let ignored_peers = RwLock::new(HashSet::new()); + let ignored_peers = RwLock::new(new_hash_set()); let mut supported_protocols = Vec::new(); @@ -199,7 +199,7 @@ where { Self { pending_messages, pending_events, - request_id_to_method_map: Mutex::new(HashMap::new()), + request_id_to_method_map: Mutex::new(new_hash_map()), ignored_peers, lsps0_client_handler, lsps0_service_handler, diff --git a/lightning/src/util/hash_tables.rs b/lightning/src/util/hash_tables.rs index 3debb42c8f0..00341d57b45 100644 --- a/lightning/src/util/hash_tables.rs +++ b/lightning/src/util/hash_tables.rs @@ -3,7 +3,7 @@ //! //! This module simply re-exports the `HashMap` used in LDK for public consumption. -pub(crate) use hashbrown::hash_map; +pub use hashbrown::hash_map; mod hashbrown_tables { #[cfg(feature = "std")] @@ -67,7 +67,8 @@ mod hashbrown_tables { /// The HashMap type used in LDK. pub type HashMap = hashbrown::HashMap; - pub(crate) type HashSet = hashbrown::HashSet; + /// The HashSet type used in LDK. + pub type HashSet = hashbrown::HashSet; pub(crate) type OccupiedHashMapEntry<'a, K, V> = hashbrown::hash_map::OccupiedEntry<'a, K, V, RandomState>; @@ -96,9 +97,11 @@ mod hashbrown_tables { res } - pub(crate) fn new_hash_set() -> HashSet { + /// Builds a new [`HashSet`]. + pub fn new_hash_set() -> HashSet { HashSet::with_hasher(RandomState::new()) } + /// Builds a new [`HashSet`] with the given capacity. pub(crate) fn hash_set_with_capacity(cap: usize) -> HashSet { HashSet::with_capacity_and_hasher(cap, RandomState::new()) } From 9c9b4a8df7efd65498f07ad4ad9c35f0aeb9718a Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Dec 2024 10:56:34 +0100 Subject: [PATCH 05/15] Use `lightning::sync` via symlink in `lightning-liquidity` .. to which end we also need to make some additions to `debug_sync` and `nostd_sync`. --- lightning-liquidity/Cargo.toml | 4 + lightning-liquidity/src/events.rs | 10 +- lightning-liquidity/src/lib.rs | 2 + lightning-liquidity/src/sync/debug_sync.rs | 1 + lightning-liquidity/src/sync/fairrwlock.rs | 1 + lightning-liquidity/src/sync/mod.rs | 10 +- lightning-liquidity/src/sync/nostd_sync.rs | 112 +----------------- .../src/sync/test_lockorder_checks.rs | 1 + lightning/src/sync/debug_sync.rs | 21 ++++ lightning/src/sync/nostd_sync.rs | 1 + 10 files changed, 39 insertions(+), 124 deletions(-) create mode 120000 lightning-liquidity/src/sync/debug_sync.rs create mode 120000 lightning-liquidity/src/sync/fairrwlock.rs mode change 100644 => 120000 lightning-liquidity/src/sync/mod.rs mode change 100644 => 120000 lightning-liquidity/src/sync/nostd_sync.rs create mode 120000 lightning-liquidity/src/sync/test_lockorder_checks.rs diff --git a/lightning-liquidity/Cargo.toml b/lightning-liquidity/Cargo.toml index 3ee224b3e8b..9e76b0c7c68 100644 --- a/lightning-liquidity/Cargo.toml +++ b/lightning-liquidity/Cargo.toml @@ -16,6 +16,7 @@ categories = ["cryptography::cryptocurrencies"] [features] default = ["std"] std = ["lightning/std"] +backtrace = ["dep:backtrace"] [dependencies] lightning = { version = "0.0.124", path = "../lightning", default-features = false } @@ -27,6 +28,7 @@ bitcoin = { version = "0.32.2", default-features = false, features = ["serde"] } chrono = { version = "0.4", default-features = false, features = ["serde", "alloc"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" +backtrace = { version = "0.3", optional = true } [dev-dependencies] lightning = { version = "0.0.124", path = "../lightning", default-features = false, features = ["_test_utils"] } @@ -43,4 +45,6 @@ level = "forbid" check-cfg = [ "cfg(lsps1_service)", "cfg(c_bindings)", + "cfg(backtrace)", + "cfg(ldk_bench)", ] diff --git a/lightning-liquidity/src/events.rs b/lightning-liquidity/src/events.rs index 8384161d777..06d2346419c 100644 --- a/lightning-liquidity/src/events.rs +++ b/lightning-liquidity/src/events.rs @@ -28,7 +28,7 @@ pub(crate) struct EventQueue { queue: Arc>>, waker: Arc>>, #[cfg(feature = "std")] - condvar: std::sync::Condvar, + condvar: crate::sync::Condvar, } impl EventQueue { @@ -37,7 +37,7 @@ impl EventQueue { let waker = Arc::new(Mutex::new(None)); #[cfg(feature = "std")] { - let condvar = std::sync::Condvar::new(); + let condvar = crate::sync::Condvar::new(); Self { queue, waker, condvar } } #[cfg(not(feature = "std"))] @@ -67,8 +67,10 @@ impl EventQueue { #[cfg(feature = "std")] pub fn wait_next_event(&self) -> Event { - let mut queue = - self.condvar.wait_while(self.queue.lock().unwrap(), |queue| queue.is_empty()).unwrap(); + let mut queue = self + .condvar + .wait_while(self.queue.lock().unwrap(), |queue: &mut VecDeque| queue.is_empty()) + .unwrap(); let event = queue.pop_front().expect("non-empty queue"); let should_notify = !queue.is_empty(); diff --git a/lightning-liquidity/src/lib.rs b/lightning-liquidity/src/lib.rs index 4562eadbf1b..369972a1339 100644 --- a/lightning-liquidity/src/lib.rs +++ b/lightning-liquidity/src/lib.rs @@ -62,6 +62,8 @@ pub mod lsps1; pub mod lsps2; mod manager; pub mod message_queue; +#[allow(dead_code)] +#[allow(unused_imports)] mod sync; #[cfg(test)] mod tests; diff --git a/lightning-liquidity/src/sync/debug_sync.rs b/lightning-liquidity/src/sync/debug_sync.rs new file mode 120000 index 00000000000..c89152a37c2 --- /dev/null +++ b/lightning-liquidity/src/sync/debug_sync.rs @@ -0,0 +1 @@ +../../../lightning/src/sync/debug_sync.rs \ No newline at end of file diff --git a/lightning-liquidity/src/sync/fairrwlock.rs b/lightning-liquidity/src/sync/fairrwlock.rs new file mode 120000 index 00000000000..6bd61c3af5b --- /dev/null +++ b/lightning-liquidity/src/sync/fairrwlock.rs @@ -0,0 +1 @@ +../../../lightning/src/sync/fairrwlock.rs \ No newline at end of file diff --git a/lightning-liquidity/src/sync/mod.rs b/lightning-liquidity/src/sync/mod.rs deleted file mode 100644 index e20f15853ef..00000000000 --- a/lightning-liquidity/src/sync/mod.rs +++ /dev/null @@ -1,9 +0,0 @@ -#![allow(unused_imports)] - -#[cfg(feature = "std")] -pub use std::sync::{Arc, Condvar, Mutex, MutexGuard, RwLock, RwLockReadGuard, RwLockWriteGuard}; - -#[cfg(not(feature = "std"))] -mod nostd_sync; -#[cfg(not(feature = "std"))] -pub use nostd_sync::*; diff --git a/lightning-liquidity/src/sync/mod.rs b/lightning-liquidity/src/sync/mod.rs new file mode 120000 index 00000000000..4acfaefccde --- /dev/null +++ b/lightning-liquidity/src/sync/mod.rs @@ -0,0 +1 @@ +../../../lightning/src/sync/mod.rs \ No newline at end of file diff --git a/lightning-liquidity/src/sync/nostd_sync.rs b/lightning-liquidity/src/sync/nostd_sync.rs deleted file mode 100644 index c5dc277b57f..00000000000 --- a/lightning-liquidity/src/sync/nostd_sync.rs +++ /dev/null @@ -1,111 +0,0 @@ -#![allow(dead_code)] -//! This file was copied from `rust-lightning`. -pub use ::alloc::sync::Arc; -use core::cell::{Ref, RefCell, RefMut}; -use core::fmt; -use core::ops::{Deref, DerefMut}; - -pub type LockResult = Result; - -pub struct Mutex { - inner: RefCell, -} - -impl fmt::Debug for Mutex { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let t = self.lock().unwrap(); - fmt::Debug::fmt(t.deref(), f) - } -} - -#[must_use = "if unused the Mutex will immediately unlock"] -pub struct MutexGuard<'a, T: ?Sized + 'a> { - lock: RefMut<'a, T>, -} - -impl Deref for MutexGuard<'_, T> { - type Target = T; - - fn deref(&self) -> &T { - &self.lock.deref() - } -} - -impl DerefMut for MutexGuard<'_, T> { - fn deref_mut(&mut self) -> &mut T { - self.lock.deref_mut() - } -} - -impl Mutex { - pub fn new(inner: T) -> Mutex { - Mutex { inner: RefCell::new(inner) } - } - - pub fn lock<'a>(&'a self) -> LockResult> { - Ok(MutexGuard { lock: self.inner.borrow_mut() }) - } - - pub fn try_lock<'a>(&'a self) -> LockResult> { - Ok(MutexGuard { lock: self.inner.borrow_mut() }) - } - - pub fn into_inner(self) -> LockResult { - Ok(self.inner.into_inner()) - } -} - -pub struct RwLock { - inner: RefCell, -} - -pub struct RwLockReadGuard<'a, T: ?Sized + 'a> { - lock: Ref<'a, T>, -} - -pub struct RwLockWriteGuard<'a, T: ?Sized + 'a> { - lock: RefMut<'a, T>, -} - -impl Deref for RwLockReadGuard<'_, T> { - type Target = T; - - fn deref(&self) -> &T { - &self.lock.deref() - } -} - -impl Deref for RwLockWriteGuard<'_, T> { - type Target = T; - - fn deref(&self) -> &T { - &self.lock.deref() - } -} - -impl DerefMut for RwLockWriteGuard<'_, T> { - fn deref_mut(&mut self) -> &mut T { - self.lock.deref_mut() - } -} - -impl RwLock { - pub fn new(inner: T) -> RwLock { - RwLock { inner: RefCell::new(inner) } - } - - pub fn read<'a>(&'a self) -> LockResult> { - Ok(RwLockReadGuard { lock: self.inner.borrow() }) - } - - pub fn write<'a>(&'a self) -> LockResult> { - Ok(RwLockWriteGuard { lock: self.inner.borrow_mut() }) - } - - pub fn try_write<'a>(&'a self) -> LockResult> { - match self.inner.try_borrow_mut() { - Ok(lock) => Ok(RwLockWriteGuard { lock }), - Err(_) => Err(()), - } - } -} diff --git a/lightning-liquidity/src/sync/nostd_sync.rs b/lightning-liquidity/src/sync/nostd_sync.rs new file mode 120000 index 00000000000..e736108def6 --- /dev/null +++ b/lightning-liquidity/src/sync/nostd_sync.rs @@ -0,0 +1 @@ +../../../lightning/src/sync/nostd_sync.rs \ No newline at end of file diff --git a/lightning-liquidity/src/sync/test_lockorder_checks.rs b/lightning-liquidity/src/sync/test_lockorder_checks.rs new file mode 120000 index 00000000000..ad6e3b2cf26 --- /dev/null +++ b/lightning-liquidity/src/sync/test_lockorder_checks.rs @@ -0,0 +1 @@ +../../../lightning/src/sync/test_lockorder_checks.rs \ No newline at end of file diff --git a/lightning/src/sync/debug_sync.rs b/lightning/src/sync/debug_sync.rs index 183c074701e..f142328e45c 100644 --- a/lightning/src/sync/debug_sync.rs +++ b/lightning/src/sync/debug_sync.rs @@ -1,4 +1,5 @@ pub use alloc::sync::Arc; +use core::fmt; use core::ops::{Deref, DerefMut}; use core::time::Duration; @@ -65,6 +66,11 @@ impl Condvar { pub fn notify_all(&self) { self.inner.notify_all(); } + + #[allow(unused)] + pub fn notify_one(&self) { + self.inner.notify_one(); + } } thread_local! { @@ -254,6 +260,21 @@ impl Mutex { } } +impl fmt::Debug for Mutex { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let mut d = f.debug_struct("Mutex"); + match self.try_lock() { + Ok(guard) => { + d.field("data", &&*guard); + }, + Err(()) => { + d.field("data", &format_args!("")); + }, + } + d.finish_non_exhaustive() + } +} + #[must_use = "if unused the Mutex will immediately unlock"] pub struct MutexGuard<'a, T: Sized + 'a> { mutex: &'a Mutex, diff --git a/lightning/src/sync/nostd_sync.rs b/lightning/src/sync/nostd_sync.rs index b3963da762e..26d372ecae9 100644 --- a/lightning/src/sync/nostd_sync.rs +++ b/lightning/src/sync/nostd_sync.rs @@ -5,6 +5,7 @@ use core::ops::{Deref, DerefMut}; pub type LockResult = Result; +#[derive(Debug)] pub struct Mutex { inner: RefCell, } From 7e42b36e0f6bca5f678ef71f4c852be5c15b4517 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Dec 2024 12:35:08 +0100 Subject: [PATCH 06/15] Upper-bound the event queue size We add a size limit on the event queue, after which we'll just start dropping events to ensure we could never OOM. Additionally, we document the requirement that users need to handle generated events ASAP. --- lightning-liquidity/src/events.rs | 9 ++++++++- lightning-liquidity/src/manager.rs | 24 ++++++++++++++++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lightning-liquidity/src/events.rs b/lightning-liquidity/src/events.rs index 06d2346419c..3db772deec8 100644 --- a/lightning-liquidity/src/events.rs +++ b/lightning-liquidity/src/events.rs @@ -24,6 +24,9 @@ use crate::sync::{Arc, Mutex}; use core::future::Future; use core::task::{Poll, Waker}; +/// The maximum queue size we allow before starting to drop events. +pub const MAX_EVENT_QUEUE_SIZE: usize = 1000; + pub(crate) struct EventQueue { queue: Arc>>, waker: Arc>>, @@ -47,7 +50,11 @@ impl EventQueue { pub fn enqueue(&self, event: Event) { { let mut queue = self.queue.lock().unwrap(); - queue.push_back(event); + if queue.len() < MAX_EVENT_QUEUE_SIZE { + queue.push_back(event); + } else { + return; + } } if let Some(waker) = self.waker.lock().unwrap().take() { diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index 023e307644c..e0e07569cb1 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -378,6 +378,12 @@ where { /// Blocks the current thread until next event is ready and returns it. /// /// Typically you would spawn a thread or task that calls this in a loop. + /// + /// **Note**: Users must handle events as soon as possible to avoid an increased event queue + /// memory footprint. We will start dropping any generated events after + /// [`MAX_EVENT_QUEUE_SIZE`] has been reached. + /// + /// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE #[cfg(feature = "std")] pub fn wait_next_event(&self) -> Event { self.pending_events.wait_next_event() @@ -386,6 +392,12 @@ where { /// Returns `Some` if an event is ready. /// /// Typically you would spawn a thread or task that calls this in a loop. + /// + /// **Note**: Users must handle events as soon as possible to avoid an increased event queue + /// memory footprint. We will start dropping any generated events after + /// [`MAX_EVENT_QUEUE_SIZE`] has been reached. + /// + /// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE pub fn next_event(&self) -> Option { self.pending_events.next_event() } @@ -393,6 +405,12 @@ where { /// Asynchronously polls the event queue and returns once the next event is ready. /// /// Typically you would spawn a thread or task that calls this in a loop. + /// + /// **Note**: Users must handle events as soon as possible to avoid an increased event queue + /// memory footprint. We will start dropping any generated events after + /// [`MAX_EVENT_QUEUE_SIZE`] has been reached. + /// + /// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE pub async fn next_event_async(&self) -> Event { self.pending_events.next_event_async().await } @@ -400,6 +418,12 @@ where { /// Returns and clears all events without blocking. /// /// Typically you would spawn a thread or task that calls this in a loop. + /// + /// **Note**: Users must handle events as soon as possible to avoid an increased event queue + /// memory footprint. We will start dropping any generated events after + /// [`MAX_EVENT_QUEUE_SIZE`] has been reached. + /// + /// [`MAX_EVENT_QUEUE_SIZE`]: crate::events::MAX_EVENT_QUEUE_SIZE pub fn get_and_clear_pending_events(&self) -> Vec { self.pending_events.get_and_clear_pending_events() } From 3f2e232814968ecab09616fad231519ef77dc881 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Dec 2024 12:43:14 +0100 Subject: [PATCH 07/15] LSPS2: Introduce `MAX_PENDING_REQUESTS_PER_PEER` service limit We introduce a new `MAX_PENDING_REQUESTS_PER_PEER` limit for the number of pending (get_info and buy) requests per peer. --- lightning-liquidity/src/lsps0/ser.rs | 2 +- lightning-liquidity/src/lsps2/service.rs | 117 +++++++++++++++++------ 2 files changed, 88 insertions(+), 31 deletions(-) diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index 5cf1fe73770..b1f2a42994a 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -41,7 +41,7 @@ pub(crate) const JSONRPC_ERROR_FIELD_KEY: &str = "error"; pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_CODE: i32 = -32700; pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE: &str = "parse error"; -pub(crate) const _LSPS0_CLIENT_REJECTED_ERROR_CODE: i32 = 1; +pub(crate) const LSPS0_CLIENT_REJECTED_ERROR_CODE: i32 = 1; #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub(crate) enum LSPSMethod { diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 0fbacfccc00..89feb7b170f 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -10,7 +10,9 @@ //! Contains the main LSPS2 server-side object, [`LSPS2ServiceHandler`]. use crate::events::{Event, EventQueue}; -use crate::lsps0::ser::{ProtocolMessageHandler, RequestId, ResponseError}; +use crate::lsps0::ser::{ + ProtocolMessageHandler, RequestId, ResponseError, LSPS0_CLIENT_REJECTED_ERROR_CODE, +}; use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; @@ -40,6 +42,8 @@ use crate::lsps2::msgs::{ LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, }; +const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; + /// Server-side configuration options for JIT channels. #[derive(Clone, Debug)] pub struct LSPS2ServiceConfig { @@ -982,21 +986,49 @@ where fn handle_get_info_request( &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: GetInfoRequest, ) -> Result<(), LightningError> { - let mut outer_state_lock = self.per_peer_state.write().unwrap(); - let inner_state_lock: &mut Mutex = - outer_state_lock.entry(*counterparty_node_id).or_insert(Mutex::new(PeerState::new())); - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); - - let event = Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { - request_id, - counterparty_node_id: *counterparty_node_id, - token: params.token, - }); - self.pending_events.enqueue(event); - Ok(()) + let (result, response) = { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let inner_state_lock: &mut Mutex = outer_state_lock + .entry(*counterparty_node_id) + .or_insert(Mutex::new(PeerState::new())); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); + + let event = Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id: *counterparty_node_id, + token: params.token, + }); + self.pending_events.enqueue(event); + (Ok(()), None) + } else { + let response = LSPS2Response::GetInfoError(ResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: "Reached maximum number of pending requests. Please try again later." + .to_string(), + data: None, + }); + let msg = Some(LSPS2Message::Response(request_id, response).into()); + + let err = format!( + "Peer {} reached maximum number of pending requests: {}", + counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER + ); + + let result = + Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); + (result, msg) + } + }; + + if let Some(msg) = response { + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result } fn handle_buy_request( @@ -1070,7 +1102,6 @@ where } // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. - if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.config.promise_secret) { let response = LSPS2Response::BuyError(ResponseError { code: LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, @@ -1085,26 +1116,52 @@ where }); } - { + let (result, response) = { let mut outer_state_lock = self.per_peer_state.write().unwrap(); let inner_state_lock = outer_state_lock .entry(*counterparty_node_id) .or_insert(Mutex::new(PeerState::new())); let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - } - let event = Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { - request_id, - counterparty_node_id: *counterparty_node_id, - opening_fee_params: params.opening_fee_params, - payment_size_msat: params.payment_size_msat, - }); - self.pending_events.enqueue(event); + if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { + peer_state_lock + .pending_requests + .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - Ok(()) + let event = Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + }); + self.pending_events.enqueue(event); + + (Ok(()), None) + } else { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: "Reached maximum number of pending requests. Please try again later." + .to_string(), + data: None, + }); + let msg = Some(LSPS2Message::Response(request_id, response).into()); + + let err = format!( + "Peer {} reached maximum number of pending requests: {}", + counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER + ); + let result = + Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); + + (result, msg) + } + }; + + if let Some(msg) = response { + self.pending_messages.enqueue(counterparty_node_id, msg); + } + + result } } From 1f13c80e9213df2b391431bbb4417dcf1f253587 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 11 Dec 2024 11:50:51 +0100 Subject: [PATCH 08/15] LSPS2: DRY up pending request insertion/removal .. which is a prefactor to also start checking the total number of pending requests in the next commit. --- lightning-liquidity/src/lsps2/service.rs | 146 ++++++++++++----------- 1 file changed, 78 insertions(+), 68 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 89feb7b170f..fbc11b3ad6d 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -11,14 +11,14 @@ use crate::events::{Event, EventQueue}; use crate::lsps0::ser::{ - ProtocolMessageHandler, RequestId, ResponseError, LSPS0_CLIENT_REJECTED_ERROR_CODE, + LSPSMessage, ProtocolMessageHandler, RequestId, ResponseError, LSPS0_CLIENT_REJECTED_ERROR_CODE, }; use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; use crate::message_queue::MessageQueue; use crate::prelude::{new_hash_map, HashMap, String, ToString, Vec}; -use crate::sync::{Arc, Mutex, RwLock}; +use crate::sync::{Arc, Mutex, MutexGuard, RwLock}; use lightning::events::HTLCDestination; use lightning::ln::channelmanager::{AChannelManager, InterceptId}; @@ -506,9 +506,9 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state = inner_state_lock.lock().unwrap(); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); - match peer_state.pending_requests.remove(&request_id) { + match self.remove_pending_request(&mut peer_state_lock, &request_id) { Some(LSPS2Request::GetInfo(_)) => { let response = LSPS2Response::GetInfoError(ResponseError { code: LSPS2_GET_INFO_REQUEST_UNRECOGNIZED_OR_STALE_TOKEN_ERROR_CODE, @@ -562,9 +562,9 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state = inner_state_lock.lock().unwrap(); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); - match peer_state.pending_requests.remove(&request_id) { + match self.remove_pending_request(&mut peer_state_lock, &request_id) { Some(LSPS2Request::GetInfo(_)) => { let response = LSPS2Response::GetInfo(GetInfoResponse { opening_fee_params_menu: opening_fee_params_menu @@ -621,9 +621,9 @@ where match outer_state_lock.get(counterparty_node_id) { Some(inner_state_lock) => { - let mut peer_state = inner_state_lock.lock().unwrap(); + let mut peer_state_lock = inner_state_lock.lock().unwrap(); - match peer_state.pending_requests.remove(&request_id) { + match self.remove_pending_request(&mut peer_state_lock, &request_id) { Some(LSPS2Request::Buy(buy_request)) => { { let mut peer_by_intercept_scid = @@ -638,10 +638,10 @@ where user_channel_id, ); - peer_state + peer_state_lock .intercept_scid_by_user_channel_id .insert(user_channel_id, intercept_scid); - peer_state + peer_state_lock .insert_outbound_channel(intercept_scid, outbound_jit_channel); let response = LSPS2Response::Buy(BuyResponse { @@ -992,35 +992,24 @@ where .entry(*counterparty_node_id) .or_insert(Mutex::new(PeerState::new())); let mut peer_state_lock = inner_state_lock.lock().unwrap(); - if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); - - let event = Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { - request_id, - counterparty_node_id: *counterparty_node_id, - token: params.token, - }); - self.pending_events.enqueue(event); - (Ok(()), None) - } else { - let response = LSPS2Response::GetInfoError(ResponseError { - code: LSPS0_CLIENT_REJECTED_ERROR_CODE, - message: "Reached maximum number of pending requests. Please try again later." - .to_string(), - data: None, - }); - let msg = Some(LSPS2Message::Response(request_id, response).into()); - - let err = format!( - "Peer {} reached maximum number of pending requests: {}", - counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER - ); + let request = LSPS2Request::GetInfo(params.clone()); + match self.insert_pending_request( + &mut peer_state_lock, + request_id.clone(), + *counterparty_node_id, + request, + ) { + (Ok(()), msg) => { + let event = Event::LSPS2Service(LSPS2ServiceEvent::GetInfo { + request_id, + counterparty_node_id: *counterparty_node_id, + token: params.token, + }); + self.pending_events.enqueue(event); - let result = - Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); - (result, msg) + (Ok(()), msg) + }, + (e, msg) => (e, msg), } }; @@ -1123,37 +1112,25 @@ where .or_insert(Mutex::new(PeerState::new())); let mut peer_state_lock = inner_state_lock.lock().unwrap(); - if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { - peer_state_lock - .pending_requests - .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - - let event = Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { - request_id, - counterparty_node_id: *counterparty_node_id, - opening_fee_params: params.opening_fee_params, - payment_size_msat: params.payment_size_msat, - }); - self.pending_events.enqueue(event); - - (Ok(()), None) - } else { - let response = LSPS2Response::BuyError(ResponseError { - code: LSPS0_CLIENT_REJECTED_ERROR_CODE, - message: "Reached maximum number of pending requests. Please try again later." - .to_string(), - data: None, - }); - let msg = Some(LSPS2Message::Response(request_id, response).into()); - - let err = format!( - "Peer {} reached maximum number of pending requests: {}", - counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER - ); - let result = - Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); + let request = LSPS2Request::Buy(params.clone()); + match self.insert_pending_request( + &mut peer_state_lock, + request_id.clone(), + *counterparty_node_id, + request, + ) { + (Ok(()), msg) => { + let event = Event::LSPS2Service(LSPS2ServiceEvent::BuyRequest { + request_id, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + }); + self.pending_events.enqueue(event); - (result, msg) + (Ok(()), msg) + }, + (e, msg) => (e, msg), } }; @@ -1163,6 +1140,39 @@ where result } + + fn insert_pending_request<'a>( + &self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: RequestId, + counterparty_node_id: PublicKey, request: LSPS2Request, + ) -> (Result<(), LightningError>, Option) { + if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { + peer_state_lock.pending_requests.insert(request_id, request); + (Ok(()), None) + } else { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: "Reached maximum number of pending requests. Please try again later." + .to_string(), + data: None, + }); + let msg = Some(LSPS2Message::Response(request_id, response).into()); + + let err = format!( + "Peer {} reached maximum number of pending requests: {}", + counterparty_node_id, MAX_PENDING_REQUESTS_PER_PEER + ); + let result = + Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); + + (result, msg) + } + } + + fn remove_pending_request<'a>( + &self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: &RequestId, + ) -> Option { + peer_state_lock.pending_requests.remove(request_id) + } } impl ProtocolMessageHandler for LSPS2ServiceHandler From f75f124e02c1f84278079f09be664a78acac6628 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 11 Dec 2024 12:17:27 +0100 Subject: [PATCH 09/15] LSPS2: Enforce a limit on total pending requests To this end we introduce a new counter keeping track of overall requests pending and reject inbound requests if they would put us over the limit. --- lightning-liquidity/src/lsps2/service.rs | 80 +++++++++++++++++++++--- 1 file changed, 72 insertions(+), 8 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index fbc11b3ad6d..b9f165cfe09 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -32,6 +32,7 @@ use lightning_types::payment::PaymentHash; use bitcoin::secp256k1::PublicKey; use core::ops::Deref; +use core::sync::atomic::{AtomicUsize, Ordering}; use crate::lsps2::msgs::{ BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, LSPS2Message, LSPS2Request, @@ -43,6 +44,7 @@ use crate::lsps2::msgs::{ }; const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; +const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; /// Server-side configuration options for JIT channels. #[derive(Clone, Debug)] @@ -470,6 +472,7 @@ where per_peer_state: RwLock>>, peer_by_intercept_scid: RwLock>, peer_by_channel_id: RwLock>, + total_pending_requests: AtomicUsize, config: LSPS2ServiceConfig, } @@ -488,6 +491,7 @@ where per_peer_state: RwLock::new(new_hash_map()), peer_by_intercept_scid: RwLock::new(new_hash_map()), peer_by_channel_id: RwLock::new(new_hash_map()), + total_pending_requests: AtomicUsize::new(0), channel_manager, config, } @@ -1145,8 +1149,27 @@ where &self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: RequestId, counterparty_node_id: PublicKey, request: LSPS2Request, ) -> (Result<(), LightningError>, Option) { + if self.total_pending_requests.load(Ordering::Relaxed) >= MAX_TOTAL_PENDING_REQUESTS { + let response = LSPS2Response::BuyError(ResponseError { + code: LSPS0_CLIENT_REJECTED_ERROR_CODE, + message: "Reached maximum number of pending requests. Please try again later." + .to_string(), + data: None, + }); + let msg = Some(LSPS2Message::Response(request_id, response).into()); + + let err = format!( + "Peer {} reached maximum number of total pending requests: {}", + counterparty_node_id, MAX_TOTAL_PENDING_REQUESTS + ); + let result = + Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Debug) }); + return (result, msg); + } + if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { peer_state_lock.pending_requests.insert(request_id, request); + self.total_pending_requests.fetch_add(1, Ordering::Relaxed); (Ok(()), None) } else { let response = LSPS2Response::BuyError(ResponseError { @@ -1171,7 +1194,43 @@ where fn remove_pending_request<'a>( &self, peer_state_lock: &mut MutexGuard<'a, PeerState>, request_id: &RequestId, ) -> Option { - peer_state_lock.pending_requests.remove(request_id) + match peer_state_lock.pending_requests.remove(request_id) { + Some(req) => { + let res = self.total_pending_requests.fetch_update( + Ordering::Relaxed, + Ordering::Relaxed, + |x| Some(x.saturating_sub(1)), + ); + match res { + Ok(previous_value) if previous_value == 0 => debug_assert!( + false, + "total_pending_requests counter out-of-sync! This should never happen!" + ), + Err(previous_value) if previous_value == 0 => debug_assert!( + false, + "total_pending_requests counter out-of-sync! This should never happen!" + ), + _ => {}, + } + Some(req) + }, + res => res, + } + } + + #[cfg(debug_assertions)] + fn verify_pending_request_counter(&self) { + let mut num_requests = 0; + let outer_state_lock = self.per_peer_state.read().unwrap(); + for (_, inner) in outer_state_lock.iter() { + let inner_state_lock = inner.lock().unwrap(); + num_requests += inner_state_lock.pending_requests.len(); + } + debug_assert_eq!( + num_requests, + self.total_pending_requests.load(Ordering::Relaxed), + "total_pending_requests counter out-of-sync! This should never happen!" + ); } } @@ -1186,13 +1245,18 @@ where &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { match message { - LSPS2Message::Request(request_id, request) => match request { - LSPS2Request::GetInfo(params) => { - self.handle_get_info_request(request_id, counterparty_node_id, params) - }, - LSPS2Request::Buy(params) => { - self.handle_buy_request(request_id, counterparty_node_id, params) - }, + LSPS2Message::Request(request_id, request) => { + let res = match request { + LSPS2Request::GetInfo(params) => { + self.handle_get_info_request(request_id, counterparty_node_id, params) + }, + LSPS2Request::Buy(params) => { + self.handle_buy_request(request_id, counterparty_node_id, params) + }, + }; + #[cfg(debug_assertions)] + self.verify_pending_request_counter(); + res }, _ => { debug_assert!( From 6451a432cc88bb092a29a14e8b861f187ea5b425 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Dec 2024 13:50:33 +0100 Subject: [PATCH 10/15] LSPS2: Clean pending `get_info` request state on disconnection We clean up any `get_info` request state when peers disconnect. --- lightning-liquidity/src/lsps2/service.rs | 13 +++++++++++++ lightning-liquidity/src/manager.rs | 6 +++++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index b9f165cfe09..49aff2bf3bd 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -459,6 +459,11 @@ impl PeerState { fn insert_outbound_channel(&mut self, intercept_scid: u64, channel: OutboundJITChannel) { self.outbound_channels_by_intercept_scid.insert(intercept_scid, channel); } + + fn peer_disconnected(&mut self) { + // Clean any pending `get_info` requests. + self.pending_requests.retain(|_, entry| !matches!(entry, LSPS2Request::GetInfo(_))); + } } /// The main object allowing to send and receive LSPS2 messages. @@ -1232,6 +1237,14 @@ where "total_pending_requests counter out-of-sync! This should never happen!" ); } + + pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { + let outer_state_lock = self.per_peer_state.write().unwrap(); + if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.peer_disconnected(); + } + } } impl ProtocolMessageHandler for LSPS2ServiceHandler diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index e0e07569cb1..a7e2a7913a1 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -612,7 +612,11 @@ where features } - fn peer_disconnected(&self, _: bitcoin::secp256k1::PublicKey) {} + fn peer_disconnected(&self, counterparty_node_id: bitcoin::secp256k1::PublicKey) { + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { + lsps2_service_handler.peer_disconnected(counterparty_node_id); + } + } fn peer_connected( &self, _: bitcoin::secp256k1::PublicKey, _: &lightning::ln::msgs::Init, _: bool, ) -> Result<(), ()> { From b39c8b09baa5fbc1cbaef9933936163d89580c2f Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 9 Dec 2024 14:36:16 +0100 Subject: [PATCH 11/15] LSPS2: Prune expired buy requests on disconnection .. we clean up any pending buy requests that hit their `valid_until` time when the counterparty disconnects. --- lightning-liquidity/src/lsps2/service.rs | 15 +++++++-- lightning-liquidity/src/lsps2/utils.rs | 41 +++++++++++++++--------- 2 files changed, 37 insertions(+), 19 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 49aff2bf3bd..d2e67fa1396 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -15,7 +15,9 @@ use crate::lsps0::ser::{ }; use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; -use crate::lsps2::utils::{compute_opening_fee, is_valid_opening_fee_params}; +use crate::lsps2::utils::{ + compute_opening_fee, is_expired_opening_fee_params, is_valid_opening_fee_params, +}; use crate::message_queue::MessageQueue; use crate::prelude::{new_hash_map, HashMap, String, ToString, Vec}; use crate::sync::{Arc, Mutex, MutexGuard, RwLock}; @@ -461,8 +463,15 @@ impl PeerState { } fn peer_disconnected(&mut self) { - // Clean any pending `get_info` requests. - self.pending_requests.retain(|_, entry| !matches!(entry, LSPS2Request::GetInfo(_))); + self.pending_requests.retain(|_, entry| { + match entry { + LSPS2Request::GetInfo(_) => false, + LSPS2Request::Buy(request) => { + // Prune any expired buy requests. + !is_expired_opening_fee_params(&request.opening_fee_params) + }, + } + }); } } diff --git a/lightning-liquidity/src/lsps2/utils.rs b/lightning-liquidity/src/lsps2/utils.rs index a48456ae769..8a085b76c22 100644 --- a/lightning-liquidity/src/lsps2/utils.rs +++ b/lightning-liquidity/src/lsps2/utils.rs @@ -14,23 +14,9 @@ use std::time::{SystemTime, UNIX_EPOCH}; pub fn is_valid_opening_fee_params( fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], ) -> bool { - #[cfg(feature = "std")] - { - // TODO: We need to find a way to check expiry times in no-std builds. - let seconds_since_epoch = SystemTime::now() - .duration_since(UNIX_EPOCH) - .expect("system clock to be ahead of the unix epoch") - .as_secs(); - let valid_until_seconds_since_epoch = fee_params - .valid_until - .timestamp() - .try_into() - .expect("expiration to be ahead of unix epoch"); - if seconds_since_epoch > valid_until_seconds_since_epoch { - return false; - } + if is_expired_opening_fee_params(fee_params) { + return false; } - let mut hmac = HmacEngine::::new(promise_secret); hmac.input(&fee_params.min_fee_msat.to_be_bytes()); hmac.input(&fee_params.proportional.to_be_bytes()); @@ -44,6 +30,29 @@ pub fn is_valid_opening_fee_params( promise == fee_params.promise } +/// Determines if the given parameters are expired, or still valid. +#[cfg_attr(not(feature = "std"), allow(unused_variables))] +pub fn is_expired_opening_fee_params(fee_params: &OpeningFeeParams) -> bool { + #[cfg(feature = "std")] + { + let seconds_since_epoch = SystemTime::now() + .duration_since(UNIX_EPOCH) + .expect("system clock to be ahead of the unix epoch") + .as_secs(); + let valid_until_seconds_since_epoch = fee_params + .valid_until + .timestamp() + .try_into() + .expect("expiration to be ahead of unix epoch"); + seconds_since_epoch > valid_until_seconds_since_epoch + } + #[cfg(not(feature = "std"))] + { + // TODO: We need to find a way to check expiry times in no-std builds. + false + } +} + /// Computes the opening fee given a payment size and the fee parameters. /// /// Returns [`Option::None`] when the computation overflows. From 776ede44cb8d17d847469d9d58babd7e0e553f21 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Tue, 10 Dec 2024 09:36:09 +0100 Subject: [PATCH 12/15] LSPS2: Also prune expired `OutboundJITChannels` pending initial payments We're now also pruning any expired `OutboundJITChannels` if we haven't seen any related HTLCs. --- lightning-liquidity/src/lsps2/service.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index d2e67fa1396..578a2f14e50 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -435,6 +435,18 @@ impl OutboundJITChannel { self.state = new_state; Ok(action) } + + fn is_prunable(&self) -> bool { + // We deem an OutboundJITChannel prunable if our offer expired and we haven't intercepted + // any HTLCs initiating the flow yet. + let is_pending_initial_payment = match self.state { + OutboundJITChannelState::PendingInitialPayment { .. } => true, + _ => false, + }; + + let is_expired = is_expired_opening_fee_params(&self.opening_fee_params); + is_pending_initial_payment && is_expired + } } struct PeerState { @@ -472,6 +484,16 @@ impl PeerState { }, } }); + + self.outbound_channels_by_intercept_scid.retain(|intercept_scid, entry| { + if entry.is_prunable() { + // We abort the flow, and prune any data kept. + self.intercept_scid_by_channel_id.retain(|_, iscid| intercept_scid != iscid); + self.intercept_scid_by_user_channel_id.retain(|_, iscid| intercept_scid != iscid); + return false; + } + true + }); } } From 440962e4fe03a2d3c0c04f458c1fc629fa915a10 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Wed, 11 Dec 2024 13:45:31 +0100 Subject: [PATCH 13/15] LSPS2: Prune empty `PeerState`s In addition to pruning expired requests on peer disconnection we also regularly prune for all peers on block connection, and also remove the entire `PeerState` if it's empty after pruning (i.e., has no pending requsts or in-flight channels left). --- lightning-liquidity/src/lsps2/service.rs | 32 ++++++++++++++++++++---- lightning-liquidity/src/manager.rs | 3 +++ 2 files changed, 30 insertions(+), 5 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 578a2f14e50..e60efa5c196 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -474,7 +474,7 @@ impl PeerState { self.outbound_channels_by_intercept_scid.insert(intercept_scid, channel); } - fn peer_disconnected(&mut self) { + fn prune_expired_request_state(&mut self) { self.pending_requests.retain(|_, entry| { match entry { LSPS2Request::GetInfo(_) => false, @@ -495,6 +495,11 @@ impl PeerState { true }); } + + fn is_prunable(&self) -> bool { + // Return whether the entire state is empty. + self.pending_requests.is_empty() && self.outbound_channels_by_intercept_scid.is_empty() + } } /// The main object allowing to send and receive LSPS2 messages. @@ -1270,12 +1275,29 @@ where } pub(crate) fn peer_disconnected(&self, counterparty_node_id: PublicKey) { - let outer_state_lock = self.per_peer_state.write().unwrap(); - if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { - let mut peer_state_lock = inner_state_lock.lock().unwrap(); - peer_state_lock.peer_disconnected(); + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + let is_prunable = + if let Some(inner_state_lock) = outer_state_lock.get(&counterparty_node_id) { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.prune_expired_request_state(); + peer_state_lock.is_prunable() + } else { + return; + }; + if is_prunable { + outer_state_lock.remove(&counterparty_node_id); } } + + #[allow(clippy::bool_comparison)] + pub(crate) fn prune_peer_state(&self) { + let mut outer_state_lock = self.per_peer_state.write().unwrap(); + outer_state_lock.retain(|_, inner_state_lock| { + let mut peer_state_lock = inner_state_lock.lock().unwrap(); + peer_state_lock.prune_expired_request_state(); + peer_state_lock.is_prunable() == false + }); + } } impl ProtocolMessageHandler for LSPS2ServiceHandler diff --git a/lightning-liquidity/src/manager.rs b/lightning-liquidity/src/manager.rs index a7e2a7913a1..0c03e5128f8 100644 --- a/lightning-liquidity/src/manager.rs +++ b/lightning-liquidity/src/manager.rs @@ -684,6 +684,9 @@ where fn best_block_updated(&self, _header: &bitcoin::block::Header, _height: u32) { // TODO: Call best_block_updated on all sub-modules that require it, e.g., LSPS1MessageHandler. + if let Some(lsps2_service_handler) = self.lsps2_service_handler.as_ref() { + lsps2_service_handler.prune_peer_state(); + } } fn get_relevant_txids(&self) -> Vec<(bitcoin::Txid, u32, Option)> { From 7a8952110cd10e5dfd5b45c0a55f98db015f3e19 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Fri, 13 Dec 2024 10:28:18 +0100 Subject: [PATCH 14/15] LSPS2: Include channels pending intial payment in the per-peer limit We include any `OutboundJITChannel` that has not made it further than `PendingInitialPayment` in the per-peer request limit, and will of course prune it once it expires. --- lightning-liquidity/src/lsps2/service.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index e60efa5c196..955fcfab4ce 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -436,16 +436,15 @@ impl OutboundJITChannel { Ok(action) } + fn is_pending_initial_payment(&self) -> bool { + matches!(self.state, OutboundJITChannelState::PendingInitialPayment { .. }) + } + fn is_prunable(&self) -> bool { // We deem an OutboundJITChannel prunable if our offer expired and we haven't intercepted // any HTLCs initiating the flow yet. - let is_pending_initial_payment = match self.state { - OutboundJITChannelState::PendingInitialPayment { .. } => true, - _ => false, - }; - let is_expired = is_expired_opening_fee_params(&self.opening_fee_params); - is_pending_initial_payment && is_expired + self.is_pending_initial_payment() && is_expired } } @@ -496,6 +495,16 @@ impl PeerState { }); } + fn pending_requests_and_channels(&self) -> usize { + let pending_requests = self.pending_requests.len(); + let pending_outbound_channels = self + .outbound_channels_by_intercept_scid + .iter() + .filter(|(_, v)| v.is_pending_initial_payment()) + .count(); + pending_requests + pending_outbound_channels + } + fn is_prunable(&self) -> bool { // Return whether the entire state is empty. self.pending_requests.is_empty() && self.outbound_channels_by_intercept_scid.is_empty() @@ -1208,7 +1217,7 @@ where return (result, msg); } - if peer_state_lock.pending_requests.len() < MAX_PENDING_REQUESTS_PER_PEER { + if peer_state_lock.pending_requests_and_channels() < MAX_PENDING_REQUESTS_PER_PEER { peer_state_lock.pending_requests.insert(request_id, request); self.total_pending_requests.fetch_add(1, Ordering::Relaxed); (Ok(()), None) From f68c6c5be130c23ab17e05aa000683e4c4593ef1 Mon Sep 17 00:00:00 2001 From: Elias Rohrer Date: Mon, 16 Dec 2024 11:00:11 +0100 Subject: [PATCH 15/15] LSPS2: Limit the total number of peers While LDK/`ChannelManager` should already introduce an upper-bound on the number of peers, here we assert that our `PeerState` map can't grow unboundedly. To this end, we simply return an `Internal error` and abort when we would hit the limit of 100000 peers. --- lightning-liquidity/src/lsps0/ser.rs | 2 + lightning-liquidity/src/lsps2/service.rs | 50 ++++++++++++++++++++---- 2 files changed, 45 insertions(+), 7 deletions(-) diff --git a/lightning-liquidity/src/lsps0/ser.rs b/lightning-liquidity/src/lsps0/ser.rs index b1f2a42994a..afac232966a 100644 --- a/lightning-liquidity/src/lsps0/ser.rs +++ b/lightning-liquidity/src/lsps0/ser.rs @@ -40,6 +40,8 @@ pub(crate) const JSONRPC_RESULT_FIELD_KEY: &str = "result"; pub(crate) const JSONRPC_ERROR_FIELD_KEY: &str = "error"; pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_CODE: i32 = -32700; pub(crate) const JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE: &str = "parse error"; +pub(crate) const JSONRPC_INTERNAL_ERROR_ERROR_CODE: i32 = -32603; +pub(crate) const JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE: &str = "Internal error"; pub(crate) const LSPS0_CLIENT_REJECTED_ERROR_CODE: i32 = 1; diff --git a/lightning-liquidity/src/lsps2/service.rs b/lightning-liquidity/src/lsps2/service.rs index 955fcfab4ce..d4682a9b346 100644 --- a/lightning-liquidity/src/lsps2/service.rs +++ b/lightning-liquidity/src/lsps2/service.rs @@ -11,7 +11,9 @@ use crate::events::{Event, EventQueue}; use crate::lsps0::ser::{ - LSPSMessage, ProtocolMessageHandler, RequestId, ResponseError, LSPS0_CLIENT_REJECTED_ERROR_CODE, + LSPSMessage, ProtocolMessageHandler, RequestId, ResponseError, + JSONRPC_INTERNAL_ERROR_ERROR_CODE, JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE, + LSPS0_CLIENT_REJECTED_ERROR_CODE, }; use crate::lsps2::event::LSPS2ServiceEvent; use crate::lsps2::payment_queue::{InterceptedHTLC, PaymentQueue}; @@ -19,6 +21,7 @@ use crate::lsps2::utils::{ compute_opening_fee, is_expired_opening_fee_params, is_valid_opening_fee_params, }; use crate::message_queue::MessageQueue; +use crate::prelude::hash_map::Entry; use crate::prelude::{new_hash_map, HashMap, String, ToString, Vec}; use crate::sync::{Arc, Mutex, MutexGuard, RwLock}; @@ -47,6 +50,7 @@ use crate::lsps2::msgs::{ const MAX_PENDING_REQUESTS_PER_PEER: usize = 10; const MAX_TOTAL_PENDING_REQUESTS: usize = 1000; +const MAX_TOTAL_PEERS: usize = 100000; /// Server-side configuration options for JIT channels. #[derive(Clone, Debug)] @@ -511,6 +515,40 @@ impl PeerState { } } +macro_rules! get_or_insert_peer_state_entry { + ($self: ident, $outer_state_lock: expr, $counterparty_node_id: expr) => {{ + // Return an internal error and abort if we hit the maximum allowed number of total peers. + let is_limited_by_max_total_peers = $outer_state_lock.len() >= MAX_TOTAL_PEERS; + match $outer_state_lock.entry(*$counterparty_node_id) { + Entry::Vacant(e) => { + if is_limited_by_max_total_peers { + let error_response = ResponseError { + code: JSONRPC_INTERNAL_ERROR_ERROR_CODE, + message: JSONRPC_INTERNAL_ERROR_ERROR_MESSAGE.to_string(), data: None, + }; + + let msg = LSPSMessage::Invalid(error_response); + drop($outer_state_lock); + $self.pending_messages.enqueue($counterparty_node_id, msg); + + let err = format!( + "Dropping request from peer {} due to reaching maximally allowed number of total peers: {}", + $counterparty_node_id, MAX_TOTAL_PEERS + ); + + return Err(LightningError { err, action: ErrorAction::IgnoreAndLog(Level::Error) }); + } else { + e.insert(Mutex::new(PeerState::new())) + } + } + Entry::Occupied(e) => { + e.into_mut() + } + } + + }} +} + /// The main object allowing to send and receive LSPS2 messages. pub struct LSPS2ServiceHandler where @@ -1042,9 +1080,8 @@ where ) -> Result<(), LightningError> { let (result, response) = { let mut outer_state_lock = self.per_peer_state.write().unwrap(); - let inner_state_lock: &mut Mutex = outer_state_lock - .entry(*counterparty_node_id) - .or_insert(Mutex::new(PeerState::new())); + let inner_state_lock = + get_or_insert_peer_state_entry!(self, outer_state_lock, counterparty_node_id); let mut peer_state_lock = inner_state_lock.lock().unwrap(); let request = LSPS2Request::GetInfo(params.clone()); match self.insert_pending_request( @@ -1161,9 +1198,8 @@ where let (result, response) = { let mut outer_state_lock = self.per_peer_state.write().unwrap(); - let inner_state_lock = outer_state_lock - .entry(*counterparty_node_id) - .or_insert(Mutex::new(PeerState::new())); + let inner_state_lock = + get_or_insert_peer_state_entry!(self, outer_state_lock, counterparty_node_id); let mut peer_state_lock = inner_state_lock.lock().unwrap(); let request = LSPS2Request::Buy(params.clone());