From 9e33ddde4d64cc74f2cb98db5abcc7bf20f6d90d Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 11:14:33 -0400 Subject: [PATCH 01/16] update ldk to 117 --- .github/workflows/build.yml | 4 ---- Cargo.toml | 5 ++--- 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 20b78cd..37e925d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -28,8 +28,6 @@ jobs: - name: Pin crates for MSRV if: matrix.msrv run: | - # Tokio MSRV on versions 1.17 through 1.26 is rustc 1.49. Above 1.26 MSRV is 1.56. - cargo update -p tokio --precise "1.14.1" --verbose # The serde_json crate switched to Rust edition 2021 starting with v1.0.101, i.e., has MSRV of 1.56 cargo update -p serde_json --precise "1.0.99" --verbose cargo update -p serde --precise "1.0.156" --verbose @@ -37,8 +35,6 @@ jobs: cargo update -p quote --precise "1.0.30" --verbose # The proc-macro2 crate switched to Rust edition 2021 starting with v1.0.66, i.e., has MSRV of 1.56 cargo update -p proc-macro2 --precise "1.0.65" --verbose - # Sadly the log crate is always a dependency of tokio until 1.20, and has no reasonable MSRV guarantees - cargo update -p log --precise "0.4.18" --verbose # The memchr crate switched to Rust edition 2021 starting with v2.6.0 cargo update -p memchr --precise "2.5.0" --verbose - name: Cargo check diff --git a/Cargo.toml b/Cargo.toml index eee987c..ba722a3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,9 +8,8 @@ description = "Types and primitives to integrate a spec-compliant LSP with an LD # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -lightning = { version = "0.0.116", features = ["max_level_trace", "std"] } -lightning-invoice = "0.24.0" -lightning-net-tokio = "0.0.116" +lightning = { version = "0.0.117", default-features = false, features = ["max_level_trace", "std"] } +lightning-invoice = "0.25.0" bitcoin = "0.29.0" From 5a5c99e3b1561381233fe08652bf1ed9734a7acd Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 11:29:31 -0400 Subject: [PATCH 02/16] add LSPS2 messages --- src/jit_channel/mod.rs | 1 + src/jit_channel/msgs.rs | 263 +++++++++++++++++++++++++++++++ src/transport/message_handler.rs | 16 +- src/transport/msgs.rs | 124 ++++++++++++++- 4 files changed, 397 insertions(+), 7 deletions(-) create mode 100644 src/jit_channel/msgs.rs diff --git a/src/jit_channel/mod.rs b/src/jit_channel/mod.rs index 0d948d9..bcb97db 100644 --- a/src/jit_channel/mod.rs +++ b/src/jit_channel/mod.rs @@ -8,3 +8,4 @@ // licenses. //! Types and primitives that implement the LSPS3: JIT Channel Negotiation specification. +pub mod msgs; diff --git a/src/jit_channel/msgs.rs b/src/jit_channel/msgs.rs new file mode 100644 index 0000000..98ed55a --- /dev/null +++ b/src/jit_channel/msgs.rs @@ -0,0 +1,263 @@ +use std::convert::TryFrom; + +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; +use serde::{Deserialize, Serialize}; + +use crate::transport::msgs::{LSPSMessage, RequestId, ResponseError}; +use crate::utils; + +pub(crate) const LSPS2_GET_VERSIONS_METHOD_NAME: &str = "lsps2.get_versions"; +pub(crate) const LSPS2_GET_INFO_METHOD_NAME: &str = "lsps2.get_info"; +pub(crate) const LSPS2_BUY_METHOD_NAME: &str = "lsps2.buy"; + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] +#[serde(default)] +pub struct GetVersionsRequest {} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetVersionsResponse { + pub versions: Vec, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetInfoRequest { + pub version: u16, + pub token: Option, +} + +/// Fees and parameters for a JIT Channel. +/// +/// The client will pay max(min_fee_msat, proportional*(payment_size_msat/1_000_000)). +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 formatted date for which these params are valid. + pub valid_until: String, + /// number of blocks that the LSP promises it will keep the channel alive without closing, after confirmation. + pub min_lifetime: u32, + /// Maximum number of blocks that the client is allowed to set its to_self_delay parameter. + pub max_client_to_self_delay: u32, +} + +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.as_bytes()); + hmac.input(&self.min_lifetime.to_be_bytes()); + hmac.input(&self.max_client_to_self_delay.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).into_inner(); + 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, + promise, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// Fees and parameters for a JIT Channel with promise. +/// +/// The client will pay max(min_fee_msat, proportional*(payment_size_msat/1_000_000)). +pub struct OpeningFeeParams { + /// 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 formatted date for which these params are valid. + pub valid_until: String, + /// number of blocks that the LSP promises it will keep the channel alive without closing, after confirmation. + pub min_lifetime: u32, + /// Maximum number of blocks that the client is allowed to set its to_self_delay parameter. + pub max_client_to_self_delay: u32, + /// Field used by the LSP to validate that these parameters were actually given out by them. + pub promise: String, +} + +impl OpeningFeeParams { + /// Determine that these parameters are valid given the secret used to generate the promise. + // TODO: add validation check that valid_until >= now() + pub fn is_valid(&self, promise_secret: &[u8; 32]) -> bool { + 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.as_bytes()); + hmac.input(&self.min_lifetime.to_be_bytes()); + hmac.input(&self.max_client_to_self_delay.to_be_bytes()); + let promise_bytes = Hmac::from_engine(hmac).into_inner(); + let promise = utils::hex_str(&promise_bytes[..]); + promise == self.promise + } +} + +/// Information about the parameters a LSP is willing to offer clients +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct GetInfoResponse { + /// A set of opening fee parameters. + pub opening_fee_params_menu: Vec, + /// The minimum payment size required to open a channel. + pub min_payment_size_msat: u64, + /// The maximum payment size the lsp will tolerate. + pub max_payment_size_msat: u64, +} + +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuyRequest { + pub version: u16, + pub opening_fee_params: OpeningFeeParams, + #[serde(skip_serializing_if = "Option::is_none")] + pub payment_size_msat: Option, +} + +/// A response from a buy request made by a client +/// +/// Includes information needed to construct an invoice. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct BuyResponse { + /// The short channel id used by LSP to identify need to open channel. + pub jit_channel_scid: String, + /// 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)] +pub enum Request { + GetVersions(GetVersionsRequest), + GetInfo(GetInfoRequest), + Buy(BuyRequest), +} + +impl Request { + pub fn method(&self) -> &str { + match self { + Request::GetVersions(_) => LSPS2_GET_VERSIONS_METHOD_NAME, + Request::GetInfo(_) => LSPS2_GET_INFO_METHOD_NAME, + Request::Buy(_) => LSPS2_BUY_METHOD_NAME, + } + } +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Response { + GetVersions(GetVersionsResponse), + GetInfo(GetInfoResponse), + GetInfoError(ResponseError), + Buy(BuyResponse), + BuyError(ResponseError), +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Message { + Request(RequestId, Request), + Response(RequestId, Response), +} + +impl TryFrom for Message { + 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: Message) -> Self { + LSPSMessage::LSPS2(message) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn into_opening_fee_params_produces_valid_promise() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = "2023-05-20".to_string(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.clone(), + min_lifetime, + max_client_to_self_delay, + }; + + 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!(opening_fee_params.is_valid(&promise_secret)); + } + + #[test] + fn changing_single_field_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = "2023-05-20".to_string(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until, + min_lifetime, + max_client_to_self_delay, + }; + + 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!(!opening_fee_params.is_valid(&promise_secret)); + } + + #[test] + fn wrong_secret_produced_invalid_params() { + let min_fee_msat = 100; + let proportional = 21; + let valid_until = "2023-05-20".to_string(); + let min_lifetime = 144; + let max_client_to_self_delay = 128; + + let raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until, + min_lifetime, + max_client_to_self_delay, + }; + + let promise_secret = [1u8; 32]; + let other_secret = [2u8; 32]; + + let opening_fee_params = raw.into_opening_fee_params(&promise_secret); + assert!(!opening_fee_params.is_valid(&other_secret)); + } +} diff --git a/src/transport/message_handler.rs b/src/transport/message_handler.rs index 6a7830d..91ac509 100644 --- a/src/transport/message_handler.rs +++ b/src/transport/message_handler.rs @@ -1,5 +1,5 @@ use crate::events::{Event, EventQueue}; -use crate::transport::msgs::{LSPSMessage, RawLSPSMessage, LSPS_MESSAGE_TYPE}; +use crate::transport::msgs::{LSPSMessage, RawLSPSMessage, LSPS_MESSAGE_TYPE_ID}; use crate::transport::protocol::LSPS0MessageHandler; use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; @@ -20,7 +20,6 @@ use bitcoin::BlockHash; use std::collections::HashMap; use std::convert::TryFrom; -use std::io; use std::ops::Deref; use std::sync::{Arc, Mutex, RwLock}; @@ -154,6 +153,15 @@ where { LSPSMessage::LSPS0(msg) => { self.lsps0_message_handler.handle_message(msg, sender_node_id)?; } + _ => { + return Err(LightningError { + err: format!( + "Received message without message handler configured. From node = {:?}", + sender_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } } Ok(()) } @@ -188,11 +196,11 @@ where { type CustomMessage = RawLSPSMessage; - fn read( + fn read( &self, message_type: u16, buffer: &mut RD, ) -> Result, lightning::ln::msgs::DecodeError> { match message_type { - LSPS_MESSAGE_TYPE => Ok(Some(RawLSPSMessage::read(buffer)?)), + LSPS_MESSAGE_TYPE_ID => Ok(Some(RawLSPSMessage::read(buffer)?)), _ => Ok(None), } } diff --git a/src/transport/msgs.rs b/src/transport/msgs.rs index d90f240..5a14260 100644 --- a/src/transport/msgs.rs +++ b/src/transport/msgs.rs @@ -1,9 +1,12 @@ +use crate::jit_channel; + use lightning::impl_writeable_msg; use lightning::ln::wire; use serde::de; use serde::de::{MapAccess, Visitor}; use serde::ser::SerializeStruct; use serde::{Deserialize, Deserializer, Serialize}; +use serde_json::json; use std::collections::HashMap; use std::convert::TryFrom; use std::fmt; @@ -20,10 +23,13 @@ const JSONRPC_INVALID_MESSAGE_ERROR_CODE: i32 = -32700; const JSONRPC_INVALID_MESSAGE_ERROR_MESSAGE: &str = "parse error"; const LSPS0_LISTPROTOCOLS_METHOD_NAME: &str = "lsps0.list_protocols"; -pub const LSPS_MESSAGE_TYPE: u16 = 37913; +/// The Lightning message type id for LSPS messages +pub const LSPS_MESSAGE_TYPE_ID: u16 = 37913; +/// 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, } @@ -31,11 +37,11 @@ impl_writeable_msg!(RawLSPSMessage, { payload }, {}); impl wire::Type for RawLSPSMessage { fn type_id(&self) -> u16 { - LSPS_MESSAGE_TYPE + LSPS_MESSAGE_TYPE_ID } } -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(Clone, Debug, PartialEq, Eq, Hash)] pub struct RequestId(pub String); #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] @@ -86,6 +92,7 @@ impl TryFrom for LSPS0Message { match message { LSPSMessage::Invalid => Err(()), LSPSMessage::LSPS0(message) => Ok(message), + LSPSMessage::LSPS2(_) => Err(()), } } } @@ -100,6 +107,7 @@ impl From for LSPSMessage { pub enum LSPSMessage { Invalid, LSPS0(LSPS0Message), + LSPS2(jit_channel::msgs::Message), } impl LSPSMessage { @@ -116,6 +124,9 @@ impl LSPSMessage { LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { Some((request_id.0.clone(), request.method().to_string())) } + LSPSMessage::LSPS2(jit_channel::msgs::Message::Request(request_id, request)) => { + Some((request_id.0.clone(), request.method().to_string())) + } _ => None, } } @@ -154,6 +165,43 @@ impl Serialize for LSPSMessage { } } } + LSPSMessage::LSPS2(jit_channel::msgs::Message::Request(request_id, request)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + jsonrpc_object.serialize_field(JSONRPC_METHOD_FIELD_KEY, request.method())?; + + match request { + jit_channel::msgs::Request::GetVersions(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + jit_channel::msgs::Request::GetInfo(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + jit_channel::msgs::Request::Buy(params) => { + jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? + } + } + } + LSPSMessage::LSPS2(jit_channel::msgs::Message::Response(request_id, response)) => { + jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; + + match response { + jit_channel::msgs::Response::GetVersions(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + jit_channel::msgs::Response::GetInfo(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + jit_channel::msgs::Response::GetInfoError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + } + jit_channel::msgs::Response::Buy(result) => { + jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? + } + jit_channel::msgs::Response::BuyError(error) => { + jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? + } + } + } LSPSMessage::Invalid => { let error = ResponseError { code: JSONRPC_INVALID_MESSAGE_ERROR_CODE, @@ -224,6 +272,30 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { LSPS0Request::ListProtocols(ListProtocolsRequest {}), ))) } + jit_channel::msgs::LSPS2_GET_VERSIONS_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + RequestId(id), + jit_channel::msgs::Request::GetVersions(request), + ))) + } + jit_channel::msgs::LSPS2_GET_INFO_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + RequestId(id), + jit_channel::msgs::Request::GetInfo(request), + ))) + } + jit_channel::msgs::LSPS2_BUY_METHOD_NAME => { + let request = serde_json::from_value(params.unwrap_or(json!({}))) + .map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + RequestId(id), + jit_channel::msgs::Request::Buy(request), + ))) + } _ => Err(de::Error::custom(format!( "Received request with unknown method: {}", method @@ -248,6 +320,52 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) } } + jit_channel::msgs::LSPS2_GET_VERSIONS_METHOD_NAME => { + if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + RequestId(id), + jit_channel::msgs::Response::GetVersions(response), + ))) + } else { + Err(de::Error::custom("Received invalid lsps2.getversions response.")) + } + } + jit_channel::msgs::LSPS2_GET_INFO_METHOD_NAME => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + RequestId(id), + jit_channel::msgs::Response::GetInfoError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + RequestId(id), + jit_channel::msgs::Response::GetInfo(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + } + jit_channel::msgs::LSPS2_BUY_METHOD_NAME => { + if let Some(error) = error { + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + RequestId(id), + jit_channel::msgs::Response::BuyError(error), + ))) + } else if let Some(result) = result { + let response = + serde_json::from_value(result).map_err(de::Error::custom)?; + Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + RequestId(id), + jit_channel::msgs::Response::Buy(response), + ))) + } else { + Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) + } + } _ => Err(de::Error::custom(format!( "Received response for an unknown request method: {}", method From 56601c719d1f48251d281170f6b79c64f473bd4f Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 11:33:43 -0400 Subject: [PATCH 03/16] add LSPS2 events --- src/events.rs | 9 ++-- src/jit_channel/event.rs | 104 +++++++++++++++++++++++++++++++++++++++ src/jit_channel/mod.rs | 1 + src/lib.rs | 1 + 4 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/jit_channel/event.rs diff --git a/src/events.rs b/src/events.rs index ca03e6c..a6bea14 100644 --- a/src/events.rs +++ b/src/events.rs @@ -12,7 +12,7 @@ //! //! Because we don't have a built-in runtime, it's up to the end-user to poll //! [`crate::LiquidityManager::get_and_clear_pending_events()`] to receive events. - +use crate::jit_channel; use std::collections::VecDeque; use std::sync::{Condvar, Mutex}; @@ -53,6 +53,9 @@ impl EventQueue { } } -/// Event which you should probably take some action in response to. +/// An event which you should probably take some action in response to. #[derive(Debug, Clone, PartialEq, Eq)] -pub enum Event {} +pub enum Event { + /// A LSPS2 (JIT Channel) protocol event + LSPS2(jit_channel::event::Event), +} diff --git a/src/jit_channel/event.rs b/src/jit_channel/event.rs new file mode 100644 index 0000000..ba41444 --- /dev/null +++ b/src/jit_channel/event.rs @@ -0,0 +1,104 @@ +// This file is Copyright its original authors, visible in version contror +// 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. + +use bitcoin::secp256k1::PublicKey; + +use super::msgs::OpeningFeeParams; +use crate::transport::msgs::RequestId; + +/// An Event which you should probably take some action in response to. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum Event { + /// A request from a client for information about JIT Channel parameters. + /// + /// You must calculate the paramaeters for this client and pass them to + /// [`crate::LiquidityManager::opening_fee_params_generated`]. + GetInfo { + /// An identifier that must be passed to [`crate::LiquidityManager::opening_fee_params_generated`]. + request_id: RequestId, + /// The node id of the client making the information request. + counterparty_node_id: PublicKey, + /// The protocol version they would like to use. + version: u16, + /// An optional token that can be used as an api key, coupon code, etc. + token: Option, + }, + /// Information from LSP about their current fee and channel parameters. + /// + /// You must call [`crate::LiquidityManager::opening_fee_params_selected`] with the fee parameter + /// you want to use if you wish to proceed opening a channel. + GetInfoResponse { + /// Needs to be passed to [`crate::LiquidityManager::opening_fee_params_selected`]. + channel_id: u128, + /// 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, + /// The min payment size allowed when opening the channel. + min_payment_size_msat: u64, + /// The max payment size allowed when opening the channel. + max_payment_size_msat: u64, + /// The user_channel_id value passed in to [`crate::LiquidityManager::create_invoice`]. + user_channel_id: u128, + }, + /// 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 scid and cltv_expiry_delta for them to use + /// and call [`crate::LiquidityManager::invoice_parameters_generated`]. + BuyRequest { + /// An identifier that must be passed into [`crate::LiquidityManager::invoice_parameters_generated`]. + request_id: RequestId, + /// The client node id that is making this request. + counterparty_node_id: PublicKey, + /// The version of the protocol they would like to use. + version: u16, + /// 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, + }, + /// Use the provided fields to generate an invoice and give to payer. + /// + /// When the invoice is paid the LSP will open a channel to you + /// with the previously agreed upon parameters. + InvoiceGenerationReady { + /// The node id of the LSP. + counterparty_node_id: PublicKey, + /// The short channel id to use in the route hint. + 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, + /// The trust model the lsp expects. + client_trusts_lsp: bool, + /// The user_channel_id value passed in to [`crate::LiquidityManager::create_invoice`]. + user_channel_id: u128, + }, + /// You should open a channel using [`lightning::ln::channelmanager::ChannelManager::create_channel`]. + OpenChannel { + /// The node to open channel with + their_network_key: PublicKey, + /// The intercepted htlc amount in msats + inbound_amount_msat: u64, + /// The amount the client expects to receive before fees are taken out + expected_outbound_amount_msat: u64, + /// The amount to forward after fees + amt_to_forward_msat: u64, + /// The fee earned for opening the channel + opening_fee_msat: u64, + /// An internal id used to track channel open + user_channel_id: u128, + }, +} diff --git a/src/jit_channel/mod.rs b/src/jit_channel/mod.rs index bcb97db..82a98c3 100644 --- a/src/jit_channel/mod.rs +++ b/src/jit_channel/mod.rs @@ -8,4 +8,5 @@ // licenses. //! Types and primitives that implement the LSPS3: JIT Channel Negotiation specification. +pub mod event; pub mod msgs; diff --git a/src/lib.rs b/src/lib.rs index 4919017..65d21a7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,4 +24,5 @@ mod jit_channel; mod transport; mod utils; +pub use jit_channel::event::Event as JITChannelEvent; pub use transport::message_handler::{LiquidityManager, LiquidityProviderConfig}; From 0c903f901a079cf4cfb11e071edcfa5f5fa6625e Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 11:51:33 -0400 Subject: [PATCH 04/16] add LSPS2 message handler --- src/jit_channel/channel_manager.rs | 1119 ++++++++++++++++++++++++++++ src/jit_channel/mod.rs | 2 + src/jit_channel/scid_utils.rs | 71 ++ src/utils.rs | 10 + 4 files changed, 1202 insertions(+) create mode 100644 src/jit_channel/channel_manager.rs create mode 100644 src/jit_channel/scid_utils.rs diff --git a/src/jit_channel/channel_manager.rs b/src/jit_channel/channel_manager.rs new file mode 100644 index 0000000..4a002e7 --- /dev/null +++ b/src/jit_channel/channel_manager.rs @@ -0,0 +1,1119 @@ +// This file is Copyright its original authors, visible in version contror +// 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. + +use std::collections::HashMap; +use std::convert::TryInto; +use std::ops::Deref; +use std::sync::{Arc, Mutex, RwLock}; + +use bitcoin::secp256k1::PublicKey; +use lightning::chain; +use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; +use lightning::ln::channelmanager::{ChannelManager, InterceptId}; +use lightning::ln::msgs::{ + ChannelMessageHandler, ErrorAction, LightningError, OnionMessageHandler, RoutingMessageHandler, +}; +use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor}; +use lightning::ln::ChannelId; +use lightning::routing::router::Router; +use lightning::sign::{EntropySource, NodeSigner, SignerProvider}; +use lightning::util::errors::APIError; +use lightning::util::logger::{Level, Logger}; + +use crate::events::EventQueue; +use crate::jit_channel::msgs::Message; +use crate::jit_channel::scid_utils; +use crate::transport::message_handler::ProtocolMessageHandler; +use crate::transport::msgs::{LSPSMessage, RequestId}; +use crate::utils; +use crate::{events::Event, transport::msgs::ResponseError}; + +use super::msgs::{ + BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsRequest, + GetVersionsResponse, OpeningFeeParams, RawOpeningFeeParams, Request, Response, +}; + +const SUPPORTED_SPEC_VERSION: u16 = 1; + +struct ChannelStateError(String); + +impl From for LightningError { + fn from(value: ChannelStateError) -> Self { + LightningError { err: value.0, action: ErrorAction::IgnoreAndLog(Level::Info) } + } +} + +struct InboundJITChannelConfig { + pub user_id: u128, + pub token: Option, + pub payment_size_msat: Option, +} + +#[derive(PartialEq, Debug)] +enum InboundJITChannelState { + VersionsRequested, + MenuRequested, + PendingMenuSelection, + BuyRequested, + PendingPayment { client_trusts_lsp: bool, scid: String }, +} + +impl InboundJITChannelState { + fn versions_received(&self, versions: Vec) -> Result { + if !versions.contains(&SUPPORTED_SPEC_VERSION) { + return Err(ChannelStateError(format!( + "LSP does not support our specification version. ours = {}. theirs = {:?}", + SUPPORTED_SPEC_VERSION, versions + ))); + } + + match self { + InboundJITChannelState::VersionsRequested => Ok(InboundJITChannelState::MenuRequested), + state => Err(ChannelStateError(format!( + "Received unexpected get_versions response. JIT Channel was in state: {:?}", + state + ))), + } + } + + fn info_received(&self) -> Result { + match self { + InboundJITChannelState::MenuRequested => { + Ok(InboundJITChannelState::PendingMenuSelection) + } + state => Err(ChannelStateError(format!( + "Received unexpected get_info response. JIT Channel was in state: {:?}", + state + ))), + } + } + + fn opening_fee_params_selected(&self) -> Result { + match self { + InboundJITChannelState::PendingMenuSelection => { + Ok(InboundJITChannelState::BuyRequested) + } + state => Err(ChannelStateError(format!( + "Opening fee params selected when JIT Channel was in state: {:?}", + state + ))), + } + } + + fn invoice_params_received( + &self, client_trusts_lsp: bool, scid: String, + ) -> Result { + match self { + InboundJITChannelState::BuyRequested => { + Ok(InboundJITChannelState::PendingPayment { client_trusts_lsp, scid }) + } + state => Err(ChannelStateError(format!( + "Invoice params received when JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct InboundJITChannel { + id: u128, + state: InboundJITChannelState, + config: InboundJITChannelConfig, +} + +impl InboundJITChannel { + pub fn new( + id: u128, user_id: u128, payment_size_msat: Option, token: Option, + ) -> Self { + Self { + id, + config: InboundJITChannelConfig { user_id, payment_size_msat, token }, + state: InboundJITChannelState::VersionsRequested, + } + } + + pub fn versions_received(&mut self, versions: Vec) -> Result<(), LightningError> { + self.state = self.state.versions_received(versions)?; + Ok(()) + } + + pub fn info_received(&mut self) -> Result<(), LightningError> { + self.state = self.state.info_received()?; + Ok(()) + } + + pub fn opening_fee_params_selected(&mut self) -> Result<(), LightningError> { + self.state = self.state.opening_fee_params_selected()?; + Ok(()) + } + + pub fn invoice_params_received( + &mut self, client_trusts_lsp: bool, jit_channel_scid: String, + ) -> Result<(), LightningError> { + self.state = self.state.invoice_params_received(client_trusts_lsp, jit_channel_scid)?; + Ok(()) + } +} + +#[derive(PartialEq, Debug)] +enum OutboundJITChannelState { + InvoiceParametersGenerated { + scid: u64, + cltv_expiry_delta: u32, + payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + }, + PendingChannelOpen { + intercept_id: InterceptId, + opening_fee_msat: u64, + amt_to_forward_msat: u64, + }, + ChannelReady { + intercept_id: InterceptId, + amt_to_forward_msat: u64, + }, +} + +impl OutboundJITChannelState { + pub fn new( + scid: u64, cltv_expiry_delta: u32, payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + ) -> Self { + OutboundJITChannelState::InvoiceParametersGenerated { + scid, + cltv_expiry_delta, + payment_size_msat, + opening_fee_params, + } + } + + pub fn htlc_intercepted( + &self, expected_outbound_amount_msat: u64, intercept_id: InterceptId, + ) -> Result { + match self { + OutboundJITChannelState::InvoiceParametersGenerated { opening_fee_params, .. } => { + let opening_fee_msat: Option = utils::compute_opening_fee( + expected_outbound_amount_msat, + opening_fee_params.min_fee_msat, + opening_fee_params.proportional as u64, + ); + + if let Some(opening_fee_msat) = opening_fee_msat { + Ok(OutboundJITChannelState::PendingChannelOpen { + intercept_id, + opening_fee_msat, + amt_to_forward_msat: expected_outbound_amount_msat - opening_fee_msat, + }) + } else { + Err(ChannelStateError(format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_outbound_amount_msat = {}", opening_fee_params.min_fee_msat, opening_fee_params.proportional, expected_outbound_amount_msat))) + } + } + state => Err(ChannelStateError(format!( + "Invoice params received when JIT Channel was in state: {:?}", + state + ))), + } + } + + pub fn channel_ready(&self) -> Result { + match self { + OutboundJITChannelState::PendingChannelOpen { + intercept_id, + amt_to_forward_msat, + .. + } => Ok(OutboundJITChannelState::ChannelReady { + intercept_id: *intercept_id, + amt_to_forward_msat: *amt_to_forward_msat, + }), + state => Err(ChannelStateError(format!( + "Channel ready received when JIT Channel was in state: {:?}", + state + ))), + } + } +} + +struct OutboundJITChannel { + state: OutboundJITChannelState, +} + +impl OutboundJITChannel { + pub fn new( + scid: u64, cltv_expiry_delta: u32, payment_size_msat: Option, + opening_fee_params: OpeningFeeParams, + ) -> Self { + Self { + state: OutboundJITChannelState::new( + scid, + cltv_expiry_delta, + payment_size_msat, + opening_fee_params, + ), + } + } + + pub fn htlc_intercepted( + &mut self, expected_outbound_amount_msat: u64, intercept_id: InterceptId, + ) -> Result<(u64, u64), LightningError> { + self.state = self.state.htlc_intercepted(expected_outbound_amount_msat, intercept_id)?; + + match &self.state { + OutboundJITChannelState::PendingChannelOpen { + opening_fee_msat, + amt_to_forward_msat, + .. + } => Ok((*opening_fee_msat, *amt_to_forward_msat)), + impossible_state => Err(LightningError { + err: format!( + "Impossible state transition during htlc_intercepted to {:?}", + impossible_state + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }), + } + } + + pub fn channel_ready(&mut self) -> Result<(InterceptId, u64), LightningError> { + self.state = self.state.channel_ready()?; + + match &self.state { + OutboundJITChannelState::ChannelReady { intercept_id, amt_to_forward_msat } => { + Ok((*intercept_id, *amt_to_forward_msat)) + } + impossible_state => Err(LightningError { + err: format!( + "Impossible state transition during channel_ready to {:?}", + impossible_state + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }), + } + } +} + +#[derive(Default)] +struct PeerState { + inbound_channels_by_id: HashMap, + outbound_channels_by_scid: HashMap, + request_to_cid: HashMap, + pending_requests: HashMap, +} + +impl PeerState { + pub fn insert_inbound_channel(&mut self, jit_channel_id: u128, channel: InboundJITChannel) { + self.inbound_channels_by_id.insert(jit_channel_id, channel); + } + + pub fn insert_outbound_channel(&mut self, scid: u64, channel: OutboundJITChannel) { + self.outbound_channels_by_scid.insert(scid, channel); + } + + pub fn insert_request(&mut self, request_id: RequestId, jit_channel_id: u128) { + self.request_to_cid.insert(request_id, jit_channel_id); + } + + pub fn remove_inbound_channel(&mut self, jit_channel_id: u128) { + self.inbound_channels_by_id.remove(&jit_channel_id); + } + + pub fn remove_outbound_channel(&mut self, scid: u64) { + self.outbound_channels_by_scid.remove(&scid); + } +} + +pub struct JITChannelManager< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, +> where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + entropy_source: ES, + peer_manager: Mutex>>>, + channel_manager: Arc>, + pending_messages: Arc>>, + pending_events: Arc, + per_peer_state: RwLock>>, + peer_by_scid: RwLock>, + promise_secret: [u8; 32], +} + +impl< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, + > JITChannelManager +where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + pub(crate) fn new( + entropy_source: ES, promise_secret: [u8; 32], + pending_messages: Arc>>, + pending_events: Arc, + channel_manager: Arc>, + ) -> Self { + Self { + entropy_source, + promise_secret, + pending_messages, + pending_events, + per_peer_state: RwLock::new(HashMap::new()), + peer_by_scid: RwLock::new(HashMap::new()), + peer_manager: Mutex::new(None), + channel_manager, + } + } + + fn map_scid_to_peer(&self, scid: u64, counterparty_node_id: PublicKey) { + let mut peer_by_scid = self.peer_by_scid.write().unwrap(); + peer_by_scid.insert(scid, counterparty_node_id); + } + + pub fn set_peer_manager( + &self, peer_manager: Arc>, + ) { + *self.peer_manager.lock().unwrap() = Some(peer_manager); + } + + pub fn create_invoice( + &self, counterparty_node_id: PublicKey, payment_size_msat: Option, + token: Option, user_channel_id: u128, + ) { + let jit_channel_id = self.generate_jit_channel_id(); + let channel = + InboundJITChannel::new(jit_channel_id, user_channel_id, payment_size_msat, token); + + 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 peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.insert_inbound_channel(jit_channel_id, channel); + + let request_id = self.generate_request_id(); + peer_state.insert_request(request_id.clone(), jit_channel_id); + + { + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(( + counterparty_node_id, + Message::Request(request_id, Request::GetVersions(GetVersionsRequest {})).into(), + )); + } + + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.process_events(); + } + } + + pub fn opening_fee_params_generated( + &self, counterparty_node_id: PublicKey, request_id: RequestId, + opening_fee_params_menu: Vec, min_payment_size_msat: u64, + max_payment_size_msat: u64, + ) -> Result<(), APIError> { + 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(Request::GetInfo(_)) => { + let response = Response::GetInfo(GetInfoResponse { + opening_fee_params_menu: opening_fee_params_menu + .into_iter() + .map(|param| param.into_opening_fee_params(&self.promise_secret)) + .collect(), + min_payment_size_msat, + max_payment_size_msat, + }); + self.enqueue_response(counterparty_node_id, request_id, response); + Ok(()) + } + _ => Err(APIError::APIMisuseError { + err: format!( + "No pending get_info request for request_id: {:?}", + request_id + ), + }), + } + } + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + }), + } + } + + pub fn opening_fee_params_selected( + &self, counterparty_node_id: PublicKey, jit_channel_id: u128, + opening_fee_params: OpeningFeeParams, + ) -> Result<(), APIError> { + 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.inbound_channels_by_id.get_mut(&jit_channel_id) + { + if let Err(e) = jit_channel.opening_fee_params_selected() { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(APIError::APIMisuseError { err: e.err }); + } + + let request_id = self.generate_request_id(); + let payment_size_msat = jit_channel.config.payment_size_msat; + peer_state.insert_request(request_id.clone(), jit_channel_id); + + { + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(( + counterparty_node_id, + Message::Request( + request_id, + Request::Buy(BuyRequest { + version: SUPPORTED_SPEC_VERSION, + opening_fee_params, + payment_size_msat, + }), + ) + .into(), + )); + } + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.process_events(); + } + } else { + return Err(APIError::APIMisuseError { + err: format!("Channel with id {} not found", jit_channel_id), + }); + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No existing state with counterparty {}", counterparty_node_id), + }) + } + } + + Ok(()) + } + + pub fn invoice_parameters_generated( + &self, counterparty_node_id: PublicKey, request_id: RequestId, scid: u64, + cltv_expiry_delta: u32, client_trusts_lsp: bool, + ) -> Result<(), APIError> { + 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(Request::Buy(buy_request)) => { + self.map_scid_to_peer(scid, counterparty_node_id.clone()); + let outbound_jit_channel = OutboundJITChannel::new( + scid, + cltv_expiry_delta, + buy_request.payment_size_msat, + buy_request.opening_fee_params, + ); + + peer_state.insert_outbound_channel(scid, outbound_jit_channel); + + 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); + + let jit_channel_scid = format!("{}x{}x{}", block, tx_index, vout); + + self.enqueue_response( + counterparty_node_id, + request_id, + Response::Buy(BuyResponse { + jit_channel_scid, + lsp_cltv_expiry_delta: cltv_expiry_delta, + client_trusts_lsp, + }), + ); + + Ok(()) + } + _ => Err(APIError::APIMisuseError { + err: format!("No pending buy request for request_id: {:?}", request_id), + }), + } + } + None => Err(APIError::APIMisuseError { + err: format!("No state for the counterparty exists: {:?}", counterparty_node_id), + }), + } + } + + pub(crate) fn htlc_intercepted( + &self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64, + expected_outbound_amount_msat: u64, + ) -> Result<(), APIError> { + let peer_by_scid = self.peer_by_scid.read().unwrap(); + if let Some(counterparty_node_id) = peer_by_scid.get(&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_scid.get_mut(&scid) { + // TODO: Need to support MPP payments. If payment_amount_msat is known, needs to queue intercepted HTLCs in a map by payment_hash + // LiquidityManager will need to be regularly polled so it can continually check if the payment amount has been received + // and can release the payment or if the channel valid_until has expired and should be failed. + // Can perform check each time HTLC is received and on interval? I guess interval only needs to check expiration as + // we can only reach threshold when htlc is intercepted. + + match jit_channel + .htlc_intercepted(expected_outbound_amount_msat, intercept_id) + { + Ok((opening_fee_msat, amt_to_forward_msat)) => { + self.enqueue_event(Event::LSPS2( + crate::JITChannelEvent::OpenChannel { + their_network_key: counterparty_node_id.clone(), + inbound_amount_msat, + expected_outbound_amount_msat, + amt_to_forward_msat, + opening_fee_msat, + user_channel_id: scid as u128, + }, + )); + } + Err(e) => { + self.channel_manager.fail_intercepted_htlc(intercept_id)?; + // remove channel? + return Err(APIError::APIMisuseError { err: e.err }); + } + } + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty found for scid: {}", scid), + }); + } + } + } + + Ok(()) + } + + // figure out which intercept id is waiting on this channel and enqueue ForwardInterceptedHTLC event + pub(crate) fn channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) -> Result<(), APIError> { + if let Ok(scid) = user_channel_id.try_into() { + 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_scid.get_mut(&scid) { + match jit_channel.channel_ready() { + Ok((intercept_id, amt_to_forward_msat)) => { + self.channel_manager.forward_intercepted_htlc( + intercept_id, + channel_id, + *counterparty_node_id, + amt_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 + ), + }); + } + } + None => { + return Err(APIError::APIMisuseError { + err: format!("No counterparty state for: {}", counterparty_node_id), + }); + } + } + } + + Ok(()) + } + + fn generate_jit_channel_id(&self) -> u128 { + let bytes = self.entropy_source.get_secure_random_bytes(); + let mut id_bytes: [u8; 16] = [0; 16]; + id_bytes.copy_from_slice(&bytes[0..16]); + u128::from_be_bytes(id_bytes) + } + + fn generate_request_id(&self) -> RequestId { + let bytes = self.entropy_source.get_secure_random_bytes(); + RequestId(utils::hex_str(&bytes[0..16])) + } + + fn enqueue_response( + &self, counterparty_node_id: PublicKey, request_id: RequestId, response: Response, + ) { + { + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages + .push((counterparty_node_id, Message::Response(request_id, response).into())); + } + + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.process_events(); + } + } + + fn enqueue_event(&self, event: Event) { + self.pending_events.enqueue(event); + } + + fn handle_get_versions_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + // not sure best way to extract a vec to a constant? lazy_static? + self.enqueue_response( + *counterparty_node_id, + request_id, + Response::GetVersions(GetVersionsResponse { versions: vec![1] }), + ); + Ok(()) + } + + fn handle_get_versions_response( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, result: GetVersionsResponse, + ) -> 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_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received get_versions response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let jit_channel = peer_state + .inbound_channels_by_id + .get_mut(&jit_channel_id) + .ok_or(LightningError { + err: format!( + "Received get_versions response for an unknown channel: {:?}", + jit_channel_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let token = jit_channel.config.token.clone(); + + if let Err(e) = jit_channel.versions_received(result.versions) { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + + let request_id = self.generate_request_id(); + peer_state.insert_request(request_id.clone(), jit_channel_id); + + { + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(( + *counterparty_node_id, + Message::Request( + request_id, + Request::GetInfo(GetInfoRequest { + version: SUPPORTED_SPEC_VERSION, + token, + }), + ) + .into(), + )); + } + + if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { + peer_manager.process_events(); + } + } + None => { + return Err(LightningError { + err: format!( + "Received get_versions response from unknown peer: {:?}", + counterparty_node_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + }) + } + } + + 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::default())); + let peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.pending_requests.insert(request_id.clone(), Request::GetInfo(params.clone())); + + self.enqueue_event(Event::LSPS2(super::event::Event::GetInfo { + request_id, + counterparty_node_id: *counterparty_node_id, + version: params.version, + token: params.token, + })); + Ok(()) + } + + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received get_info response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let jit_channel = peer_state + .inbound_channels_by_id + .get_mut(&jit_channel_id) + .ok_or(LightningError { + err: format!( + "Received get_info response for an unknown channel: {:?}", + jit_channel_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Err(e) = jit_channel.info_received() { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + + self.enqueue_event(Event::LSPS2(super::event::Event::GetInfoResponse { + counterparty_node_id: *counterparty_node_id, + opening_fee_params_menu: result.opening_fee_params_menu, + min_payment_size_msat: result.min_payment_size_msat, + max_payment_size_msat: result.max_payment_size_msat, + channel_id: jit_channel.id, + user_channel_id: jit_channel.config.user_id, + })); + } + 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received get_info error for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let _jit_channel = peer_state + .inbound_channels_by_id + .remove(&jit_channel_id) + .ok_or(LightningError { + err: format!( + "Received get_info error for an unknown channel: {:?}", + jit_channel_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_request( + &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: BuyRequest, + ) -> Result<(), LightningError> { + // TODO: need to perform check on `params.version`. + // TODO: if payment_size_msat is specified, make sure opening_fee is >= payment_size_msat. + // TODO: if payment_size_msat is specified, make sure opening_fee does not hit overflow error. + // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. + + if params.opening_fee_params.is_valid(&self.promise_secret) { + 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 peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.pending_requests.insert(request_id.clone(), Request::Buy(params.clone())); + + self.enqueue_event(Event::LSPS2(super::event::Event::BuyRequest { + request_id, + version: params.version, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + })); + } + Ok(()) + } + + 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_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!( + "Received buy response for an unknown request: {:?}", + request_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let jit_channel = peer_state + .inbound_channels_by_id + .get_mut(&jit_channel_id) + .ok_or(LightningError { + err: format!( + "Received buy response for an unknown channel: {:?}", + jit_channel_id + ), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + if let Err(e) = jit_channel.invoice_params_received( + result.client_trusts_lsp, + result.jit_channel_scid.clone(), + ) { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + + if let Ok(scid) = + scid_utils::scid_from_human_readable_string(&result.jit_channel_scid) + { + self.enqueue_event(Event::LSPS2(super::event::Event::InvoiceGenerationReady { + counterparty_node_id: *counterparty_node_id, + scid, + cltv_expiry_delta: result.lsp_cltv_expiry_delta, + payment_size_msat: jit_channel.config.payment_size_msat, + client_trusts_lsp: result.client_trusts_lsp, + user_channel_id: jit_channel.config.user_id, + })); + } else { + return Err(LightningError { + err: format!( + "Received buy response with an invalid 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(); + + let jit_channel_id = + peer_state.request_to_cid.remove(&request_id).ok_or(LightningError { + err: format!("Received buy error for an unknown request: {:?}", request_id), + action: ErrorAction::IgnoreAndLog(Level::Info), + })?; + + let _jit_channel = peer_state + .inbound_channels_by_id + .remove(&jit_channel_id) + .ok_or(LightningError { + err: format!( + "Received buy error for an unknown channel: {:?}", + jit_channel_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< + ES: Deref, + M: Deref, + T: Deref, + F: Deref, + R: Deref, + SP: Deref, + Descriptor: SocketDescriptor, + L: Deref, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, + NS: Deref, + > ProtocolMessageHandler + for JITChannelManager +where + ES::Target: EntropySource, + M::Target: chain::Watch<::Signer>, + T::Target: BroadcasterInterface, + F::Target: FeeEstimator, + R::Target: Router, + SP::Target: SignerProvider, + L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, + NS::Target: NodeSigner, +{ + type ProtocolMessage = Message; + const PROTOCOL_NUMBER: Option = Some(2); + + fn handle_message( + &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, + ) -> Result<(), LightningError> { + match message { + Message::Request(request_id, request) => match request { + super::msgs::Request::GetVersions(_) => { + self.handle_get_versions_request(request_id, counterparty_node_id) + } + super::msgs::Request::GetInfo(params) => { + self.handle_get_info_request(request_id, counterparty_node_id, params) + } + super::msgs::Request::Buy(params) => { + self.handle_buy_request(request_id, counterparty_node_id, params) + } + }, + Message::Response(request_id, response) => match response { + super::msgs::Response::GetVersions(result) => { + self.handle_get_versions_response(request_id, counterparty_node_id, result) + } + super::msgs::Response::GetInfo(result) => { + self.handle_get_info_response(request_id, counterparty_node_id, result) + } + super::msgs::Response::GetInfoError(error) => { + self.handle_get_info_error(request_id, counterparty_node_id, error) + } + super::msgs::Response::Buy(result) => { + self.handle_buy_response(request_id, counterparty_node_id, result) + } + super::msgs::Response::BuyError(error) => { + self.handle_buy_error(request_id, counterparty_node_id, error) + } + }, + } + } +} diff --git a/src/jit_channel/mod.rs b/src/jit_channel/mod.rs index 82a98c3..2d58382 100644 --- a/src/jit_channel/mod.rs +++ b/src/jit_channel/mod.rs @@ -8,5 +8,7 @@ // licenses. //! Types and primitives that implement the LSPS3: JIT Channel Negotiation specification. +pub mod channel_manager; pub mod event; pub mod msgs; +pub mod scid_utils; diff --git a/src/jit_channel/scid_utils.rs b/src/jit_channel/scid_utils.rs new file mode 100644 index 0000000..ce83457 --- /dev/null +++ b/src/jit_channel/scid_utils.rs @@ -0,0 +1,71 @@ +#[derive(Debug, PartialEq, Eq)] +pub enum ShortChannelIdError { + InvalidScid, +} + +/// Maximum transaction index that can be used in a `short_channel_id`. +/// This value is based on the 3-bytes available for tx index. +pub const MAX_SCID_TX_INDEX: u64 = 0x00ffffff; + +/// Maximum vout index that can be used in a `short_channel_id`. This +/// value is based on the 2-bytes available for the vout index. +pub const MAX_SCID_VOUT_INDEX: u64 = 0xffff; + +/// Extracts the block height (most significant 3-bytes) from the `short_channel_id` +pub fn block_from_scid(short_channel_id: &u64) -> u32 { + (short_channel_id >> 40) as u32 +} + +/// Extracts the tx index (bytes [2..4]) from the `short_channel_id` +pub fn tx_index_from_scid(short_channel_id: &u64) -> u32 { + ((short_channel_id >> 16) & MAX_SCID_TX_INDEX) as u32 +} + +/// Extracts the vout (bytes [0..2]) from the `short_channel_id` +pub fn vout_from_scid(short_channel_id: &u64) -> u16 { + ((short_channel_id) & MAX_SCID_VOUT_INDEX) as u16 +} + +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(ShortChannelIdError::InvalidScid)? + .parse() + .map_err(|_e| ShortChannelIdError::InvalidScid)?; + let tx_index: u64 = parts + .next() + .ok_or(ShortChannelIdError::InvalidScid)? + .parse() + .map_err(|_e| ShortChannelIdError::InvalidScid)?; + let vout_index: u64 = parts + .next() + .ok_or(ShortChannelIdError::InvalidScid)? + .parse() + .map_err(|_e| ShortChannelIdError::InvalidScid)?; + + Ok((block << 40) | (tx_index << 16) | vout_index) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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/src/utils.rs b/src/utils.rs index 067ce0b..da40131 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -67,3 +67,13 @@ pub fn parse_pubkey(pubkey_str: &str) -> Result { Ok(pubkey.unwrap()) } + +pub fn compute_opening_fee( + payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u64, +) -> Option { + let t1 = payment_size_msat.checked_mul(opening_fee_proportional)?; + let t2 = t1.checked_add(999999)?; + let t3 = t2.checked_div(1000000)?; + let t4 = std::cmp::max(t3, opening_fee_min_fee_msat); + Some(t4) +} From e3a6ee270aba5e230b8127d2aa0dd45e5c614c10 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 12:18:16 -0400 Subject: [PATCH 05/16] setup LSPS2 message handler in LiquidityManager --- src/transport/message_handler.rs | 132 +++++++++++++++++++++++++++---- 1 file changed, 115 insertions(+), 17 deletions(-) diff --git a/src/transport/message_handler.rs b/src/transport/message_handler.rs index 91ac509..6b30bc2 100644 --- a/src/transport/message_handler.rs +++ b/src/transport/message_handler.rs @@ -1,4 +1,5 @@ use crate::events::{Event, EventQueue}; +use crate::jit_channel::channel_manager::JITChannelManager; use crate::transport::msgs::{LSPSMessage, RawLSPSMessage, LSPS_MESSAGE_TYPE_ID}; use crate::transport::protocol::LSPS0MessageHandler; @@ -6,8 +7,10 @@ use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; use lightning::ln::channelmanager::{ChainParameters, ChannelManager}; use lightning::ln::features::{InitFeatures, NodeFeatures}; -use lightning::ln::msgs::{ErrorAction, LightningError}; -use lightning::ln::peer_handler::CustomMessageHandler; +use lightning::ln::msgs::{ + ChannelMessageHandler, ErrorAction, LightningError, OnionMessageHandler, RoutingMessageHandler, +}; +use lightning::ln::peer_handler::{CustomMessageHandler, SocketDescriptor}; use lightning::ln::wire::CustomMessageReader; use lightning::routing::router::Router; use lightning::sign::{EntropySource, NodeSigner, SignerProvider}; @@ -42,20 +45,48 @@ pub(crate) trait ProtocolMessageHandler { /// /// Allows end-user to configure options when using the [`LiquidityManager`] /// to provide liquidity services to clients. -pub struct LiquidityProviderConfig; +pub struct LiquidityProviderConfig { + /// Optional configuration for jit channels + /// should you want to support them + pub jit_channels: Option, +} + +/// Configuration options for jit channels +/// A configuration used for the creation of Just In Time Channels. +pub struct JITChannelsConfig { + /// 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], +} /// The main interface into LSP functionality. /// /// Should be used as a [`CustomMessageHandler`] for your /// [`lightning::ln::peer_handler::PeerManager`]'s [`lightning::ln::peer_handler::MessageHandler`]. +/// +/// Should provide a reference to your [`lightning::ln::peer_handler::PeerManager`] by calling +/// [`LiquidityManager::set_peer_manager()`] post construction. This allows the [`LiquidityManager`] to +/// wake the [`lightning::ln::peer_handler::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. +/// +/// Users must forward the [`lightning::events::Event::HTLCIntercepted`] event parameters to [`LiquidityManager::htlc_intercepted()`] +/// and the [`lightning::events::Event::ChannelReady`] event parameters to [`LiquidityManager::channel_ready()`]. pub struct LiquidityManager< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, > where @@ -66,6 +97,10 @@ pub struct LiquidityManager< R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -73,6 +108,8 @@ pub struct LiquidityManager< pending_events: Arc, request_id_to_method_map: Mutex>, lsps0_message_handler: LSPS0MessageHandler, + lsps2_message_handler: + Option>, provider_config: Option, channel_manager: Arc>, chain_source: Option, @@ -81,16 +118,21 @@ pub struct LiquidityManager< } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > LiquidityManager + > LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -99,10 +141,14 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { - /// Constructor for the LiquidityManager + /// Constructor for the ['LiquidityManager'] /// /// Sets up the required protocol message handlers based on the given [`LiquidityProviderConfig`]. pub fn new( @@ -112,15 +158,29 @@ where ) -> Self where { let pending_messages = Arc::new(Mutex::new(vec![])); + let pending_events = Arc::new(EventQueue::default()); let lsps0_message_handler = - LSPS0MessageHandler::new(entropy_source, vec![], Arc::clone(&pending_messages)); + LSPS0MessageHandler::new(entropy_source.clone(), vec![], Arc::clone(&pending_messages)); + + let lsps2_message_handler = provider_config.as_ref().and_then(|config| { + config.jit_channels.as_ref().map(|jit_channels_config| { + JITChannelManager::new( + entropy_source.clone(), + jit_channels_config.promise_secret, + Arc::clone(&pending_messages), + Arc::clone(&pending_events), + Arc::clone(&channel_manager), + ) + }) + }); Self { pending_messages, - pending_events: Arc::new(EventQueue::default()), + pending_events, request_id_to_method_map: Mutex::new(HashMap::new()), lsps0_message_handler, + lsps2_message_handler, provider_config, channel_manager, chain_source, @@ -173,16 +233,22 @@ where { } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > CustomMessageReader for LiquidityManager + > CustomMessageReader + for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -191,6 +257,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -207,16 +277,22 @@ where } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > CustomMessageHandler for LiquidityManager + > CustomMessageHandler + for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -225,6 +301,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -282,16 +362,21 @@ where } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > Listen for LiquidityManager + > Listen for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -300,6 +385,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { @@ -337,16 +426,21 @@ where } impl< - ES: Deref, + ES: Deref + Clone, M: Deref, T: Deref, F: Deref, R: Deref, SP: Deref, L: Deref, + Descriptor: SocketDescriptor, + RM: Deref, + CM: Deref, + OM: Deref, + CMH: Deref, NS: Deref, C: Deref, - > Confirm for LiquidityManager + > Confirm for LiquidityManager where ES::Target: EntropySource, M::Target: chain::Watch<::Signer>, @@ -355,6 +449,10 @@ where R::Target: Router, SP::Target: SignerProvider, L::Target: Logger, + RM::Target: RoutingMessageHandler, + CM::Target: ChannelMessageHandler, + OM::Target: OnionMessageHandler, + CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, C::Target: Filter, { From 1fbedb710d13d57545dcf36faaf0cc3a41e5d480 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 12:35:53 -0400 Subject: [PATCH 06/16] expose lsps2 api in LiquidityManager --- src/transport/message_handler.rs | 191 ++++++++++++++++++++++++++++--- 1 file changed, 178 insertions(+), 13 deletions(-) diff --git a/src/transport/message_handler.rs b/src/transport/message_handler.rs index 6b30bc2..96739bd 100644 --- a/src/transport/message_handler.rs +++ b/src/transport/message_handler.rs @@ -1,19 +1,23 @@ use crate::events::{Event, EventQueue}; use crate::jit_channel::channel_manager::JITChannelManager; +use crate::jit_channel::msgs::{OpeningFeeParams, RawOpeningFeeParams}; +use crate::transport::msgs::RequestId; use crate::transport::msgs::{LSPSMessage, RawLSPSMessage, LSPS_MESSAGE_TYPE_ID}; use crate::transport::protocol::LSPS0MessageHandler; use lightning::chain::chaininterface::{BroadcasterInterface, FeeEstimator}; use lightning::chain::{self, BestBlock, Confirm, Filter, Listen}; -use lightning::ln::channelmanager::{ChainParameters, ChannelManager}; +use lightning::ln::channelmanager::{ChainParameters, ChannelManager, InterceptId}; use lightning::ln::features::{InitFeatures, NodeFeatures}; use lightning::ln::msgs::{ ChannelMessageHandler, ErrorAction, LightningError, OnionMessageHandler, RoutingMessageHandler, }; -use lightning::ln::peer_handler::{CustomMessageHandler, SocketDescriptor}; +use lightning::ln::peer_handler::{CustomMessageHandler, PeerManager, SocketDescriptor}; use lightning::ln::wire::CustomMessageReader; +use lightning::ln::ChannelId; use lightning::routing::router::Router; use lightning::sign::{EntropySource, NodeSigner, SignerProvider}; +use lightning::util::errors::APIError; use lightning::util::logger::{Level, Logger}; use lightning::util::ser::Readable; @@ -203,6 +207,165 @@ where { self.pending_events.get_and_clear_pending_events() } + /// Set a [`lightning::ln::peer_handler::PeerManager`] reference for the message handlers + /// + /// This allows the message handlers to wake the [`lightning::ln::peer_handler::PeerManager`] by calling + /// [`lightning::ln::peer_handler::PeerManager::process_events()`] after enqueing messages to be sent. + /// + /// Without this the messages will be sent based on whatever polling interval + /// your background processor uses. + pub fn set_peer_manager( + &self, peer_manager: Arc>, + ) { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.set_peer_manager(peer_manager); + } + } + + /// Initiate the creation of an invoice that when paid will open a channel + /// with enough inbound liquidity to be able to receive the payment. + /// + /// `counterparty_node_id` is the node_id of the LSP you would like to use. + /// + /// 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. + /// + /// `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. + pub fn create_invoice( + &self, counterparty_node_id: PublicKey, payment_size_msat: Option, + token: Option, user_channel_id: u128, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.create_invoice( + counterparty_node_id, + payment_size_msat, + token, + user_channel_id, + ); + Ok(()) + } else { + Err(APIError::APIMisuseError { + err: "JIT Channels were not configured when LSPManager was instantiated" + .to_string(), + }) + } + } + + /// Used by LSP to provide fee parameters to a client requesting a JIT Channel. + /// + /// Should be called in response to receiving a [`crate::JITChannelEvent::GetInfo`] event. + pub fn opening_fee_params_generated( + &self, counterparty_node_id: PublicKey, request_id: RequestId, + opening_fee_params_menu: Vec, min_payment_size_msat: u64, + max_payment_size_msat: u64, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.opening_fee_params_generated( + counterparty_node_id, + request_id, + opening_fee_params_menu, + min_payment_size_msat, + max_payment_size_msat, + ) + } else { + Err(APIError::APIMisuseError { + err: "JIT Channels were not configured when LSPManager was instantiated" + .to_string(), + }) + } + } + + /// Used by client to confirm which channel parameters to use for the JIT Channel buy request. + /// + /// Should be called in response to receiving a [`crate::JITChannelEvent::GetInfoResponse`] event. + pub fn opening_fee_params_selected( + &self, counterparty_node_id: PublicKey, channel_id: u128, + opening_fee_params: OpeningFeeParams, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.opening_fee_params_selected( + counterparty_node_id, + channel_id, + opening_fee_params, + ) + } else { + Err(APIError::APIMisuseError { + err: "JIT Channels were not configured when LSPManager was instantiated" + .to_string(), + }) + } + } + + /// Used by LSP to provide client with the scid and cltv_expiry_delta to use in their invoice + /// + /// Should be called in response to receiving a [`crate::JITChannelEvent::BuyRequest`] event. + pub fn invoice_parameters_generated( + &self, counterparty_node_id: PublicKey, request_id: RequestId, scid: u64, + cltv_expiry_delta: u32, client_trusts_lsp: bool, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.invoice_parameters_generated( + counterparty_node_id, + request_id, + scid, + cltv_expiry_delta, + client_trusts_lsp, + ) + } else { + Err(APIError::APIMisuseError { + err: "JIT Channels were not configured when LSPManager was instantiated" + .to_string(), + }) + } + } + + /// Forward [`lightning::events::Event::HTLCIntercepted`] event parameters into this function. + /// + /// Will fail the intercepted HTLC if the scid matches a payment we are expecting + /// but the payment amount is incorrect or the expiry has passed. + /// + /// Will generate a [`crate::JITChannelEvent::OpenChannel`] event if the scid matches a payment we are expected + /// and the payment amount is correct and the offer has not expired. + /// + /// Will do nothing if the scid does not match any of the ones we gave out. + pub fn htlc_intercepted( + &self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64, + expected_outbound_amount_msat: u64, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.htlc_intercepted( + scid, + intercept_id, + inbound_amount_msat, + expected_outbound_amount_msat, + )?; + } + + Ok(()) + } + + /// Forward [`lightning::events::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. + pub fn channel_ready( + &self, user_channel_id: u128, channel_id: &ChannelId, counterparty_node_id: &PublicKey, + ) -> Result<(), APIError> { + if let Some(lsps2_message_handler) = &self.lsps2_message_handler { + lsps2_message_handler.channel_ready( + user_channel_id, + channel_id, + counterparty_node_id, + )?; + } + + Ok(()) + } + fn handle_lsps_message( &self, msg: LSPSMessage, sender_node_id: &PublicKey, ) -> Result<(), lightning::ln::msgs::LightningError> { @@ -213,15 +376,14 @@ where { LSPSMessage::LSPS0(msg) => { self.lsps0_message_handler.handle_message(msg, sender_node_id)?; } - _ => { - return Err(LightningError { - err: format!( - "Received message without message handler configured. From node = {:?}", - sender_node_id - ), - action: ErrorAction::IgnoreAndLog(Level::Info), - }); - } + LSPSMessage::LSPS2(msg) => match &self.lsps2_message_handler { + Some(lsps2_message_handler) => { + lsps2_message_handler.handle_message(msg, sender_node_id)?; + } + None => { + return Err(LightningError { err: format!("Received LSPS2 message without LSPS2 message handler configured. From node = {:?}", sender_node_id), action: ErrorAction::IgnoreAndLog(Level::Info)}); + } + }, } Ok(()) } @@ -311,9 +473,12 @@ where fn handle_custom_message( &self, msg: Self::CustomMessage, sender_node_id: &PublicKey, ) -> Result<(), lightning::ln::msgs::LightningError> { - let mut request_id_to_method_map = self.request_id_to_method_map.lock().unwrap(); + 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) + }; - match LSPSMessage::from_str_with_id_map(&msg.payload, &mut request_id_to_method_map) { + match message { Ok(msg) => self.handle_lsps_message(msg, sender_node_id), Err(_) => { self.enqueue_message(*sender_node_id, LSPSMessage::Invalid); From 9c954cc2866ff837a3de0c9fcc4e019cb4accab5 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 5 Oct 2023 12:37:56 -0400 Subject: [PATCH 07/16] expose types to lib users --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index 65d21a7..dba71d4 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,4 +25,8 @@ mod transport; mod utils; pub use jit_channel::event::Event as JITChannelEvent; -pub use transport::message_handler::{LiquidityManager, LiquidityProviderConfig}; +pub use jit_channel::msgs::{BuyResponse, GetInfoResponse, OpeningFeeParams, RawOpeningFeeParams}; +pub use transport::message_handler::{ + JITChannelsConfig, LiquidityManager, LiquidityProviderConfig, +}; +pub use transport::msgs::{RawLSPSMessage, LSPS_MESSAGE_TYPE_ID}; From 7c18cb1aaa4599ea81acf568e02281b1c4fa079c Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 19 Oct 2023 12:13:52 -0400 Subject: [PATCH 08/16] fix review comments --- Cargo.toml | 1 + src/channel_request/mod.rs | 2 +- src/events.rs | 8 +- src/jit_channel/channel_manager.rs | 258 +++++++++++++++-------------- src/jit_channel/event.rs | 74 ++++++--- src/jit_channel/mod.rs | 15 +- src/jit_channel/msgs.rs | 144 ++++++++++------ src/jit_channel/scid_utils.rs | 71 -------- src/jit_channel/utils.rs | 37 +++++ src/lib.rs | 6 +- src/transport/message_handler.rs | 83 ++++++---- src/transport/mod.rs | 2 +- src/transport/msgs.rs | 82 ++++----- src/utils.rs | 59 ++++++- 14 files changed, 472 insertions(+), 370 deletions(-) delete mode 100644 src/jit_channel/scid_utils.rs create mode 100644 src/jit_channel/utils.rs diff --git a/Cargo.toml b/Cargo.toml index ba722a3..a554754 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,5 +13,6 @@ lightning-invoice = "0.25.0" bitcoin = "0.29.0" +chrono = { version = "0.4.31", features = ["serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" diff --git a/src/channel_request/mod.rs b/src/channel_request/mod.rs index 673242d..b3b102c 100644 --- a/src/channel_request/mod.rs +++ b/src/channel_request/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 ) -> Result { - if !versions.contains(&SUPPORTED_SPEC_VERSION) { - return Err(ChannelStateError(format!( - "LSP does not support our specification version. ours = {}. theirs = {:?}", - SUPPORTED_SPEC_VERSION, versions - ))); - } + let max_shared_version = versions + .iter() + .filter(|version| SUPPORTED_SPEC_VERSIONS.contains(version)) + .max() + .cloned() + .ok_or(ChannelStateError(format!( + "LSP does not support any of our specification versions. ours = {:?}. theirs = {:?}", + SUPPORTED_SPEC_VERSIONS, versions + )))?; match self { - InboundJITChannelState::VersionsRequested => Ok(InboundJITChannelState::MenuRequested), + InboundJITChannelState::VersionsRequested => { + Ok(InboundJITChannelState::MenuRequested { version: max_shared_version }) + } state => Err(ChannelStateError(format!( "Received unexpected get_versions response. JIT Channel was in state: {:?}", state @@ -84,8 +90,8 @@ impl InboundJITChannelState { fn info_received(&self) -> Result { match self { - InboundJITChannelState::MenuRequested => { - Ok(InboundJITChannelState::PendingMenuSelection) + InboundJITChannelState::MenuRequested { version } => { + Ok(InboundJITChannelState::PendingMenuSelection { version: *version }) } state => Err(ChannelStateError(format!( "Received unexpected get_info response. JIT Channel was in state: {:?}", @@ -96,8 +102,8 @@ impl InboundJITChannelState { fn opening_fee_params_selected(&self) -> Result { match self { - InboundJITChannelState::PendingMenuSelection => { - Ok(InboundJITChannelState::BuyRequested) + InboundJITChannelState::PendingMenuSelection { version } => { + Ok(InboundJITChannelState::BuyRequested { version: *version }) } state => Err(ChannelStateError(format!( "Opening fee params selected when JIT Channel was in state: {:?}", @@ -107,11 +113,11 @@ impl InboundJITChannelState { } fn invoice_params_received( - &self, client_trusts_lsp: bool, scid: String, + &self, client_trusts_lsp: bool, short_channel_id: JitChannelScid, ) -> Result { match self { - InboundJITChannelState::BuyRequested => { - Ok(InboundJITChannelState::PendingPayment { client_trusts_lsp, scid }) + InboundJITChannelState::BuyRequested { .. } => { + Ok(InboundJITChannelState::PendingPayment { client_trusts_lsp, short_channel_id }) } state => Err(ChannelStateError(format!( "Invoice params received when JIT Channel was in state: {:?}", @@ -138,9 +144,16 @@ impl InboundJITChannel { } } - pub fn versions_received(&mut self, versions: Vec) -> Result<(), LightningError> { + pub fn versions_received(&mut self, versions: Vec) -> Result { self.state = self.state.versions_received(versions)?; - Ok(()) + + match self.state { + InboundJITChannelState::MenuRequested { version } => Ok(version), + _ => Err(LightningError { + action: ErrorAction::IgnoreAndLog(Level::Error), + err: "impossible state transition".to_string(), + }), + } } pub fn info_received(&mut self) -> Result<(), LightningError> { @@ -148,13 +161,20 @@ impl InboundJITChannel { Ok(()) } - pub fn opening_fee_params_selected(&mut self) -> Result<(), LightningError> { + pub fn opening_fee_params_selected(&mut self) -> Result { self.state = self.state.opening_fee_params_selected()?; - Ok(()) + + match self.state { + InboundJITChannelState::BuyRequested { version } => Ok(version), + _ => Err(LightningError { + action: ErrorAction::IgnoreAndLog(Level::Error), + err: "impossible state transition".to_string(), + }), + } } pub fn invoice_params_received( - &mut self, client_trusts_lsp: bool, jit_channel_scid: String, + &mut self, client_trusts_lsp: bool, jit_channel_scid: JitChannelScid, ) -> Result<(), LightningError> { self.state = self.state.invoice_params_received(client_trusts_lsp, jit_channel_scid)?; Ok(()) @@ -164,7 +184,7 @@ impl InboundJITChannel { #[derive(PartialEq, Debug)] enum OutboundJITChannelState { InvoiceParametersGenerated { - scid: u64, + short_channel_id: u64, cltv_expiry_delta: u32, payment_size_msat: Option, opening_fee_params: OpeningFeeParams, @@ -182,11 +202,11 @@ enum OutboundJITChannelState { impl OutboundJITChannelState { pub fn new( - scid: u64, cltv_expiry_delta: u32, payment_size_msat: Option, + short_channel_id: u64, cltv_expiry_delta: u32, payment_size_msat: Option, opening_fee_params: OpeningFeeParams, ) -> Self { OutboundJITChannelState::InvoiceParametersGenerated { - scid, + short_channel_id, cltv_expiry_delta, payment_size_msat, opening_fee_params, @@ -198,21 +218,15 @@ impl OutboundJITChannelState { ) -> Result { match self { OutboundJITChannelState::InvoiceParametersGenerated { opening_fee_params, .. } => { - let opening_fee_msat: Option = utils::compute_opening_fee( + compute_opening_fee( expected_outbound_amount_msat, opening_fee_params.min_fee_msat, - opening_fee_params.proportional as u64, - ); - - if let Some(opening_fee_msat) = opening_fee_msat { - Ok(OutboundJITChannelState::PendingChannelOpen { - intercept_id, - opening_fee_msat, - amt_to_forward_msat: expected_outbound_amount_msat - opening_fee_msat, - }) - } else { - Err(ChannelStateError(format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_outbound_amount_msat = {}", opening_fee_params.min_fee_msat, opening_fee_params.proportional, expected_outbound_amount_msat))) - } + opening_fee_params.proportional, + ).map(|opening_fee_msat| OutboundJITChannelState::PendingChannelOpen { + intercept_id, + opening_fee_msat, + amt_to_forward_msat: expected_outbound_amount_msat - opening_fee_msat, + }).ok_or(ChannelStateError(format!("Could not compute valid opening fee with min_fee_msat = {}, proportional = {}, and expected_outbound_amount_msat = {}", opening_fee_params.min_fee_msat, opening_fee_params.proportional, expected_outbound_amount_msat))) } state => Err(ChannelStateError(format!( "Invoice params received when JIT Channel was in state: {:?}", @@ -302,7 +316,7 @@ struct PeerState { inbound_channels_by_id: HashMap, outbound_channels_by_scid: HashMap, request_to_cid: HashMap, - pending_requests: HashMap, + pending_requests: HashMap, } impl PeerState { @@ -412,11 +426,6 @@ where } } - fn map_scid_to_peer(&self, scid: u64, counterparty_node_id: PublicKey) { - let mut peer_by_scid = self.peer_by_scid.write().unwrap(); - peer_by_scid.insert(scid, counterparty_node_id); - } - pub fn set_peer_manager( &self, peer_manager: Arc>, ) { @@ -445,7 +454,8 @@ where let mut pending_messages = self.pending_messages.lock().unwrap(); pending_messages.push(( counterparty_node_id, - Message::Request(request_id, Request::GetVersions(GetVersionsRequest {})).into(), + LSPS2Message::Request(request_id, LSPS2Request::GetVersions(GetVersionsRequest {})) + .into(), )); } @@ -466,8 +476,8 @@ where let mut peer_state = inner_state_lock.lock().unwrap(); match peer_state.pending_requests.remove(&request_id) { - Some(Request::GetInfo(_)) => { - let response = Response::GetInfo(GetInfoResponse { + 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.promise_secret)) @@ -503,10 +513,13 @@ where if let Some(jit_channel) = peer_state.inbound_channels_by_id.get_mut(&jit_channel_id) { - if let Err(e) = jit_channel.opening_fee_params_selected() { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(APIError::APIMisuseError { err: e.err }); - } + let version = match jit_channel.opening_fee_params_selected() { + Ok(version) => version, + Err(e) => { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(APIError::APIMisuseError { err: e.err }); + } + }; let request_id = self.generate_request_id(); let payment_size_msat = jit_channel.config.payment_size_msat; @@ -516,10 +529,10 @@ where let mut pending_messages = self.pending_messages.lock().unwrap(); pending_messages.push(( counterparty_node_id, - Message::Request( + LSPS2Message::Request( request_id, - Request::Buy(BuyRequest { - version: SUPPORTED_SPEC_VERSION, + LSPS2Request::Buy(BuyRequest { + version, opening_fee_params, payment_size_msat, }), @@ -557,8 +570,12 @@ where let mut peer_state = inner_state_lock.lock().unwrap(); match peer_state.pending_requests.remove(&request_id) { - Some(Request::Buy(buy_request)) => { - self.map_scid_to_peer(scid, counterparty_node_id.clone()); + Some(LSPS2Request::Buy(buy_request)) => { + { + let mut peer_by_scid = self.peer_by_scid.write().unwrap(); + peer_by_scid.insert(scid, counterparty_node_id); + } + let outbound_jit_channel = OutboundJITChannel::new( scid, cltv_expiry_delta, @@ -568,17 +585,11 @@ where peer_state.insert_outbound_channel(scid, outbound_jit_channel); - 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); - - let jit_channel_scid = format!("{}x{}x{}", block, tx_index, vout); - self.enqueue_response( counterparty_node_id, request_id, - Response::Buy(BuyResponse { - jit_channel_scid, + LSPS2Response::Buy(BuyResponse { + jit_channel_scid: scid.into(), lsp_cltv_expiry_delta: cltv_expiry_delta, client_trusts_lsp, }), @@ -618,20 +629,19 @@ where .htlc_intercepted(expected_outbound_amount_msat, intercept_id) { Ok((opening_fee_msat, amt_to_forward_msat)) => { - self.enqueue_event(Event::LSPS2( - crate::JITChannelEvent::OpenChannel { - their_network_key: counterparty_node_id.clone(), - inbound_amount_msat, - expected_outbound_amount_msat, - amt_to_forward_msat, - opening_fee_msat, - user_channel_id: scid as u128, - }, - )); + self.enqueue_event(Event::LSPS2(LSPS2Event::OpenChannel { + their_network_key: counterparty_node_id.clone(), + inbound_amount_msat, + expected_outbound_amount_msat, + amt_to_forward_msat, + opening_fee_msat, + user_channel_id: scid as u128, + })); } Err(e) => { self.channel_manager.fail_intercepted_htlc(intercept_id)?; - // remove channel? + peer_state.outbound_channels_by_scid.remove(&scid); + // TODO: cleanup peer_by_scid return Err(APIError::APIMisuseError { err: e.err }); } } @@ -709,12 +719,12 @@ where } fn enqueue_response( - &self, counterparty_node_id: PublicKey, request_id: RequestId, response: Response, + &self, counterparty_node_id: PublicKey, request_id: RequestId, response: LSPS2Response, ) { { let mut pending_messages = self.pending_messages.lock().unwrap(); pending_messages - .push((counterparty_node_id, Message::Response(request_id, response).into())); + .push((counterparty_node_id, LSPS2Message::Response(request_id, response).into())); } if let Some(peer_manager) = self.peer_manager.lock().unwrap().as_ref() { @@ -729,11 +739,12 @@ where fn handle_get_versions_request( &self, request_id: RequestId, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { - // not sure best way to extract a vec to a constant? lazy_static? self.enqueue_response( *counterparty_node_id, request_id, - Response::GetVersions(GetVersionsResponse { versions: vec![1] }), + LSPS2Response::GetVersions(GetVersionsResponse { + versions: SUPPORTED_SPEC_VERSIONS.to_vec(), + }), ); Ok(()) } @@ -768,10 +779,13 @@ where let token = jit_channel.config.token.clone(); - if let Err(e) = jit_channel.versions_received(result.versions) { - peer_state.remove_inbound_channel(jit_channel_id); - return Err(e); - } + let version = match jit_channel.versions_received(result.versions) { + Ok(version) => version, + Err(e) => { + peer_state.remove_inbound_channel(jit_channel_id); + return Err(e); + } + }; let request_id = self.generate_request_id(); peer_state.insert_request(request_id.clone(), jit_channel_id); @@ -780,12 +794,9 @@ where let mut pending_messages = self.pending_messages.lock().unwrap(); pending_messages.push(( *counterparty_node_id, - Message::Request( + LSPS2Message::Request( request_id, - Request::GetInfo(GetInfoRequest { - version: SUPPORTED_SPEC_VERSION, - token, - }), + LSPS2Request::GetInfo(GetInfoRequest { version, token }), ) .into(), )); @@ -817,9 +828,11 @@ where .entry(*counterparty_node_id) .or_insert(Mutex::new(PeerState::default())); let peer_state = inner_state_lock.get_mut().unwrap(); - peer_state.pending_requests.insert(request_id.clone(), Request::GetInfo(params.clone())); + peer_state + .pending_requests + .insert(request_id.clone(), LSPS2Request::GetInfo(params.clone())); - self.enqueue_event(Event::LSPS2(super::event::Event::GetInfo { + self.enqueue_event(Event::LSPS2(LSPS2Event::GetInfo { request_id, counterparty_node_id: *counterparty_node_id, version: params.version, @@ -861,12 +874,12 @@ where return Err(e); } - self.enqueue_event(Event::LSPS2(super::event::Event::GetInfoResponse { + self.enqueue_event(Event::LSPS2(LSPS2Event::GetInfoResponse { counterparty_node_id: *counterparty_node_id, opening_fee_params_menu: result.opening_fee_params_menu, min_payment_size_msat: result.min_payment_size_msat, max_payment_size_msat: result.max_payment_size_msat, - channel_id: jit_channel.id, + jit_channel_id: jit_channel.id, user_channel_id: jit_channel.config.user_id, })); } @@ -901,16 +914,15 @@ where action: ErrorAction::IgnoreAndLog(Level::Info), })?; - let _jit_channel = peer_state - .inbound_channels_by_id - .remove(&jit_channel_id) - .ok_or(LightningError { + peer_state.inbound_channels_by_id.remove(&jit_channel_id).ok_or( + LightningError { err: format!( "Received get_info error for an unknown channel: {:?}", jit_channel_id ), action: ErrorAction::IgnoreAndLog(Level::Info), - })?; + }, + )?; Ok(()) } None => { @@ -927,15 +939,17 @@ where // TODO: if payment_size_msat is specified, make sure opening_fee does not hit overflow error. // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. - if params.opening_fee_params.is_valid(&self.promise_secret) { + if is_valid_opening_fee_params(¶ms.opening_fee_params, &self.promise_secret) { 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 peer_state = inner_state_lock.get_mut().unwrap(); - peer_state.pending_requests.insert(request_id.clone(), Request::Buy(params.clone())); + peer_state + .pending_requests + .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - self.enqueue_event(Event::LSPS2(super::event::Event::BuyRequest { + self.enqueue_event(Event::LSPS2(LSPS2Event::BuyRequest { request_id, version: params.version, counterparty_node_id: *counterparty_node_id, @@ -982,10 +996,8 @@ where return Err(e); } - if let Ok(scid) = - scid_utils::scid_from_human_readable_string(&result.jit_channel_scid) - { - self.enqueue_event(Event::LSPS2(super::event::Event::InvoiceGenerationReady { + if let Ok(scid) = result.jit_channel_scid.to_scid() { + self.enqueue_event(Event::LSPS2(LSPS2Event::InvoiceGenerationReady { counterparty_node_id: *counterparty_node_id, scid, cltv_expiry_delta: result.lsp_cltv_expiry_delta, @@ -996,7 +1008,7 @@ where } else { return Err(LightningError { err: format!( - "Received buy response with an invalid scid {}", + "Received buy response with an invalid scid {:?}", result.jit_channel_scid ), action: ErrorAction::IgnoreAndLog(Level::Info), @@ -1079,38 +1091,38 @@ where CMH::Target: CustomMessageHandler, NS::Target: NodeSigner, { - type ProtocolMessage = Message; + type ProtocolMessage = LSPS2Message; const PROTOCOL_NUMBER: Option = Some(2); fn handle_message( &self, message: Self::ProtocolMessage, counterparty_node_id: &PublicKey, ) -> Result<(), LightningError> { match message { - Message::Request(request_id, request) => match request { - super::msgs::Request::GetVersions(_) => { + LSPS2Message::Request(request_id, request) => match request { + LSPS2Request::GetVersions(_) => { self.handle_get_versions_request(request_id, counterparty_node_id) } - super::msgs::Request::GetInfo(params) => { + LSPS2Request::GetInfo(params) => { self.handle_get_info_request(request_id, counterparty_node_id, params) } - super::msgs::Request::Buy(params) => { + LSPS2Request::Buy(params) => { self.handle_buy_request(request_id, counterparty_node_id, params) } }, - Message::Response(request_id, response) => match response { - super::msgs::Response::GetVersions(result) => { + LSPS2Message::Response(request_id, response) => match response { + LSPS2Response::GetVersions(result) => { self.handle_get_versions_response(request_id, counterparty_node_id, result) } - super::msgs::Response::GetInfo(result) => { + LSPS2Response::GetInfo(result) => { self.handle_get_info_response(request_id, counterparty_node_id, result) } - super::msgs::Response::GetInfoError(error) => { + LSPS2Response::GetInfoError(error) => { self.handle_get_info_error(request_id, counterparty_node_id, error) } - super::msgs::Response::Buy(result) => { + LSPS2Response::Buy(result) => { self.handle_buy_response(request_id, counterparty_node_id, result) } - super::msgs::Response::BuyError(error) => { + LSPS2Response::BuyError(error) => { self.handle_buy_error(request_id, counterparty_node_id, error) } }, diff --git a/src/jit_channel/event.rs b/src/jit_channel/event.rs index ba41444..116e22a 100644 --- a/src/jit_channel/event.rs +++ b/src/jit_channel/event.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 , }, - /// Information from LSP about their current fee and channel parameters. + /// Information from the LSP about their current fee rates and channel parameters. /// - /// You must call [`crate::LiquidityManager::opening_fee_params_selected`] with the fee parameter + /// You must call [`LiquidityManager::opening_fee_params_selected`] with the fee parameter /// you want to use if you wish to proceed opening a channel. + /// + /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::opening_fee_params_selected GetInfoResponse { - /// Needs to be passed to [`crate::LiquidityManager::opening_fee_params_selected`]. - channel_id: u128, + /// This is a randomly generated identifier used to track the JIT channel state. + /// It is not related in anyway to the eventual lightning channel id. + /// It needs to be passed to [`LiquidityManager::opening_fee_params_selected`]. + /// + /// [`LiquidityManager::opening_fee_params_selected`]: crate::LiquidityManager::opening_fee_params_selected + jit_channel_id: u128, /// 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. @@ -45,19 +55,25 @@ pub enum Event { min_payment_size_msat: u64, /// The max payment size allowed when opening the channel. max_payment_size_msat: u64, - /// The user_channel_id value passed in to [`crate::LiquidityManager::create_invoice`]. + /// The user_channel_id value passed in to [`LiquidityManager::jit_channel_create_invoice`]. + /// + /// [`LiquidityManager::jit_channel_create_invoice`]: crate::LiquidityManager::jit_channel_create_invoice user_channel_id: u128, }, /// 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 + /// 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 scid and cltv_expiry_delta for them to use - /// and call [`crate::LiquidityManager::invoice_parameters_generated`]. + /// You must generate an scid and `cltv_expiry_delta` for them to use + /// and call [`LiquidityManager::invoice_parameters_generated`]. + /// + /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated BuyRequest { - /// An identifier that must be passed into [`crate::LiquidityManager::invoice_parameters_generated`]. + /// An identifier that must be passed into [`LiquidityManager::invoice_parameters_generated`]. + /// + /// [`LiquidityManager::invoice_parameters_generated`]: crate::LiquidityManager::invoice_parameters_generated request_id: RequestId, /// The client node id that is making this request. counterparty_node_id: PublicKey, @@ -77,28 +93,32 @@ pub enum Event { counterparty_node_id: PublicKey, /// The short channel id to use in the route hint. scid: u64, - /// The cltv_expiry_delta to use in the route hint. + /// The `cltv_expiry_delta` to use in the route hint. cltv_expiry_delta: u32, /// The initial payment size you specified. payment_size_msat: Option, - /// The trust model the lsp expects. + /// The trust model the LSP expects. client_trusts_lsp: bool, - /// The user_channel_id value passed in to [`crate::LiquidityManager::create_invoice`]. + /// The `user_channel_id` value passed in to [`LiquidityManager::jit_channel_create_invoice`]. + /// + /// [`LiquidityManager::jit_channel_create_invoice`]: crate::LiquidityManager::jit_channel_create_invoice user_channel_id: u128, }, - /// You should open a channel using [`lightning::ln::channelmanager::ChannelManager::create_channel`]. + /// 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 + /// The node to open channel with. their_network_key: PublicKey, - /// The intercepted htlc amount in msats + /// The intercepted HTLC amount in msats. inbound_amount_msat: u64, - /// The amount the client expects to receive before fees are taken out + /// The amount the client expects to receive before fees are taken out. expected_outbound_amount_msat: u64, - /// The amount to forward after fees + /// The amount to forward after fees. amt_to_forward_msat: u64, - /// The fee earned for opening the channel + /// The fee earned for opening the channel. opening_fee_msat: u64, - /// An internal id used to track channel open + /// An internal id used to track channel open. user_channel_id: u128, }, } diff --git a/src/jit_channel/mod.rs b/src/jit_channel/mod.rs index 2d58382..2cee68b 100644 --- a/src/jit_channel/mod.rs +++ b/src/jit_channel/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 , } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +/// A request made to an LSP to learn their current channel fees and parameters. pub struct GetInfoRequest { + /// What version of the protocol we want to use. pub version: u16, + /// An optional token to provide to the LSP. pub token: Option, } -/// Fees and parameters for a JIT Channel. +/// Fees and parameters for a JIT Channel without the promise. /// -/// The client will pay max(min_fee_msat, proportional*(payment_size_msat/1_000_000)). +/// 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 formatted date for which these params are valid. - pub valid_until: String, - /// number of blocks that the LSP promises it will keep the channel alive without closing, after confirmation. + /// 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, - /// Maximum number of blocks that the client is allowed to set its to_self_delay parameter. + /// T maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. pub max_client_to_self_delay: u32, } @@ -48,7 +55,7 @@ impl RawOpeningFeeParams { 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.as_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()); let promise_bytes = Hmac::from_engine(hmac).into_inner(); @@ -65,41 +72,27 @@ impl RawOpeningFeeParams { } #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] -/// Fees and parameters for a JIT Channel with promise. +/// Fees and parameters for a JIT Channel including the promise. /// -/// The client will pay max(min_fee_msat, proportional*(payment_size_msat/1_000_000)). +/// 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. pub min_fee_msat: u64, /// A fee proportional to the size of the initial payment. pub proportional: u32, - /// An ISO8601 formatted date for which these params are valid. - pub valid_until: String, - /// number of blocks that the LSP promises it will keep the channel alive without closing, after confirmation. + /// 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, - /// Maximum number of blocks that the client is allowed to set its to_self_delay parameter. + /// The maximum number of blocks that the client is allowed to set its `to_self_delay` parameter. pub max_client_to_self_delay: u32, - /// Field used by the LSP to validate that these parameters were actually given out by them. + /// The HMAC used to verify the authenticity of these parameters. pub promise: String, } -impl OpeningFeeParams { - /// Determine that these parameters are valid given the secret used to generate the promise. - // TODO: add validation check that valid_until >= now() - pub fn is_valid(&self, promise_secret: &[u8; 32]) -> bool { - 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.as_bytes()); - hmac.input(&self.min_lifetime.to_be_bytes()); - hmac.input(&self.max_client_to_self_delay.to_be_bytes()); - let promise_bytes = Hmac::from_engine(hmac).into_inner(); - let promise = utils::hex_str(&promise_bytes[..]); - promise == self.promise - } -} - -/// Information about the parameters a LSP is willing to offer clients +/// A response to a [`GetInfoRequest`] #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct GetInfoResponse { /// A set of opening fee parameters. @@ -110,21 +103,46 @@ pub struct GetInfoResponse { pub max_payment_size_msat: u64, } +/// A request to buy a JIT channel. #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct BuyRequest { + /// The version of the protocol to use. pub version: u16, + /// The fee parameters you would like to use. pub opening_fee_params: OpeningFeeParams, + /// The size of the initial payment you expect to receive. #[serde(skip_serializing_if = "Option::is_none")] pub payment_size_msat: Option, } -/// A response from a buy request made by a client +/// A newtype that holds a `short_channel_id` in human readable format of BBBxTTTx000. +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct JitChannelScid(String); + +impl From for JitChannelScid { + fn from(scid: u64) -> Self { + let block = utils::block_from_scid(&scid); + let tx_index = utils::tx_index_from_scid(&scid); + let vout = utils::vout_from_scid(&scid); + + Self(format!("{}x{}x{}", block, tx_index, vout)) + } +} + +impl JitChannelScid { + /// Try to convert a [`JitChannelScid`] 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 short channel id used by LSP to identify need to open channel. - pub jit_channel_scid: String, + pub jit_channel_scid: JitChannelScid, /// The locktime expiry delta the lsp requires. pub lsp_cltv_expiry_delta: u32, /// A flag that indicates who is trusting who. @@ -133,38 +151,52 @@ pub struct BuyResponse { } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Request { +/// An enum that captures all the valid JSON-RPC requests in the LSPS2 protocol. +pub enum LSPS2Request { + /// A request to learn what versions an LSP supports. GetVersions(GetVersionsRequest), + /// 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), } -impl Request { +impl LSPS2Request { + /// Get the JSON-RPC method name for the underlying request. pub fn method(&self) -> &str { match self { - Request::GetVersions(_) => LSPS2_GET_VERSIONS_METHOD_NAME, - Request::GetInfo(_) => LSPS2_GET_INFO_METHOD_NAME, - Request::Buy(_) => LSPS2_BUY_METHOD_NAME, + LSPS2Request::GetVersions(_) => LSPS2_GET_VERSIONS_METHOD_NAME, + LSPS2Request::GetInfo(_) => LSPS2_GET_INFO_METHOD_NAME, + LSPS2Request::Buy(_) => LSPS2_BUY_METHOD_NAME, } } } #[derive(Clone, Debug, PartialEq, Eq)] -pub enum Response { +/// An enum that captures all the valid JSON-RPC responses in the LSPS2 protocol. +pub enum LSPS2Response { + /// A successful response to a [`LSPS2Request::GetVersions`] request. GetVersions(GetVersionsResponse), + /// 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)] -pub enum Message { - Request(RequestId, Request), - Response(RequestId, Response), +/// 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 Message { +impl TryFrom for LSPS2Message { type Error = (); fn try_from(message: LSPSMessage) -> Result { @@ -176,8 +208,8 @@ impl TryFrom for Message { } } -impl From for LSPSMessage { - fn from(message: Message) -> Self { +impl From for LSPSMessage { + fn from(message: LSPS2Message) -> Self { LSPSMessage::LSPS2(message) } } @@ -185,19 +217,21 @@ impl From for LSPSMessage { #[cfg(test)] mod tests { use super::*; + use crate::jit_channel::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 = "2023-05-20".to_string(); + let valid_until: chrono::DateTime = + chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap().into(); let min_lifetime = 144; let max_client_to_self_delay = 128; let raw = RawOpeningFeeParams { min_fee_msat, proportional, - valid_until: valid_until.clone(), + valid_until: valid_until.clone().into(), min_lifetime, max_client_to_self_delay, }; @@ -212,21 +246,21 @@ mod tests { 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!(opening_fee_params.is_valid(&promise_secret)); + 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 = "2023-05-20".to_string(); + 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 raw = RawOpeningFeeParams { min_fee_msat, proportional, - valid_until, + valid_until: valid_until.into(), min_lifetime, max_client_to_self_delay, }; @@ -235,21 +269,21 @@ mod tests { let mut opening_fee_params = raw.into_opening_fee_params(&promise_secret); opening_fee_params.min_fee_msat = min_fee_msat + 1; - assert!(!opening_fee_params.is_valid(&promise_secret)); + 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 = "2023-05-20".to_string(); + 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 raw = RawOpeningFeeParams { min_fee_msat, proportional, - valid_until, + valid_until: valid_until.into(), min_lifetime, max_client_to_self_delay, }; @@ -258,6 +292,6 @@ mod tests { let other_secret = [2u8; 32]; let opening_fee_params = raw.into_opening_fee_params(&promise_secret); - assert!(!opening_fee_params.is_valid(&other_secret)); + assert!(is_valid_opening_fee_params(&opening_fee_params, &promise_secret)); } } diff --git a/src/jit_channel/scid_utils.rs b/src/jit_channel/scid_utils.rs deleted file mode 100644 index ce83457..0000000 --- a/src/jit_channel/scid_utils.rs +++ /dev/null @@ -1,71 +0,0 @@ -#[derive(Debug, PartialEq, Eq)] -pub enum ShortChannelIdError { - InvalidScid, -} - -/// Maximum transaction index that can be used in a `short_channel_id`. -/// This value is based on the 3-bytes available for tx index. -pub const MAX_SCID_TX_INDEX: u64 = 0x00ffffff; - -/// Maximum vout index that can be used in a `short_channel_id`. This -/// value is based on the 2-bytes available for the vout index. -pub const MAX_SCID_VOUT_INDEX: u64 = 0xffff; - -/// Extracts the block height (most significant 3-bytes) from the `short_channel_id` -pub fn block_from_scid(short_channel_id: &u64) -> u32 { - (short_channel_id >> 40) as u32 -} - -/// Extracts the tx index (bytes [2..4]) from the `short_channel_id` -pub fn tx_index_from_scid(short_channel_id: &u64) -> u32 { - ((short_channel_id >> 16) & MAX_SCID_TX_INDEX) as u32 -} - -/// Extracts the vout (bytes [0..2]) from the `short_channel_id` -pub fn vout_from_scid(short_channel_id: &u64) -> u16 { - ((short_channel_id) & MAX_SCID_VOUT_INDEX) as u16 -} - -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(ShortChannelIdError::InvalidScid)? - .parse() - .map_err(|_e| ShortChannelIdError::InvalidScid)?; - let tx_index: u64 = parts - .next() - .ok_or(ShortChannelIdError::InvalidScid)? - .parse() - .map_err(|_e| ShortChannelIdError::InvalidScid)?; - let vout_index: u64 = parts - .next() - .ok_or(ShortChannelIdError::InvalidScid)? - .parse() - .map_err(|_e| ShortChannelIdError::InvalidScid)?; - - Ok((block << 40) | (tx_index << 16) | vout_index) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[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/src/jit_channel/utils.rs b/src/jit_channel/utils.rs new file mode 100644 index 0000000..385d802 --- /dev/null +++ b/src/jit_channel/utils.rs @@ -0,0 +1,37 @@ +use bitcoin::hashes::hmac::{Hmac, HmacEngine}; +use bitcoin::hashes::sha256::Hash as Sha256; +use bitcoin::hashes::{Hash, HashEngine}; + +use crate::jit_channel::msgs::OpeningFeeParams; +use crate::utils; + +/// Determines if the given parameters are valid given the secret used to generate the promise. +// TODO: add validation check that valid_until >= now() +pub fn is_valid_opening_fee_params( + fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], +) -> bool { + 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()); + let promise_bytes = Hmac::from_engine(hmac).into_inner(); + 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: u32, +) -> Option { + let t1 = payment_size_msat.checked_mul(opening_fee_proportional.into())?; + let t2 = t1.checked_add(999999)?; + let t3 = t2.checked_div(1000000)?; + let t4 = std::cmp::max(t3, opening_fee_min_fee_msat); + Some(t4) +} diff --git a/src/lib.rs b/src/lib.rs index dba71d4..6c928fd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 , } -/// Configuration options for jit channels -/// A configuration used for the creation of Just In Time Channels. +/// Configuration options for JIT channels. pub struct JITChannelsConfig { - /// Used to calculate the promise for channel parameters supplied to clients + /// Used to calculate the promise for channel parameters supplied to clients. /// - /// Note: If this changes then old promises given out will be considered invalid + /// Note: If this changes then old promises given out will be considered invalid. pub promise_secret: [u8; 32], } /// The main interface into LSP functionality. /// /// Should be used as a [`CustomMessageHandler`] for your -/// [`lightning::ln::peer_handler::PeerManager`]'s [`lightning::ln::peer_handler::MessageHandler`]. +/// [`PeerManager`]'s [`MessageHandler`]. /// -/// Should provide a reference to your [`lightning::ln::peer_handler::PeerManager`] by calling -/// [`LiquidityManager::set_peer_manager()`] post construction. This allows the [`LiquidityManager`] to -/// wake the [`lightning::ln::peer_handler::PeerManager`] when there are pending messages to be sent. +/// Should provide a reference to your [`PeerManager`] by calling +/// [`LiquidityManager::set_peer_manager`] 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 +/// Users need to continually poll [`LiquidityManager::get_and_clear_pending_events`] in order to surface /// [`Event`]'s that likely need to be handled. /// -/// Users must forward the [`lightning::events::Event::HTLCIntercepted`] event parameters to [`LiquidityManager::htlc_intercepted()`] -/// and the [`lightning::events::Event::ChannelReady`] event parameters to [`LiquidityManager::channel_ready()`]. +/// Users must forward the [`Event::HTLCIntercepted`] event parameters to [`LiquidityManager::htlc_intercepted`] +/// and the [`Event::ChannelReady`] event parameters to [`LiquidityManager::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< ES: Deref + Clone, M: Deref, @@ -152,7 +156,7 @@ where NS::Target: NodeSigner, C::Target: Filter, { - /// Constructor for the ['LiquidityManager'] + /// Constructor for the [`LiquidityManager`]. /// /// Sets up the required protocol message handlers based on the given [`LiquidityProviderConfig`]. pub fn new( @@ -193,27 +197,29 @@ where { } } - /// Blocks until next event is ready and returns it + /// Blocks until next event is ready and returns it. /// - /// Typically you would spawn a thread or task that calls this in a loop + /// Typically you would spawn a thread or task that calls this in a loop. pub fn wait_next_event(&self) -> Event { self.pending_events.wait_next_event() } - /// Returns and clears all events without blocking + /// Returns and clears all events without blocking. /// - /// Typically you would spawn a thread or task that calls this in a loop + /// 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() } - /// Set a [`lightning::ln::peer_handler::PeerManager`] reference for the message handlers + /// Set a [`PeerManager`] reference for the message handlers. /// - /// This allows the message handlers to wake the [`lightning::ln::peer_handler::PeerManager`] by calling - /// [`lightning::ln::peer_handler::PeerManager::process_events()`] after enqueing messages to be sent. + /// This allows the message handlers to wake the [`PeerManager`] by calling + /// [`PeerManager::process_events`] after enqueing messages to be sent. /// /// Without this the messages will be sent based on whatever polling interval /// your background processor uses. + /// + /// [`PeerManager`]: lightning::ln::peer_handler::PeerManager pub fn set_peer_manager( &self, peer_manager: Arc>, ) { @@ -227,15 +233,15 @@ where { /// /// `counterparty_node_id` is the node_id of the LSP you would like to use. /// - /// if `payment_size_msat` is [`Option::Some`] then the invoice will be for a fixed amount + /// 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 + /// 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. /// /// `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. - pub fn create_invoice( + /// It can be used by the LSP as an API key, coupon code, or some other way to identify a user. + pub fn jit_channel_create_invoice( &self, counterparty_node_id: PublicKey, payment_size_msat: Option, token: Option, user_channel_id: u128, ) -> Result<(), APIError> { @@ -257,7 +263,9 @@ where { /// Used by LSP to provide fee parameters to a client requesting a JIT Channel. /// - /// Should be called in response to receiving a [`crate::JITChannelEvent::GetInfo`] event. + /// Should be called in response to receiving a [`LSPS2Event::GetInfo`] event. + /// + /// [`LSPS2Event::GetInfo`]: crate::jit_channel::LSPS2Event::GetInfo pub fn opening_fee_params_generated( &self, counterparty_node_id: PublicKey, request_id: RequestId, opening_fee_params_menu: Vec, min_payment_size_msat: u64, @@ -280,8 +288,12 @@ where { } /// Used by client to confirm which channel parameters to use for the JIT Channel buy request. + /// The client agrees to paying an opening fee equal to + /// `max(min_fee_msat, proportional*(payment_size_msat/1_000_000))`. + /// + /// Should be called in response to receiving a [`LSPS2Event::GetInfoResponse`] event. /// - /// Should be called in response to receiving a [`crate::JITChannelEvent::GetInfoResponse`] event. + /// [`LSPS2Event::GetInfoResponse`]: crate::jit_channel::LSPS2Event::GetInfoResponse pub fn opening_fee_params_selected( &self, counterparty_node_id: PublicKey, channel_id: u128, opening_fee_params: OpeningFeeParams, @@ -300,9 +312,11 @@ where { } } - /// Used by LSP to provide client with the scid and cltv_expiry_delta to use in their invoice + /// Used by LSP to provide client with the scid and cltv_expiry_delta to use in their invoice. /// - /// Should be called in response to receiving a [`crate::JITChannelEvent::BuyRequest`] event. + /// Should be called in response to receiving a [`LSPS2Event::BuyRequest`] event. + /// + /// [`LSPS2Event::BuyRequest`]: crate::jit_channel::LSPS2Event::BuyRequest pub fn invoice_parameters_generated( &self, counterparty_node_id: PublicKey, request_id: RequestId, scid: u64, cltv_expiry_delta: u32, client_trusts_lsp: bool, @@ -323,15 +337,18 @@ where { } } - /// Forward [`lightning::events::Event::HTLCIntercepted`] event parameters into this function. + /// Forward [`Event::HTLCIntercepted`] event parameters into this function. /// /// Will fail the intercepted HTLC if the scid matches a payment we are expecting /// but the payment amount is incorrect or the expiry has passed. /// - /// Will generate a [`crate::JITChannelEvent::OpenChannel`] event if the scid matches a payment we are expected + /// Will generate a [`LSPS2Event::OpenChannel`] event if the scid matches a payment we are expected /// and the payment amount is correct and the offer has not expired. /// /// Will do nothing if the scid does not match any of the ones we gave out. + /// + /// [`Event::HTLCIntercepted`]: lightning::events::Event::HTLCIntercepted + /// [`LSPS2Event::OpenChannel`]: crate::jit_channel::LSPS2Event::OpenChannel pub fn htlc_intercepted( &self, scid: u64, intercept_id: InterceptId, inbound_amount_msat: u64, expected_outbound_amount_msat: u64, @@ -348,10 +365,12 @@ where { Ok(()) } - /// Forward [`lightning::events::Event::ChannelReady`] event parameters into this function. + /// 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> { diff --git a/src/transport/mod.rs b/src/transport/mod.rs index fef2f5f..00185eb 100644 --- a/src/transport/mod.rs +++ b/src/transport/mod.rs @@ -1,4 +1,4 @@ -// This file is Copyright its original authors, visible in version contror +// This file is Copyright its original authors, visible in version control // history. // // This file is licensed under the Apache License, Version 2.0 for LSPSMessage { pub enum LSPSMessage { Invalid, LSPS0(LSPS0Message), - LSPS2(jit_channel::msgs::Message), + LSPS2(LSPS2Message), } impl LSPSMessage { @@ -124,7 +126,7 @@ impl LSPSMessage { LSPSMessage::LSPS0(LSPS0Message::Request(request_id, request)) => { Some((request_id.0.clone(), request.method().to_string())) } - LSPSMessage::LSPS2(jit_channel::msgs::Message::Request(request_id, request)) => { + LSPSMessage::LSPS2(LSPS2Message::Request(request_id, request)) => { Some((request_id.0.clone(), request.method().to_string())) } _ => None, @@ -165,39 +167,39 @@ impl Serialize for LSPSMessage { } } } - LSPSMessage::LSPS2(jit_channel::msgs::Message::Request(request_id, request)) => { + 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, request.method())?; match request { - jit_channel::msgs::Request::GetVersions(params) => { + LSPS2Request::GetVersions(params) => { jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? } - jit_channel::msgs::Request::GetInfo(params) => { + LSPS2Request::GetInfo(params) => { jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? } - jit_channel::msgs::Request::Buy(params) => { + LSPS2Request::Buy(params) => { jsonrpc_object.serialize_field(JSONRPC_PARAMS_FIELD_KEY, params)? } } } - LSPSMessage::LSPS2(jit_channel::msgs::Message::Response(request_id, response)) => { + LSPSMessage::LSPS2(LSPS2Message::Response(request_id, response)) => { jsonrpc_object.serialize_field(JSONRPC_ID_FIELD_KEY, &request_id.0)?; match response { - jit_channel::msgs::Response::GetVersions(result) => { + LSPS2Response::GetVersions(result) => { jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? } - jit_channel::msgs::Response::GetInfo(result) => { + LSPS2Response::GetInfo(result) => { jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? } - jit_channel::msgs::Response::GetInfoError(error) => { + LSPS2Response::GetInfoError(error) => { jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? } - jit_channel::msgs::Response::Buy(result) => { + LSPS2Response::Buy(result) => { jsonrpc_object.serialize_field(JSONRPC_RESULT_FIELD_KEY, result)? } - jit_channel::msgs::Response::BuyError(error) => { + LSPS2Response::BuyError(error) => { jsonrpc_object.serialize_field(JSONRPC_ERROR_FIELD_KEY, error)? } } @@ -272,28 +274,28 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { LSPS0Request::ListProtocols(ListProtocolsRequest {}), ))) } - jit_channel::msgs::LSPS2_GET_VERSIONS_METHOD_NAME => { + LSPS2_GET_VERSIONS_METHOD_NAME => { let request = serde_json::from_value(params.unwrap_or(json!({}))) .map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( RequestId(id), - jit_channel::msgs::Request::GetVersions(request), + LSPS2Request::GetVersions(request), ))) } - jit_channel::msgs::LSPS2_GET_INFO_METHOD_NAME => { + LSPS2_GET_INFO_METHOD_NAME => { let request = serde_json::from_value(params.unwrap_or(json!({}))) .map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( RequestId(id), - jit_channel::msgs::Request::GetInfo(request), + LSPS2Request::GetInfo(request), ))) } - jit_channel::msgs::LSPS2_BUY_METHOD_NAME => { + LSPS2_BUY_METHOD_NAME => { let request = serde_json::from_value(params.unwrap_or(json!({}))) .map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Request( + Ok(LSPSMessage::LSPS2(LSPS2Message::Request( RequestId(id), - jit_channel::msgs::Request::Buy(request), + LSPS2Request::Buy(request), ))) } _ => Err(de::Error::custom(format!( @@ -320,47 +322,47 @@ impl<'de, 'a> Visitor<'de> for LSPSMessageVisitor<'a> { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) } } - jit_channel::msgs::LSPS2_GET_VERSIONS_METHOD_NAME => { + LSPS2_GET_VERSIONS_METHOD_NAME => { if let Some(result) = result { let response = serde_json::from_value(result).map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( RequestId(id), - jit_channel::msgs::Response::GetVersions(response), + LSPS2Response::GetVersions(response), ))) } else { - Err(de::Error::custom("Received invalid lsps2.getversions response.")) + Err(de::Error::custom("Received invalid lsps2.get_versions response.")) } } - jit_channel::msgs::LSPS2_GET_INFO_METHOD_NAME => { + LSPS2_GET_INFO_METHOD_NAME => { if let Some(error) = error { - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( RequestId(id), - jit_channel::msgs::Response::GetInfoError(error), + LSPS2Response::GetInfoError(error), ))) } else if let Some(result) = result { let response = serde_json::from_value(result).map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( RequestId(id), - jit_channel::msgs::Response::GetInfo(response), + LSPS2Response::GetInfo(response), ))) } else { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) } } - jit_channel::msgs::LSPS2_BUY_METHOD_NAME => { + LSPS2_BUY_METHOD_NAME => { if let Some(error) = error { - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( RequestId(id), - jit_channel::msgs::Response::BuyError(error), + LSPS2Response::BuyError(error), ))) } else if let Some(result) = result { let response = serde_json::from_value(result).map_err(de::Error::custom)?; - Ok(LSPSMessage::LSPS2(jit_channel::msgs::Message::Response( + Ok(LSPSMessage::LSPS2(LSPS2Message::Response( RequestId(id), - jit_channel::msgs::Response::Buy(response), + LSPS2Response::Buy(response), ))) } else { Err(de::Error::custom("Received invalid JSON-RPC object: one of method, result, or error required")) diff --git a/src/utils.rs b/src/utils.rs index da40131..c5969db 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -4,6 +4,39 @@ use std::{fmt::Write, ops::Deref}; use crate::transport::msgs::RequestId; +/// Maximum transaction index that can be used in a `short_channel_id`. +/// This value is based on the 3-bytes available for tx index. +pub const MAX_SCID_TX_INDEX: u64 = 0x00ffffff; + +/// Maximum vout index that can be used in a `short_channel_id`. This +/// value is based on the 2-bytes available for the vout index. +pub const MAX_SCID_VOUT_INDEX: u64 = 0xffff; + +/// Extracts the block height (most significant 3-bytes) from the `short_channel_id`. +pub fn block_from_scid(short_channel_id: &u64) -> u32 { + (short_channel_id >> 40) as u32 +} + +/// Extracts the tx index (bytes [2..4]) from the `short_channel_id`. +pub fn tx_index_from_scid(short_channel_id: &u64) -> u32 { + ((short_channel_id >> 16) & MAX_SCID_TX_INDEX) as u32 +} + +/// Extracts the vout (bytes [0..2]) from the `short_channel_id`. +pub fn vout_from_scid(short_channel_id: &u64) -> u16 { + ((short_channel_id) & MAX_SCID_VOUT_INDEX) as u16 +} + +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, @@ -68,12 +101,22 @@ pub fn parse_pubkey(pubkey_str: &str) -> Result { Ok(pubkey.unwrap()) } -pub fn compute_opening_fee( - payment_size_msat: u64, opening_fee_min_fee_msat: u64, opening_fee_proportional: u64, -) -> Option { - let t1 = payment_size_msat.checked_mul(opening_fee_proportional)?; - let t2 = t1.checked_add(999999)?; - let t3 = t2.checked_div(1000000)?; - let t4 = std::cmp::max(t3, opening_fee_min_fee_msat); - Some(t4) +#[cfg(test)] +mod tests { + use super::*; + + #[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); + } } From 8a61a5e5668042ee8a4c39e2adc6661c7a92dce5 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Thu, 19 Oct 2023 12:53:43 -0400 Subject: [PATCH 09/16] disable default chrono features to remove wasm-bindgen dep --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index a554754..497ac5f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ lightning-invoice = "0.25.0" bitcoin = "0.29.0" -chrono = { version = "0.4.31", features = ["serde"] } +chrono = { version = "0.4.31", default-features = false, features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" From ae6fd6a83b7110a15ca06fdb3b3970a5deb55b2d Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 10:33:54 -0400 Subject: [PATCH 10/16] add myself as an author --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 497ac5f..4be6f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "ldk-lsp-client" version = "0.1.0" -authors = ["Elias Rohrer "] +authors = ["John Cantrell ", "Elias Rohrer "] edition = "2018" description = "Types and primitives to integrate a spec-compliant LSP with an LDK-based node." From b8c2157b93ce08eb5d71b2447f9e5589b16d00b0 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 11:00:03 -0400 Subject: [PATCH 11/16] add version check to handling of buy request --- src/jit_channel/channel_manager.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/jit_channel/channel_manager.rs b/src/jit_channel/channel_manager.rs index 3f61ddd..8bb8cf7 100644 --- a/src/jit_channel/channel_manager.rs +++ b/src/jit_channel/channel_manager.rs @@ -934,7 +934,26 @@ where fn handle_buy_request( &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: BuyRequest, ) -> Result<(), LightningError> { - // TODO: need to perform check on `params.version`. + if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { + let mut pending_messages = self.pending_messages.lock().unwrap(); + pending_messages.push(( + *counterparty_node_id, + LSPS2Message::Response( + request_id, + LSPS2Response::BuyError(ResponseError { + code: 1, + message: format!("version {} is not supported", params.version), + data: Some(format!("Supported versions are {:?}", SUPPORTED_SPEC_VERSIONS)), + }), + ) + .into(), + )); + + return Err(LightningError { + err: format!("client requested unsupported version {}", params.version), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } // TODO: if payment_size_msat is specified, make sure opening_fee is >= payment_size_msat. // TODO: if payment_size_msat is specified, make sure opening_fee does not hit overflow error. // TODO: if payment_size_msat is specified, make sure our node has sufficient incoming liquidity from public network to receive it. From cc4f009a56c939c8054f60de0da8b82d1d1388ab Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 11:45:12 -0400 Subject: [PATCH 12/16] move payment_size ranges to config and handle buy request validation --- src/jit_channel/channel_manager.rs | 167 ++++++++++++++++++++++------- src/jit_channel/msgs.rs | 5 + src/transport/message_handler.rs | 11 +- 3 files changed, 139 insertions(+), 44 deletions(-) diff --git a/src/jit_channel/channel_manager.rs b/src/jit_channel/channel_manager.rs index 8bb8cf7..bee78c1 100644 --- a/src/jit_channel/channel_manager.rs +++ b/src/jit_channel/channel_manager.rs @@ -31,13 +31,16 @@ use crate::jit_channel::utils::{compute_opening_fee, is_valid_opening_fee_params use crate::jit_channel::LSPS2Event; use crate::transport::message_handler::ProtocolMessageHandler; use crate::transport::msgs::{LSPSMessage, RequestId}; -use crate::utils; use crate::{events::Event, transport::msgs::ResponseError}; +use crate::{utils, JITChannelsConfig}; use crate::jit_channel::msgs::{ BuyRequest, BuyResponse, GetInfoRequest, GetInfoResponse, GetVersionsRequest, GetVersionsResponse, JitChannelScid, LSPS2Message, LSPS2Request, LSPS2Response, - OpeningFeeParams, RawOpeningFeeParams, + OpeningFeeParams, RawOpeningFeeParams, LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE, + LSPS2_BUY_REQUEST_INVALID_VERSION_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE, }; const SUPPORTED_SPEC_VERSIONS: [u16; 1] = [1]; @@ -377,6 +380,8 @@ pub struct JITChannelManager< per_peer_state: RwLock>>, peer_by_scid: RwLock>, promise_secret: [u8; 32], + min_payment_size_msat: u64, + max_payment_size_msat: u64, } impl< @@ -409,14 +414,16 @@ where NS::Target: NodeSigner, { pub(crate) fn new( - entropy_source: ES, promise_secret: [u8; 32], + entropy_source: ES, config: &JITChannelsConfig, pending_messages: Arc>>, pending_events: Arc, channel_manager: Arc>, ) -> Self { Self { entropy_source, - promise_secret, + promise_secret: config.promise_secret, + min_payment_size_msat: config.min_payment_size_msat, + max_payment_size_msat: config.max_payment_size_msat, pending_messages, pending_events, per_peer_state: RwLock::new(HashMap::new()), @@ -466,8 +473,7 @@ where pub fn opening_fee_params_generated( &self, counterparty_node_id: PublicKey, request_id: RequestId, - opening_fee_params_menu: Vec, min_payment_size_msat: u64, - max_payment_size_msat: u64, + opening_fee_params_menu: Vec, ) -> Result<(), APIError> { let outer_state_lock = self.per_peer_state.read().unwrap(); @@ -482,8 +488,8 @@ where .into_iter() .map(|param| param.into_opening_fee_params(&self.promise_secret)) .collect(), - min_payment_size_msat, - max_payment_size_msat, + min_payment_size_msat: self.min_payment_size_msat, + max_payment_size_msat: self.max_payment_size_msat, }); self.enqueue_response(counterparty_node_id, request_id, response); Ok(()) @@ -935,47 +941,130 @@ where &self, request_id: RequestId, counterparty_node_id: &PublicKey, params: BuyRequest, ) -> Result<(), LightningError> { if !SUPPORTED_SPEC_VERSIONS.contains(¶ms.version) { - let mut pending_messages = self.pending_messages.lock().unwrap(); - pending_messages.push(( + self.enqueue_response( *counterparty_node_id, - LSPS2Message::Response( - request_id, - LSPS2Response::BuyError(ResponseError { - code: 1, - message: format!("version {} is not supported", params.version), - data: Some(format!("Supported versions are {:?}", SUPPORTED_SPEC_VERSIONS)), - }), - ) - .into(), - )); - + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_INVALID_VERSION_ERROR_CODE, + message: format!("version {} is not supported", params.version), + data: Some(format!("Supported versions are {:?}", SUPPORTED_SPEC_VERSIONS)), + }), + ); return Err(LightningError { err: format!("client requested unsupported version {}", params.version), action: ErrorAction::IgnoreAndLog(Level::Info), }); } - // TODO: if payment_size_msat is specified, make sure opening_fee is >= payment_size_msat. - // TODO: if payment_size_msat is specified, make sure opening_fee does not hit overflow error. + + if let Some(payment_size_msat) = params.payment_size_msat { + if payment_size_msat < self.min_payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + 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, + }), + ); + return Err(LightningError { + err: "payment size is below our minimum supported payment size".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + + if payment_size_msat > self.max_payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + 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, + }), + ); + 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, + ) { + Some(opening_fee) => { + if opening_fee >= payment_size_msat { + self.enqueue_response( + *counterparty_node_id, + request_id, + 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, + }), + ); + return Err(LightningError { + err: "payment size is too small to cover the opening fee".to_string(), + action: ErrorAction::IgnoreAndLog(Level::Info), + }); + } + } + None => { + self.enqueue_response( + *counterparty_node_id, + request_id, + LSPS2Response::BuyError(ResponseError { + code: LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE, + message: "overflow error when calculating opening_fee".to_string(), + data: None, + }), + ); + 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.promise_secret) { - 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 peer_state = inner_state_lock.get_mut().unwrap(); - peer_state - .pending_requests - .insert(request_id.clone(), LSPS2Request::Buy(params.clone())); - - self.enqueue_event(Event::LSPS2(LSPS2Event::BuyRequest { + if !is_valid_opening_fee_params(¶ms.opening_fee_params, &self.promise_secret) { + self.enqueue_response( + *counterparty_node_id, request_id, - version: params.version, - counterparty_node_id: *counterparty_node_id, - opening_fee_params: params.opening_fee_params, - payment_size_msat: params.payment_size_msat, - })); + 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, + }), + ); + 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::default())); + let peer_state = inner_state_lock.get_mut().unwrap(); + peer_state.pending_requests.insert(request_id.clone(), LSPS2Request::Buy(params.clone())); + + self.enqueue_event(Event::LSPS2(LSPS2Event::BuyRequest { + request_id, + version: params.version, + counterparty_node_id: *counterparty_node_id, + opening_fee_params: params.opening_fee_params, + payment_size_msat: params.payment_size_msat, + })); + Ok(()) } diff --git a/src/jit_channel/msgs.rs b/src/jit_channel/msgs.rs index 6b25110..85275ac 100644 --- a/src/jit_channel/msgs.rs +++ b/src/jit_channel/msgs.rs @@ -13,6 +13,11 @@ pub(crate) const LSPS2_GET_VERSIONS_METHOD_NAME: &str = "lsps2.get_versions"; 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_BUY_REQUEST_INVALID_VERSION_ERROR_CODE: i32 = 1; +pub(crate) const LSPS2_BUY_REQUEST_INVALID_OPENING_FEE_PARAMS_ERROR_CODE: i32 = 2; +pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_SMALL_ERROR_CODE: i32 = 3; +pub(crate) const LSPS2_BUY_REQUEST_PAYMENT_SIZE_TOO_LARGE_ERROR_CODE: i32 = 4; + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default)] /// A request made to an LSP to learn what versions of the protocol they support. pub struct GetVersionsRequest {} diff --git a/src/transport/message_handler.rs b/src/transport/message_handler.rs index f0b6631..300ee65 100644 --- a/src/transport/message_handler.rs +++ b/src/transport/message_handler.rs @@ -61,6 +61,10 @@ pub struct JITChannelsConfig { /// /// Note: If this changes then old promises given out will be considered invalid. pub promise_secret: [u8; 32], + /// The minimum payment size you are willing to accept. + pub min_payment_size_msat: u64, + /// The maximum payment size you are willing to accept. + pub max_payment_size_msat: u64, } /// The main interface into LSP functionality. @@ -175,7 +179,7 @@ where { config.jit_channels.as_ref().map(|jit_channels_config| { JITChannelManager::new( entropy_source.clone(), - jit_channels_config.promise_secret, + jit_channels_config, Arc::clone(&pending_messages), Arc::clone(&pending_events), Arc::clone(&channel_manager), @@ -268,16 +272,13 @@ where { /// [`LSPS2Event::GetInfo`]: crate::jit_channel::LSPS2Event::GetInfo pub fn opening_fee_params_generated( &self, counterparty_node_id: PublicKey, request_id: RequestId, - opening_fee_params_menu: Vec, min_payment_size_msat: u64, - max_payment_size_msat: u64, + opening_fee_params_menu: Vec, ) -> Result<(), APIError> { if let Some(lsps2_message_handler) = &self.lsps2_message_handler { lsps2_message_handler.opening_fee_params_generated( counterparty_node_id, request_id, opening_fee_params_menu, - min_payment_size_msat, - max_payment_size_msat, ) } else { Err(APIError::APIMisuseError { From 795a097fd25819fabfea6d817543adcf43042e60 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 11:52:27 -0400 Subject: [PATCH 13/16] add valid_until check into is_valid_opening_fee_params --- Cargo.toml | 2 +- src/jit_channel/msgs.rs | 30 ++++++++++++++++++++++++++---- src/jit_channel/utils.rs | 5 ++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4be6f86..bac5bdf 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ lightning-invoice = "0.25.0" bitcoin = "0.29.0" -chrono = { version = "0.4.31", default-features = false, features = ["std", "serde"] } +chrono = { version = "0.4.31", default-features = false, features = ["std", "serde", "clock"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" diff --git a/src/jit_channel/msgs.rs b/src/jit_channel/msgs.rs index 85275ac..3a1e16a 100644 --- a/src/jit_channel/msgs.rs +++ b/src/jit_channel/msgs.rs @@ -229,7 +229,7 @@ mod tests { let min_fee_msat = 100; let proportional = 21; let valid_until: chrono::DateTime = - chrono::DateTime::parse_from_rfc3339("2023-05-20T08:30:45Z").unwrap().into(); + chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap().into(); let min_lifetime = 144; let max_client_to_self_delay = 128; @@ -258,7 +258,7 @@ mod tests { fn changing_single_field_produced_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 valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); let min_lifetime = 144; let max_client_to_self_delay = 128; @@ -281,7 +281,7 @@ mod tests { fn wrong_secret_produced_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 valid_until = chrono::DateTime::parse_from_rfc3339("2035-05-20T08:30:45Z").unwrap(); let min_lifetime = 144; let max_client_to_self_delay = 128; @@ -297,6 +297,28 @@ mod tests { 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, &promise_secret)); + assert!(!is_valid_opening_fee_params(&opening_fee_params, &other_secret)); + } + + #[test] + 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 raw = RawOpeningFeeParams { + min_fee_msat, + proportional, + valid_until: valid_until.into(), + min_lifetime, + max_client_to_self_delay, + }; + + 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)); } } diff --git a/src/jit_channel/utils.rs b/src/jit_channel/utils.rs index 385d802..0c138d8 100644 --- a/src/jit_channel/utils.rs +++ b/src/jit_channel/utils.rs @@ -6,10 +6,13 @@ use crate::jit_channel::msgs::OpeningFeeParams; use crate::utils; /// Determines if the given parameters are valid given the secret used to generate the promise. -// TODO: add validation check that valid_until >= now() pub fn is_valid_opening_fee_params( fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], ) -> bool { + if chrono::Utc::now() > fee_params.valid_until { + 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()); From a3259ccfcc975e26e106e6c61ec6585610b4db9e Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 11:54:01 -0400 Subject: [PATCH 14/16] remove memchr pin --- .github/workflows/build.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 37e925d..143f4b9 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,8 +35,6 @@ jobs: cargo update -p quote --precise "1.0.30" --verbose # The proc-macro2 crate switched to Rust edition 2021 starting with v1.0.66, i.e., has MSRV of 1.56 cargo update -p proc-macro2 --precise "1.0.65" --verbose - # The memchr crate switched to Rust edition 2021 starting with v2.6.0 - cargo update -p memchr --precise "2.5.0" --verbose - name: Cargo check run: cargo check --release - name: Check documentation From 53d4a2009660e3042b3c7794ae45c6c712518f19 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 13:20:18 -0400 Subject: [PATCH 15/16] use SystemTime instead of chrono with clock to test validity --- Cargo.toml | 2 +- src/jit_channel/utils.rs | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index bac5bdf..4be6f86 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ lightning-invoice = "0.25.0" bitcoin = "0.29.0" -chrono = { version = "0.4.31", default-features = false, features = ["std", "serde", "clock"] } +chrono = { version = "0.4.31", default-features = false, features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0" diff --git a/src/jit_channel/utils.rs b/src/jit_channel/utils.rs index 0c138d8..22354de 100644 --- a/src/jit_channel/utils.rs +++ b/src/jit_channel/utils.rs @@ -2,6 +2,9 @@ use bitcoin::hashes::hmac::{Hmac, HmacEngine}; use bitcoin::hashes::sha256::Hash as Sha256; use bitcoin::hashes::{Hash, HashEngine}; +use std::convert::TryInto; +use std::time::{SystemTime, UNIX_EPOCH}; + use crate::jit_channel::msgs::OpeningFeeParams; use crate::utils; @@ -9,7 +12,16 @@ use crate::utils; pub fn is_valid_opening_fee_params( fee_params: &OpeningFeeParams, promise_secret: &[u8; 32], ) -> bool { - if chrono::Utc::now() > fee_params.valid_until { + 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; } From 646d54fa74d5c01b279c511a8cbf7ade9f9cce06 Mon Sep 17 00:00:00 2001 From: John Cantrell Date: Fri, 20 Oct 2023 13:43:29 -0400 Subject: [PATCH 16/16] pin chrono 0.4.24 --- .github/workflows/build.yml | 1 + Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 143f4b9..9e66a72 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -35,6 +35,7 @@ jobs: cargo update -p quote --precise "1.0.30" --verbose # The proc-macro2 crate switched to Rust edition 2021 starting with v1.0.66, i.e., has MSRV of 1.56 cargo update -p proc-macro2 --precise "1.0.65" --verbose + cargo update -p chrono --precise "0.4.24" --verbose - name: Cargo check run: cargo check --release - name: Check documentation diff --git a/Cargo.toml b/Cargo.toml index 4be6f86..18fcb08 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,6 @@ lightning-invoice = "0.25.0" bitcoin = "0.29.0" -chrono = { version = "0.4.31", default-features = false, features = ["std", "serde"] } +chrono = { version = "0.4", default-features = false, features = ["std", "serde"] } serde = { version = "1.0", default-features = false, features = ["derive", "alloc"] } serde_json = "1.0"