Skip to content
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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why do we have to send out 354 sats? Or does this only apply while we push 354 sats each time?

Assuming that's lifted, then we only push an asset value, allowing the routing node to send any amount outgoing, since we don't need to anchor any assets to that outgoing HTLC.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, this calculation only applies until we add the symmetric HTLC trick. Then we can delete both of these functions (I think).

// 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
Loading