Skip to content

Commit 290c828

Browse files
authored
Merge pull request #1673 from lightninglabs/invoice-tolerance-tweaks
Invoice manager tolerance tweaks
2 parents f0b41a2 + 4df245b commit 290c828

File tree

3 files changed

+59
-21
lines changed

3 files changed

+59
-21
lines changed

docs/release-notes/release-notes-0.7.0.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,18 @@
3535
optimized sending to V2 TAP addresses by removing the need for creating
3636
tombstone outputs on a full-value send (by using interactive transfers for V2
3737
addresses).
38+
3839
- [Updated](https://github.com/lightninglabs/taproot-assets/pull/1774)
3940
`BuyOrderRequest` and `SellOrderRequest` proto docs to mark `peer_pub_key` as
4041
required. Previously, the field was incorrectly documented as optional.
4142
This change corrects the documentation to match the current implementation.
4243

44+
- [Invoice tolerance calculations were fixed to properly account for per-HTLC
45+
conversion errors](https://github.com/lightninglabs/taproot-assets/pull/1673).
46+
This improves the accuracy of asset payment acceptance by correctly modeling
47+
rounding errors that accumulate when converting between asset units and
48+
millisatoshis across multiple HTLCs.
49+
4350
# New Features
4451

4552
## Functional Enhancements

tapchannel/aux_invoice_manager.go

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -290,12 +290,6 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
290290
// is not yet in that list.
291291
allowedMarginAssetUnits := uint64(len(req.Invoice.Htlcs) + 1)
292292

293-
// Because we do a round trip of units -> milli-sats, then split into
294-
// shards, then back to units and then back to milli-sats again, we lose
295-
// up to numHTLCs+1 asset units in the process. So we allow for an
296-
// additional unit here.
297-
allowedMarginAssetUnits++
298-
299293
// Convert the allowed margin asset units to milli-satoshis.
300294
marginAssetUnits := rfqmath.NewBigIntFixedPoint(
301295
allowedMarginAssetUnits, 0,
@@ -304,10 +298,17 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
304298
marginAssetUnits, *assetRate,
305299
)
306300

301+
// Every HTLC makes a roundtrip from msats to units and then back to
302+
// msats right here. Above we account for the asset units that may have
303+
// been lost by our peer when converting their incoming HTLC amount to
304+
// asset units, now we need to account for our loss for converting each
305+
// HTLC to msats.
306+
allowedMarginMSat += lnwire.MilliSatoshi(len(req.Invoice.Htlcs) + 1)
307+
307308
// If the sum of the accepted HTLCs plus the current HTLC amount plus
308309
// the error margin is greater than the invoice amount, we'll accept it.
309310
totalInbound := acceptedHtlcSum + resp.AmtPaid
310-
totalInboundWithMargin := totalInbound + allowedMarginMSat + 1
311+
totalInboundWithMargin := totalInbound + allowedMarginMSat
311312
invoiceValue := lnwire.MilliSatoshi(req.Invoice.ValueMsat)
312313

313314
iLog.Debugf("accepted HTLC sum: %v, current HTLC amount: %v, allowed "+
@@ -318,6 +319,12 @@ func (s *AuxInvoiceManager) handleInvoiceAccept(ctx context.Context,
318319
// If we're within the error margin, we'll increase the current HTLCs
319320
// amount to cover the error rate and make the total sum match the
320321
// invoice amount exactly.
322+
//
323+
// Note: The total calculated margin should match what is implemented in
324+
// our TestUnitConversionToleranceRapid test. That test serves as a
325+
// sanity check for our tolerance model, which should allow all
326+
// combinations of shards and asset rates to be captured by the total
327+
// calculated tolerance.
321328
if totalInboundWithMargin >= invoiceValue {
322329
resp.AmtPaid = invoiceValue - acceptedHtlcSum
323330
}

tapchannel/aux_traffic_shaper_test.go

Lines changed: 38 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ func TestUnitConversionTolerance(t *testing.T) {
1919
)
2020
var (
2121
rate = rfqmath.BigIntFixedPoint{
22-
Coefficient: rfqmath.NewBigIntFromUint64(9852216748),
22+
Coefficient: rfqmath.NewBigIntFromUint64(9_852_216_748),
2323
Scale: 0,
2424
}
2525
)
@@ -61,8 +61,12 @@ func TestUnitConversionTolerance(t *testing.T) {
6161
newMarginAssetUnits, rate,
6262
)
6363

64+
proposedMargin := rfqmath.UnitsToMilliSatoshi(marginAssetUnits, rate) +
65+
lnwire.MilliSatoshi(numHTLCs)
66+
6467
t.Logf("Old tolerance allowed in msat: %d", allowedMarginMSat)
6568
t.Logf("New tolerance allowed in msat: %d", newAllowedMarginMSat)
69+
t.Logf("Proposed tolerance allowed in msat: %d", proposedMargin)
6670
}
6771

6872
// TestUnitConversionToleranceRapid uses rapid to randomly draw invoice amounts,
@@ -74,7 +78,7 @@ func TestUnitConversionToleranceRapid(t *testing.T) {
7478
Draw(t, "invoiceAmtUnits")
7579
numHTLCs := rapid.Uint64Range(1, 16).
7680
Draw(t, "numHTLCs")
77-
coefficient := rapid.Uint64Range(1, 20_000_000_000).
81+
coefficient := rapid.Uint64Range(1, 300_000_000_000).
7882
Draw(t, "coefficient")
7983

8084
rate := rfqmath.BigIntFixedPoint{
@@ -89,26 +93,46 @@ func TestUnitConversionToleranceRapid(t *testing.T) {
8993

9094
shardSizeMSat := invoiceAmtMsat / lnwire.MilliSatoshi(numHTLCs)
9195
shardSizeFP := rfqmath.MilliSatoshiToUnits(shardSizeMSat, rate)
92-
shardSizeUnit := shardSizeFP.ScaleTo(0).ToUint64()
9396

94-
shardSumFP := rfqmath.NewBigIntFixedPoint(
95-
shardSizeUnit*numHTLCs, 0,
96-
)
97-
inboundAmountMSat := rfqmath.UnitsToMilliSatoshi(
98-
shardSumFP, rate,
99-
)
97+
// In order to account for the full invoice amt we need to also
98+
// somehow carry the remainder of invoiceAmt/numHTLCs. We do so
99+
// by appending it to the last shard.
100+
invoiceMod := invoiceAmtMsat % lnwire.MilliSatoshi(numHTLCs)
101+
lastShardMsat := shardSizeMSat + invoiceMod
102+
lastShardFP := rfqmath.MilliSatoshiToUnits(lastShardMsat, rate)
103+
104+
var inboundAmountMSat lnwire.MilliSatoshi
105+
106+
// Each shard calls individually into the rfqmath library. This
107+
// allows for a total error that scales with the number of
108+
// shards. We go up to numHTLCS-1 because we want to add any
109+
// leftovers to the last shard.
110+
for range numHTLCs - 1 {
111+
shardPartialSum := rfqmath.UnitsToMilliSatoshi(
112+
shardSizeFP, rate,
113+
)
114+
115+
inboundAmountMSat += shardPartialSum
116+
}
117+
118+
// Make a single pass over the last shard, which may also carry
119+
// the remainder of the payment amt.
120+
lastShardMsat = rfqmath.UnitsToMilliSatoshi(lastShardFP, rate)
121+
inboundAmountMSat += lastShardMsat
100122

101-
newMarginAssetUnits := rfqmath.NewBigIntFixedPoint(
102-
numHTLCs+1, 0,
123+
allowedMarginUnits := rfqmath.NewBigIntFixedPoint(
124+
numHTLCs, 0,
103125
)
104-
newAllowedMarginMSat := rfqmath.UnitsToMilliSatoshi(
105-
newMarginAssetUnits, rate,
126+
marginAmt := rfqmath.UnitsToMilliSatoshi(
127+
allowedMarginUnits, rate,
106128
)
107129

130+
marginAmt += lnwire.MilliSatoshi(numHTLCs)
131+
108132
// The difference should be within the newly allowed margin.
109133
require.LessOrEqual(
110134
t,
111-
invoiceAmtMsat-inboundAmountMSat, newAllowedMarginMSat,
135+
invoiceAmtMsat-inboundAmountMSat, marginAmt,
112136
)
113137
})
114138
}

0 commit comments

Comments
 (0)