mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-03-04 09:48:19 +01:00
itest: add basic invoice HTLC modifier integration test
This commit introduces a basic integration test for the invoice HTLC modifier. The test covers scenarios where an invoice is settled with a payment that is less than the invoice amount, facilitated by the invoice HTLC modifier.
This commit is contained in:
parent
d37df75bc0
commit
9a972e1b0c
3 changed files with 421 additions and 21 deletions
|
@ -466,6 +466,10 @@ var allTestCases = []*lntest.TestCase{
|
||||||
Name: "forward interceptor restart",
|
Name: "forward interceptor restart",
|
||||||
TestFunc: testForwardInterceptorRestart,
|
TestFunc: testForwardInterceptorRestart,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "invoice HTLC modifier basic",
|
||||||
|
TestFunc: testInvoiceHtlcModifierBasic,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "zero conf channel open",
|
Name: "zero conf channel open",
|
||||||
TestFunc: testZeroConfChannelOpen,
|
TestFunc: testZeroConfChannelOpen,
|
||||||
|
|
|
@ -10,6 +10,7 @@ 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"
|
||||||
|
@ -374,8 +375,13 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
||||||
// Connect an interceptor to Bob's node.
|
// Connect an interceptor to Bob's node.
|
||||||
bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
|
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.
|
// Prepare the test cases.
|
||||||
invoiceValueAmtMsat := int64(1000)
|
invoiceValueAmtMsat := int64(20_000_000)
|
||||||
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
|
req := &lnrpc.Invoice{ValueMsat: invoiceValueAmtMsat}
|
||||||
addResponse := carol.RPC.AddInvoice(req)
|
addResponse := carol.RPC.AddInvoice(req)
|
||||||
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
|
invoice := carol.RPC.LookupInvoice(addResponse.RHash)
|
||||||
|
@ -408,10 +414,10 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
||||||
crValue := []byte("custom-records-test-value")
|
crValue := []byte("custom-records-test-value")
|
||||||
customRecords[crKey] = crValue
|
customRecords[crKey] = crValue
|
||||||
|
|
||||||
// TODO(guggero): Actually modify the amount once we have the invoice
|
// Modify the amount of the HTLC, so we send out less than the original
|
||||||
// interceptor and can accept a lower amount.
|
// amount.
|
||||||
newOutAmountMsat := packet.OutgoingAmountMsat
|
const modifyAmount = 5_000_000
|
||||||
|
newOutAmountMsat := packet.OutgoingAmountMsat - modifyAmount
|
||||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||||
OutAmountMsat: newOutAmountMsat,
|
OutAmountMsat: newOutAmountMsat,
|
||||||
|
@ -420,6 +426,17 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
|
||||||
})
|
})
|
||||||
require.NoError(ht, err, "failed to send request")
|
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.
|
// Cancel the context, which will disconnect Bob's interceptor.
|
||||||
cancelBobInterceptor()
|
cancelBobInterceptor()
|
||||||
|
|
||||||
|
@ -473,17 +490,23 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
||||||
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
|
carolInterceptor, cancelCarolInterceptor := carol.RPC.HtlcInterceptor()
|
||||||
defer cancelCarolInterceptor()
|
defer cancelCarolInterceptor()
|
||||||
|
|
||||||
req := &lnrpc.Invoice{ValueMsat: 1000}
|
// 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)
|
addResponse := dave.RPC.AddInvoice(req)
|
||||||
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
|
invoice := dave.RPC.LookupInvoice(addResponse.RHash)
|
||||||
|
|
||||||
|
customRecords := map[uint64][]byte{
|
||||||
|
65537: []byte("test"),
|
||||||
|
}
|
||||||
sendReq := &routerrpc.SendPaymentRequest{
|
sendReq := &routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: invoice.PaymentRequest,
|
PaymentRequest: invoice.PaymentRequest,
|
||||||
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
|
TimeoutSeconds: int32(wait.PaymentTimeout.Seconds()),
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
FirstHopCustomRecords: map[uint64][]byte{
|
FirstHopCustomRecords: customRecords,
|
||||||
65537: []byte("test"),
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = alice.RPC.SendPayment(sendReq)
|
_ = alice.RPC.SendPayment(sendReq)
|
||||||
|
@ -499,14 +522,10 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
||||||
require.True(ht, ok, "expected custom record")
|
require.True(ht, ok, "expected custom record")
|
||||||
require.Equal(ht, []byte("test"), val)
|
require.Equal(ht, []byte("test"), val)
|
||||||
|
|
||||||
// TODO(guggero): Actually modify the amount once we have the invoice
|
// Just resume the payment on Bob.
|
||||||
// interceptor and can accept a lower amount.
|
|
||||||
newOutAmountMsat := packet.OutgoingAmountMsat
|
|
||||||
|
|
||||||
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
err := bobInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||||
OutAmountMsat: newOutAmountMsat,
|
Action: actionResume,
|
||||||
Action: actionResumeModify,
|
|
||||||
})
|
})
|
||||||
require.NoError(ht, err, "failed to send request")
|
require.NoError(ht, err, "failed to send request")
|
||||||
|
|
||||||
|
@ -515,13 +534,32 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
|
||||||
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
|
packet = ht.ReceiveHtlcInterceptor(carolInterceptor)
|
||||||
require.Len(ht, packet.InWireCustomRecords, 0)
|
require.Len(ht, packet.InWireCustomRecords, 0)
|
||||||
|
|
||||||
// Just resume the payment on Carol.
|
// 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{
|
err = carolInterceptor.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
IncomingCircuitKey: packet.IncomingCircuitKey,
|
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||||
Action: actionResume,
|
OutAmountMsat: newOutAmountMsat,
|
||||||
|
OutWireCustomRecords: customRecords,
|
||||||
|
Action: actionResumeModify,
|
||||||
})
|
})
|
||||||
require.NoError(ht, err, "carol interceptor response")
|
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.
|
// Assert that the payment was successful.
|
||||||
var preimage lntypes.Preimage
|
var preimage lntypes.Preimage
|
||||||
copy(preimage[:], invoice.RPreimage)
|
copy(preimage[:], invoice.RPreimage)
|
||||||
|
|
358
itest/lnd_invoice_acceptor_test.go
Normal file
358
itest/lnd_invoice_acceptor_test.go
Normal file
|
@ -0,0 +1,358 @@
|
||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
|
"github.com/lightningnetwork/lnd/invoices"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/node"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testInvoiceHtlcModifierBasic tests the basic functionality of the invoice
|
||||||
|
// HTLC modifier RPC server.
|
||||||
|
func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
|
||||||
|
ts := newAcceptorTestScenario(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)
|
||||||
|
|
||||||
|
// Initiate Carol's invoice HTLC modifier.
|
||||||
|
invoiceModifier, cancelModifier := carol.RPC.InvoiceHtlcModifier()
|
||||||
|
|
||||||
|
// We need to wait a bit to make sure the gRPC stream is established
|
||||||
|
// correctly.
|
||||||
|
time.Sleep(50 * time.Millisecond)
|
||||||
|
|
||||||
|
// Make sure we get an error if we try to register a second modifier and
|
||||||
|
// then try to use it (the error won't be returned on connect, only on
|
||||||
|
// the first _read_ interaction on the stream).
|
||||||
|
mod2, err := carol.RPC.Invoice.HtlcModifier(context.Background())
|
||||||
|
require.NoError(ht, err)
|
||||||
|
_, err = mod2.Recv()
|
||||||
|
require.ErrorContains(
|
||||||
|
ht, err,
|
||||||
|
invoices.ErrInterceptorClientAlreadyConnected.Error(),
|
||||||
|
)
|
||||||
|
|
||||||
|
// We also add a normal (forwarding) HTLC interceptor at Bob, so we can
|
||||||
|
// test that custom wire messages on the HTLC are forwarded correctly to
|
||||||
|
// the invoice HTLC interceptor.
|
||||||
|
bobInterceptor, bobInterceptorCancel := bob.RPC.HtlcInterceptor()
|
||||||
|
|
||||||
|
// Prepare the test cases.
|
||||||
|
testCases := ts.prepareTestCases()
|
||||||
|
|
||||||
|
for tcIdx, tc := range testCases {
|
||||||
|
ht.Logf("Running test case: %d", tcIdx)
|
||||||
|
|
||||||
|
// Initiate a payment from Alice to Carol in a separate
|
||||||
|
// goroutine. We use a separate goroutine to avoid blocking the
|
||||||
|
// main goroutine where we will make use of the invoice
|
||||||
|
// acceptor.
|
||||||
|
sendPaymentDone := make(chan struct{})
|
||||||
|
go func() {
|
||||||
|
// Signal that all the payments have been sent.
|
||||||
|
defer close(sendPaymentDone)
|
||||||
|
|
||||||
|
_ = ts.sendPayment(tc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// First, intercept the HTLC at Bob.
|
||||||
|
packet := ht.ReceiveHtlcInterceptor(bobInterceptor)
|
||||||
|
err := bobInterceptor.Send(
|
||||||
|
&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
|
IncomingCircuitKey: packet.IncomingCircuitKey,
|
||||||
|
OutAmountMsat: packet.OutgoingAmountMsat,
|
||||||
|
OutWireCustomRecords: tc.lastHopCustomRecords,
|
||||||
|
Action: actionResumeModify,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(ht, err, "failed to send request")
|
||||||
|
|
||||||
|
modifierRequest := ht.ReceiveInvoiceHtlcModification(
|
||||||
|
invoiceModifier,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Sanity check the modifier request.
|
||||||
|
require.EqualValues(
|
||||||
|
ht, tc.invoiceAmountMsat,
|
||||||
|
modifierRequest.Invoice.ValueMsat,
|
||||||
|
)
|
||||||
|
require.EqualValues(
|
||||||
|
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
||||||
|
)
|
||||||
|
require.Equal(
|
||||||
|
ht, tc.lastHopCustomRecords,
|
||||||
|
modifierRequest.ExitHtlcWireCustomRecords,
|
||||||
|
)
|
||||||
|
|
||||||
|
// For all other packets we resolve according to the test case.
|
||||||
|
amtPaid := uint64(tc.invoiceAmountMsat)
|
||||||
|
err = invoiceModifier.Send(
|
||||||
|
&invoicesrpc.HtlcModifyResponse{
|
||||||
|
CircuitKey: modifierRequest.ExitHtlcCircuitKey,
|
||||||
|
AmtPaid: &amtPaid,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(ht, err, "failed to send request")
|
||||||
|
|
||||||
|
ht.Log("Waiting for payment send to complete")
|
||||||
|
select {
|
||||||
|
case <-sendPaymentDone:
|
||||||
|
ht.Log("Payment send attempt complete")
|
||||||
|
case <-time.After(defaultTimeout):
|
||||||
|
require.Fail(ht, "timeout waiting for payment send")
|
||||||
|
}
|
||||||
|
|
||||||
|
ht.Log("Ensure invoice status is settled")
|
||||||
|
require.Eventually(ht, func() bool {
|
||||||
|
updatedInvoice := carol.RPC.LookupInvoice(
|
||||||
|
tc.invoice.RHash,
|
||||||
|
)
|
||||||
|
|
||||||
|
return updatedInvoice.State == tc.finalInvoiceState
|
||||||
|
}, defaultTimeout, 1*time.Second)
|
||||||
|
|
||||||
|
updatedInvoice := carol.RPC.LookupInvoice(
|
||||||
|
tc.invoice.RHash,
|
||||||
|
)
|
||||||
|
|
||||||
|
require.Len(ht, updatedInvoice.Htlcs, 1)
|
||||||
|
require.Equal(
|
||||||
|
ht, tc.lastHopCustomRecords,
|
||||||
|
updatedInvoice.Htlcs[0].CustomRecords,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure the custom channel data contains the encoded
|
||||||
|
// version of the custom records.
|
||||||
|
customRecords := lnwire.CustomRecords(
|
||||||
|
updatedInvoice.Htlcs[0].CustomRecords,
|
||||||
|
)
|
||||||
|
encodedRecords, err := customRecords.Serialize()
|
||||||
|
require.NoError(ht, err)
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
ht, encodedRecords,
|
||||||
|
updatedInvoice.Htlcs[0].CustomChannelData,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need the HTLC interceptor at Bob anymore.
|
||||||
|
bobInterceptorCancel()
|
||||||
|
|
||||||
|
// After the normal test cases, we test that we can shut down Carol
|
||||||
|
// while an HTLC interception is going on. We initiate a payment from
|
||||||
|
// Alice to Carol in a separate goroutine. We use a separate goroutine
|
||||||
|
// to avoid blocking the main goroutine where we will make use of the
|
||||||
|
// invoice acceptor.
|
||||||
|
sendPaymentDone := make(chan struct{})
|
||||||
|
tc := &acceptorTestCase{
|
||||||
|
invoiceAmountMsat: 9000,
|
||||||
|
sendAmountMsat: 9000,
|
||||||
|
}
|
||||||
|
ts.createInvoice(tc)
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Signal that all the payments have been sent.
|
||||||
|
defer close(sendPaymentDone)
|
||||||
|
|
||||||
|
_ = ts.sendPayment(tc)
|
||||||
|
}()
|
||||||
|
|
||||||
|
modifierRequest := ht.ReceiveInvoiceHtlcModification(invoiceModifier)
|
||||||
|
|
||||||
|
// Sanity check the modifier request.
|
||||||
|
require.EqualValues(
|
||||||
|
ht, tc.invoiceAmountMsat, modifierRequest.Invoice.ValueMsat,
|
||||||
|
)
|
||||||
|
require.EqualValues(
|
||||||
|
ht, tc.sendAmountMsat, modifierRequest.ExitHtlcAmt,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We don't send a response to the modifier, but instead shut down and
|
||||||
|
// restart Carol.
|
||||||
|
restart := ht.SuspendNode(carol)
|
||||||
|
require.NoError(ht, restart())
|
||||||
|
|
||||||
|
ht.Log("Waiting for payment send to complete")
|
||||||
|
select {
|
||||||
|
case <-sendPaymentDone:
|
||||||
|
ht.Log("Payment send attempt complete")
|
||||||
|
case <-time.After(defaultTimeout):
|
||||||
|
require.Fail(ht, "timeout waiting for payment send")
|
||||||
|
}
|
||||||
|
|
||||||
|
cancelModifier()
|
||||||
|
|
||||||
|
// Finally, close channels.
|
||||||
|
ht.CloseChannel(alice, cpAB)
|
||||||
|
ht.CloseChannel(bob, cpBC)
|
||||||
|
}
|
||||||
|
|
||||||
|
// acceptorTestCase is a helper struct to hold test case data.
|
||||||
|
type acceptorTestCase struct {
|
||||||
|
// invoiceAmountMsat is the amount of the invoice.
|
||||||
|
invoiceAmountMsat int64
|
||||||
|
|
||||||
|
// sendAmountMsat is the amount that will be sent in the payment.
|
||||||
|
sendAmountMsat int64
|
||||||
|
|
||||||
|
// lastHopCustomRecords is a map of custom records that will be added
|
||||||
|
// to the last hop of the payment, by an HTLC interceptor at Bob.
|
||||||
|
lastHopCustomRecords map[uint64][]byte
|
||||||
|
|
||||||
|
// finalInvoiceState is the expected eventual final state of the
|
||||||
|
// invoice.
|
||||||
|
finalInvoiceState lnrpc.Invoice_InvoiceState
|
||||||
|
|
||||||
|
// payAddr is the payment address of the invoice.
|
||||||
|
payAddr []byte
|
||||||
|
|
||||||
|
// invoice is the invoice that will be paid.
|
||||||
|
invoice *lnrpc.Invoice
|
||||||
|
}
|
||||||
|
|
||||||
|
// acceptorTestScenario is a helper struct to hold the test context and provides
|
||||||
|
// helpful functionality.
|
||||||
|
type acceptorTestScenario struct {
|
||||||
|
ht *lntest.HarnessTest
|
||||||
|
alice, bob, carol *node.HarnessNode
|
||||||
|
}
|
||||||
|
|
||||||
|
// newAcceptorTestScenario initializes a new test scenario with three nodes and
|
||||||
|
// connects them to have the following topology,
|
||||||
|
//
|
||||||
|
// Alice --> Bob --> Carol
|
||||||
|
//
|
||||||
|
// Among them, Alice and Bob are standby nodes and Carol is a new node.
|
||||||
|
func newAcceptorTestScenario(ht *lntest.HarnessTest) *acceptorTestScenario {
|
||||||
|
alice, bob := ht.Alice, ht.Bob
|
||||||
|
carol := ht.NewNode("carol", nil)
|
||||||
|
|
||||||
|
ht.EnsureConnected(alice, bob)
|
||||||
|
ht.EnsureConnected(bob, carol)
|
||||||
|
|
||||||
|
return &acceptorTestScenario{
|
||||||
|
ht: ht,
|
||||||
|
alice: alice,
|
||||||
|
bob: bob,
|
||||||
|
carol: carol,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// prepareTestCases prepares test cases.
|
||||||
|
func (c *acceptorTestScenario) prepareTestCases() []*acceptorTestCase {
|
||||||
|
cases := []*acceptorTestCase{
|
||||||
|
// Send a payment with amount less than the invoice amount.
|
||||||
|
// Amount checking is skipped during the invoice settlement
|
||||||
|
// process. The sent payment should eventually result in the
|
||||||
|
// invoice being settled.
|
||||||
|
{
|
||||||
|
invoiceAmountMsat: 9000,
|
||||||
|
sendAmountMsat: 1000,
|
||||||
|
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
invoiceAmountMsat: 9000,
|
||||||
|
sendAmountMsat: 1000,
|
||||||
|
finalInvoiceState: lnrpc.Invoice_SETTLED,
|
||||||
|
lastHopCustomRecords: map[uint64][]byte{
|
||||||
|
lnwire.MinCustomRecordsTlvType: {1, 2, 3},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, t := range cases {
|
||||||
|
c.createInvoice(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
return cases
|
||||||
|
}
|
||||||
|
|
||||||
|
// createInvoice creates an invoice for the given test case.
|
||||||
|
func (c *acceptorTestScenario) createInvoice(tc *acceptorTestCase) {
|
||||||
|
inv := &lnrpc.Invoice{ValueMsat: tc.invoiceAmountMsat}
|
||||||
|
addResponse := c.carol.RPC.AddInvoice(inv)
|
||||||
|
invoice := c.carol.RPC.LookupInvoice(addResponse.RHash)
|
||||||
|
|
||||||
|
// We'll need to also decode the returned invoice so we can grab the
|
||||||
|
// payment address which is now required for ALL payments.
|
||||||
|
payReq := c.carol.RPC.DecodePayReq(invoice.PaymentRequest)
|
||||||
|
|
||||||
|
tc.invoice = invoice
|
||||||
|
tc.payAddr = payReq.PaymentAddr
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildRoute is a helper function to build a route with given hops.
|
||||||
|
func (c *acceptorTestScenario) buildRoute(amtMsat int64,
|
||||||
|
hops []*node.HarnessNode, payAddr []byte) *lnrpc.Route {
|
||||||
|
|
||||||
|
rpcHops := make([][]byte, 0, len(hops))
|
||||||
|
for _, hop := range hops {
|
||||||
|
k := hop.PubKeyStr
|
||||||
|
pubkey, err := route.NewVertexFromStr(k)
|
||||||
|
require.NoErrorf(c.ht, err, "error parsing %v: %v", k, err)
|
||||||
|
rpcHops = append(rpcHops, pubkey[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &routerrpc.BuildRouteRequest{
|
||||||
|
AmtMsat: amtMsat,
|
||||||
|
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||||
|
HopPubkeys: rpcHops,
|
||||||
|
PaymentAddr: payAddr,
|
||||||
|
}
|
||||||
|
|
||||||
|
routeResp := c.alice.RPC.BuildRoute(req)
|
||||||
|
|
||||||
|
return routeResp.Route
|
||||||
|
}
|
||||||
|
|
||||||
|
// sendPaymentAndAssertAction sends a payment from alice to carol.
|
||||||
|
func (c *acceptorTestScenario) sendPayment(
|
||||||
|
tc *acceptorTestCase) *lnrpc.HTLCAttempt {
|
||||||
|
|
||||||
|
// Build a route from alice to carol.
|
||||||
|
aliceBobCarolRoute := c.buildRoute(
|
||||||
|
tc.sendAmountMsat, []*node.HarnessNode{c.bob, c.carol},
|
||||||
|
tc.payAddr,
|
||||||
|
)
|
||||||
|
|
||||||
|
// We need to cheat a bit. We are attempting to pay an invoice with
|
||||||
|
// amount X with an HTLC of amount Y that is less than X. And then we
|
||||||
|
// use the invoice HTLC interceptor to simulate the HTLC actually
|
||||||
|
// carrying amount X (even though the actual HTLC transaction output
|
||||||
|
// only has amount Y). But in order for the invoice to be settled, we
|
||||||
|
// need to make sure that the MPP total amount record in the last hop
|
||||||
|
// is set to the invoice amount. This would also be the case in a normal
|
||||||
|
// MPP payment, where each shard only pays a fraction of the invoice.
|
||||||
|
aliceBobCarolRoute.Hops[1].MppRecord.TotalAmtMsat = tc.invoiceAmountMsat
|
||||||
|
|
||||||
|
// Send the payment.
|
||||||
|
sendReq := &routerrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: tc.invoice.RHash,
|
||||||
|
Route: aliceBobCarolRoute,
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.alice.RPC.SendToRouteV2(sendReq)
|
||||||
|
}
|
Loading…
Add table
Reference in a new issue