Skip to content

Commit

Permalink
Merge pull request #1252 from lightninglabs/enforce-min-amount
Browse files Browse the repository at this point in the history
[custom channels]: enforce minimum amounts
  • Loading branch information
guggero authored Dec 17, 2024
2 parents 7358c1b + 5fcff90 commit a9a2744
Show file tree
Hide file tree
Showing 12 changed files with 599 additions and 291 deletions.
6 changes: 1 addition & 5 deletions rfq/order.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,8 @@ import (
"github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
"github.com/lightningnetwork/lnd/lnutils"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
)
Expand Down Expand Up @@ -248,9 +246,7 @@ func (c *AssetSalePolicy) GenerateInterceptorResponse(
htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse,
error) {

outgoingAmt := lnwire.NewMSatFromSatoshis(lnwallet.DustLimitForSize(
input.UnknownWitnessSize,
))
outgoingAmt := rfqmath.DefaultOnChainHtlcMSat

// Unpack asset ID.
assetID, err := c.AssetSpecifier.UnwrapIdOrErr()
Expand Down
75 changes: 75 additions & 0 deletions rfqmath/convert.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,26 @@ import (
"math"

"github.com/btcsuite/btcd/btcutil"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet"
"github.com/lightningnetwork/lnd/lnwire"
)

var (
// DefaultOnChainHtlcSat is the default amount that we consider as the
// smallest HTLC amount that can be sent on-chain. This needs to be
// greater than the dust limit for an HTLC.
DefaultOnChainHtlcSat = lnwallet.DustLimitForSize(
input.UnknownWitnessSize,
)

// DefaultOnChainHtlcMSat is the default amount that we consider as the
// smallest HTLC amount that can be sent on-chain in milli-satoshis.
DefaultOnChainHtlcMSat = lnwire.NewMSatFromSatoshis(
DefaultOnChainHtlcSat,
)
)

// defaultArithmeticScale is the default scale used for arithmetic operations.
// This is used to ensure that we don't lose precision when doing arithmetic
// operations.
Expand Down Expand Up @@ -97,3 +114,61 @@ func UnitsToMilliSatoshi[N Int[N]](assetUnits,
// along the way.
return lnwire.MilliSatoshi(amtMsat.ScaleTo(0).ToUint64())
}

// MinTransportableUnits computes the minimum number of transportable units
// of an asset given its asset rate and the constant HTLC dust limit. This
// function can be used to enforce a minimum invoice amount to prevent
// forwarding failures due to invalid fees.
//
// Given a wallet end user A, an edge node B, an asset rate of 100 milli-
// satoshi per asset unit and a flat 0.1% routing fee (to simplify the
// scenario), the following invoice based receive events can occur:
// 1. Success case: User A creates an invoice over 5,000 units (500,000 milli-
// satoshis) that is paid by the network. An HTLC over 500,500 milli-
// satoshis arrives at B. B converts the HTLC to 5,000 units and sends
// 354,000 milli-satoshis to A.
// A receives a total "worth" of 854,000 milli-satoshis, which is already
// more than the invoice amount. But at least the forwarding rule in `lnd`
// for B is not violated (outgoing amount mSat < incoming amount mSat).
// 2. Failure case: User A creates an invoice over 3,530 units (353,000 milli-
// satoshis) that is paid by the network. An HTLC over 353,530 milli-
// satoshis arrives at B. B converts the HTLC to 3,530 units and sends
// 354,000 milli-satoshis to A.
// This fails in the `lnd` forwarding logic, because the outgoing amount
// (354,000 milli-satoshis) is greater than the incoming amount (353,530
// milli-satoshis).
func MinTransportableUnits(dustLimit lnwire.MilliSatoshi,
rate BigIntFixedPoint) BigIntFixedPoint {

// We can only transport an asset unit equivalent amount that's greater
// than the dust limit for an HTLC, since we'll always want an HTLC that
// carries an HTLC to be reflected in an on-chain output.
units := MilliSatoshiToUnits(dustLimit, rate)

// If the asset's rate is such that a single unit represents more than
// the dust limit in satoshi, then the above calculation will come out
// as 0. But we can't transport zero units, so we'll set the minimum to
// one unit.
if units.ScaleTo(0).ToUint64() == 0 {
units = NewBigIntFixedPoint(1, 0)
}

return units
}

// MinTransportableMSat computes the minimum amount of milli-satoshis that can
// be represented in a Lightning Network payment when transferring an asset,
// given the asset rate and the constant HTLC dust limit. This function can be
// used to enforce a minimum payable amount with assets, as any invoice amount
// below this value would be uneconomical as the total amount sent would exceed
// the total invoice amount.
func MinTransportableMSat(dustLimit lnwire.MilliSatoshi,
rate BigIntFixedPoint) lnwire.MilliSatoshi {

// We can only transport at least one asset unit in an HTLC. And we
// always have to send out an HTLC with a BTC amount of 354 satoshi. So
// the minimum amount of milli-satoshi we can transport is 354,000 plus
// the milli-satoshi equivalent of a single asset unit.
oneAssetUnit := NewBigIntFixedPoint(1, 0)
return dustLimit + UnitsToMilliSatoshi(oneAssetUnit, rate)
}
47 changes: 37 additions & 10 deletions rfqmath/convert_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -373,9 +373,11 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
t.Parallel()

testCases := []struct {
invoiceAmount lnwire.MilliSatoshi
price FixedPoint[BigInt]
expectedUnits uint64
invoiceAmount lnwire.MilliSatoshi
price FixedPoint[BigInt]
expectedUnits uint64
expectedMinTransportUnits uint64
expectedMinTransportMSat lnwire.MilliSatoshi
}{
{
// 5k USD per BTC @ decimal display 2.
Expand All @@ -384,7 +386,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(5_000_00),
Scale: 2,
},
expectedUnits: 1,
expectedUnits: 1,
expectedMinTransportUnits: 1,
expectedMinTransportMSat: 20_354_000,
},
{
// 5k USD per BTC @ decimal display 6.
Expand All @@ -393,7 +397,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(5_000_00),
Scale: 2,
}.ScaleTo(6),
expectedUnits: 10_000,
expectedUnits: 10_000,
expectedMinTransportUnits: 1,
expectedMinTransportMSat: 20_354_000,
},
{
// 50k USD per BTC @ decimal display 6.
Expand All @@ -402,7 +408,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(50_702_00),
Scale: 2,
}.ScaleTo(6),
expectedUnits: 1000,
expectedUnits: 1000,
expectedMinTransportUnits: 1,
expectedMinTransportMSat: 2_326_308,
},
{
// 50M USD per BTC @ decimal display 6.
Expand All @@ -411,7 +419,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(50_702_000_00),
Scale: 2,
}.ScaleTo(6),
expectedUnits: 62595061158,
expectedUnits: 62595061158,
expectedMinTransportUnits: 179,
expectedMinTransportMSat: 355_972,
},
{
// 50k USD per BTC @ decimal display 6.
Expand All @@ -420,7 +430,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(50_702_12),
Scale: 2,
}.ScaleTo(6),
expectedUnits: 2_570,
expectedUnits: 2_570,
expectedMinTransportUnits: 1,
expectedMinTransportMSat: 2_326_304,
},
{
// 7.341M JPY per BTC @ decimal display 6.
Expand All @@ -429,7 +441,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(7_341_847),
Scale: 0,
}.ScaleTo(6),
expectedUnits: 367_092,
expectedUnits: 367_092,
expectedMinTransportUnits: 25,
expectedMinTransportMSat: 367_620,
},
{
// 7.341M JPY per BTC @ decimal display 2.
Expand All @@ -438,7 +452,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
Coefficient: newBig(7_341_847),
Scale: 0,
}.ScaleTo(4),
expectedUnits: 3_670,
expectedUnits: 3_670,
expectedMinTransportUnits: 25,
expectedMinTransportMSat: 367_620,
},
}

Expand All @@ -454,6 +470,17 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {

diff := tc.invoiceAmount - mSat
require.LessOrEqual(t, diff, uint64(2), "mSAT diff")

minUnitsFP := MinTransportableUnits(
DefaultOnChainHtlcMSat, tc.price,
)
minUnits := minUnitsFP.ScaleTo(0).ToUint64()
require.Equal(t, tc.expectedMinTransportUnits, minUnits)

minMSat := MinTransportableMSat(
DefaultOnChainHtlcMSat, tc.price,
)
require.Equal(t, tc.expectedMinTransportMSat, minMSat)
})
}
}
Expand Down
Loading

0 comments on commit a9a2744

Please sign in to comment.