htlcswitch: add receiver-side inbound fee support

This commit is contained in:
Joost Jager committed 2024-03-31 16:49:19 +02:00
1 parent 3e6adbf1c0
commit e8c97deaef
10 files changed
+388 -19

No files matched your search

+1
View File
@@ -247,6 +247,7 @@ type ChannelLink interface {
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
inboundFee models.InboundFee,
heightNow uint32, scid lnwire.ShortChannelID) *LinkError
// CheckHtlcTransit should return a nil error if the passed HTLC details
+30 -9
View File
@@ -2780,28 +2780,43 @@ func (l *channelLink) UpdateForwardingPolicy(
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
inboundFee models.InboundFee,
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {
l.RLock()
policy := l.cfg.FwrdingPolicy
l.RUnlock()
// Using the amount of the incoming HTLC, we'll calculate the expected
// fee this incoming HTLC must carry in order to satisfy the
// constraints of the outgoing link.
expectedFee := ExpectedFee(policy, amtToForward)
// Using the outgoing HTLC amount, we'll calculate the outgoing
// fee this incoming HTLC must carry in order to satisfy the constraints
// of the outgoing link.
outFee := ExpectedFee(policy, amtToForward)
// Then calculate the inbound fee that we charge based on the sum of
// outgoing HTLC amount and outgoing fee.
inFee := inboundFee.CalcFee(amtToForward + outFee)
// Add up both fee components. It is important to calculate both fees
// separately. An alternative way of calculating is to first determine
// an aggregate fee and apply that to the outgoing HTLC amount. However,
// rounding may cause the result to be slightly higher than in the case
// of separately rounded fee components. This potentially causes failed
// forwards for senders and is something to be avoided.
expectedFee := inFee + int64(outFee)
// If the actual fee is less than our expected fee, then we'll reject
// this HTLC as it didn't provide a sufficient amount of fees, or the
// values have been tampered with, or the send used incorrect/dated
// information to construct the forwarding information for this hop. In
// any case, we'll cancel this HTLC. We're checking for this case first
// to leak as little information as possible.
actualFee := incomingHtlcAmt - amtToForward
// any case, we'll cancel this HTLC.
actualFee := int64(incomingHtlcAmt) - int64(amtToForward)
if incomingHtlcAmt < amtToForward || actualFee < expectedFee {
l.log.Warnf("outgoing htlc(%x) has insufficient fee: "+
"expected %v, got %v",
payHash[:], int64(expectedFee), int64(actualFee))
"expected %v, got %v: incoming=%v, outgoing=%v, "+
"inboundFee=%v",
payHash[:], expectedFee, actualFee,
incomingHtlcAmt, amtToForward, inboundFee,
)
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
@@ -3330,6 +3345,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
// round of processing.
chanIterator.EncodeNextHop(buf)
inboundFee := l.cfg.FwrdingPolicy.InboundFee
updatePacket := &htlcPacket{
incomingChanID: l.ShortChanID(),
incomingHTLCID: pd.HtlcIndex,
@@ -3342,6 +3359,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
incomingTimeout: pd.Timeout,
outgoingTimeout: fwdInfo.OutgoingCTLV,
customRecords: pld.CustomRecords(),
inboundFee: inboundFee,
}
switchPackets = append(
switchPackets, updatePacket,
@@ -3394,6 +3412,8 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
// have been added to switchPackets at the top of this
// section.
if fwdPkg.State == channeldb.FwdStateLockedIn {
inboundFee := l.cfg.FwrdingPolicy.InboundFee
updatePacket := &htlcPacket{
incomingChanID: l.ShortChanID(),
incomingHTLCID: pd.HtlcIndex,
@@ -3406,6 +3426,7 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
incomingTimeout: pd.Timeout,
outgoingTimeout: fwdInfo.OutgoingCTLV,
customRecords: pld.CustomRecords(),
inboundFee: inboundFee,
}
fwdPkg.FwdFilter.Set(idx)
+246 -8
View File
@@ -643,6 +643,206 @@ func testChannelLinkMultiHopPayment(t *testing.T,
}
}
func TestChannelLinkInboundFee(t *testing.T) {
t.Parallel()
t.Run("negative", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -500,
Rate: -100,
}
// Bob is supposed to sent Carol 1000000 msats. For this, he
// will charge an out fee of 1000 msat (the default hop network
// policy). Bob's inbound fee is based on the sum of outgoing
// htlc amount and the out fee that Bob charges. The value of
// this sum is 1001000. The proportional component of the
// inbound fee is -0.01% of the sum, which is -100 (rounded
// up). Added to this is the base inbound fee of -500, making
// for a total inbound fee of -600.
const expectedBobInFee = -600
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
t.Run("negative overpaid", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -500,
Rate: -100,
}
// Alice is not aware of the inbound discount and pays the full
// outbound fee.
const expectedBobInFee = 0
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
t.Run("negative total", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: -5000,
}
const expectedBobInFee = -5000
// Bob's inbound discount exceeds his outbound fee. Forwards
// carrying a negative total fee should be rejected.
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, true,
)
})
t.Run("positive", func(t *testing.T) {
t.Parallel()
bobInboundFee := models.InboundFee{
Base: 1_000,
Rate: 100_000,
}
const expectedBobInFee = 101_100
testChannelLinkInboundFee(
t, bobInboundFee, expectedBobInFee, false,
)
})
}
func testChannelLinkInboundFee(t *testing.T, //nolint:thelper
bobInboundFee models.InboundFee, expectedBobInFee int64,
expectedFail bool) {
channels, _, err := createClusterChannels(
t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5,
)
require.NoError(t, err, "unable to create channel")
n := newThreeHopNetwork(t, channels.aliceToBob, channels.bobToAlice,
channels.bobToCarol, channels.carolToBob, testStartingHeight)
require.NoError(t, n.start())
defer n.stop()
bobPolicy := n.globalPolicy
bobPolicy.InboundFee = bobInboundFee
n.firstBobChannelLink.UpdateForwardingPolicy(bobPolicy)
// Set an inbound fee for Carol. Because Carol is the payee, the fee
// should not be applied.
carolPolicy := n.globalPolicy
carolPolicy.InboundFee = models.InboundFee{
Base: -2_000,
Rate: -200_000,
}
n.carolChannelLink.UpdateForwardingPolicy(carolPolicy)
carolBandwidthBefore := n.carolChannelLink.Bandwidth()
firstBobBandwidthBefore := n.firstBobChannelLink.Bandwidth()
secondBobBandwidthBefore := n.secondBobChannelLink.Bandwidth()
aliceBandwidthBefore := n.aliceChannelLink.Bandwidth()
const (
expectedCarolInboundFee = 0
// Expect Bob's outbound fee to match the default hop network
// policy.
expectedBobOutboundFee = 1_000
)
amount := lnwire.MilliSatoshi(1_000_000)
htlcAmt := lnwire.MilliSatoshi(1_000_000 +
expectedCarolInboundFee + expectedBobOutboundFee +
expectedBobInFee,
)
totalTimelock := uint32(112)
hops := []*hop.Payload{
{
FwdInfo: hop.ForwardingInfo{
NextHop: n.carolChannelLink.
ShortChanID(),
AmountToForward: 1_000_000,
OutgoingCTLV: 106,
},
},
{
FwdInfo: hop.ForwardingInfo{
AmountToForward: 1_000_000,
OutgoingCTLV: 106,
},
},
}
receiver := n.carolServer
firstHop := n.firstBobChannelLink.ShortChanID()
rhash, err := makePayment(
n.aliceServer, n.carolServer, firstHop, hops, amount, htlcAmt,
totalTimelock,
).Wait(30 * time.Second)
if expectedFail {
require.Error(t, err)
return
}
require.NoError(t, err, "unable to send payment")
// Wait for Alice and Bob's second link to receive the revocation.
time.Sleep(2 * time.Second)
// Check that Carol invoice was settled and bandwidth of HTLC
// links were changed.
invoice, err := receiver.registry.LookupInvoice(
context.Background(), rhash,
)
require.NoError(t, err, "unable to get invoice")
require.Equal(t, invpkg.ContractSettled, invoice.State,
"carol invoice haven't been settled")
expectedAliceBandwidth := aliceBandwidthBefore - htlcAmt
require.Equalf(t,
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedAliceBandwidth, n.aliceChannelLink.Bandwidth(),
)
expectedBobBandwidth1 := firstBobBandwidthBefore + htlcAmt
require.Equalf(t,
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth1, n.firstBobChannelLink.Bandwidth(),
)
bobCarolDelta := lnwire.MilliSatoshi(
int64(amount) + expectedCarolInboundFee,
)
expectedBobBandwidth2 := secondBobBandwidthBefore - bobCarolDelta
require.Equalf(t,
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedBobBandwidth2, n.secondBobChannelLink.Bandwidth(),
)
expectedCarolBandwidth := carolBandwidthBefore + bobCarolDelta
require.Equalf(t,
expectedCarolBandwidth, n.carolChannelLink.Bandwidth(),
"channel bandwidth incorrect: expected %v, got %v",
expectedCarolBandwidth, n.carolChannelLink.Bandwidth(),
)
}
// TestChannelLinkCancelFullCommitment tests the ability for links to cancel
// forwarded HTLCs once all of their commitment slots are full.
func TestChannelLinkCancelFullCommitment(t *testing.T) {
@@ -5994,7 +6194,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("satisfied", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
@@ -6002,7 +6204,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("below minhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 100, 50,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailAmountBelowMinimum); !ok {
t.Fatalf("expected FailAmountBelowMinimum failure code")
}
@@ -6010,7 +6214,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("above maxhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1200,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected FailTemporaryChannelFailure failure code")
}
@@ -6018,7 +6224,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("insufficient fee", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1005, 1000,
200, 150, 0, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
@@ -6031,7 +6239,7 @@ func TestCheckHtlcForward(t *testing.T) {
result := link.CheckHtlcForward(
hash, 100005, 100000, 200,
150, 0, lnwire.ShortChannelID{},
150, models.InboundFee{}, 0, lnwire.ShortChannelID{},
)
_, ok := result.WireMessage().(*lnwire.FailFeeInsufficient)
require.True(t, ok, "expected FailFeeInsufficient failure code")
@@ -6039,7 +6247,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("expiry too soon", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 190, lnwire.ShortChannelID{})
200, 150, models.InboundFee{}, 190,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooSoon); !ok {
t.Fatalf("expected FailExpiryTooSoon failure code")
}
@@ -6047,7 +6257,9 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("incorrect cltv expiry", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 190, 0, lnwire.ShortChannelID{})
200, 190, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailIncorrectCltvExpiry); !ok {
t.Fatalf("expected FailIncorrectCltvExpiry failure code")
}
@@ -6057,11 +6269,37 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("cltv expiry too far in the future", func(t *testing.T) {
// Check that expiry isn't too far in the future.
result := link.CheckHtlcForward(hash, 1500, 1000,
10200, 10100, 0, lnwire.ShortChannelID{})
10200, 10100, models.InboundFee{}, 0,
lnwire.ShortChannelID{},
)
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooFar); !ok {
t.Fatalf("expected FailExpiryTooFar failure code")
}
})
t.Run("inbound fee satisfied", func(t *testing.T) {
t.Parallel()
result := link.CheckHtlcForward(hash, 1000+10-2-1, 1000,
200, 150, models.InboundFee{Base: -2, Rate: -1_000},
0, lnwire.ShortChannelID{})
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
})
t.Run("inbound fee insufficient", func(t *testing.T) {
t.Parallel()
result := link.CheckHtlcForward(hash, 1000+10-10-101-1, 1000,
200, 150, models.InboundFee{Base: -10, Rate: -100_000},
0, lnwire.ShortChannelID{})
msg := result.WireMessage()
if _, ok := msg.(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
})
}
// TestChannelLinkCanceledInvoice in this test checks the interaction
+1 -1
View File
@@ -834,7 +834,7 @@ func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
func (f *mockChannelLink) UpdateForwardingPolicy(_ models.ForwardingPolicy) {
}
func (f *mockChannelLink) CheckHtlcForward([32]byte, lnwire.MilliSatoshi,
lnwire.MilliSatoshi, uint32, uint32, uint32,
lnwire.MilliSatoshi, uint32, uint32, models.InboundFee, uint32,
lnwire.ShortChannelID) *LinkError {
return f.checkHtlcForwardResult
+4
View File
@@ -2,6 +2,7 @@ package htlcswitch
import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
@@ -103,6 +104,9 @@ type htlcPacket struct {
// but receives a channel_update with the alias SCID. Instead, the
// payer should receive a channel_update with the public SCID.
originalOutgoingChanID lnwire.ShortChannelID
// inboundFee is the fee schedule of the incoming channel.
inboundFee models.InboundFee
}
// inKey returns the circuit key used to identify the incoming htlc.
+3 -1
View File
@@ -1178,7 +1178,9 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
failure = link.CheckHtlcForward(
htlc.PaymentHash, packet.incomingAmount,
packet.amount, packet.incomingTimeout,
packet.outgoingTimeout, currentHeight,
packet.outgoingTimeout,
packet.inboundFee,
currentHeight,
packet.originalOutgoingChanID,
)
}