From d5166052851215b22ea86696730f8c2923c4ef78 Mon Sep 17 00:00:00 2001 From: joschisan <122358257+joschisan@users.noreply.github.com> Date: Mon, 5 Aug 2024 16:44:37 +0200 Subject: [PATCH] fix: omit ln fee for direct swaps --- .../ln-gateway/src/gateway_module_v2/mod.rs | 2 +- gateway/ln-gateway/src/lib.rs | 41 +++++++++++------- gateway/ln-gateway/src/rpc/rpc_server.rs | 5 ++- modules/fedimint-lnv2-client/src/api.rs | 27 ++++++------ modules/fedimint-lnv2-client/src/lib.rs | 43 ++++++++++++------- modules/fedimint-lnv2-client/src/send_sm.rs | 40 ++++++----------- modules/fedimint-lnv2-tests/tests/mock.rs | 18 ++++---- modules/fedimint-lnv2-tests/tests/tests.rs | 4 +- 8 files changed, 97 insertions(+), 83 deletions(-) diff --git a/gateway/ln-gateway/src/gateway_module_v2/mod.rs b/gateway/ln-gateway/src/gateway_module_v2/mod.rs index 019b79efc76..d9309e674fb 100644 --- a/gateway/ln-gateway/src/gateway_module_v2/mod.rs +++ b/gateway/ln-gateway/src/gateway_module_v2/mod.rs @@ -267,7 +267,7 @@ impl GatewayClientModuleV2 { let min_contract_amount = self .gateway .routing_info_v2(&payload.federation_id) - .await + .await? .ok_or(anyhow!("Routing Info not available"))? .send_fee_minimum .add_fee(amount); diff --git a/gateway/ln-gateway/src/lib.rs b/gateway/ln-gateway/src/lib.rs index a8fa21e61b8..3318737361c 100644 --- a/gateway/ln-gateway/src/lib.rs +++ b/gateway/ln-gateway/src/lib.rs @@ -1434,20 +1434,29 @@ impl Gateway { /// Returns payment information that LNv2 clients can use to instruct this /// Gateway to pay an invoice or receive a payment. - pub async fn routing_info_v2(&self, federation_id: &FederationId) -> Option { - Some(RoutingInfo { - public_key: self.public_key_v2(federation_id).await?, - send_fee_default: PaymentFee::SEND_FEE_LIMIT_DEFAULT, - // The base fee ensures that the gateway does not loose sats sending the payment due to - // fees paid on the transaction claiming the outgoing contract or subsequent - // transactions spending the newly issued ecash - send_fee_minimum: PaymentFee::SEND_FEE_MINIMUM, - // The base fee ensures that the gateway does not loose sats receiving the payment due - // to fees paid on the transaction funding the incoming contract - receive_fee: PaymentFee::RECEIVE_FEE_LIMIT_DEFAULT, - expiration_delta_default: 500, - expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2, - }) + pub async fn routing_info_v2( + &self, + federation_id: &FederationId, + ) -> anyhow::Result> { + let context = self.get_lightning_context().await?; + + Ok(self + .public_key_v2(federation_id) + .await + .map(|public_key| RoutingInfo { + lightning_public_key: context.lightning_public_key, + public_key, + send_fee_default: PaymentFee::SEND_FEE_LIMIT_DEFAULT, + // The base fee ensures that the gateway does not loose sats sending the payment due + // to fees paid on the transaction claiming the outgoing contract or + // subsequent transactions spending the newly issued ecash + send_fee_minimum: PaymentFee::SEND_FEE_MINIMUM, + // The base fee ensures that the gateway does not loose sats receiving the payment + // due to fees paid on the transaction funding the incoming contract + receive_fee: PaymentFee::RECEIVE_FEE_LIMIT_DEFAULT, + expiration_delta_default: 500, + expiration_delta_minimum: EXPIRATION_DELTA_MINIMUM_V2, + })) } pub async fn select_client_v2( @@ -1489,8 +1498,8 @@ impl Gateway { let payment_info = self .routing_info_v2(&payload.federation_id) - .await - .ok_or(anyhow!("Payment Info not available"))?; + .await? + .ok_or(anyhow!("Unknown federation"))?; if payload.contract.commitment.refund_pk != payment_info.public_key { bail!("The outgoing contract keyed to another gateway"); diff --git a/gateway/ln-gateway/src/rpc/rpc_server.rs b/gateway/ln-gateway/src/rpc/rpc_server.rs index babaedda1eb..62c170656e6 100644 --- a/gateway/ln-gateway/src/rpc/rpc_server.rs +++ b/gateway/ln-gateway/src/rpc/rpc_server.rs @@ -371,7 +371,10 @@ async fn routing_info_v2( Extension(gateway): Extension>, Json(federation_id): Json, ) -> Json { - Json(json!(gateway.routing_info_v2(&federation_id).await)) + Json(json!(gateway + .routing_info_v2(&federation_id) + .await + .map_err(|e| e.to_string()))) } async fn pay_bolt11_invoice_v2( diff --git a/modules/fedimint-lnv2-client/src/api.rs b/modules/fedimint-lnv2-client/src/api.rs index 12dfc80c61b..2414d46e1c5 100644 --- a/modules/fedimint-lnv2-client/src/api.rs +++ b/modules/fedimint-lnv2-client/src/api.rs @@ -193,7 +193,7 @@ pub trait GatewayConnection: std::fmt::Debug { &self, gateway_api: GatewayEndpoint, payload: CreateBolt11InvoicePayload, - ) -> Result, GatewayError>; + ) -> Result; async fn try_gateway_send_payment( &self, @@ -202,7 +202,7 @@ pub trait GatewayConnection: std::fmt::Debug { contract: OutgoingContract, invoice: LightningInvoice, auth: Signature, - ) -> anyhow::Result, String>>; + ) -> Result, GatewayError>; } #[derive(Debug)] @@ -227,16 +227,17 @@ impl GatewayConnection for RealGatewayConnection { .send() .await .map_err(|e| GatewayError::Unreachable(e.to_string()))? - .json::>() + .json::, String>>() .await - .map_err(|e| GatewayError::InvalidJsonResponse(e.to_string())) + .map_err(|e| GatewayError::InvalidJsonResponse(e.to_string()))? + .map_err(|e| GatewayError::Request(e.to_string())) } async fn fetch_invoice( &self, gateway_api: GatewayEndpoint, payload: CreateBolt11InvoicePayload, - ) -> Result, GatewayError> { + ) -> Result { reqwest::Client::new() .post( gateway_api @@ -251,7 +252,8 @@ impl GatewayConnection for RealGatewayConnection { .map_err(|e| GatewayError::Unreachable(e.to_string()))? .json::>() .await - .map_err(|e| GatewayError::InvalidJsonResponse(e.to_string())) + .map_err(|e| GatewayError::InvalidJsonResponse(e.to_string()))? + .map_err(|e| GatewayError::Request(e.to_string())) } async fn try_gateway_send_payment( @@ -261,8 +263,8 @@ impl GatewayConnection for RealGatewayConnection { contract: OutgoingContract, invoice: LightningInvoice, auth: Signature, - ) -> anyhow::Result, String>> { - let result = reqwest::Client::new() + ) -> Result, GatewayError> { + reqwest::Client::new() .post( gateway_api .into_url() @@ -277,10 +279,11 @@ impl GatewayConnection for RealGatewayConnection { auth, }) .send() - .await? + .await + .map_err(|e| GatewayError::Unreachable(e.to_string()))? .json::, String>>() - .await?; - - Ok(result) + .await + .map_err(|e| GatewayError::InvalidJsonResponse(e.to_string()))? + .map_err(|e| GatewayError::Request(e.to_string())) } } diff --git a/modules/fedimint-lnv2-client/src/lib.rs b/modules/fedimint-lnv2-client/src/lib.rs index 63b8ed59236..0210c46e977 100644 --- a/modules/fedimint-lnv2-client/src/lib.rs +++ b/modules/fedimint-lnv2-client/src/lib.rs @@ -73,10 +73,13 @@ pub struct ReceiveOperationMeta { pub contract: IncomingContract, } -/// Number of blocks until outgoing lightning contracts time out and user +/// Number of blocks until outgoing lightning contracts times out and user /// client can refund it unilaterally pub const EXPIRATION_DELTA_LIMIT_DEFAULT: u64 = 500; +/// A two hour buffer in case either the client or gateway go offline +pub const CONTRACT_CONFIRMATION_BUFFER: u64 = 12; + /// Default expiration time for lightning invoices pub const INVOICE_EXPIRATION_SECONDS_DEFAULT: u32 = 24 * 60 * 60; @@ -177,6 +180,7 @@ pub enum LightningInvoice { #[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize, Decodable, Encodable)] pub struct RoutingInfo { + pub lightning_public_key: PublicKey, pub public_key: PublicKey, pub send_fee_minimum: PaymentFee, pub send_fee_default: PaymentFee, @@ -185,6 +189,16 @@ pub struct RoutingInfo { pub expiration_delta_minimum: u64, } +impl RoutingInfo { + pub fn send_parameters(&self, invoice: &Bolt11Invoice) -> (PaymentFee, u64) { + if invoice.recover_payee_pub_key() == self.lightning_public_key { + (self.send_fee_minimum.clone(), self.expiration_delta_minimum) + } else { + (self.send_fee_default.clone(), self.expiration_delta_default) + } + } +} + #[derive( Debug, Clone, Eq, PartialEq, PartialOrd, Hash, Serialize, Deserialize, Decodable, Encodable, )] @@ -401,22 +415,22 @@ impl LightningClientModule { .expect("32 bytes, within curve order") .keypair(secp256k1::SECP256K1); - let payment_info = self + let routing_info = self .gateway_conn .fetch_routing_info(gateway_api.clone(), &self.federation_id) .await .map_err(SendPaymentError::GatewayError)? .ok_or(SendPaymentError::UnknownFederation)?; - if !payment_info.send_fee_default.le(&payment_fee_limit) { - return Err(SendPaymentError::PaymentFeeExceedsLimit( - payment_info.send_fee_default, - )); + let (send_fee, expiration_delta) = routing_info.send_parameters(&invoice); + + if !send_fee.le(&payment_fee_limit) { + return Err(SendPaymentError::PaymentFeeExceedsLimit(send_fee)); } - if expiration_delta_limit < payment_info.expiration_delta_default { + if expiration_delta_limit < expiration_delta { return Err(SendPaymentError::ExpirationDeltaExceedsLimit( - payment_info.expiration_delta_default, + expiration_delta, )); } @@ -428,9 +442,9 @@ impl LightningClientModule { let contract = OutgoingContract { payment_hash: *invoice.payment_hash(), - amount: payment_info.send_fee_default.add_fee(amount), - expiration: consensus_block_count + payment_info.expiration_delta_default, - claim_pk: payment_info.public_key, + amount: send_fee.add_fee(amount), + expiration: consensus_block_count + expiration_delta + CONTRACT_CONFIRMATION_BUFFER, + claim_pk: routing_info.public_key, refund_pk: refund_keypair.public_key(), ephemeral_pk, }; @@ -723,8 +737,7 @@ impl LightningClientModule { .gateway_conn .fetch_invoice(gateway_api, payload) .await - .map_err(FetchInvoiceError::GatewayError)? - .map_err(FetchInvoiceError::CreateInvoiceError)?; + .map_err(FetchInvoiceError::GatewayError)?; if invoice.payment_hash() != &contract.commitment.payment_hash { return Err(FetchInvoiceError::InvalidInvoicePaymentHash); @@ -877,6 +890,8 @@ pub enum GatewayError { Unreachable(String), #[error("The gateway returned an invalid response: {0}")] InvalidJsonResponse(String), + #[error("The gateway returned an error for this request: {0}")] + Request(String), } #[derive(Error, Debug, Clone, Eq, PartialEq)] @@ -918,8 +933,6 @@ pub enum FetchInvoiceError { PaymentFeeExceedsLimit(PaymentFee), #[error("The total fees required to complete this payment exceed its amount")] DustAmount, - #[error("The gateway considered our request for an invoice invalid: {0}")] - CreateInvoiceError(String), #[error("The invoice's payment hash is incorrect")] InvalidInvoicePaymentHash, #[error("The invoice's amount is incorrect")] diff --git a/modules/fedimint-lnv2-client/src/send_sm.rs b/modules/fedimint-lnv2-client/src/send_sm.rs index cd5ea084075..625f9fe34f9 100644 --- a/modules/fedimint-lnv2-client/src/send_sm.rs +++ b/modules/fedimint-lnv2-client/src/send_sm.rs @@ -172,32 +172,20 @@ impl SendStateMachine { ) .await { - Ok(gateway_response) => match gateway_response { - Ok(gateway_response) => { - if contract.verify_gateway_response(&gateway_response) { - return gateway_response; - } - - error!( - ?gateway_response, - ?contract, - ?invoice, - ?federation_id, - ?gateway_api, - "Invalid gateway response" - ); - } - Err(error) => { - error!( - ?error, - ?contract, - ?invoice, - ?federation_id, - ?gateway_api, - "Gateway returned error" - ); + Ok(send_result) => { + if contract.verify_gateway_response(&send_result) { + return send_result; } - }, + + error!( + ?send_result, + ?contract, + ?invoice, + ?federation_id, + ?gateway_api, + "Invalid gateway response" + ); + } Err(error) => { error!( ?error, @@ -205,7 +193,7 @@ impl SendStateMachine { ?invoice, ?federation_id, ?gateway_api, - "Error while trying to reach gateway" + "Error while trying to send payment via gateway" ); } } diff --git a/modules/fedimint-lnv2-tests/tests/mock.rs b/modules/fedimint-lnv2-tests/tests/mock.rs index 044fbfc5588..2cdabbc15da 100644 --- a/modules/fedimint-lnv2-tests/tests/mock.rs +++ b/modules/fedimint-lnv2-tests/tests/mock.rs @@ -1,6 +1,5 @@ use std::time::Duration; -use anyhow::bail; use bitcoin::hashes::{sha256, Hash}; use bitcoin::secp256k1::{SecretKey, SECP256K1}; use fedimint_core::config::FederationId; @@ -88,6 +87,7 @@ impl GatewayConnection for MockGatewayConnection { _federation_id: &FederationId, ) -> Result, GatewayError> { Ok(Some(RoutingInfo { + lightning_public_key: self.keypair.public_key(), public_key: self.keypair.public_key(), send_fee_default: PaymentFee::SEND_FEE_LIMIT_DEFAULT, send_fee_minimum: PaymentFee::SEND_FEE_MINIMUM, @@ -101,8 +101,8 @@ impl GatewayConnection for MockGatewayConnection { &self, _gateway_api: GatewayEndpoint, payload: CreateBolt11InvoicePayload, - ) -> Result, GatewayError> { - Ok(Ok(InvoiceBuilder::new(Currency::Regtest) + ) -> Result { + Ok(InvoiceBuilder::new(Currency::Regtest) .description(String::new()) .payment_hash(payload.contract.commitment.payment_hash) .current_timestamp() @@ -111,7 +111,7 @@ impl GatewayConnection for MockGatewayConnection { .amount_milli_satoshis(payload.invoice_amount.msats) .expiry_time(Duration::from_secs(payload.expiry_time as u64)) .build_signed(|m| SECP256K1.sign_ecdsa_recoverable(m, &self.keypair.secret_key())) - .unwrap())) + .unwrap()) } async fn try_gateway_send_payment( @@ -121,20 +121,18 @@ impl GatewayConnection for MockGatewayConnection { contract: OutgoingContract, invoice: LightningInvoice, _auth: Signature, - ) -> anyhow::Result, String>> { + ) -> Result, GatewayError> { match invoice { LightningInvoice::Bolt11(invoice) => { if *invoice.payment_secret() == PaymentSecret(GATEWAY_CRASH_PAYMENT_SECRET) { - bail!("Failed to connect to gateway"); + return Err(GatewayError::Unreachable(String::new())); } if *invoice.payment_secret() == PaymentSecret(UNPAYABLE_PAYMENT_SECRET) { - return Ok(Ok(Err(self - .keypair - .sign_schnorr(contract.forfeit_message())))); + return Ok(Err(self.keypair.sign_schnorr(contract.forfeit_message()))); } - Ok(Ok(Ok(MOCK_INVOICE_PREIMAGE))) + Ok(Ok(MOCK_INVOICE_PREIMAGE)) } } } diff --git a/modules/fedimint-lnv2-tests/tests/tests.rs b/modules/fedimint-lnv2-tests/tests/tests.rs index b995f7938b8..526461dd161 100644 --- a/modules/fedimint-lnv2-tests/tests/tests.rs +++ b/modules/fedimint-lnv2-tests/tests/tests.rs @@ -12,7 +12,7 @@ use fedimint_dummy_server::DummyInit; use fedimint_lnv2_client::{ Bolt11InvoiceDescription, LightningClientInit, LightningClientModule, LightningClientStateMachines, LightningOperationMeta, PaymentFee, ReceiveState, - SendPaymentError, SendState, EXPIRATION_DELTA_LIMIT_DEFAULT, + SendPaymentError, SendState, CONTRACT_CONFIRMATION_BUFFER, EXPIRATION_DELTA_LIMIT_DEFAULT, }; use fedimint_lnv2_common::config::LightningGenParams; use fedimint_lnv2_common::{LightningInput, LightningInputV0, OutgoingWitness}; @@ -175,7 +175,7 @@ async fn unilateral_refund_of_outgoing_contracts() -> anyhow::Result<()> { fixtures .bitcoin() - .mine_blocks(EXPIRATION_DELTA_LIMIT_DEFAULT) + .mine_blocks(EXPIRATION_DELTA_LIMIT_DEFAULT + CONTRACT_CONFIRMATION_BUFFER) .await; assert_eq!(sub.ok().await?, SendState::Refunding);