Skip to content

Commit 851b363

Browse files
GeorgeTsagkguggero
authored andcommitted
tapchannel: add an extra unit of tolerance to invoice acceptor
When converting an mSAT amount to units and vice versa we frequently get an off-by-1-unit result. This is already accounted for in our invoice acceptor by adding the value of as many asset units as HTLCs in the invoice. What we didn't account for is that the UnitsToMilliSatoshi calculation for the margin can also be off by 1 msat. Allowing for an extra unit of tolerance should fix those off-by-one errors.
1 parent ce81f8f commit 851b363

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)