Skip to content

Commit

Permalink
rpcserver+taprpc: reject small invoice payments
Browse files Browse the repository at this point in the history
With this commit we reject the payment of small invoices by using
assets, if the total amount of sending 1 asset unit plus the
protocol-dictated 354 satoshi for a non-dust HTLC is higher than the
total invoice amount attempting to be paid.
The user can override this decision with a new flag if they wish to
proceed with the uneconomical payment.
  • Loading branch information
guggero committed Dec 17, 2024
1 parent ddd99db commit 5fcff90
Show file tree
Hide file tree
Showing 4 changed files with 170 additions and 69 deletions.
75 changes: 74 additions & 1 deletion rpcserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -7074,8 +7074,24 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
// amount based on the asset-to-BTC conversion rate.
sellOrder := taprpc.MarshalAcceptedSellQuote(*quote)

// paymentMaxAmt is the maximum amount that the counterparty is
// expected to pay. This is the amount that the invoice is
// asking for plus the fee limit in milli-satoshis.
paymentMaxAmt, _, err := r.parseRequest(pReq)
if err != nil {
return err
}

// Check if the payment requires overpayment based on the quote.
err = checkOverpayment(
sellOrder, paymentMaxAmt, req.AllowOverpay,
)
if err != nil {
return err
}

// Send out the information about the quote on the stream.
err := stream.Send(&tchrpc.SendPaymentResponse{
err = stream.Send(&tchrpc.SendPaymentResponse{
Result: &tchrpc.SendPaymentResponse_AcceptedSellOrder{
AcceptedSellOrder: sellOrder,
},
Expand Down Expand Up @@ -7177,6 +7193,14 @@ func (r *rpcServer) SendPayment(req *tchrpc.SendPaymentRequest,
return fmt.Errorf("unexpected response type: %T", r)
}

// Check if the payment requires overpayment based on the quote.
err = checkOverpayment(
acceptedQuote, paymentMaxAmt, req.AllowOverpay,
)
if err != nil {
return err
}

// Send out the information about the quote on the stream.
err = stream.Send(&tchrpc.SendPaymentResponse{
Result: &tchrpc.SendPaymentResponse_AcceptedSellOrder{
Expand Down Expand Up @@ -7320,6 +7344,55 @@ func (r *rpcServer) parseRequest(
return paymentMaxAmt, expiry, nil
}

// checkOverpayment checks if paying a certain invoice amount requires
// overpayment when using assets to pay, given the rate from the accepted quote
// and the minimum non-dust HTLC amount dictated by the protocol.
func checkOverpayment(quote *rfqrpc.PeerAcceptedSellQuote,
paymentAmount lnwire.MilliSatoshi, allowOverpay bool) error {

rateFP, err := rfqrpc.UnmarshalFixedPoint(quote.BidAssetRate)
if err != nil {
return fmt.Errorf("cannot unmarshal asset rate: %w", err)
}

// If the calculated asset amount is zero, we can't pay this amount
// using assets, so we'll reject the payment even if the user has set
// the override flag.
if quote.AssetAmount == 0 {
oneUnit := rfqmath.NewBigIntFixedPoint(1, 0)
oneUnitAsMSat := rfqmath.UnitsToMilliSatoshi(oneUnit, *rateFP)
return fmt.Errorf("rejecting payment of %v (invoice amount + "+
"user-defined routing fee limit), smallest payable "+
"amount with assets is equivalent to %v",
paymentAmount, oneUnitAsMSat)
}

srvrLog.Debugf("Checking if payment is economical (min transportable "+
"mSat: %d, paymentAmount: %d, allowOverpay=%v)",
quote.MinTransportableMsat, paymentAmount, allowOverpay)

// If the override flag is set, we ignore this check and return early.
if allowOverpay {
return nil
}

// If the payment amount is less than the minimal transportable amount
// dictated by the quote, we'll return an error to inform the user. They
// can still override this check if they want to proceed anyway.
if lnwire.MilliSatoshi(quote.MinTransportableMsat) > paymentAmount {
return fmt.Errorf("rejecting payment of %v (invoice "+
"amount + user-defined routing fee limit), minimum "+
"amount for an asset payment is %v mSAT with the "+
"current rate of %v units/BTC; override this check "+
"by specifying the allow_overpay flag",
paymentAmount, quote.MinTransportableMsat,
rateFP.String())
}

// The amount checks out, we can proceed with the payment.
return nil
}

// AddInvoice is a wrapper around lnd's lnrpc.AddInvoice method with asset
// specific parameters. It allows RPC users to create invoices that correspond
// to the specified asset amount.
Expand Down
152 changes: 84 additions & 68 deletions taprpc/tapchannelrpc/tapchannel.pb.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 8 additions & 0 deletions taprpc/tapchannelrpc/tapchannel.proto
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,14 @@ message SendPaymentRequest {
// payment will immediately be dispatched, skipping the rfq negotiation
// phase, and using the following rfq id instead.
bytes rfq_id = 5;

// If a small invoice should be paid that is below the amount that always
// needs to be sent out to carry a single asset unit, then by default the
// payment is rejected. If this flag is set, then the payment will be
// allowed to proceed, even if it is uneconomical, meaning that more sats
// are sent out to the network than the invoice amount plus routing fees
// require to be paid.
bool allow_overpay = 6;
}

message SendPaymentResponse {
Expand Down
Loading

0 comments on commit 5fcff90

Please sign in to comment.