mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-06-21 14:22:46 +02:00
multi: introduce new traffic shaper method.
We introduce a new specific fail resolution error when the external HTLC interceptor denies the incoming HTLC. Moreover we introduce a new traffic shaper method which moves the implementation of asset HTLC to the external layers. Moreover itests are adopted to reflect this new change.
This commit is contained in:
parent
768882dc9a
commit
a863534f41
@ -496,4 +496,9 @@ type AuxTrafficShaper interface {
|
|||||||
PaymentBandwidth(htlcBlob, commitmentBlob fn.Option[tlv.Blob],
|
PaymentBandwidth(htlcBlob, commitmentBlob fn.Option[tlv.Blob],
|
||||||
linkBandwidth,
|
linkBandwidth,
|
||||||
htlcAmt lnwire.MilliSatoshi) (lnwire.MilliSatoshi, error)
|
htlcAmt lnwire.MilliSatoshi) (lnwire.MilliSatoshi, error)
|
||||||
|
|
||||||
|
// IsCustomHTLC returns true if the HTLC carries the set of relevant
|
||||||
|
// custom records to put it under the purview of the traffic shaper,
|
||||||
|
// meaning that it's from a custom channel.
|
||||||
|
IsCustomHTLC(htlcRecords lnwire.CustomRecords) bool
|
||||||
}
|
}
|
||||||
|
@ -3875,21 +3875,20 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// In case the traffic shaper is active, we'll check if the HTLC has
|
||||||
|
// custom records and skip the amount check in the onion payload below.
|
||||||
|
isCustomHTLC := fn.MapOptionZ(
|
||||||
|
l.cfg.AuxTrafficShaper,
|
||||||
|
func(ts AuxTrafficShaper) bool {
|
||||||
|
return ts.IsCustomHTLC(add.CustomRecords)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// As we're the exit hop, we'll double check the hop-payload included in
|
// As we're the exit hop, we'll double check the hop-payload included in
|
||||||
// the HTLC to ensure that it was crafted correctly by the sender and
|
// the HTLC to ensure that it was crafted correctly by the sender and
|
||||||
// is compatible with the HTLC we were extended.
|
// is compatible with the HTLC we were extended. If an external
|
||||||
//
|
// validator is active we might bypass the amount check.
|
||||||
// For a special case, if the fwdInfo doesn't have any blinded path
|
if !isCustomHTLC && add.Amount < fwdInfo.AmountToForward {
|
||||||
// information, and the incoming HTLC had special extra data, then
|
|
||||||
// we'll skip this amount check. The invoice acceptor will make sure we
|
|
||||||
// reject the HTLC if it's not containing the correct amount after
|
|
||||||
// examining the custom data.
|
|
||||||
hasBlindedPath := fwdInfo.NextBlinding.IsSome()
|
|
||||||
customHTLC := len(add.CustomRecords) > 0 && !hasBlindedPath
|
|
||||||
log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
|
|
||||||
hasBlindedPath, customHTLC)
|
|
||||||
|
|
||||||
if !customHTLC && add.Amount < fwdInfo.AmountToForward {
|
|
||||||
l.log.Errorf("onion payload of incoming htlc(%x) has "+
|
l.log.Errorf("onion payload of incoming htlc(%x) has "+
|
||||||
"incompatible value: expected <=%v, got %v",
|
"incompatible value: expected <=%v, got %v",
|
||||||
add.PaymentHash, add.Amount, fwdInfo.AmountToForward)
|
add.PaymentHash, add.Amount, fwdInfo.AmountToForward)
|
||||||
|
@ -1117,13 +1117,15 @@ func (i *InvoiceRegistry) notifyExitHopHtlcLocked(
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a cancel signal was set for the htlc set, we set
|
// The error `ExternalValidationFailed` error
|
||||||
// the resolution as a failure with an underpayment
|
// information will be packed in the
|
||||||
// indication. Something was wrong with this htlc, so
|
// `FailIncorrectDetails` msg when sending the msg to
|
||||||
// we probably can't settle the invoice at all.
|
// the peer. Error codes are defined by the BOLT 04
|
||||||
|
// specification. The error text can be arbitrary
|
||||||
|
// therefore we return a custom error msg.
|
||||||
resolution = NewFailResolution(
|
resolution = NewFailResolution(
|
||||||
ctx.circuitKey, ctx.currentHeight,
|
ctx.circuitKey, ctx.currentHeight,
|
||||||
ResultAmountTooLow,
|
ExternalValidationFailed,
|
||||||
)
|
)
|
||||||
|
|
||||||
// We cancel all HTLCs which are in the accepted state.
|
// We cancel all HTLCs which are in the accepted state.
|
||||||
|
@ -136,9 +136,6 @@ func (s *HtlcModificationInterceptor) Intercept(clientRequest HtlcModifyRequest,
|
|||||||
// Wait for the client to respond or an error to occur.
|
// Wait for the client to respond or an error to occur.
|
||||||
select {
|
select {
|
||||||
case response := <-responseChan:
|
case response := <-responseChan:
|
||||||
log.Debugf("Received invoice HTLC interceptor response: %v",
|
|
||||||
response)
|
|
||||||
|
|
||||||
responseCallback(*response)
|
responseCallback(*response)
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
@ -120,6 +120,10 @@ const (
|
|||||||
// ResultAmpReconstruction is returned when the derived child
|
// ResultAmpReconstruction is returned when the derived child
|
||||||
// hash/preimage pairs were invalid for at least one HTLC in the set.
|
// hash/preimage pairs were invalid for at least one HTLC in the set.
|
||||||
ResultAmpReconstruction
|
ResultAmpReconstruction
|
||||||
|
|
||||||
|
// ExternalValidationFailed is returned when the external validation
|
||||||
|
// failed.
|
||||||
|
ExternalValidationFailed
|
||||||
)
|
)
|
||||||
|
|
||||||
// String returns a string representation of the result.
|
// String returns a string representation of the result.
|
||||||
@ -189,6 +193,9 @@ func (f FailResolutionResult) FailureString() string {
|
|||||||
case ResultAmpReconstruction:
|
case ResultAmpReconstruction:
|
||||||
return "amp reconstruction failed"
|
return "amp reconstruction failed"
|
||||||
|
|
||||||
|
case ExternalValidationFailed:
|
||||||
|
return "external validation failed"
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return "unknown failure resolution result"
|
return "unknown failure resolution result"
|
||||||
}
|
}
|
||||||
@ -202,7 +209,8 @@ func (f FailResolutionResult) IsSetFailure() bool {
|
|||||||
ResultAmpReconstruction,
|
ResultAmpReconstruction,
|
||||||
ResultHtlcSetTotalTooLow,
|
ResultHtlcSetTotalTooLow,
|
||||||
ResultHtlcSetTotalMismatch,
|
ResultHtlcSetTotalMismatch,
|
||||||
ResultHtlcSetOverpayment:
|
ResultHtlcSetOverpayment,
|
||||||
|
ExternalValidationFailed:
|
||||||
|
|
||||||
return true
|
return true
|
||||||
|
|
||||||
|
@ -454,14 +454,6 @@ var allTestCases = []*lntest.TestCase{
|
|||||||
Name: "forward interceptor",
|
Name: "forward interceptor",
|
||||||
TestFunc: testForwardInterceptorBasic,
|
TestFunc: testForwardInterceptorBasic,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
Name: "forward interceptor modified htlc",
|
|
||||||
TestFunc: testForwardInterceptorModifiedHtlc,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
Name: "forward interceptor wire records",
|
|
||||||
TestFunc: testForwardInterceptorWireRecords,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
Name: "forward interceptor restart",
|
Name: "forward interceptor restart",
|
||||||
TestFunc: testForwardInterceptorRestart,
|
TestFunc: testForwardInterceptorRestart,
|
||||||
|
@ -10,7 +10,6 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/lightningnetwork/lnd/chainreg"
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
"github.com/lightningnetwork/lnd/lntest"
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
"github.com/lightningnetwork/lnd/lntest/node"
|
"github.com/lightningnetwork/lnd/lntest/node"
|
||||||
@ -351,242 +350,6 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) {
|
|||||||
ht.CloseChannel(bob, cpBC)
|
ht.CloseChannel(bob, cpBC)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testForwardInterceptorModifiedHtlc tests that the interceptor can modify the
|
|
||||||
// amount and custom records of an intercepted HTLC and resume it.
|
|
||||||
func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
|
||||||
// Initialize the test context with 3 connected nodes.
|
|
||||||
ts := newInterceptorTestScenario(ht)
|
|
||||||
|
|
||||||
alice, bob, carol := ts.alice, ts.bob, ts.carol
|
|
||||||
|
|
||||||
// Open and wait for channels.
|
|
||||||
const chanAmt = btcutil.Amount(300000)
|
|
||||||
p := lntest.OpenChannelParams{Amt: chanAmt}
|
|
||||||
reqs := []*lntest.OpenChannelRequest{
|
|
||||||
{Local: alice, Remote: bob, Param: p},
|
|
||||||
{Local: bob, Remote: carol, Param: p},
|
|
||||||
}
|
|
||||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
|
||||||
cpAB, cpBC := resp[0], resp[1]
|
|
||||||
|
|
||||||
// Make sure Alice is aware of channel Bob=>Carol.
|
|
||||||
ht.AssertTopologyChannelOpen(alice, cpBC)
|
|
||||||
|
|
||||||
// Connect an interceptor to Bob's node.
|
|
||||||
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
|
|
||||||
|
|
||||||
// We're going to modify the payment amount and want Carol to accept the
|
|
||||||
// payment, so we set up an invoice acceptor on Dave.
|
|
||||||
carolAcceptor, carolCancel := carol.RPC.InvoiceHtlcModifier()
|
|
||||||
defer carolCancel()
|
|
||||||
|
|
||||||
// Prepare the test cases.
|
|
||||||
invoiceValueAmtMsat := int64(20_000_000)
|
|
||||||
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
|
|
||||||
addResponse := carol.RPC.AddInvoice(req)
|
|
||||||
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
|
|
||||||
tc := &interceptorTestCase{
|
|
||||||
amountMsat: invoiceValueAmtMsat,
|
|
||||||
invoice: invoice,
|
|
||||||
payAddr: invoice.PaymentAddr,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We initiate a payment from Alice.
|
|
||||||
done := make(chan struct{})
|
|
||||||
go func() {
|
|
||||||
// Signal that all the payments have been sent.
|
|
||||||
defer close(done)
|
|
||||||
|
|
||||||
ts.sendPaymentAndAssertAction(tc)
|
|
||||||
}()
|
|
||||||
|
|
||||||
// We start the htlc interceptor with a simple implementation that saves
|
|
||||||
// all intercepted packets. These packets are held to simulate a
|
|
||||||
// pending payment.
|
|
||||||
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
|
|
||||||
|
|
||||||
// Resume the intercepted HTLC with a modified amount and custom
|
|
||||||
// records.
|
|
||||||
customRecords := make(map[uint64][]byte)
|
|
||||||
|
|
||||||
// Add custom records entry.
|
|
||||||
crKey := uint64(65537)
|
|
||||||
crValue := []byte("custom-records-test-value")
|
|
||||||
customRecords[crKey] = crValue
|
|
||||||
|
|
||||||
// Modify the amount of the HTLC, so we send out less than the original
|
|
||||||
// amount.
|
|
||||||
const modifyAmount = 5_000_000
|
|
||||||
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
|
|
||||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
|
||||||
OutAmountMsat: newOutAmountMsat,
|
|
||||||
OutWireCustomRecords: customRecords,
|
|
||||||
Action: actionResumeModify,
|
|
||||||
})
|
|
||||||
require.NoError(ht, err, "failed to send request")
|
|
||||||
|
|
||||||
invoicePacket := ht.ReceiveInvoiceHtlcModification(carolAcceptor)
|
|
||||||
require.EqualValues(
|
|
||||||
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
|
|
||||||
)
|
|
||||||
amtPaid := newOutAmountMsat + modifyAmount
|
|
||||||
err = carolAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
|
|
||||||
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
|
|
||||||
AmtPaid: &amtPaid,
|
|
||||||
})
|
|
||||||
require.NoError(ht, err, "carol acceptor response")
|
|
||||||
|
|
||||||
// Cancel the context, which will disconnect Bob's interceptor.
|
|
||||||
cancelBobInterceptor()
|
|
||||||
|
|
||||||
// Make sure all goroutines are finished.
|
|
||||||
select {
|
|
||||||
case <-done:
|
|
||||||
case <-time.After(defaultTimeout):
|
|
||||||
require.Fail(ht, "timeout waiting for sending payment")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that the payment was successful.
|
|
||||||
var preimage lntypes.Preimage
|
|
||||||
copy(preimage[:], invoice.RPreimage)
|
|
||||||
ht.AssertPaymentStatus(alice, preimage, lnrpc.Payment_SUCCEEDED)
|
|
||||||
|
|
||||||
// Finally, close channels.
|
|
||||||
ht.CloseChannel(alice, cpAB)
|
|
||||||
ht.CloseChannel(bob, cpBC)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testForwardInterceptorWireRecords tests that the interceptor can read any
|
|
||||||
// wire custom records provided by the sender of a payment as part of the
|
|
||||||
// update_add_htlc message.
|
|
||||||
func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
|
||||||
// Initialize the test context with 3 connected nodes.
|
|
||||||
ts := newInterceptorTestScenario(ht)
|
|
||||||
|
|
||||||
alice, bob, carol, dave := ts.alice, ts.bob, ts.carol, ts.dave
|
|
||||||
|
|
||||||
// Open and wait for channels.
|
|
||||||
const chanAmt = btcutil.Amount(300000)
|
|
||||||
p := lntest.OpenChannelParams{Amt: chanAmt}
|
|
||||||
reqs := []*lntest.OpenChannelRequest{
|
|
||||||
{Local: alice, Remote: bob, Param: p},
|
|
||||||
{Local: bob, Remote: carol, Param: p},
|
|
||||||
{Local: carol, Remote: dave, Param: p},
|
|
||||||
}
|
|
||||||
resp := ht.OpenMultiChannelsAsync(reqs)
|
|
||||||
cpAB, cpBC, cpCD := resp[0], resp[1], resp[2]
|
|
||||||
|
|
||||||
// Make sure Alice is aware of channel Bob=>Carol.
|
|
||||||
ht.AssertTopologyChannelOpen(alice, cpBC)
|
|
||||||
|
|
||||||
// Connect an interceptor to Bob's node.
|
|
||||||
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
|
|
||||||
defer cancelBobInterceptor()
|
|
||||||
|
|
||||||
// Also connect an interceptor on Carol's node to check whether we're
|
|
||||||
// relaying the TLVs send in update_add_htlc over Alice -> Bob on the
|
|
||||||
// Bob -> Carol link.
|
|
||||||
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
|
|
||||||
defer cancelCarolInterceptor()
|
|
||||||
|
|
||||||
// We're going to modify the payment amount and want Dave to accept the
|
|
||||||
// payment, so we set up an invoice acceptor on Dave.
|
|
||||||
daveAcceptor, daveCancel := dave.RPC.InvoiceHtlcModifier()
|
|
||||||
defer daveCancel()
|
|
||||||
|
|
||||||
req := &lnrpc.Invoice{ValueMsat: 20_000_000}
|
|
||||||
addResponse := dave.RPC.AddInvoice(req)
|
|
||||||
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
|
|
||||||
|
|
||||||
customRecords := map[uint64][]byte{
|
|
||||||
65537: []byte("test"),
|
|
||||||
}
|
|
||||||
sendReq := &routerrpc.SendPaymentRequest{
|
|
||||||
PaymentRequest: invoice.PaymentRequest,
|
|
||||||
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
|
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
|
||||||
FirstHopCustomRecords: customRecords,
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = alice.RPC.SendPayment(sendReq)
|
|
||||||
|
|
||||||
// We start the htlc interceptor with a simple implementation that saves
|
|
||||||
// all intercepted packets. These packets are held to simulate a
|
|
||||||
// pending payment.
|
|
||||||
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
|
|
||||||
|
|
||||||
require.Len(ht, packet.InWireCustomRecords, 1)
|
|
||||||
|
|
||||||
val, ok := packet.InWireCustomRecords[65537]
|
|
||||||
require.True(ht, ok, "expected custom record")
|
|
||||||
require.Equal(ht, []byte("test"), val)
|
|
||||||
|
|
||||||
// Just resume the payment on Bob.
|
|
||||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
|
||||||
Action: actionResume,
|
|
||||||
})
|
|
||||||
require.NoError(ht, err, "failed to send request")
|
|
||||||
|
|
||||||
// Assert that the Alice -> Bob custom records in update_add_htlc are
|
|
||||||
// not propagated on the Bob -> Carol link.
|
|
||||||
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
|
|
||||||
require.Len(ht, packet.InWireCustomRecords, 0)
|
|
||||||
|
|
||||||
// We're going to tell Carol to forward 5k sats less to Dave. We need to
|
|
||||||
// set custom records on the HTLC as well, to make sure the HTLC isn't
|
|
||||||
// rejected outright and actually gets to the invoice acceptor.
|
|
||||||
const modifyAmount = 5_000_000
|
|
||||||
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
|
|
||||||
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
|
||||||
OutAmountMsat: newOutAmountMsat,
|
|
||||||
OutWireCustomRecords: customRecords,
|
|
||||||
Action: actionResumeModify,
|
|
||||||
})
|
|
||||||
require.NoError(ht, err, "carol interceptor response")
|
|
||||||
|
|
||||||
// The payment should get to Dave, and we should be able to intercept
|
|
||||||
// and modify it, telling Dave to accept it.
|
|
||||||
invoicePacket := ht.ReceiveInvoiceHtlcModification(daveAcceptor)
|
|
||||||
require.EqualValues(
|
|
||||||
ht, newOutAmountMsat, invoicePacket.ExitHtlcAmt,
|
|
||||||
)
|
|
||||||
amtPaid := newOutAmountMsat + modifyAmount
|
|
||||||
err = daveAcceptor.Send(&invoicesrpc.HtlcModifyResponse{
|
|
||||||
CircuitKey: invoicePacket.ExitHtlcCircuitKey,
|
|
||||||
AmtPaid: &amtPaid,
|
|
||||||
})
|
|
||||||
require.NoError(ht, err, "dave acceptor response")
|
|
||||||
|
|
||||||
// Assert that the payment was successful.
|
|
||||||
var preimage lntypes.Preimage
|
|
||||||
copy(preimage[:], invoice.RPreimage)
|
|
||||||
ht.AssertPaymentStatus(
|
|
||||||
alice, preimage, lnrpc.Payment_SUCCEEDED,
|
|
||||||
func(p *lnrpc.Payment) error {
|
|
||||||
recordsEqual := reflect.DeepEqual(
|
|
||||||
p.FirstHopCustomRecords,
|
|
||||||
sendReq.FirstHopCustomRecords,
|
|
||||||
)
|
|
||||||
if !recordsEqual {
|
|
||||||
return fmt.Errorf("expected custom records to "+
|
|
||||||
"be equal, got %v expected %v",
|
|
||||||
p.FirstHopCustomRecords,
|
|
||||||
sendReq.FirstHopCustomRecords)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Finally, close channels.
|
|
||||||
ht.CloseChannel(alice, cpAB)
|
|
||||||
ht.CloseChannel(bob, cpBC)
|
|
||||||
ht.CloseChannel(carol, cpCD)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testForwardInterceptorRestart tests that the interceptor can read any wire
|
// testForwardInterceptorRestart tests that the interceptor can read any wire
|
||||||
// custom records provided by the sender of a payment as part of the
|
// custom records provided by the sender of a payment as part of the
|
||||||
// update_add_htlc message and that those records are persisted correctly and
|
// update_add_htlc message and that those records are persisted correctly and
|
||||||
|
@ -164,3 +164,7 @@ func (*mockTrafficShaper) ProduceHtlcExtraData(totalAmount lnwire.MilliSatoshi,
|
|||||||
|
|
||||||
return totalAmount, nil, nil
|
return totalAmount, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (*mockTrafficShaper) IsCustomHTLC(_ lnwire.CustomRecords) bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user