Skip to content

Commit a9a2744

Browse files
authored
Merge pull request #1252 from lightninglabs/enforce-min-amount
[custom channels]: enforce minimum amounts
2 parents 7358c1b + 5fcff90 commit a9a2744

File tree

12 files changed

+599
-291
lines changed

12 files changed

+599
-291
lines changed

rfq/order.go

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,8 @@ import (
1414
"github.com/lightninglabs/taproot-assets/rfqmath"
1515
"github.com/lightninglabs/taproot-assets/rfqmsg"
1616
"github.com/lightningnetwork/lnd/channeldb/models"
17-
"github.com/lightningnetwork/lnd/input"
1817
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
1918
"github.com/lightningnetwork/lnd/lnutils"
20-
"github.com/lightningnetwork/lnd/lnwallet"
2119
"github.com/lightningnetwork/lnd/lnwire"
2220
"github.com/lightningnetwork/lnd/tlv"
2321
)
@@ -248,9 +246,7 @@ func (c *AssetSalePolicy) GenerateInterceptorResponse(
248246
htlc lndclient.InterceptedHtlc) (*lndclient.InterceptedHtlcResponse,
249247
error) {
250248

251-
outgoingAmt := lnwire.NewMSatFromSatoshis(lnwallet.DustLimitForSize(
252-
input.UnknownWitnessSize,
253-
))
249+
outgoingAmt := rfqmath.DefaultOnChainHtlcMSat
254250

255251
// Unpack asset ID.
256252
assetID, err := c.AssetSpecifier.UnwrapIdOrErr()

rfqmath/convert.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,26 @@ import (
44
"math"
55

66
"github.com/btcsuite/btcd/btcutil"
7+
"github.com/lightningnetwork/lnd/input"
8+
"github.com/lightningnetwork/lnd/lnwallet"
79
"github.com/lightningnetwork/lnd/lnwire"
810
)
911

12+
var (
13+
// DefaultOnChainHtlcSat is the default amount that we consider as the
14+
// smallest HTLC amount that can be sent on-chain. This needs to be
15+
// greater than the dust limit for an HTLC.
16+
DefaultOnChainHtlcSat = lnwallet.DustLimitForSize(
17+
input.UnknownWitnessSize,
18+
)
19+
20+
// DefaultOnChainHtlcMSat is the default amount that we consider as the
21+
// smallest HTLC amount that can be sent on-chain in milli-satoshis.
22+
DefaultOnChainHtlcMSat = lnwire.NewMSatFromSatoshis(
23+
DefaultOnChainHtlcSat,
24+
)
25+
)
26+
1027
// defaultArithmeticScale is the default scale used for arithmetic operations.
1128
// This is used to ensure that we don't lose precision when doing arithmetic
1229
// operations.
@@ -97,3 +114,61 @@ func UnitsToMilliSatoshi[N Int[N]](assetUnits,
97114
// along the way.
98115
return lnwire.MilliSatoshi(amtMsat.ScaleTo(0).ToUint64())
99116
}
117+
118+
// MinTransportableUnits computes the minimum number of transportable units
119+
// of an asset given its asset rate and the constant HTLC dust limit. This
120+
// function can be used to enforce a minimum invoice amount to prevent
121+
// forwarding failures due to invalid fees.
122+
//
123+
// Given a wallet end user A, an edge node B, an asset rate of 100 milli-
124+
// satoshi per asset unit and a flat 0.1% routing fee (to simplify the
125+
// scenario), the following invoice based receive events can occur:
126+
// 1. Success case: User A creates an invoice over 5,000 units (500,000 milli-
127+
// satoshis) that is paid by the network. An HTLC over 500,500 milli-
128+
// satoshis arrives at B. B converts the HTLC to 5,000 units and sends
129+
// 354,000 milli-satoshis to A.
130+
// A receives a total "worth" of 854,000 milli-satoshis, which is already
131+
// more than the invoice amount. But at least the forwarding rule in `lnd`
132+
// for B is not violated (outgoing amount mSat < incoming amount mSat).
133+
// 2. Failure case: User A creates an invoice over 3,530 units (353,000 milli-
134+
// satoshis) that is paid by the network. An HTLC over 353,530 milli-
135+
// satoshis arrives at B. B converts the HTLC to 3,530 units and sends
136+
// 354,000 milli-satoshis to A.
137+
// This fails in the `lnd` forwarding logic, because the outgoing amount
138+
// (354,000 milli-satoshis) is greater than the incoming amount (353,530
139+
// milli-satoshis).
140+
func MinTransportableUnits(dustLimit lnwire.MilliSatoshi,
141+
rate BigIntFixedPoint) BigIntFixedPoint {
142+
143+
// We can only transport an asset unit equivalent amount that's greater
144+
// than the dust limit for an HTLC, since we'll always want an HTLC that
145+
// carries an HTLC to be reflected in an on-chain output.
146+
units := MilliSatoshiToUnits(dustLimit, rate)
147+
148+
// If the asset's rate is such that a single unit represents more than
149+
// the dust limit in satoshi, then the above calculation will come out
150+
// as 0. But we can't transport zero units, so we'll set the minimum to
151+
// one unit.
152+
if units.ScaleTo(0).ToUint64() == 0 {
153+
units = NewBigIntFixedPoint(1, 0)
154+
}
155+
156+
return units
157+
}
158+
159+
// MinTransportableMSat computes the minimum amount of milli-satoshis that can
160+
// be represented in a Lightning Network payment when transferring an asset,
161+
// given the asset rate and the constant HTLC dust limit. This function can be
162+
// used to enforce a minimum payable amount with assets, as any invoice amount
163+
// below this value would be uneconomical as the total amount sent would exceed
164+
// the total invoice amount.
165+
func MinTransportableMSat(dustLimit lnwire.MilliSatoshi,
166+
rate BigIntFixedPoint) lnwire.MilliSatoshi {
167+
168+
// We can only transport at least one asset unit in an HTLC. And we
169+
// always have to send out an HTLC with a BTC amount of 354 satoshi. So
170+
// the minimum amount of milli-satoshi we can transport is 354,000 plus
171+
// the milli-satoshi equivalent of a single asset unit.
172+
oneAssetUnit := NewBigIntFixedPoint(1, 0)
173+
return dustLimit + UnitsToMilliSatoshi(oneAssetUnit, rate)
174+
}

rfqmath/convert_test.go

Lines changed: 37 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -373,9 +373,11 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
373373
t.Parallel()
374374

375375
testCases := []struct {
376-
invoiceAmount lnwire.MilliSatoshi
377-
price FixedPoint[BigInt]
378-
expectedUnits uint64
376+
invoiceAmount lnwire.MilliSatoshi
377+
price FixedPoint[BigInt]
378+
expectedUnits uint64
379+
expectedMinTransportUnits uint64
380+
expectedMinTransportMSat lnwire.MilliSatoshi
379381
}{
380382
{
381383
// 5k USD per BTC @ decimal display 2.
@@ -384,7 +386,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
384386
Coefficient: newBig(5_000_00),
385387
Scale: 2,
386388
},
387-
expectedUnits: 1,
389+
expectedUnits: 1,
390+
expectedMinTransportUnits: 1,
391+
expectedMinTransportMSat: 20_354_000,
388392
},
389393
{
390394
// 5k USD per BTC @ decimal display 6.
@@ -393,7 +397,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
393397
Coefficient: newBig(5_000_00),
394398
Scale: 2,
395399
}.ScaleTo(6),
396-
expectedUnits: 10_000,
400+
expectedUnits: 10_000,
401+
expectedMinTransportUnits: 1,
402+
expectedMinTransportMSat: 20_354_000,
397403
},
398404
{
399405
// 50k USD per BTC @ decimal display 6.
@@ -402,7 +408,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
402408
Coefficient: newBig(50_702_00),
403409
Scale: 2,
404410
}.ScaleTo(6),
405-
expectedUnits: 1000,
411+
expectedUnits: 1000,
412+
expectedMinTransportUnits: 1,
413+
expectedMinTransportMSat: 2_326_308,
406414
},
407415
{
408416
// 50M USD per BTC @ decimal display 6.
@@ -411,7 +419,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
411419
Coefficient: newBig(50_702_000_00),
412420
Scale: 2,
413421
}.ScaleTo(6),
414-
expectedUnits: 62595061158,
422+
expectedUnits: 62595061158,
423+
expectedMinTransportUnits: 179,
424+
expectedMinTransportMSat: 355_972,
415425
},
416426
{
417427
// 50k USD per BTC @ decimal display 6.
@@ -420,7 +430,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
420430
Coefficient: newBig(50_702_12),
421431
Scale: 2,
422432
}.ScaleTo(6),
423-
expectedUnits: 2_570,
433+
expectedUnits: 2_570,
434+
expectedMinTransportUnits: 1,
435+
expectedMinTransportMSat: 2_326_304,
424436
},
425437
{
426438
// 7.341M JPY per BTC @ decimal display 6.
@@ -429,7 +441,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
429441
Coefficient: newBig(7_341_847),
430442
Scale: 0,
431443
}.ScaleTo(6),
432-
expectedUnits: 367_092,
444+
expectedUnits: 367_092,
445+
expectedMinTransportUnits: 25,
446+
expectedMinTransportMSat: 367_620,
433447
},
434448
{
435449
// 7.341M JPY per BTC @ decimal display 2.
@@ -438,7 +452,9 @@ func TestConvertMilliSatoshiToUnits(t *testing.T) {
438452
Coefficient: newBig(7_341_847),
439453
Scale: 0,
440454
}.ScaleTo(4),
441-
expectedUnits: 3_670,
455+
expectedUnits: 3_670,
456+
expectedMinTransportUnits: 25,
457+
expectedMinTransportMSat: 367_620,
442458
},
443459
}
444460

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

455471
diff := tc.invoiceAmount - mSat
456472
require.LessOrEqual(t, diff, uint64(2), "mSAT diff")
473+
474+
minUnitsFP := MinTransportableUnits(
475+
DefaultOnChainHtlcMSat, tc.price,
476+
)
477+
minUnits := minUnitsFP.ScaleTo(0).ToUint64()
478+
require.Equal(t, tc.expectedMinTransportUnits, minUnits)
479+
480+
minMSat := MinTransportableMSat(
481+
DefaultOnChainHtlcMSat, tc.price,
482+
)
483+
require.Equal(t, tc.expectedMinTransportMSat, minMSat)
457484
})
458485
}
459486
}

0 commit comments

Comments
 (0)