Skip to content

Commit

Permalink
Merge pull request fedimint#5881 from joschisan/lnv2_send_fee_opt
Browse files Browse the repository at this point in the history
fix: omit ln fee for direct swaps
  • Loading branch information
joschisan authored Aug 19, 2024
2 parents b2a8f0b + d516605 commit e55f89a
Show file tree
Hide file tree
Showing 8 changed files with 97 additions and 83 deletions.
2 changes: 1 addition & 1 deletion gateway/ln-gateway/src/gateway_module_v2/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
41 changes: 25 additions & 16 deletions gateway/ln-gateway/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1569,20 +1569,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<RoutingInfo> {
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<Option<RoutingInfo>> {
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(
Expand Down Expand Up @@ -1624,8 +1633,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");
Expand Down
5 changes: 4 additions & 1 deletion gateway/ln-gateway/src/rpc/rpc_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -386,7 +386,10 @@ async fn routing_info_v2(
Extension(gateway): Extension<Arc<Gateway>>,
Json(federation_id): Json<FederationId>,
) -> Json<Value> {
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(
Expand Down
27 changes: 15 additions & 12 deletions modules/fedimint-lnv2-client/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,7 +197,7 @@ pub trait GatewayConnection: std::fmt::Debug {
&self,
gateway_api: GatewayEndpoint,
payload: CreateBolt11InvoicePayload,
) -> Result<Result<Bolt11Invoice, String>, GatewayError>;
) -> Result<Bolt11Invoice, GatewayError>;

async fn try_gateway_send_payment(
&self,
Expand All @@ -206,7 +206,7 @@ pub trait GatewayConnection: std::fmt::Debug {
contract: OutgoingContract,
invoice: LightningInvoice,
auth: Signature,
) -> anyhow::Result<Result<Result<[u8; 32], Signature>, String>>;
) -> Result<Result<[u8; 32], Signature>, GatewayError>;
}

#[derive(Debug)]
Expand All @@ -231,16 +231,17 @@ impl GatewayConnection for RealGatewayConnection {
.send()
.await
.map_err(|e| GatewayError::Unreachable(e.to_string()))?
.json::<Option<RoutingInfo>>()
.json::<Result<Option<RoutingInfo>, 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<Result<Bolt11Invoice, String>, GatewayError> {
) -> Result<Bolt11Invoice, GatewayError> {
reqwest::Client::new()
.post(
gateway_api
Expand All @@ -255,7 +256,8 @@ impl GatewayConnection for RealGatewayConnection {
.map_err(|e| GatewayError::Unreachable(e.to_string()))?
.json::<Result<Bolt11Invoice, 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 try_gateway_send_payment(
Expand All @@ -265,8 +267,8 @@ impl GatewayConnection for RealGatewayConnection {
contract: OutgoingContract,
invoice: LightningInvoice,
auth: Signature,
) -> anyhow::Result<Result<Result<[u8; 32], Signature>, String>> {
let result = reqwest::Client::new()
) -> Result<Result<[u8; 32], Signature>, GatewayError> {
reqwest::Client::new()
.post(
gateway_api
.into_url()
Expand All @@ -281,10 +283,11 @@ impl GatewayConnection for RealGatewayConnection {
auth,
})
.send()
.await?
.await
.map_err(|e| GatewayError::Unreachable(e.to_string()))?
.json::<Result<Result<[u8; 32], Signature>, String>>()
.await?;

Ok(result)
.await
.map_err(|e| GatewayError::InvalidJsonResponse(e.to_string()))?
.map_err(|e| GatewayError::Request(e.to_string()))
}
}
43 changes: 28 additions & 15 deletions modules/fedimint-lnv2-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -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,
Expand All @@ -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,
)]
Expand Down Expand Up @@ -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,
));
}

Expand All @@ -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,
};
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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")]
Expand Down
40 changes: 14 additions & 26 deletions modules/fedimint-lnv2-client/src/send_sm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -172,40 +172,28 @@ 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,
?contract,
?invoice,
?federation_id,
?gateway_api,
"Error while trying to reach gateway"
"Error while trying to send payment via gateway"
);
}
}
Expand Down
18 changes: 8 additions & 10 deletions modules/fedimint-lnv2-tests/tests/mock.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -88,6 +87,7 @@ impl GatewayConnection for MockGatewayConnection {
_federation_id: &FederationId,
) -> Result<Option<RoutingInfo>, 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,
Expand All @@ -101,8 +101,8 @@ impl GatewayConnection for MockGatewayConnection {
&self,
_gateway_api: GatewayEndpoint,
payload: CreateBolt11InvoicePayload,
) -> Result<Result<Bolt11Invoice, String>, GatewayError> {
Ok(Ok(InvoiceBuilder::new(Currency::Regtest)
) -> Result<Bolt11Invoice, GatewayError> {
Ok(InvoiceBuilder::new(Currency::Regtest)
.description(String::new())
.payment_hash(payload.contract.commitment.payment_hash)
.current_timestamp()
Expand All @@ -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(
Expand All @@ -121,20 +121,18 @@ impl GatewayConnection for MockGatewayConnection {
contract: OutgoingContract,
invoice: LightningInvoice,
_auth: Signature,
) -> anyhow::Result<Result<Result<[u8; 32], Signature>, String>> {
) -> Result<Result<[u8; 32], Signature>, 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))
}
}
}
Expand Down
4 changes: 2 additions & 2 deletions modules/fedimint-lnv2-tests/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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);
Expand Down

0 comments on commit e55f89a

Please sign in to comment.