Skip to content

Commit 85bbfcb

Browse files
authored
Merge pull request #1639 from lightninglabs/invoice-acceptor-rfqmath-margin
tapchannel: add a one unit extra tolerance to invoice acceptor
2 parents 4d5b18f + 851b363 commit 85bbfcb

File tree

2 files changed

+126
-1
lines changed

2 files changed

+126
-1
lines changed

tapchannel/aux_invoice_manager.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,8 +264,19 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
264264

265265
// We assume that each shard can have a rounding error of up to 1 asset
266266
// unit. So we allow the final amount to be off by up to 1 asset unit
267-
// per accepted HTLC (plus the one we're currently processing).
267+
// per accepted HTLC (plus the one we're currently processing). The
268+
// plus one here is because the invoice only has previously accepted
269+
// HTLCs committed to it, but we're processing the current HTLC which
270+
// is not yet in that list.
268271
allowedMarginAssetUnits := uint64(len(req.Invoice.Htlcs) + 1)
272+
273+
// Because we do a round trip of units -> milli-sats, then split into
274+
// shards, then back to units and then back to milli-sats again, we lose
275+
// up to numHTLCs+1 asset units in the process. So we allow for an
276+
// additional unit here.
277+
allowedMarginAssetUnits++
278+
279+
// Convert the allowed margin asset units to milli-satoshis.
269280
marginAssetUnits := rfqmath.NewBigIntFixedPoint(
270281
allowedMarginAssetUnits, 0,
271282
)
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
package tapchannel
2+
3+
import (
4+
"testing"
5+
6+
"github.com/lightninglabs/taproot-assets/rfqmath"
7+
"github.com/lightningnetwork/lnd/lnwire"
8+
"github.com/stretchr/testify/require"
9+
"pgregory.net/rapid"
10+
)
11+
12+
// TestUnitConversionTolerance demonstrates the unit conversion loss of
13+
// precision issue that lead to the increased tolerance value in the invoice
14+
// manager.
15+
func TestUnitConversionTolerance(t *testing.T) {
16+
const (
17+
invoiceAmtUnits = 6_000_000
18+
numHTLCs = 2
19+
)
20+
var (
21+
rate = rfqmath.BigIntFixedPoint{
22+
Coefficient: rfqmath.NewBigIntFromUint64(9852216748),
23+
Scale: 0,
24+
}
25+
)
26+
27+
t.Logf("Initial amount in asset units: %d", invoiceAmtUnits)
28+
29+
assetAmountFP := rfqmath.NewBigIntFixedPoint(invoiceAmtUnits, 0)
30+
invoiceAmtMsat := rfqmath.UnitsToMilliSatoshi(assetAmountFP, rate)
31+
32+
t.Logf("Invoice amount in msat: %d", invoiceAmtMsat)
33+
34+
numAssetUnitsFp := rfqmath.MilliSatoshiToUnits(invoiceAmtMsat, rate)
35+
numAssetUnits := numAssetUnitsFp.ScaleTo(0).ToUint64()
36+
37+
msatPerUnit := float64(invoiceAmtMsat) / float64(numAssetUnits)
38+
t.Logf("Calculated msat per asset unit: %.2f", msatPerUnit)
39+
40+
t.Logf("Number of asset units after round trip: %d", numAssetUnits)
41+
42+
shardSizeMSat := invoiceAmtMsat / numHTLCs
43+
44+
shardSizeFP := rfqmath.MilliSatoshiToUnits(shardSizeMSat, rate)
45+
shardSizeUnit := shardSizeFP.ScaleTo(0).ToUint64()
46+
t.Logf("Sum of %d shards in asset units: %d", numHTLCs,
47+
shardSizeUnit*numHTLCs)
48+
49+
shardSumFP := rfqmath.NewBigIntFixedPoint(shardSizeUnit*numHTLCs, 0)
50+
inboundAmountMSat := rfqmath.UnitsToMilliSatoshi(shardSumFP, rate)
51+
52+
t.Logf("Inbound amount in msat: %d", inboundAmountMSat)
53+
t.Logf("Total tolerance required in msat: %d",
54+
invoiceAmtMsat-inboundAmountMSat)
55+
56+
marginAssetUnits := rfqmath.NewBigIntFixedPoint(numHTLCs, 0)
57+
allowedMarginMSat := rfqmath.UnitsToMilliSatoshi(marginAssetUnits, rate)
58+
59+
newMarginAssetUnits := rfqmath.NewBigIntFixedPoint(numHTLCs+1, 0)
60+
newAllowedMarginMSat := rfqmath.UnitsToMilliSatoshi(
61+
newMarginAssetUnits, rate,
62+
)
63+
64+
t.Logf("Old tolerance allowed in msat: %d", allowedMarginMSat)
65+
t.Logf("New tolerance allowed in msat: %d", newAllowedMarginMSat)
66+
}
67+
68+
// TestUnitConversionToleranceRapid uses rapid to randomly draw invoice amounts,
69+
// HTLC counts, and coefficients to test unit conversion tolerance. This ensures
70+
// the conversion logic is robust against a wide range of values.
71+
func TestUnitConversionToleranceRapid(t *testing.T) {
72+
rapid.Check(t, func(t *rapid.T) {
73+
invoiceAmtUnits := rapid.Uint64Range(1, 10_000_000).
74+
Draw(t, "invoiceAmtUnits")
75+
numHTLCs := rapid.Uint64Range(1, 16).
76+
Draw(t, "numHTLCs")
77+
coefficient := rapid.Uint64Range(1, 20_000_000_000).
78+
Draw(t, "coefficient")
79+
80+
rate := rfqmath.BigIntFixedPoint{
81+
Coefficient: rfqmath.NewBigIntFromUint64(coefficient),
82+
Scale: 0,
83+
}
84+
85+
assetAmountFP := rfqmath.NewBigIntFixedPoint(invoiceAmtUnits, 0)
86+
invoiceAmtMsat := rfqmath.UnitsToMilliSatoshi(
87+
assetAmountFP, rate,
88+
)
89+
90+
shardSizeMSat := invoiceAmtMsat / lnwire.MilliSatoshi(numHTLCs)
91+
shardSizeFP := rfqmath.MilliSatoshiToUnits(shardSizeMSat, rate)
92+
shardSizeUnit := shardSizeFP.ScaleTo(0).ToUint64()
93+
94+
shardSumFP := rfqmath.NewBigIntFixedPoint(
95+
shardSizeUnit*numHTLCs, 0,
96+
)
97+
inboundAmountMSat := rfqmath.UnitsToMilliSatoshi(
98+
shardSumFP, rate,
99+
)
100+
101+
newMarginAssetUnits := rfqmath.NewBigIntFixedPoint(
102+
numHTLCs+1, 0,
103+
)
104+
newAllowedMarginMSat := rfqmath.UnitsToMilliSatoshi(
105+
newMarginAssetUnits, rate,
106+
)
107+
108+
// The difference should be within the newly allowed margin.
109+
require.LessOrEqual(
110+
t,
111+
invoiceAmtMsat-inboundAmountMSat, newAllowedMarginMSat,
112+
)
113+
})
114+
}

0 commit comments

Comments
 (0)