From 8bab787ae1396f1de56f161efc1aa0c583c6bf0c Mon Sep 17 00:00:00 2001 From: Justin Moeller Date: Mon, 3 Feb 2025 14:33:35 -0600 Subject: [PATCH] refactor: move LNv1 module into its own crate --- Cargo.lock | 27 +++ Cargo.toml | 2 + fedimint-eventlog/Cargo.toml | 1 + fedimint-eventlog/src/lib.rs | 98 +++++++++- gateway/fedimint-lightning/src/lib.rs | 2 +- gateway/ln-gateway/Cargo.toml | 3 +- gateway/ln-gateway/src/client.rs | 4 +- gateway/ln-gateway/src/error.rs | 2 +- gateway/ln-gateway/src/events.rs | 113 +----------- gateway/ln-gateway/src/federation_manager.rs | 2 +- .../src/gateway_module_v2/events.rs | 30 ++-- gateway/ln-gateway/src/lib.rs | 169 ++++++++++++++++-- gateway/ln-gateway/src/rpc/mod.rs | 16 +- gateway/ln-gateway/tests/tests.rs | 12 +- modules/fedimint-gw-client/Cargo.toml | 41 +++++ .../fedimint-gw-client/src}/complete.rs | 18 +- .../fedimint-gw-client/src}/events.rs | 30 ++-- .../fedimint-gw-client/src/lib.rs | 121 ++++++++++--- .../fedimint-gw-client/src}/pay.rs | 143 +++------------ 19 files changed, 505 insertions(+), 329 deletions(-) create mode 100644 modules/fedimint-gw-client/Cargo.toml rename {gateway/ln-gateway/src/state_machine => modules/fedimint-gw-client/src}/complete.rs (94%) rename {gateway/ln-gateway/src/state_machine => modules/fedimint-gw-client/src}/events.rs (94%) rename gateway/ln-gateway/src/state_machine/mod.rs => modules/fedimint-gw-client/src/lib.rs (89%) rename {gateway/ln-gateway/src/state_machine => modules/fedimint-gw-client/src}/pay.rs (87%) diff --git a/Cargo.lock b/Cargo.lock index 247f8cc9bb5..8ce64f471bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2886,6 +2886,7 @@ dependencies = [ "fedimint-core", "fedimint-logging", "futures", + "itertools 0.13.0", "serde", "serde_json", "test-log", @@ -2928,6 +2929,31 @@ dependencies = [ "tokio", ] +[[package]] +name = "fedimint-gw-client" +version = "0.7.0-alpha" +dependencies = [ + "anyhow", + "aquamarine", + "async-stream", + "async-trait", + "bitcoin", + "erased-serde 0.4.5", + "fedimint-api-client", + "fedimint-client", + "fedimint-core", + "fedimint-eventlog", + "fedimint-lightning", + "fedimint-ln-client", + "fedimint-ln-common", + "futures", + "lightning-invoice", + "serde", + "thiserror 2.0.11", + "tokio-stream", + "tracing", +] + [[package]] name = "fedimint-hkdf" version = "0.7.0-alpha" @@ -3033,6 +3059,7 @@ dependencies = [ "fedimint-dummy-common", "fedimint-dummy-server", "fedimint-eventlog", + "fedimint-gw-client", "fedimint-lightning", "fedimint-ln-client", "fedimint-ln-common", diff --git a/Cargo.toml b/Cargo.toml index 5f1526bb3e5..56939e244a2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ members = [ "modules/fedimint-empty-client", "modules/fedimint-empty-common", "modules/fedimint-empty-server", + "modules/fedimint-gw-client", "modules/fedimint-ln-client", "modules/fedimint-ln-common", "modules/fedimint-ln-server", @@ -136,6 +137,7 @@ fedimint-dummy-common = { path = "./modules/fedimint-dummy-common", version = "= fedimint-dummy-server = { path = "./modules/fedimint-dummy-server", version = "=0.7.0-alpha" } fedimint-empty-common = { path = "./modules/fedimint-empty-common", version = "=0.7.0-alpha" } fedimint-eventlog = { path = "./fedimint-eventlog", version = "=0.7.0-alpha" } +fedimint-gw-client = { path = "./modules/fedimint-gw-client", version = "=0.7.0-alpha" } fedimint-lightning = { package = "fedimint-lightning", path = "./gateway/fedimint-lightning", version = "=0.7.0-alpha" } fedimint-lnv2-client = { path = "./modules/fedimint-lnv2-client", version = "=0.7.0-alpha" } fedimint-lnv2-common = { path = "./modules/fedimint-lnv2-common", version = "=0.7.0-alpha" } diff --git a/fedimint-eventlog/Cargo.toml b/fedimint-eventlog/Cargo.toml index 86a8bd78d53..5944610c600 100644 --- a/fedimint-eventlog/Cargo.toml +++ b/fedimint-eventlog/Cargo.toml @@ -21,6 +21,7 @@ async-trait = { workspace = true } fedimint-core = { workspace = true } fedimint-logging = { workspace = true } futures = { workspace = true } +itertools = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tokio = { workspace = true, features = ["time", "macros", "rt"] } diff --git a/fedimint-eventlog/src/lib.rs b/fedimint-eventlog/src/lib.rs index 0bebdde5f37..a100a677dad 100644 --- a/fedimint-eventlog/src/lib.rs +++ b/fedimint-eventlog/src/lib.rs @@ -22,9 +22,10 @@ use fedimint_core::db::{ }; use fedimint_core::encoding::{Decodable, Encodable}; use fedimint_core::task::{MaybeSend, MaybeSync}; -use fedimint_core::{apply, async_trait_maybe_send, impl_db_lookup, impl_db_record}; +use fedimint_core::{apply, async_trait_maybe_send, impl_db_lookup, impl_db_record, Amount}; use fedimint_logging::LOG_CLIENT_EVENT_LOG; use futures::{Future, StreamExt}; +use itertools::Itertools; use serde::{Deserialize, Serialize}; use tokio::sync::{broadcast, watch}; use tracing::{debug, trace}; @@ -440,6 +441,101 @@ where } } +/// Filters the `PersistedLogEntries` by the `EventKind` and +/// `ModuleKind`. +pub fn filter_events_by_kind<'a, I>( + all_events: I, + module_kind: ModuleKind, + event_kind: EventKind, +) -> impl Iterator + 'a +where + I: IntoIterator + 'a, +{ + all_events.into_iter().filter(move |e| { + if let Some((m, _)) = &e.module { + e.event_kind == event_kind && *m == module_kind + } else { + false + } + }) +} + +/// Joins two sets of events on a predicate. +/// +/// This function computes a "nested loop join" by first computing the cross +/// product of the start event vector and the success/failure event vectors. The +/// resulting cartesian product is then filtered according to the join predicate +/// supplied in the parameters. +/// +/// This function is intended for small data sets. If the data set relations +/// grow, this function should implement a different join algorithm or be moved +/// out of the gateway. +pub fn join_events<'a, L, R, Res>( + events_l: &'a [&PersistedLogEntry], + events_r: &'a [&PersistedLogEntry], + predicate: impl Fn(L, R, u64) -> Option + 'a, +) -> impl Iterator + 'a +where + L: Event, + R: Event, +{ + events_l + .iter() + .cartesian_product(events_r) + .filter_map(move |(l, r)| { + if let Some(latency) = r.timestamp.checked_sub(l.timestamp) { + let event_l: L = + serde_json::from_value(l.value.clone()).expect("could not parse JSON"); + let event_r: R = + serde_json::from_value(r.value.clone()).expect("could not parse JSON"); + predicate(event_l, event_r, latency) + } else { + None + } + }) +} + +/// Helper struct for storing computed data about outgoing and incoming +/// payments. +#[derive(Debug, Default)] +pub struct StructuredPaymentEvents { + pub latencies: Vec, + pub fees: Vec, + pub latencies_failure: Vec, +} + +impl StructuredPaymentEvents { + pub fn new( + success_stats: &[(u64, Amount)], + failure_stats: Vec, + ) -> StructuredPaymentEvents { + let mut events = StructuredPaymentEvents { + latencies: success_stats.iter().map(|(l, _)| *l).collect(), + fees: success_stats.iter().map(|(_, f)| *f).collect(), + latencies_failure: failure_stats, + }; + events.sort(); + events + } + + /// Combines this `StructuredPaymentEvents` with the `other` + /// `StructuredPaymentEvents` by appending all of the internal vectors. + pub fn combine(&mut self, other: &mut StructuredPaymentEvents) { + self.latencies.append(&mut other.latencies); + self.fees.append(&mut other.fees); + self.latencies_failure.append(&mut other.latencies_failure); + self.sort(); + } + + /// Sorts this `StructuredPaymentEvents` by sorting all of the internal + /// vectors. + fn sort(&mut self) { + self.latencies.sort_unstable(); + self.fees.sort_unstable(); + self.latencies_failure.sort_unstable(); + } +} + #[cfg(test)] mod tests { use std::sync::atomic::AtomicU8; diff --git a/gateway/fedimint-lightning/src/lib.rs b/gateway/fedimint-lightning/src/lib.rs index 3dd10d329ca..143443c3ace 100644 --- a/gateway/fedimint-lightning/src/lib.rs +++ b/gateway/fedimint-lightning/src/lib.rs @@ -400,7 +400,7 @@ pub struct CloseChannelsWithPeerRequest { pub pubkey: secp256k1::PublicKey, } -// Trait that specifies how to interact with the gateway's lightning node. +// TODO: Move into `fedimint-gateway-v2` crate #[async_trait] pub trait LightningV2Manager: Debug + Send + Sync { async fn contains_incoming_contract(&self, payment_image: PaymentImage) -> bool; diff --git a/gateway/ln-gateway/Cargo.toml b/gateway/ln-gateway/Cargo.toml index fe603692608..03bcf739bb5 100644 --- a/gateway/ln-gateway/Cargo.toml +++ b/gateway/ln-gateway/Cargo.toml @@ -39,6 +39,7 @@ fedimint-bip39 = { version = "=0.7.0-alpha", path = "../../fedimint-bip39" } fedimint-client = { path = "../../fedimint-client", version = "=0.7.0-alpha" } fedimint-core = { workspace = true } fedimint-eventlog = { workspace = true } +fedimint-gw-client = { workspace = true } fedimint-lightning = { path = "../fedimint-lightning", version = "=0.7.0-alpha" } fedimint-ln-client = { workspace = true } fedimint-ln-common = { workspace = true } @@ -51,7 +52,6 @@ fedimint-wallet-client = { workspace = true } futures = { workspace = true } futures-util = { workspace = true } hex = { workspace = true } -itertools = { workspace = true } lightning-invoice = { workspace = true } lockable = "0.1.1" prost = "0.13.4" @@ -82,6 +82,7 @@ fedimint-lnv2-server = { workspace = true } fedimint-testing = { workspace = true } fedimint-unknown-common = { workspace = true } fedimint-unknown-server = { workspace = true } +itertools = { workspace = true } [build-dependencies] fedimint-build = { workspace = true } diff --git a/gateway/ln-gateway/src/client.rs b/gateway/ln-gateway/src/client.rs index 069a80c4d69..5ebf297f7ec 100644 --- a/gateway/ln-gateway/src/client.rs +++ b/gateway/ln-gateway/src/client.rs @@ -13,11 +13,11 @@ use fedimint_core::config::FederationId; use fedimint_core::core::ModuleKind; use fedimint_core::db::{Database, IDatabaseTransactionOpsCoreTyped}; use fedimint_core::module::registry::ModuleDecoderRegistry; +use fedimint_gw_client::GatewayClientInit; use crate::db::{FederationConfig, GatewayDbExt}; use crate::error::AdminGatewayError; use crate::gateway_module_v2::GatewayClientInitV2; -use crate::state_machine::GatewayClientInit; use crate::{AdminResult, Gateway}; #[derive(Debug, Clone)] @@ -72,7 +72,7 @@ impl GatewayClientBuilder { if gateway.is_running_lnv1() { registry.attach(GatewayClientInit { federation_index, - gateway: gateway.clone(), + lightning_manager: gateway.clone(), }); } diff --git a/gateway/ln-gateway/src/error.rs b/gateway/ln-gateway/src/error.rs index 7ea1b68fd36..a2ede62c066 100644 --- a/gateway/ln-gateway/src/error.rs +++ b/gateway/ln-gateway/src/error.rs @@ -4,13 +4,13 @@ use axum::response::{IntoResponse, Response}; use fedimint_core::config::{FederationId, FederationIdPrefix}; use fedimint_core::envs::is_env_var_set; use fedimint_core::fmt_utils::OptStacktrace; +use fedimint_gw_client::pay::OutgoingPaymentError; use fedimint_lightning::LightningRpcError; use reqwest::StatusCode; use thiserror::Error; use tracing::error; use crate::envs::FM_DEBUG_GATEWAY_ENV; -use crate::state_machine::pay::OutgoingPaymentError; /// Errors that unauthenticated endpoints can encounter. For privacy reasons, /// the error messages are intended to be redacted before returning to the diff --git a/gateway/ln-gateway/src/events.rs b/gateway/ln-gateway/src/events.rs index 88ce8be6f6e..10024647366 100644 --- a/gateway/ln-gateway/src/events.rs +++ b/gateway/ln-gateway/src/events.rs @@ -1,23 +1,18 @@ use std::sync::Arc; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use fedimint_client::ClientHandle; -use fedimint_core::core::ModuleKind; -use fedimint_core::util::{get_average, get_median}; -use fedimint_core::Amount; use fedimint_eventlog::{ DBTransactionEventLogExt, Event, EventKind, EventLogId, PersistedLogEntry, }; use fedimint_mint_client::event::{OOBNotesReissued, OOBNotesSpent}; use fedimint_wallet_client::events::{DepositConfirmed, WithdrawRequest}; -use itertools::Itertools; use crate::gateway_module_v2::events::{ CompleteLightningPaymentSucceeded, IncomingPaymentFailed, IncomingPaymentStarted, IncomingPaymentSucceeded, OutgoingPaymentFailed, OutgoingPaymentStarted, OutgoingPaymentSucceeded, }; -use crate::rpc::PaymentStats; pub const ALL_GATEWAY_EVENTS: [EventKind; 11] = [ OutgoingPaymentStarted::KIND, @@ -108,109 +103,3 @@ pub async fn get_events_for_duration( batch_start = batch_start.saturating_add(BATCH_SIZE); } } - -/// Filters the given `PersistedLogEntry` slice by the `EventKind` and -/// `ModuleKind`. -pub(crate) fn filter_events<'a, I>( - all_events: I, - event_kind: EventKind, - module_kind: ModuleKind, -) -> impl Iterator + 'a -where - I: IntoIterator + 'a, -{ - all_events.into_iter().filter(move |e| { - if let Some((m, _)) = &e.module { - e.event_kind == event_kind && *m == module_kind - } else { - false - } - }) -} - -/// Joins two sets of events on a predicate. -/// -/// This function computes a "nested loop join" by first computing the cross -/// product of the start event vector and the success/failure event vectors. The -/// resulting cartesian product is then filtered according to the join predicate -/// supplied in the parameters. -/// -/// This function is intended for small data sets. If the data set relations -/// grow, this function should implement a different join algorithm or be moved -/// out of the gateway. -pub(crate) fn join_events<'a, L, R, Res>( - events_l: &'a [&PersistedLogEntry], - events_r: &'a [&PersistedLogEntry], - predicate: impl Fn(L, R, u64) -> Option + 'a, -) -> impl Iterator + 'a -where - L: Event, - R: Event, -{ - events_l - .iter() - .cartesian_product(events_r) - .filter_map(move |(l, r)| { - if let Some(latency) = r.timestamp.checked_sub(l.timestamp) { - let event_l: L = - serde_json::from_value(l.value.clone()).expect("could not parse JSON"); - let event_r: R = - serde_json::from_value(r.value.clone()).expect("could not parse JSON"); - predicate(event_l, event_r, latency) - } else { - None - } - }) -} - -/// Helper struct for storing computed data about outgoing and incoming -/// payments. -#[derive(Debug, Default)] -pub struct StructuredPaymentEvents { - latencies: Vec, - fees: Vec, - latencies_failure: Vec, -} - -impl StructuredPaymentEvents { - pub fn new( - success_stats: &[(u64, Amount)], - failure_stats: Vec, - ) -> StructuredPaymentEvents { - let mut events = StructuredPaymentEvents { - latencies: success_stats.iter().map(|(l, _)| *l).collect(), - fees: success_stats.iter().map(|(_, f)| *f).collect(), - latencies_failure: failure_stats, - }; - events.sort(); - events - } - - /// Combines this `StructuredPaymentEvents` with the `other` - /// `StructuredPaymentEvents` by appending all of the internal vectors. - pub fn combine(&mut self, other: &mut StructuredPaymentEvents) { - self.latencies.append(&mut other.latencies); - self.fees.append(&mut other.fees); - self.latencies_failure.append(&mut other.latencies_failure); - self.sort(); - } - - /// Sorts this `StructuredPaymentEvents` by sorting all of the internal - /// vectors. - fn sort(&mut self) { - self.latencies.sort_unstable(); - self.fees.sort_unstable(); - self.latencies_failure.sort_unstable(); - } - - /// Computes the payment statistics for the given input data. - pub fn compute_payment_stats(&self) -> PaymentStats { - PaymentStats { - average_latency: get_average(&self.latencies).map(Duration::from_micros), - median_latency: get_median(&self.latencies).map(Duration::from_micros), - total_fees: Amount::from_msats(self.fees.iter().map(|a| a.msats).sum()), - total_success: self.latencies.len(), - total_failure: self.latencies_failure.len(), - } - } -} diff --git a/gateway/ln-gateway/src/federation_manager.rs b/gateway/ln-gateway/src/federation_manager.rs index bf7df041674..3f6eab00dfb 100644 --- a/gateway/ln-gateway/src/federation_manager.rs +++ b/gateway/ln-gateway/src/federation_manager.rs @@ -7,13 +7,13 @@ use fedimint_client::ClientHandleArc; use fedimint_core::config::{FederationId, FederationIdPrefix, JsonClientConfig}; use fedimint_core::db::{DatabaseTransaction, NonCommittable}; use fedimint_core::util::Spanned; +use fedimint_gw_client::GatewayClientModule; use tracing::info; use crate::db::GatewayDbtxNcExt; use crate::error::{AdminGatewayError, FederationNotConnected}; use crate::gateway_module_v2::GatewayClientModuleV2; use crate::rpc::FederationInfo; -use crate::state_machine::GatewayClientModule; use crate::AdminResult; /// The first index that the gateway will assign to a federation. diff --git a/gateway/ln-gateway/src/gateway_module_v2/events.rs b/gateway/ln-gateway/src/gateway_module_v2/events.rs index 4440a0d73d6..3f7ff5fb21f 100644 --- a/gateway/ln-gateway/src/gateway_module_v2/events.rs +++ b/gateway/ln-gateway/src/gateway_module_v2/events.rs @@ -3,13 +3,15 @@ use std::time::SystemTime; use fedimint_core::config::FederationId; use fedimint_core::core::ModuleKind; use fedimint_core::Amount; -use fedimint_eventlog::{Event, EventKind, PersistedLogEntry}; +use fedimint_eventlog::{ + filter_events_by_kind, join_events, Event, EventKind, PersistedLogEntry, + StructuredPaymentEvents, +}; use fedimint_lnv2_common::contracts::{Commitment, OutgoingContract, PaymentImage}; use serde::{Deserialize, Serialize}; use serde_millis; use super::send_sm::Cancelled; -use crate::events::{filter_events, join_events, StructuredPaymentEvents}; /// Event that is emitted when an outgoing payment attempt is initiated. #[derive(Serialize, Deserialize, Debug)] @@ -144,22 +146,22 @@ impl Event for CompleteLightningPaymentSucceeded { pub fn compute_lnv2_stats( all_events: &[PersistedLogEntry], ) -> (StructuredPaymentEvents, StructuredPaymentEvents) { - let outgoing_start_events = filter_events( + let outgoing_start_events = filter_events_by_kind( all_events, - OutgoingPaymentStarted::KIND, fedimint_lnv2_common::KIND, + OutgoingPaymentStarted::KIND, ) .collect::>(); - let outgoing_success_events = filter_events( + let outgoing_success_events = filter_events_by_kind( all_events, - OutgoingPaymentSucceeded::KIND, fedimint_lnv2_common::KIND, + OutgoingPaymentSucceeded::KIND, ) .collect::>(); - let outgoing_failure_events = filter_events( + let outgoing_failure_events = filter_events_by_kind( all_events, - OutgoingPaymentFailed::KIND, fedimint_lnv2_common::KIND, + OutgoingPaymentFailed::KIND, ) .collect::>(); @@ -193,22 +195,22 @@ pub fn compute_lnv2_stats( ) .collect::>(); - let incoming_start_events = filter_events( + let incoming_start_events = filter_events_by_kind( all_events, - IncomingPaymentStarted::KIND, fedimint_lnv2_common::KIND, + IncomingPaymentStarted::KIND, ) .collect::>(); - let incoming_success_events = filter_events( + let incoming_success_events = filter_events_by_kind( all_events, - IncomingPaymentSucceeded::KIND, fedimint_lnv2_common::KIND, + IncomingPaymentSucceeded::KIND, ) .collect::>(); - let incoming_failure_events = filter_events( + let incoming_failure_events = filter_events_by_kind( all_events, - IncomingPaymentFailed::KIND, fedimint_lnv2_common::KIND, + IncomingPaymentFailed::KIND, ) .collect::>(); diff --git a/gateway/ln-gateway/src/lib.rs b/gateway/ln-gateway/src/lib.rs index 578c5fc2336..64caa1a3b04 100644 --- a/gateway/ln-gateway/src/lib.rs +++ b/gateway/ln-gateway/src/lib.rs @@ -21,7 +21,6 @@ mod events; mod federation_manager; pub mod gateway_module_v2; pub mod rpc; -pub mod state_machine; mod types; use std::collections::{BTreeMap, BTreeSet}; @@ -32,7 +31,7 @@ use std::str::FromStr; use std::sync::Arc; use std::time::{Duration, UNIX_EPOCH}; -use anyhow::{anyhow, Context}; +use anyhow::{anyhow, ensure, Context}; use async_trait::async_trait; use bitcoin::hashes::sha256; use bitcoin::{Address, Network, Txid}; @@ -43,7 +42,7 @@ use config::{GatewayOpts, LightningMode}; use db::GatewayDbtxNcExt; use envs::FM_GATEWAY_SKIP_WAIT_FOR_SYNC_ENV; use error::FederationNotConnected; -use events::{StructuredPaymentEvents, ALL_GATEWAY_EVENTS}; +use events::ALL_GATEWAY_EVENTS; use federation_manager::FederationManager; use fedimint_api_client::api::net::Connector; use fedimint_bip39::{Bip39RootSecretStrategy, Language, Mnemonic}; @@ -67,17 +66,22 @@ use fedimint_core::util::{SafeUrl, Spanned}; use fedimint_core::{ fedimint_build_code_version_env, get_network_for_address, Amount, BitcoinAmountOrAll, }; -use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId}; +use fedimint_eventlog::{DBTransactionEventLogExt, EventLogId, StructuredPaymentEvents}; +use fedimint_gw_client::events::compute_lnv1_stats; +use fedimint_gw_client::pay::{OutgoingPaymentError, OutgoingPaymentErrorType}; +use fedimint_gw_client::{GatewayClientModule, GatewayExtPayStates, IGatewayClientV1}; use fedimint_lightning::ldk::{self, GatewayLdkChainSourceConfig}; use fedimint_lightning::lnd::GatewayLndClient; use fedimint_lightning::{ CloseChannelsWithPeerRequest, CloseChannelsWithPeerResponse, CreateInvoiceRequest, ILnRpcClient, InterceptPaymentRequest, InterceptPaymentResponse, InvoiceDescription, - LightningContext, LightningRpcError, LightningV2Manager, OpenChannelRequest, PaymentAction, - RouteHtlcStream, SendOnchainRequest, + LightningContext, LightningRpcError, LightningV2Manager, OpenChannelRequest, + PayInvoiceResponse, PaymentAction, RouteHtlcStream, SendOnchainRequest, }; +use fedimint_ln_client::pay::PaymentData; use fedimint_ln_common::config::LightningClientConfig; -use fedimint_ln_common::contracts::Preimage; +use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount; +use fedimint_ln_common::contracts::{IdentifiableContract, Preimage}; use fedimint_ln_common::LightningCommonInit; use fedimint_lnv2_common::contracts::{IncomingContract, PaymentImage}; use fedimint_lnv2_common::gateway_api::{ @@ -93,17 +97,15 @@ use fedimint_wallet_client::{ }; use futures::stream::StreamExt; use gateway_module_v2::events::compute_lnv2_stats; -use lightning_invoice::Bolt11Invoice; +use lightning_invoice::{Bolt11Invoice, RoutingFees}; use rand::thread_rng; use rpc::{ CreateInvoiceForOperatorPayload, DepositAddressRecheckPayload, FederationInfo, GatewayFedConfig, GatewayInfo, LeaveFedPayload, MnemonicResponse, PayInvoiceForOperatorPayload, - PaymentLogPayload, PaymentLogResponse, PaymentSummaryPayload, PaymentSummaryResponse, - ReceiveEcashPayload, ReceiveEcashResponse, SetFeesPayload, SpendEcashPayload, - SpendEcashResponse, WithdrawResponse, V1_API_ENDPOINT, + PaymentLogPayload, PaymentLogResponse, PaymentStats, PaymentSummaryPayload, + PaymentSummaryResponse, ReceiveEcashPayload, ReceiveEcashResponse, SetFeesPayload, + SpendEcashPayload, SpendEcashResponse, WithdrawResponse, V1_API_ENDPOINT, }; -use state_machine::events::compute_lnv1_stats; -use state_machine::{GatewayClientModule, GatewayExtPayStates}; use tokio::sync::RwLock; use tracing::{debug, error, info, info_span, warn}; @@ -1137,6 +1139,8 @@ impl Gateway { GW_ANNOUNCEMENT_TTL, federation_config.lightning_fee.into(), lightning_context, + self.versioned_api.clone(), + self.gateway_id, ) .await; } @@ -1645,8 +1649,8 @@ impl Gateway { } Ok(PaymentSummaryResponse { - outgoing: outgoing.compute_payment_stats(), - incoming: incoming.compute_payment_stats(), + outgoing: PaymentStats::compute(&outgoing), + incoming: PaymentStats::compute(&incoming), }) } @@ -1672,6 +1676,8 @@ impl Gateway { let route_hints = route_hints.clone(); let lightning_context = lightning_context.clone(); let federation_config = federation_config.clone(); + let api = self.versioned_api.clone(); + let gateway_id = self.gateway_id; if let Err(e) = register_task_group .spawn_cancellable("register_federation", async move { @@ -1684,6 +1690,8 @@ impl Gateway { GW_ANNOUNCEMENT_TTL, federation_config.lightning_fee.into(), lightning_context, + api, + gateway_id, ) .await; }) @@ -2218,3 +2226,134 @@ impl LightningV2Manager for Gateway { .is_some() } } + +#[async_trait] +impl IGatewayClientV1 for Gateway { + async fn verify_preimage_authentication( + &self, + payment_hash: sha256::Hash, + preimage_auth: sha256::Hash, + contract: OutgoingContractAccount, + ) -> std::result::Result<(), OutgoingPaymentError> { + let mut dbtx = self.gateway_db.begin_transaction().await; + if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await { + if secret_hash != preimage_auth { + return Err(OutgoingPaymentError { + error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage, + contract_id: contract.contract.contract_id(), + contract: Some(contract), + }); + } + } else { + // Committing the `preimage_auth` to the database can fail if two users try to + // pay the same invoice at the same time. + dbtx.save_new_preimage_authentication(payment_hash, preimage_auth) + .await; + return dbtx + .commit_tx_result() + .await + .map_err(|_| OutgoingPaymentError { + error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid, + contract_id: contract.contract.contract_id(), + contract: Some(contract), + }); + } + + Ok(()) + } + + async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()> { + let lightning_context = self.get_lightning_context().await?; + + if matches!(payment_data, PaymentData::PrunedInvoice { .. }) { + ensure!( + lightning_context.lnrpc.supports_private_payments(), + "Private payments are not supported by the lightning node" + ); + } + + Ok(()) + } + + async fn get_routing_fees(&self, federation_id: FederationId) -> Option { + let mut gateway_dbtx = self.gateway_db.begin_transaction_nc().await; + gateway_dbtx + .load_federation_config(federation_id) + .await + .map(|c| c.lightning_fee.into()) + } + + async fn get_client(&self, federation_id: &FederationId) -> Option> { + self.federation_manager + .read() + .await + .client(federation_id) + .cloned() + } + + async fn get_client_for_invoice( + &self, + payment_data: PaymentData, + ) -> Option> { + let rhints = payment_data.route_hints(); + match rhints.first().and_then(|rh| rh.0.last()) { + None => None, + Some(hop) => match self.get_lightning_context().await { + Ok(lightning_context) => { + if hop.src_node_id != lightning_context.lightning_public_key { + return None; + } + + self.federation_manager + .read() + .await + .get_client_for_index(hop.short_channel_id) + } + Err(_) => None, + }, + } + } + + async fn pay( + &self, + payment_data: PaymentData, + max_delay: u64, + max_fee: Amount, + ) -> std::result::Result { + let lightning_context = self.get_lightning_context().await?; + + match payment_data { + PaymentData::Invoice(invoice) => { + lightning_context + .lnrpc + .pay(invoice, max_delay, max_fee) + .await + } + PaymentData::PrunedInvoice(invoice) => { + lightning_context + .lnrpc + .pay_private(invoice, max_delay, max_fee) + .await + } + } + } + + async fn complete_htlc( + &self, + htlc: InterceptPaymentResponse, + ) -> std::result::Result<(), LightningRpcError> { + // Wait until the lightning node is online to complete the HTLC. + let lightning_context = loop { + match self.get_lightning_context().await { + Ok(lightning_context) => break lightning_context, + Err(e) => { + warn!("Trying to complete HTLC but got {e}, will keep retrying..."); + sleep(Duration::from_secs(5)).await; + continue; + } + } + }; + + lightning_context.lnrpc.complete_htlc(htlc).await + } +} diff --git a/gateway/ln-gateway/src/rpc/mod.rs b/gateway/ln-gateway/src/rpc/mod.rs index 60a96f0f702..20e17965ddb 100644 --- a/gateway/ln-gateway/src/rpc/mod.rs +++ b/gateway/ln-gateway/src/rpc/mod.rs @@ -8,8 +8,9 @@ use bitcoin::address::NetworkUnchecked; use bitcoin::{Address, Network}; use fedimint_core::config::{FederationId, JsonClientConfig}; use fedimint_core::core::OperationId; +use fedimint_core::util::{get_average, get_median}; use fedimint_core::{secp256k1, Amount, BitcoinAmountOrAll}; -use fedimint_eventlog::{EventKind, EventLogId, PersistedLogEntry}; +use fedimint_eventlog::{EventKind, EventLogId, PersistedLogEntry, StructuredPaymentEvents}; use fedimint_mint_client::OOBNotes; use fedimint_wallet_client::PegOutFees; use lightning_invoice::Bolt11Invoice; @@ -255,6 +256,19 @@ pub struct PaymentStats { pub total_failure: usize, } +impl PaymentStats { + /// Computes the payment statistics for the given structured payment events. + pub fn compute(events: &StructuredPaymentEvents) -> Self { + PaymentStats { + average_latency: get_average(&events.latencies).map(Duration::from_micros), + median_latency: get_median(&events.latencies).map(Duration::from_micros), + total_fees: Amount::from_msats(events.fees.iter().map(|a| a.msats).sum()), + total_success: events.latencies.len(), + total_failure: events.latencies_failure.len(), + } + } +} + #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PaymentSummaryPayload { pub start_millis: u64, diff --git a/gateway/ln-gateway/tests/tests.rs b/gateway/ln-gateway/tests/tests.rs index ef373c386a8..60e6e25e8df 100644 --- a/gateway/ln-gateway/tests/tests.rs +++ b/gateway/ln-gateway/tests/tests.rs @@ -23,6 +23,12 @@ use fedimint_dummy_client::{DummyClientInit, DummyClientModule}; use fedimint_dummy_common::config::DummyGenParams; use fedimint_dummy_server::DummyInit; use fedimint_eventlog::Event; +use fedimint_gw_client::pay::{ + OutgoingContractError, OutgoingPaymentError, OutgoingPaymentErrorType, +}; +use fedimint_gw_client::{ + GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, GatewayMeta, Htlc, +}; use fedimint_ln_client::api::LnFederationApi; use fedimint_ln_client::pay::{PayInvoicePayload, PaymentData}; use fedimint_ln_client::{ @@ -56,12 +62,6 @@ use ln_gateway::gateway_module_v2::events::{ }; use ln_gateway::gateway_module_v2::{FinalReceiveState, GatewayClientModuleV2}; use ln_gateway::rpc::{PaymentLogPayload, SetFeesPayload}; -use ln_gateway::state_machine::pay::{ - OutgoingContractError, OutgoingPaymentError, OutgoingPaymentErrorType, -}; -use ln_gateway::state_machine::{ - GatewayClientModule, GatewayExtPayStates, GatewayExtReceiveStates, GatewayMeta, Htlc, -}; use ln_gateway::Gateway; use secp256k1::{Keypair, PublicKey}; use tpe::G1Affine; diff --git a/modules/fedimint-gw-client/Cargo.toml b/modules/fedimint-gw-client/Cargo.toml new file mode 100644 index 00000000000..5853d4f66b6 --- /dev/null +++ b/modules/fedimint-gw-client/Cargo.toml @@ -0,0 +1,41 @@ +[package] +name = "fedimint-gw-client" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +description = "fedimint-gw-client is a crate for servicing lightning payments on behalf of fedimint clients" +license = { workspace = true } +readme = { workspace = true } +repository = { workspace = true } + +[package.metadata.docs.rs] +rustc-args = ["--cfg", "tokio_unstable"] + +[package.metadata.cargo-udeps.ignore] +# cargo udeps can't detect that one +normal = ["aquamarine"] + +[lib] +name = "fedimint_gw_client" +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +aquamarine = { workspace = true } +async-stream = { workspace = true } +async-trait = { workspace = true } +bitcoin = { workspace = true } +erased-serde = { workspace = true } +fedimint-api-client = { path = "../../fedimint-api-client", version = "=0.7.0-alpha" } +fedimint-client = { path = "../../fedimint-client", version = "=0.7.0-alpha" } +fedimint-core = { workspace = true } +fedimint-eventlog = { workspace = true } +fedimint-lightning = { path = "../../gateway/fedimint-lightning", version = "=0.7.0-alpha" } +fedimint-ln-client = { workspace = true } +fedimint-ln-common = { workspace = true } +futures = { workspace = true } +lightning-invoice = { workspace = true } +serde = { workspace = true } +thiserror = { workspace = true } +tokio-stream = { workspace = true } +tracing = { workspace = true, features = ["log"] } diff --git a/gateway/ln-gateway/src/state_machine/complete.rs b/modules/fedimint-gw-client/src/complete.rs similarity index 94% rename from gateway/ln-gateway/src/state_machine/complete.rs rename to modules/fedimint-gw-client/src/complete.rs index 82832785cf5..75fd7e22019 100644 --- a/gateway/ln-gateway/src/state_machine/complete.rs +++ b/modules/fedimint-gw-client/src/complete.rs @@ -1,11 +1,9 @@ use std::fmt; -use std::time::Duration; use fedimint_client::sm::{ClientSMDatabaseTransaction, State, StateTransition}; use fedimint_client::DynGlobalClientContext; use fedimint_core::core::OperationId; use fedimint_core::encoding::{Decodable, Encodable}; -use fedimint_core::task::sleep; use fedimint_lightning::{InterceptPaymentResponse, PaymentAction}; use fedimint_ln_client::incoming::IncomingSmStates; use fedimint_ln_common::contracts::Preimage; @@ -246,18 +244,6 @@ impl CompleteHtlcState { common: GatewayCompleteCommon, htlc_outcome: HtlcOutcome, ) -> Result<(), CompleteHtlcError> { - // Wait until the lightning node is online to complete the HTLC. - let lightning_context = loop { - match context.gateway.get_lightning_context().await { - Ok(lightning_context) => break lightning_context, - Err(e) => { - warn!("Trying to complete HTLC but got {e}, will keep retrying..."); - sleep(Duration::from_secs(5)).await; - continue; - } - } - }; - let htlc = InterceptPaymentResponse { action: match htlc_outcome { HtlcOutcome::Success(preimage) => PaymentAction::Settle(preimage), @@ -268,8 +254,8 @@ impl CompleteHtlcState { htlc_id: common.htlc_id, }; - lightning_context - .lnrpc + context + .lightning_manager .complete_htlc(htlc) .await .map_err(|_| CompleteHtlcError::FailedToCompleteHtlc) diff --git a/gateway/ln-gateway/src/state_machine/events.rs b/modules/fedimint-gw-client/src/events.rs similarity index 94% rename from gateway/ln-gateway/src/state_machine/events.rs rename to modules/fedimint-gw-client/src/events.rs index 1a15b42cb9b..7211b7b72df 100644 --- a/gateway/ln-gateway/src/state_machine/events.rs +++ b/modules/fedimint-gw-client/src/events.rs @@ -1,12 +1,14 @@ use fedimint_core::core::{ModuleKind, OperationId}; use fedimint_core::Amount; -use fedimint_eventlog::{Event, EventKind, PersistedLogEntry}; +use fedimint_eventlog::{ + filter_events_by_kind, join_events, Event, EventKind, PersistedLogEntry, + StructuredPaymentEvents, +}; use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount; use fedimint_ln_common::contracts::ContractId; use serde::{Deserialize, Serialize}; use super::pay::OutgoingPaymentError; -use crate::events::{filter_events, join_events, StructuredPaymentEvents}; /// LNv1 event that is emitted when an outgoing payment attempt is initiated. #[derive(Serialize, Deserialize, Debug)] @@ -142,22 +144,22 @@ impl Event for CompleteLightningPaymentSucceeded { pub fn compute_lnv1_stats( all_events: &[PersistedLogEntry], ) -> (StructuredPaymentEvents, StructuredPaymentEvents) { - let outgoing_start_events = filter_events( + let outgoing_start_events = filter_events_by_kind( all_events, - OutgoingPaymentStarted::KIND, fedimint_ln_common::KIND, + OutgoingPaymentStarted::KIND, ) .collect::>(); - let outgoing_success_events = filter_events( + let outgoing_success_events = filter_events_by_kind( all_events, - OutgoingPaymentSucceeded::KIND, fedimint_ln_common::KIND, + OutgoingPaymentSucceeded::KIND, ) .collect::>(); - let outgoing_failure_events = filter_events( + let outgoing_failure_events = filter_events_by_kind( all_events, - OutgoingPaymentFailed::KIND, fedimint_ln_common::KIND, + OutgoingPaymentFailed::KIND, ) .collect::>(); @@ -192,22 +194,22 @@ pub fn compute_lnv1_stats( ) .collect::>(); - let incoming_start_events = filter_events( + let incoming_start_events = filter_events_by_kind( all_events, - IncomingPaymentStarted::KIND, fedimint_ln_common::KIND, + IncomingPaymentStarted::KIND, ) .collect::>(); - let incoming_success_events = filter_events( + let incoming_success_events = filter_events_by_kind( all_events, - IncomingPaymentSucceeded::KIND, fedimint_ln_common::KIND, + IncomingPaymentSucceeded::KIND, ) .collect::>(); - let incoming_failure_events = filter_events( + let incoming_failure_events = filter_events_by_kind( all_events, - IncomingPaymentFailed::KIND, fedimint_ln_common::KIND, + IncomingPaymentFailed::KIND, ) .collect::>(); let incoming_success_stats = diff --git a/gateway/ln-gateway/src/state_machine/mod.rs b/modules/fedimint-gw-client/src/lib.rs similarity index 89% rename from gateway/ln-gateway/src/state_machine/mod.rs rename to modules/fedimint-gw-client/src/lib.rs index 84865519b66..d083f8195fc 100644 --- a/gateway/ln-gateway/src/state_machine/mod.rs +++ b/modules/fedimint-gw-client/src/lib.rs @@ -4,14 +4,16 @@ pub mod pay; use std::collections::BTreeMap; use std::fmt; +use std::fmt::Debug; use std::sync::Arc; use std::time::Duration; -use anyhow::ensure; use async_stream::stream; +use async_trait::async_trait; use bitcoin::hashes::{sha256, Hash}; use bitcoin::key::Secp256k1; -use bitcoin::secp256k1::All; +use bitcoin::secp256k1::{All, PublicKey}; +use complete::{GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState}; use events::{IncomingPaymentStarted, OutgoingPaymentStarted}; use fedimint_api_client::api::DynModuleApi; use fedimint_client::derivable_secret::ChildId; @@ -24,13 +26,20 @@ use fedimint_client::sm::{Context, DynState, ModuleNotifier, State}; use fedimint_client::transaction::{ ClientOutput, ClientOutputBundle, ClientOutputSM, TransactionBuilder, }; -use fedimint_client::{sm_enum_variant_translation, AddStateMachinesError, DynGlobalClientContext}; +use fedimint_client::{ + sm_enum_variant_translation, AddStateMachinesError, ClientHandleArc, DynGlobalClientContext, +}; +use fedimint_core::config::FederationId; use fedimint_core::core::{Decoder, IntoDynInstance, ModuleInstanceId, ModuleKind, OperationId}; use fedimint_core::db::{AutocommitError, DatabaseTransaction}; use fedimint_core::encoding::{Decodable, Encodable}; use fedimint_core::module::{ApiVersion, ModuleInit, MultiApiVersion}; +use fedimint_core::util::{SafeUrl, Spanned}; use fedimint_core::{apply, async_trait_maybe_send, secp256k1, Amount, OutPoint}; -use fedimint_lightning::{InterceptPaymentRequest, LightningContext}; +use fedimint_lightning::{ + InterceptPaymentRequest, InterceptPaymentResponse, LightningContext, LightningRpcError, + PayInvoiceResponse, +}; use fedimint_ln_client::api::LnFederationApi; use fedimint_ln_client::incoming::{ FundingOfferState, IncomingSmCommon, IncomingSmError, IncomingSmStates, IncomingStateMachine, @@ -41,6 +50,7 @@ use fedimint_ln_client::{ RealGatewayConnection, }; use fedimint_ln_common::config::LightningClientConfig; +use fedimint_ln_common::contracts::outgoing::OutgoingContractAccount; use fedimint_ln_common::contracts::{ContractId, Preimage}; use fedimint_ln_common::route_hints::RouteHint; use fedimint_ln_common::{ @@ -59,10 +69,6 @@ use self::pay::{ GatewayPayCommon, GatewayPayInvoice, GatewayPayStateMachine, GatewayPayStates, OutgoingPaymentError, }; -use crate::state_machine::complete::{ - GatewayCompleteCommon, GatewayCompleteStates, WaitForPreimageState, -}; -use crate::Gateway; /// The high-level state of a reissue operation started with /// [`GatewayClientModule::gateway_pay_bolt11_invoice`]. @@ -116,7 +122,7 @@ pub enum GatewayMeta { #[derive(Debug, Clone)] pub struct GatewayClientInit { pub federation_index: u64, - pub gateway: Arc, + pub lightning_manager: Arc, } impl ModuleInit for GatewayClientInit { @@ -151,7 +157,7 @@ impl ClientModuleInit for GatewayClientInit { module_api: args.module_api().clone(), federation_index: self.federation_index, client_ctx: args.context(), - gateway: self.gateway.clone(), + lightning_manager: self.lightning_manager.clone(), }) } } @@ -162,8 +168,8 @@ pub struct GatewayClientContext { secp: Secp256k1, pub ln_decoder: Decoder, notifier: ModuleNotifier, - gateway: Arc, pub client_ctx: ClientContext, + pub lightning_manager: Arc, } impl Context for GatewayClientContext { @@ -192,7 +198,7 @@ pub struct GatewayClientModule { federation_index: u64, module_api: DynModuleApi, client_ctx: ClientContext, - gateway: Arc, + pub lightning_manager: Arc, } impl ClientModule for GatewayClientModule { @@ -208,8 +214,8 @@ impl ClientModule for GatewayClientModule { secp: Secp256k1::new(), ln_decoder: self.decoder(), notifier: self.notifier.clone(), - gateway: self.gateway.clone(), client_ctx: self.client_ctx.clone(), + lightning_manager: self.lightning_manager.clone(), } } @@ -242,6 +248,8 @@ impl GatewayClientModule { ttl: Duration, fees: RoutingFees, lightning_context: LightningContext, + api: SafeUrl, + gateway_id: PublicKey, ) -> LightningGatewayAnnouncement { LightningGatewayAnnouncement { info: LightningGateway { @@ -249,10 +257,10 @@ impl GatewayClientModule { gateway_redeem_key: self.redeem_key.public_key(), node_pub_key: lightning_context.lightning_public_key, lightning_alias: lightning_context.lightning_alias, - api: self.gateway.versioned_api.clone(), + api, route_hints, fees, - gateway_id: self.gateway.gateway_id, + gateway_id, supports_private_payments: lightning_context.lnrpc.supports_private_payments(), }, ttl, @@ -371,9 +379,17 @@ impl GatewayClientModule { time_to_live: Duration, fees: RoutingFees, lightning_context: LightningContext, + api: SafeUrl, + gateway_id: PublicKey, ) { - let registration_info = - self.to_gateway_registration_info(route_hints, time_to_live, fees, lightning_context); + let registration_info = self.to_gateway_registration_info( + route_hints, + time_to_live, + fees, + lightning_context, + api, + gateway_id, + ); let gateway_id = registration_info.info.gateway_id; let federation_id = self @@ -599,17 +615,9 @@ impl GatewayClientModule { pay_invoice_payload: PayInvoicePayload, ) -> anyhow::Result { let payload = pay_invoice_payload.clone(); - let lightning_context = self.gateway.get_lightning_context().await?; - - if matches!( - pay_invoice_payload.payment_data, - PaymentData::PrunedInvoice { .. } - ) { - ensure!( - lightning_context.lnrpc.supports_private_payments(), - "Private payments are not supported by the lightning node" - ); - } + self.lightning_manager + .verify_pruned_invoice(pay_invoice_payload.payment_data) + .await?; self.client_ctx.module_db() .autocommit( @@ -862,3 +870,60 @@ impl TryFrom for SwapParameters { }) } } + +/// An interface between module implementation and the general `Gateway` +/// +/// To abstract away and decouple the core gateway from the modules, the +/// interface between them is expressed as a trait. The gateway handles +/// operations that require Lightning node access or database access. +#[async_trait] +pub trait IGatewayClientV1: Debug + Send + Sync { + /// Verifies that the supplied `preimage_auth` is the same as the + /// `preimage_auth` that initiated the payment. + /// + /// If it is not, then this will return an error because this client is not + /// authorized to receive the preimage. + async fn verify_preimage_authentication( + &self, + payment_hash: sha256::Hash, + preimage_auth: sha256::Hash, + contract: OutgoingContractAccount, + ) -> Result<(), OutgoingPaymentError>; + + /// Verify that the lightning node supports private payments if a pruned + /// invoice is supplied. + async fn verify_pruned_invoice(&self, payment_data: PaymentData) -> anyhow::Result<()>; + + /// Retrieves the federation's routing fees from the federation's config. + async fn get_routing_fees(&self, federation_id: FederationId) -> Option; + + /// Retrieve a client given a federation ID, used for swapping ecash between + /// federations. + async fn get_client(&self, federation_id: &FederationId) -> Option>; + + // Retrieve a client given an invoice. + // + // Checks if the invoice route hint last hop has source node id matching this + // gateways node pubkey and if the short channel id matches one assigned by + // this gateway to a connected federation. In this case, the gateway can + // avoid paying the invoice over the lightning network and instead perform a + // direct swap between the two federations. + async fn get_client_for_invoice( + &self, + payment_data: PaymentData, + ) -> Option>; + + /// Pay a Lightning invoice using the gateway's lightning node. + async fn pay( + &self, + payment_data: PaymentData, + max_delay: u64, + max_fee: Amount, + ) -> Result; + + /// Use the gateway's lightning node to send a complete HTLC response. + async fn complete_htlc( + &self, + htlc_response: InterceptPaymentResponse, + ) -> Result<(), LightningRpcError>; +} diff --git a/gateway/ln-gateway/src/state_machine/pay.rs b/modules/fedimint-gw-client/src/pay.rs similarity index 87% rename from gateway/ln-gateway/src/state_machine/pay.rs rename to modules/fedimint-gw-client/src/pay.rs index 04cbb61f6bc..83723b2036b 100644 --- a/gateway/ln-gateway/src/state_machine/pay.rs +++ b/modules/fedimint-gw-client/src/pay.rs @@ -1,6 +1,5 @@ use std::fmt::{self, Display}; -use bitcoin::hashes::sha256; use fedimint_client::sm::{ClientSMDatabaseTransaction, State, StateTransition}; use fedimint_client::transaction::{ ClientInput, ClientInputBundle, ClientOutput, ClientOutputBundle, @@ -9,7 +8,6 @@ use fedimint_client::{ClientHandleArc, DynGlobalClientContext}; use fedimint_core::config::FederationId; use fedimint_core::core::OperationId; use fedimint_core::encoding::{Decodable, Encodable}; -use fedimint_core::util::Spanned; use fedimint_core::{secp256k1, Amount, OutPoint, TransactionId}; use fedimint_lightning::{LightningRpcError, PayInvoiceResponse}; use fedimint_ln_client::api::LnFederationApi; @@ -26,10 +24,8 @@ use tokio_stream::StreamExt; use tracing::{debug, error, info, warn, Instrument}; use super::{GatewayClientContext, GatewayExtReceiveStates}; -use crate::db::GatewayDbtxNcExt; -use crate::state_machine::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded}; -use crate::state_machine::GatewayClientModule; -use crate::GatewayState; +use crate::events::{OutgoingPaymentFailed, OutgoingPaymentSucceeded}; +use crate::GatewayClientModule; const TIMELOCK_DELTA: u64 = 10; @@ -192,8 +188,8 @@ pub enum OutgoingPaymentErrorType { )] pub struct OutgoingPaymentError { pub error_type: OutgoingPaymentErrorType, - contract_id: ContractId, - contract: Option, + pub contract_id: ContractId, + pub contract: Option, } impl Display for OutgoingPaymentError { @@ -278,13 +274,14 @@ impl GatewayPayInvoice { ) -> GatewayPayStateMachine { debug!("Buying preimage contract {contract:?}"); // Verify that this client is authorized to receive the preimage. - if let Err(err) = Self::verify_preimage_authentication( - &context, - payload.payment_data.payment_hash(), - payload.preimage_auth, - contract.clone(), - ) - .await + if let Err(err) = context + .lightning_manager + .verify_preimage_authentication( + payload.payment_data.payment_hash(), + payload.preimage_auth, + contract.clone(), + ) + .await { warn!("Preimage authentication failed: {err} for contract {contract:?}"); return GatewayPayStateMachine { @@ -296,9 +293,10 @@ impl GatewayPayInvoice { }; } - if let Some(client) = - Self::check_swap_to_federation(context.clone(), payment_parameters.payment_data.clone()) - .await + if let Some(client) = context + .lightning_manager + .get_client_for_invoice(payment_parameters.payment_data.clone()) + .await { client .with(|client| { @@ -363,9 +361,9 @@ impl GatewayPayInvoice { }); } - let mut gateway_dbtx = context.gateway.gateway_db.begin_transaction_nc().await; - let config = gateway_dbtx - .load_federation_config(federation_id) + let routing_fees = context + .lightning_manager + .get_routing_fees(federation_id) .await .ok_or(OutgoingPaymentError { error_type: OutgoingPaymentErrorType::InvalidFederationConfiguration, @@ -378,7 +376,7 @@ impl GatewayPayInvoice { context.redeem_key, consensus_block_count.unwrap(), &payment_data, - config.lightning_fee.into(), + routing_fees, ) .map_err(|e| { warn!("Invalid outgoing contract: {e:?}"); @@ -416,28 +414,10 @@ impl GatewayPayInvoice { .expect("We already checked that an amount was supplied"), ); - let Ok(lightning_context) = context.gateway.get_lightning_context().await else { - return Self::gateway_pay_cancel_contract( - LightningRpcError::FailedToConnect, - contract, - common, - ); - }; - - let payment_result = match buy_preimage.payment_data { - PaymentData::Invoice(invoice) => { - lightning_context - .lnrpc - .pay(invoice, max_delay, max_fee) - .await - } - PaymentData::PrunedInvoice(invoice) => { - lightning_context - .lnrpc - .pay_private(invoice, buy_preimage.max_delay, max_fee) - .await - } - }; + let payment_result = context + .lightning_manager + .pay(buy_preimage.payment_data, max_delay, max_fee) + .await; match payment_result { Ok(PayInvoiceResponse { preimage, .. }) => { @@ -542,43 +522,6 @@ impl GatewayPayInvoice { } } - /// Verifies that the supplied `preimage_auth` is the same as the - /// `preimage_auth` that initiated the payment. If it is not, then this - /// will return an error because this client is not authorized to receive - /// the preimage. - async fn verify_preimage_authentication( - context: &GatewayClientContext, - payment_hash: sha256::Hash, - preimage_auth: sha256::Hash, - contract: OutgoingContractAccount, - ) -> Result<(), OutgoingPaymentError> { - let mut dbtx = context.gateway.gateway_db.begin_transaction().await; - if let Some(secret_hash) = dbtx.load_preimage_authentication(payment_hash).await { - if secret_hash != preimage_auth { - return Err(OutgoingPaymentError { - error_type: OutgoingPaymentErrorType::InvalidInvoicePreimage, - contract_id: contract.contract.contract_id(), - contract: Some(contract), - }); - } - } else { - // Committing the `preimage_auth` to the database can fail if two users try to - // pay the same invoice at the same time. - dbtx.save_new_preimage_authentication(payment_hash, preimage_auth) - .await; - return dbtx - .commit_tx_result() - .await - .map_err(|_| OutgoingPaymentError { - error_type: OutgoingPaymentErrorType::InvoiceAlreadyPaid, - contract_id: contract.contract.contract_id(), - contract: Some(contract), - }); - } - - Ok(()) - } - fn validate_outgoing_account( account: &OutgoingContractAccount, redeem_key: bitcoin::key::Keypair, @@ -628,36 +571,6 @@ impl GatewayPayInvoice { payment_data: payment_data.clone(), }) } - - // Checks if the invoice route hint last hop has source node id matching this - // gateways node pubkey and if the short channel id matches one assigned by - // this gateway to a connected federation. In this case, the gateway can - // avoid paying the invoice over the lightning network and instead perform a - // direct swap between the two federations. - async fn check_swap_to_federation( - context: GatewayClientContext, - payment_data: PaymentData, - ) -> Option> { - let rhints = payment_data.route_hints(); - match rhints.first().and_then(|rh| rh.0.last()) { - None => None, - Some(hop) => match context.gateway.state.read().await.clone() { - GatewayState::Running { lightning_context } => { - if hop.src_node_id != lightning_context.lightning_public_key { - return None; - } - - context - .gateway - .federation_manager - .read() - .await - .get_client_for_index(hop.short_channel_id) - } - _ => None, - }, - } - } } #[derive(Debug, Clone, Eq, PartialEq, Decodable, Encodable, Serialize, Deserialize)] @@ -775,13 +688,11 @@ impl GatewayPayWaitForSwapPreimage { contract: OutgoingContractAccount, ) -> Result { debug!("Waiting preimage for contract {contract:?}"); + let client = context - .gateway - .federation_manager - .read() + .lightning_manager + .get_client(&federation_id) .await - .client(&federation_id) - .cloned() .ok_or(OutgoingPaymentError { contract_id: contract.contract.contract_id(), contract: Some(contract.clone()),