Merge pull request #5332 from yyforyongyu/5259-routing-fix-state
routing: fix payment state and refactor payment lifecycle tests
This commit is contained in:
commit
198ac3482c
1
go.sum
1
go.sum
@ -274,6 +274,7 @@ github.com/spf13/pflag v1.0.1 h1:aCvUg6QPl3ibpQUxyLkrEkCHtPqYJL4x9AuhqVqFis4=
|
|||||||
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||||
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
2213
lntest/itest/lnd_routing_test.go
Normal file
2213
lntest/itest/lnd_routing_test.go
Normal file
@ -0,0 +1,2213 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
)
|
||||||
|
|
||||||
|
type singleHopSendToRouteCase struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
// streaming tests streaming SendToRoute if true, otherwise tests
|
||||||
|
// synchronous SenToRoute.
|
||||||
|
streaming bool
|
||||||
|
|
||||||
|
// routerrpc submits the request to the routerrpc subserver if true,
|
||||||
|
// otherwise submits to the main rpc server.
|
||||||
|
routerrpc bool
|
||||||
|
}
|
||||||
|
|
||||||
|
var singleHopSendToRouteCases = []singleHopSendToRouteCase{
|
||||||
|
{
|
||||||
|
name: "regular main sync",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regular main stream",
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "regular routerrpc sync",
|
||||||
|
routerrpc: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mpp main sync",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mpp main stream",
|
||||||
|
streaming: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "mpp routerrpc sync",
|
||||||
|
routerrpc: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSingleHopSendToRoute tests that payments are properly processed through a
|
||||||
|
// provided route with a single hop. We'll create the following network
|
||||||
|
// topology:
|
||||||
|
// Carol --100k--> Dave
|
||||||
|
// We'll query the daemon for routes from Carol to Dave and then send payments
|
||||||
|
// by feeding the route back into the various SendToRoute RPC methods. Here we
|
||||||
|
// test all three SendToRoute endpoints, forcing each to perform both a regular
|
||||||
|
// payment and an MPP payment.
|
||||||
|
func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
for _, test := range singleHopSendToRouteCases {
|
||||||
|
test := test
|
||||||
|
|
||||||
|
t.t.Run(test.name, func(t1 *testing.T) {
|
||||||
|
ht := newHarnessTest(t1, t.lndHarness)
|
||||||
|
ht.RunTestCase(&testCase{
|
||||||
|
name: test.name,
|
||||||
|
test: func(_ *lntest.NetworkHarness, tt *harnessTest) {
|
||||||
|
testSingleHopSendToRouteCase(net, tt, test)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
||||||
|
test singleHopSendToRouteCase) {
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
const paymentAmtSat = 1000
|
||||||
|
const numPayments = 5
|
||||||
|
const amountPaid = int64(numPayments * paymentAmtSat)
|
||||||
|
|
||||||
|
ctxb := context.Background()
|
||||||
|
var networkChans []*lnrpc.ChannelPoint
|
||||||
|
|
||||||
|
// Create Carol and Dave, then establish a channel between them. Carol
|
||||||
|
// is the sole funder of the channel with 100k satoshis. The network
|
||||||
|
// topology should look like:
|
||||||
|
// Carol -> 100k -> Dave
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Carol and Dave with Carol
|
||||||
|
// being the sole funder of the channel.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointCarol)
|
||||||
|
|
||||||
|
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
carolFundPoint := wire.OutPoint{
|
||||||
|
Hash: *carolChanTXID,
|
||||||
|
Index: chanPointCarol.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all nodes to have seen all channels.
|
||||||
|
nodes := []*lntest.HarnessNode{carol, dave}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for _, node := range nodes {
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
point := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d): timeout waiting for "+
|
||||||
|
"channel(%s) open: %v", node.Name(),
|
||||||
|
node.NodeID, point, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create invoices for Dave, which expect a payment from Carol.
|
||||||
|
payReqs, rHashes, _, err := createPayReqs(
|
||||||
|
dave, paymentAmtSat, numPayments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reconstruct payment addresses.
|
||||||
|
var payAddrs [][]byte
|
||||||
|
for _, payReq := range payReqs {
|
||||||
|
ctx, _ := context.WithTimeout(
|
||||||
|
context.Background(), defaultTimeout,
|
||||||
|
)
|
||||||
|
resp, err := dave.DecodePayReq(
|
||||||
|
ctx,
|
||||||
|
&lnrpc.PayReqString{PayReq: payReq},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("decode pay req: %v", err)
|
||||||
|
}
|
||||||
|
payAddrs = append(payAddrs, resp.PaymentAddr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert Carol and Dave are synced to the chain before proceeding, to
|
||||||
|
// ensure the queried route will have a valid final CLTV once the HTLC
|
||||||
|
// reaches Dave.
|
||||||
|
_, minerHeight, err := net.Miner.Client.GetBestBlock()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get best height: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
require.NoError(t.t, waitForNodeBlockHeight(ctxt, carol, minerHeight))
|
||||||
|
require.NoError(t.t, waitForNodeBlockHeight(ctxt, dave, minerHeight))
|
||||||
|
|
||||||
|
// Query for routes to pay from Carol to Dave using the default CLTV
|
||||||
|
// config.
|
||||||
|
routesReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: dave.PubKeyStr,
|
||||||
|
Amt: paymentAmtSat,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
routes, err := carol.QueryRoutes(ctxt, routesReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get route from %s: %v",
|
||||||
|
carol.Name(), err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// There should only be one route to try, so take the first item.
|
||||||
|
r := routes.Routes[0]
|
||||||
|
|
||||||
|
// Construct a closure that will set MPP fields on the route, which
|
||||||
|
// allows us to test MPP payments.
|
||||||
|
setMPPFields := func(i int) {
|
||||||
|
hop := r.Hops[len(r.Hops)-1]
|
||||||
|
hop.TlvPayload = true
|
||||||
|
hop.MppRecord = &lnrpc.MPPRecord{
|
||||||
|
PaymentAddr: payAddrs[i],
|
||||||
|
TotalAmtMsat: paymentAmtSat * 1000,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct closures for each of the payment types covered:
|
||||||
|
// - main rpc server sync
|
||||||
|
// - main rpc server streaming
|
||||||
|
// - routerrpc server sync
|
||||||
|
sendToRouteSync := func() {
|
||||||
|
for i, rHash := range rHashes {
|
||||||
|
setMPPFields(i)
|
||||||
|
|
||||||
|
sendReq := &lnrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: rHash,
|
||||||
|
Route: r,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := carol.SendToRouteSync(
|
||||||
|
ctxt, sendReq,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send to route for "+
|
||||||
|
"%s: %v", carol.Name(), err)
|
||||||
|
}
|
||||||
|
if resp.PaymentError != "" {
|
||||||
|
t.Fatalf("received payment error from %s: %v",
|
||||||
|
carol.Name(), resp.PaymentError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendToRouteStream := func() {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
alicePayStream, err := carol.SendToRoute(ctxt) // nolint:staticcheck
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create payment stream for "+
|
||||||
|
"carol: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, rHash := range rHashes {
|
||||||
|
setMPPFields(i)
|
||||||
|
|
||||||
|
sendReq := &lnrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: rHash,
|
||||||
|
Route: routes.Routes[0],
|
||||||
|
}
|
||||||
|
err := alicePayStream.Send(sendReq)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := alicePayStream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
if resp.PaymentError != "" {
|
||||||
|
t.Fatalf("received payment error: %v",
|
||||||
|
resp.PaymentError)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sendToRouteRouterRPC := func() {
|
||||||
|
for i, rHash := range rHashes {
|
||||||
|
setMPPFields(i)
|
||||||
|
|
||||||
|
sendReq := &routerrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: rHash,
|
||||||
|
Route: r,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := carol.RouterClient.SendToRouteV2(
|
||||||
|
ctxt, sendReq,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send to route for "+
|
||||||
|
"%s: %v", carol.Name(), err)
|
||||||
|
}
|
||||||
|
if resp.Failure != nil {
|
||||||
|
t.Fatalf("received payment error from %s: %v",
|
||||||
|
carol.Name(), resp.Failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Using Carol as the node as the source, send the payments
|
||||||
|
// synchronously via the the routerrpc's SendToRoute, or via the main RPC
|
||||||
|
// server's SendToRoute streaming or sync calls.
|
||||||
|
switch {
|
||||||
|
case !test.routerrpc && test.streaming:
|
||||||
|
sendToRouteStream()
|
||||||
|
case !test.routerrpc && !test.streaming:
|
||||||
|
sendToRouteSync()
|
||||||
|
case test.routerrpc && !test.streaming:
|
||||||
|
sendToRouteRouterRPC()
|
||||||
|
default:
|
||||||
|
t.Fatalf("routerrpc does not support streaming send_to_route")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the payment's from Carol's PoV have the correct payment
|
||||||
|
// hash and amount.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
|
||||||
|
paymentsResp, err := carol.ListPayments(
|
||||||
|
ctxt, &lnrpc.ListPaymentsRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error when obtaining %s payments: %v",
|
||||||
|
carol.Name(), err)
|
||||||
|
}
|
||||||
|
if len(paymentsResp.Payments) != numPayments {
|
||||||
|
t.Fatalf("incorrect number of payments, got %v, want %v",
|
||||||
|
len(paymentsResp.Payments), numPayments)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, p := range paymentsResp.Payments {
|
||||||
|
// Assert that the payment hashes for each payment match up.
|
||||||
|
rHashHex := hex.EncodeToString(rHashes[i])
|
||||||
|
if p.PaymentHash != rHashHex {
|
||||||
|
t.Fatalf("incorrect payment hash for payment %d, "+
|
||||||
|
"want: %s got: %s",
|
||||||
|
i, rHashHex, p.PaymentHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that each payment has no invoice since the payment was
|
||||||
|
// completed using SendToRoute.
|
||||||
|
if p.PaymentRequest != "" {
|
||||||
|
t.Fatalf("incorrect payment request for payment: %d, "+
|
||||||
|
"want: \"\", got: %s",
|
||||||
|
i, p.PaymentRequest)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the payment amount is correct.
|
||||||
|
if p.ValueSat != paymentAmtSat {
|
||||||
|
t.Fatalf("incorrect payment amt for payment %d, "+
|
||||||
|
"want: %d, got: %d",
|
||||||
|
i, paymentAmtSat, p.ValueSat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert exactly one htlc was made.
|
||||||
|
if len(p.Htlcs) != 1 {
|
||||||
|
t.Fatalf("expected 1 htlc for payment %d, got: %d",
|
||||||
|
i, len(p.Htlcs))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the htlc's route is populated.
|
||||||
|
htlc := p.Htlcs[0]
|
||||||
|
if htlc.Route == nil {
|
||||||
|
t.Fatalf("expected route for payment %d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert the hop has exactly one hop.
|
||||||
|
if len(htlc.Route.Hops) != 1 {
|
||||||
|
t.Fatalf("expected 1 hop for payment %d, got: %d",
|
||||||
|
i, len(htlc.Route.Hops))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If this is an MPP test, assert the MPP record's fields are
|
||||||
|
// properly populated. Otherwise the hop should not have an MPP
|
||||||
|
// record.
|
||||||
|
hop := htlc.Route.Hops[0]
|
||||||
|
if hop.MppRecord == nil {
|
||||||
|
t.Fatalf("expected mpp record for mpp payment")
|
||||||
|
}
|
||||||
|
|
||||||
|
if hop.MppRecord.TotalAmtMsat != paymentAmtSat*1000 {
|
||||||
|
t.Fatalf("incorrect mpp total msat for payment %d "+
|
||||||
|
"want: %d, got: %d",
|
||||||
|
i, paymentAmtSat*1000,
|
||||||
|
hop.MppRecord.TotalAmtMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
expAddr := payAddrs[i]
|
||||||
|
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr) {
|
||||||
|
t.Fatalf("incorrect mpp payment addr for payment %d "+
|
||||||
|
"want: %x, got: %x",
|
||||||
|
i, expAddr, hop.MppRecord.PaymentAddr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify that the invoices's from Dave's PoV have the correct payment
|
||||||
|
// hash and amount.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
|
||||||
|
invoicesResp, err := dave.ListInvoices(
|
||||||
|
ctxt, &lnrpc.ListInvoiceRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error when obtaining %s payments: %v",
|
||||||
|
dave.Name(), err)
|
||||||
|
}
|
||||||
|
if len(invoicesResp.Invoices) != numPayments {
|
||||||
|
t.Fatalf("incorrect number of invoices, got %v, want %v",
|
||||||
|
len(invoicesResp.Invoices), numPayments)
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, inv := range invoicesResp.Invoices {
|
||||||
|
// Assert that the payment hashes match up.
|
||||||
|
if !bytes.Equal(inv.RHash, rHashes[i]) {
|
||||||
|
t.Fatalf("incorrect payment hash for invoice %d, "+
|
||||||
|
"want: %x got: %x",
|
||||||
|
i, rHashes[i], inv.RHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the amount paid to the invoice is correct.
|
||||||
|
if inv.AmtPaidSat != paymentAmtSat {
|
||||||
|
t.Fatalf("incorrect payment amt for invoice %d, "+
|
||||||
|
"want: %d, got %d",
|
||||||
|
i, paymentAmtSat, inv.AmtPaidSat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point all the channels within our proto network should be
|
||||||
|
// shifted by 5k satoshis in the direction of Dave, the sink within the
|
||||||
|
// payment flow generated above. The order of asserts corresponds to
|
||||||
|
// increasing of time is needed to embed the HTLC in commitment
|
||||||
|
// transaction, in channel Carol->Dave, order is Dave and then Carol.
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
||||||
|
carolFundPoint, int64(0), amountPaid)
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
||||||
|
carolFundPoint, amountPaid, int64(0))
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMultiHopSendToRoute tests that payments are properly processed
|
||||||
|
// through a provided route. We'll create the following network topology:
|
||||||
|
// Alice --100k--> Bob --100k--> Carol
|
||||||
|
// We'll query the daemon for routes from Alice to Carol and then
|
||||||
|
// send payments through the routes.
|
||||||
|
func testMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
var networkChans []*lnrpc.ChannelPoint
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||||
|
// being the sole funder of the channel.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointAlice)
|
||||||
|
|
||||||
|
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
aliceFundPoint := wire.OutPoint{
|
||||||
|
Hash: *aliceChanTXID,
|
||||||
|
Index: chanPointAlice.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Carol and establish a channel from Bob. Bob is the sole funder
|
||||||
|
// of the channel with 100k satoshis. The network topology should look like:
|
||||||
|
// Alice -> Bob -> Carol
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, net.Bob); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, net.Bob)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointBob)
|
||||||
|
bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
bobFundPoint := wire.OutPoint{
|
||||||
|
Hash: *bobChanTXID,
|
||||||
|
Index: chanPointBob.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all nodes to have seen all channels.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol}
|
||||||
|
nodeNames := []string{"Alice", "Bob", "Carol"}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
point := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d): timeout waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, point, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 5 invoices for Carol, which expect a payment from Alice for 1k
|
||||||
|
// satoshis with a different preimage each time.
|
||||||
|
const (
|
||||||
|
numPayments = 5
|
||||||
|
paymentAmt = 1000
|
||||||
|
)
|
||||||
|
_, rHashes, invoices, err := createPayReqs(
|
||||||
|
carol, paymentAmt, numPayments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a route from Alice to Carol for each of the invoices
|
||||||
|
// created above. We set FinalCltvDelta to 40 since by default
|
||||||
|
// QueryRoutes returns the last hop with a final cltv delta of 9 where
|
||||||
|
// as the default in htlcswitch is 40.
|
||||||
|
routesReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: carol.PubKeyStr,
|
||||||
|
Amt: paymentAmt,
|
||||||
|
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
routes, err := net.Alice.QueryRoutes(ctxt, routesReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get route: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll wait for all parties to recognize the new channels within the
|
||||||
|
// network.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't advertise his channel in time: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
|
||||||
|
// Using Alice as the source, pay to the 5 invoices from Carol created
|
||||||
|
// above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
|
||||||
|
for i, rHash := range rHashes {
|
||||||
|
// Manually set the MPP payload a new for each payment since
|
||||||
|
// the payment addr will change with each invoice, although we
|
||||||
|
// can re-use the route itself.
|
||||||
|
route := *routes.Routes[0]
|
||||||
|
route.Hops[len(route.Hops)-1].TlvPayload = true
|
||||||
|
route.Hops[len(route.Hops)-1].MppRecord = &lnrpc.MPPRecord{
|
||||||
|
PaymentAddr: invoices[i].PaymentAddr,
|
||||||
|
TotalAmtMsat: int64(
|
||||||
|
lnwire.NewMSatFromSatoshis(paymentAmt),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq := &routerrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: rHash,
|
||||||
|
Route: &route,
|
||||||
|
}
|
||||||
|
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.Failure != nil {
|
||||||
|
t.Fatalf("received payment error: %v", resp.Failure)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// When asserting the amount of satoshis moved, we'll factor in the
|
||||||
|
// default base fee, as we didn't modify the fee structure when
|
||||||
|
// creating the seed nodes in the network.
|
||||||
|
const baseFee = 1
|
||||||
|
|
||||||
|
// At this point all the channels within our proto network should be
|
||||||
|
// shifted by 5k satoshis in the direction of Carol, the sink within the
|
||||||
|
// payment flow generated above. The order of asserts corresponds to
|
||||||
|
// increasing of time is needed to embed the HTLC in commitment
|
||||||
|
// transaction, in channel Alice->Bob->Carol, order is Carol, Bob,
|
||||||
|
// Alice.
|
||||||
|
const amountPaid = int64(5000)
|
||||||
|
assertAmountPaid(t, "Bob(local) => Carol(remote)", carol,
|
||||||
|
bobFundPoint, int64(0), amountPaid)
|
||||||
|
assertAmountPaid(t, "Bob(local) => Carol(remote)", net.Bob,
|
||||||
|
bobFundPoint, amountPaid, int64(0))
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
||||||
|
aliceFundPoint, int64(0), amountPaid+(baseFee*numPayments))
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
||||||
|
aliceFundPoint, amountPaid+(baseFee*numPayments), int64(0))
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointBob, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testSendToRouteErrorPropagation tests propagation of errors that occur
|
||||||
|
// while processing a multi-hop payment through an unknown route.
|
||||||
|
func testSendToRouteErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
||||||
|
// being the sole funder of the channel.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't advertise her channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new nodes (Carol and Charlie), load her with some funds,
|
||||||
|
// then establish a connection between Carol and Charlie with a channel
|
||||||
|
// that has identical capacity to the one created above.Then we will
|
||||||
|
// get route via queryroutes call which will be fake route for Alice ->
|
||||||
|
// Bob graph.
|
||||||
|
//
|
||||||
|
// The network topology should now look like: Alice -> Bob; Carol -> Charlie.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
charlie := net.NewNode(t.t, "Charlie", nil)
|
||||||
|
defer shutdownAndAssert(net, t, charlie)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, charlie)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, charlie); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to alice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, charlie,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't advertise her channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query routes from Carol to Charlie which will be an invalid route
|
||||||
|
// for Alice -> Bob.
|
||||||
|
fakeReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: charlie.PubKeyStr,
|
||||||
|
Amt: int64(1),
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
fakeRoute, err := carol.QueryRoutes(ctxt, fakeReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable get fake route: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create 1 invoices for Bob, which expect a payment from Alice for 1k
|
||||||
|
// satoshis
|
||||||
|
const paymentAmt = 1000
|
||||||
|
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "testing",
|
||||||
|
Value: paymentAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := net.Bob.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
rHash := resp.RHash
|
||||||
|
|
||||||
|
// Using Alice as the source, pay to the 5 invoices from Bob created above.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
alicePayStream, err := net.Alice.SendToRoute(ctxt) // nolint:staticcheck
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create payment stream for alice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq := &lnrpc.SendToRouteRequest{
|
||||||
|
PaymentHash: rHash,
|
||||||
|
Route: fakeRoute.Routes[0],
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := alicePayStream.Send(sendReq); err != nil {
|
||||||
|
t.Fatalf("unable to send payment: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this place we should get an rpc error with notification
|
||||||
|
// that edge is not found on hop(0)
|
||||||
|
if _, err := alicePayStream.Recv(); err != nil && strings.Contains(err.Error(),
|
||||||
|
"edge not found") {
|
||||||
|
|
||||||
|
} else if err != nil {
|
||||||
|
t.Fatalf("payment stream has been closed but fake route has consumed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testPrivateChannels tests that a private channel can be used for
|
||||||
|
// routing by the two endpoints of the channel, but is not known by
|
||||||
|
// the rest of the nodes in the graph.
|
||||||
|
func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
var networkChans []*lnrpc.ChannelPoint
|
||||||
|
|
||||||
|
// We create the following topology:
|
||||||
|
//
|
||||||
|
// Dave --100k--> Alice --200k--> Bob
|
||||||
|
// ^ ^
|
||||||
|
// | |
|
||||||
|
// 100k 100k
|
||||||
|
// | |
|
||||||
|
// +---- Carol ----+
|
||||||
|
//
|
||||||
|
// where the 100k channel between Carol and Alice is private.
|
||||||
|
|
||||||
|
// Open a channel with 200k satoshis between Alice and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt * 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointAlice)
|
||||||
|
|
||||||
|
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
aliceFundPoint := wire.OutPoint{
|
||||||
|
Hash: *aliceChanTXID,
|
||||||
|
Index: chanPointAlice.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create Dave, and a channel to Alice of 100k.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, dave, net.Alice); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, dave, net.Alice,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointDave)
|
||||||
|
daveChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointDave)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
daveFundPoint := wire.OutPoint{
|
||||||
|
Hash: *daveChanTXID,
|
||||||
|
Index: chanPointDave.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create Carol and establish a channel from her to
|
||||||
|
// Dave of 100k.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointCarol)
|
||||||
|
|
||||||
|
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
carolFundPoint := wire.OutPoint{
|
||||||
|
Hash: *carolChanTXID,
|
||||||
|
Index: chanPointCarol.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all nodes to have seen all these channels, as they
|
||||||
|
// are all public.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
||||||
|
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
point := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d): timeout waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, point, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Now create a _private_ channel directly between Carol and
|
||||||
|
// Alice of 100k.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanOpenUpdate := openChannelStream(
|
||||||
|
ctxt, t, net, carol, net.Alice,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to open channel: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// One block is enough to make the channel ready for use, since the
|
||||||
|
// nodes have defaultNumConfs=1 set.
|
||||||
|
block := mineBlocks(t, net, 1, 1)[0]
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
chanPointPrivate, err := net.WaitForChannelOpen(ctxt, chanOpenUpdate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("error while waiting for channel open: %v", err)
|
||||||
|
}
|
||||||
|
fundingTxID, err := lnrpc.GetChanPointFundingTxid(chanPointPrivate)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
assertTxInBlock(t, block, fundingTxID)
|
||||||
|
|
||||||
|
// The channel should be listed in the peer information returned by
|
||||||
|
// both peers.
|
||||||
|
privateFundPoint := wire.OutPoint{
|
||||||
|
Hash: *fundingTxID,
|
||||||
|
Index: chanPointPrivate.OutputIndex,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.AssertChannelExists(ctxt, carol, &privateFundPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to assert channel existence: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.AssertChannelExists(ctxt, net.Alice, &privateFundPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to assert channel existence: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// The channel should be available for payments between Carol and Alice.
|
||||||
|
// We check this by sending payments from Carol to Bob, that
|
||||||
|
// collectively would deplete at least one of Carol's channels.
|
||||||
|
|
||||||
|
// Create 2 invoices for Bob, each of 70k satoshis. Since each of
|
||||||
|
// Carol's channels is of size 100k, these payments cannot succeed
|
||||||
|
// by only using one of the channels.
|
||||||
|
const numPayments = 2
|
||||||
|
const paymentAmt = 70000
|
||||||
|
payReqs, _, _, err := createPayReqs(
|
||||||
|
net.Bob, paymentAmt, numPayments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
|
||||||
|
// Let Carol pay the invoices.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, carol, carol.RouterClient, payReqs, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When asserting the amount of satoshis moved, we'll factor in the
|
||||||
|
// default base fee, as we didn't modify the fee structure when
|
||||||
|
// creating the seed nodes in the network.
|
||||||
|
const baseFee = 1
|
||||||
|
|
||||||
|
// Bob should have received 140k satoshis from Alice.
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
||||||
|
aliceFundPoint, int64(0), 2*paymentAmt)
|
||||||
|
|
||||||
|
// Alice sent 140k to Bob.
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
||||||
|
aliceFundPoint, 2*paymentAmt, int64(0))
|
||||||
|
|
||||||
|
// Alice received 70k + fee from Dave.
|
||||||
|
assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice,
|
||||||
|
daveFundPoint, int64(0), paymentAmt+baseFee)
|
||||||
|
|
||||||
|
// Dave sent 70k+fee to Alice.
|
||||||
|
assertAmountPaid(t, "Dave(local) => Alice(remote)", dave,
|
||||||
|
daveFundPoint, paymentAmt+baseFee, int64(0))
|
||||||
|
|
||||||
|
// Dave received 70k+fee of two hops from Carol.
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
||||||
|
carolFundPoint, int64(0), paymentAmt+baseFee*2)
|
||||||
|
|
||||||
|
// Carol sent 70k+fee of two hops to Dave.
|
||||||
|
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
||||||
|
carolFundPoint, paymentAmt+baseFee*2, int64(0))
|
||||||
|
|
||||||
|
// Alice received 70k+fee from Carol.
|
||||||
|
assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)",
|
||||||
|
net.Alice, privateFundPoint, int64(0), paymentAmt+baseFee)
|
||||||
|
|
||||||
|
// Carol sent 70k+fee to Alice.
|
||||||
|
assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)",
|
||||||
|
carol, privateFundPoint, paymentAmt+baseFee, int64(0))
|
||||||
|
|
||||||
|
// Alice should also be able to route payments using this channel,
|
||||||
|
// so send two payments of 60k back to Carol.
|
||||||
|
const paymentAmt60k = 60000
|
||||||
|
payReqs, _, _, err = createPayReqs(
|
||||||
|
carol, paymentAmt60k, numPayments,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create pay reqs: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
time.Sleep(time.Millisecond * 50)
|
||||||
|
|
||||||
|
// Let Bob pay the invoices.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient, payReqs, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we make sure Dave and Bob does not know about the
|
||||||
|
// private channel between Carol and Alice. We first mine
|
||||||
|
// plenty of blocks, such that the channel would have been
|
||||||
|
// announced in case it was public.
|
||||||
|
mineBlocks(t, net, 10, 0)
|
||||||
|
|
||||||
|
// We create a helper method to check how many edges each of the
|
||||||
|
// nodes know about. Carol and Alice should know about 4, while
|
||||||
|
// Bob and Dave should only know about 3, since one channel is
|
||||||
|
// private.
|
||||||
|
numChannels := func(node *lntest.HarnessNode, includeUnannounced bool) int {
|
||||||
|
req := &lnrpc.ChannelGraphRequest{
|
||||||
|
IncludeUnannounced: includeUnannounced,
|
||||||
|
}
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
chanGraph, err := node.DescribeGraph(ctxt, req)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable go describegraph: %v", err)
|
||||||
|
}
|
||||||
|
return len(chanGraph.Edges)
|
||||||
|
}
|
||||||
|
|
||||||
|
var predErr error
|
||||||
|
err = wait.Predicate(func() bool {
|
||||||
|
aliceChans := numChannels(net.Alice, true)
|
||||||
|
if aliceChans != 4 {
|
||||||
|
predErr = fmt.Errorf("expected Alice to know 4 edges, "+
|
||||||
|
"had %v", aliceChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
alicePubChans := numChannels(net.Alice, false)
|
||||||
|
if alicePubChans != 3 {
|
||||||
|
predErr = fmt.Errorf("expected Alice to know 3 public edges, "+
|
||||||
|
"had %v", alicePubChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
bobChans := numChannels(net.Bob, true)
|
||||||
|
if bobChans != 3 {
|
||||||
|
predErr = fmt.Errorf("expected Bob to know 3 edges, "+
|
||||||
|
"had %v", bobChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
carolChans := numChannels(carol, true)
|
||||||
|
if carolChans != 4 {
|
||||||
|
predErr = fmt.Errorf("expected Carol to know 4 edges, "+
|
||||||
|
"had %v", carolChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
carolPubChans := numChannels(carol, false)
|
||||||
|
if carolPubChans != 3 {
|
||||||
|
predErr = fmt.Errorf("expected Carol to know 3 public edges, "+
|
||||||
|
"had %v", carolPubChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
daveChans := numChannels(dave, true)
|
||||||
|
if daveChans != 3 {
|
||||||
|
predErr = fmt.Errorf("expected Dave to know 3 edges, "+
|
||||||
|
"had %v", daveChans)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", predErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close all channels.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointPrivate, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testUpdateChannelPolicyForPrivateChannel tests when a private channel
|
||||||
|
// updates its channel edge policy, we will use the updated policy to send our
|
||||||
|
// payment.
|
||||||
|
// The topology is created as: Alice -> Bob -> Carol, where Alice -> Bob is
|
||||||
|
// public and Bob -> Carol is private. After an invoice is created by Carol,
|
||||||
|
// Bob will update the base fee via UpdateChannelPolicy, we will test that
|
||||||
|
// Alice will not fail the payment and send it using the updated channel
|
||||||
|
// policy.
|
||||||
|
func testUpdateChannelPolicyForPrivateChannel(net *lntest.NetworkHarness,
|
||||||
|
t *harnessTest) {
|
||||||
|
|
||||||
|
ctxb := context.Background()
|
||||||
|
defer ctxb.Done()
|
||||||
|
|
||||||
|
// We'll create the following topology first,
|
||||||
|
// Alice <--public:100k--> Bob <--private:100k--> Carol
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Alice and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAliceBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer closeChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, chanPointAliceBob, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get Alice's funding point.
|
||||||
|
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAliceBob)
|
||||||
|
require.NoError(t.t, err, "unable to get txid")
|
||||||
|
aliceFundPoint := wire.OutPoint{
|
||||||
|
Hash: *aliceChanTXID,
|
||||||
|
Index: chanPointAliceBob.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a new node Carol.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// Connect Carol to Bob.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
require.NoError(t.t,
|
||||||
|
net.ConnectNodes(ctxt, carol, net.Bob),
|
||||||
|
"unable to connect carol to bob",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel with 100k satoshis between Bob and Carol.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBobCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
defer closeChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, chanPointBobCarol, false,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Get Bob's funding point.
|
||||||
|
bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBobCarol)
|
||||||
|
require.NoError(t.t, err, "unable to get txid")
|
||||||
|
bobFundPoint := wire.OutPoint{
|
||||||
|
Hash: *bobChanTXID,
|
||||||
|
Index: chanPointBobCarol.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should have the following topology now,
|
||||||
|
// Alice <--public:100k--> Bob <--private:100k--> Carol
|
||||||
|
//
|
||||||
|
// Now we will create an invoice for Carol.
|
||||||
|
const paymentAmt = 20000
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "routing hints",
|
||||||
|
Value: paymentAmt,
|
||||||
|
Private: true,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := carol.AddInvoice(ctxt, invoice)
|
||||||
|
require.NoError(t.t, err, "unable to create invoice for carol")
|
||||||
|
|
||||||
|
// Bob now updates the channel edge policy for the private channel.
|
||||||
|
const (
|
||||||
|
baseFeeMSat = 33000
|
||||||
|
)
|
||||||
|
timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
||||||
|
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||||
|
BaseFeeMsat: baseFeeMSat,
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||||
|
ChanPoint: chanPointBobCarol,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
_, err = net.Bob.UpdateChannelPolicy(ctxt, updateFeeReq)
|
||||||
|
require.NoError(t.t, err, "unable to update chan policy")
|
||||||
|
|
||||||
|
// Alice pays the invoices. She will use the updated baseFeeMSat in the
|
||||||
|
// payment
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
payReqs := []string{resp.PaymentRequest}
|
||||||
|
require.NoError(t.t,
|
||||||
|
completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient, payReqs, true,
|
||||||
|
), "unable to send payment",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check that Alice did make the payment with two HTLCs, one failed and
|
||||||
|
// one succeeded.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
|
||||||
|
paymentsResp, err := net.Alice.ListPayments(
|
||||||
|
ctxt, &lnrpc.ListPaymentsRequest{},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err, "failed to obtain payments for Alice")
|
||||||
|
require.Equal(t.t, 1, len(paymentsResp.Payments), "expected 1 payment")
|
||||||
|
|
||||||
|
htlcs := paymentsResp.Payments[0].Htlcs
|
||||||
|
require.Equal(t.t, 2, len(htlcs), "expected to have 2 HTLCs")
|
||||||
|
require.Equal(
|
||||||
|
t.t, lnrpc.HTLCAttempt_FAILED, htlcs[0].Status,
|
||||||
|
"the first HTLC attempt should fail",
|
||||||
|
)
|
||||||
|
require.Equal(
|
||||||
|
t.t, lnrpc.HTLCAttempt_SUCCEEDED, htlcs[1].Status,
|
||||||
|
"the second HTLC attempt should succeed",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Carol should have received 20k satoshis from Bob.
|
||||||
|
assertAmountPaid(t, "Carol(remote) [<=private] Bob(local)",
|
||||||
|
carol, bobFundPoint, 0, paymentAmt)
|
||||||
|
|
||||||
|
// Bob should have sent 20k satoshis to Carol.
|
||||||
|
assertAmountPaid(t, "Bob(local) [private=>] Carol(remote)",
|
||||||
|
net.Bob, bobFundPoint, paymentAmt, 0)
|
||||||
|
|
||||||
|
// Calcuate the amount in satoshis.
|
||||||
|
amtExpected := int64(paymentAmt + baseFeeMSat/1000)
|
||||||
|
|
||||||
|
// Bob should have received 20k satoshis + fee from Alice.
|
||||||
|
assertAmountPaid(t, "Bob(remote) <= Alice(local)",
|
||||||
|
net.Bob, aliceFundPoint, 0, amtExpected)
|
||||||
|
|
||||||
|
// Alice should have sent 20k satoshis + fee to Bob.
|
||||||
|
assertAmountPaid(t, "Alice(local) => Bob(remote)",
|
||||||
|
net.Alice, aliceFundPoint, amtExpected, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testInvoiceRoutingHints tests that the routing hints for an invoice are
|
||||||
|
// created properly.
|
||||||
|
func testInvoiceRoutingHints(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Throughout this test, we'll be opening a channel between Alice and
|
||||||
|
// several other parties.
|
||||||
|
//
|
||||||
|
// First, we'll create a private channel between Alice and Bob. This
|
||||||
|
// will be the only channel that will be considered as a routing hint
|
||||||
|
// throughout this test. We'll include a push amount since we currently
|
||||||
|
// require channels to have enough remote balance to cover the invoice's
|
||||||
|
// payment.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: chanAmt / 2,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then, we'll create Carol's node and open a public channel between her
|
||||||
|
// and Alice. This channel will not be considered as a routing hint due
|
||||||
|
// to it being public.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Alice, carol); err != nil {
|
||||||
|
t.Fatalf("unable to connect alice to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: chanAmt / 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// We'll also create a public channel between Bob and Carol to ensure
|
||||||
|
// that Bob gets selected as the only routing hint. We do this as
|
||||||
|
// we should only include routing hints for nodes that are publicly
|
||||||
|
// advertised, otherwise we'd end up leaking information about nodes
|
||||||
|
// that wish to stay unadvertised.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
||||||
|
t.Fatalf("unable to connect alice to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBobCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: chanAmt / 2,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Then, we'll create Dave's node and open a private channel between him
|
||||||
|
// and Alice. We will not include a push amount in order to not consider
|
||||||
|
// this channel as a routing hint as it will not have enough remote
|
||||||
|
// balance for the invoice's amount.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Alice, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect alice to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Finally, we'll create Eve's node and open a private channel between
|
||||||
|
// her and Alice. This time though, we'll take Eve's node down after the
|
||||||
|
// channel has been created to avoid populating routing hints for
|
||||||
|
// inactive channels.
|
||||||
|
eve := net.NewNode(t.t, "Eve", nil)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Alice, eve); err != nil {
|
||||||
|
t.Fatalf("unable to connect alice to eve: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointEve := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, eve,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
PushAmt: chanAmt / 2,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Make sure all the channels have been opened.
|
||||||
|
chanNames := []string{
|
||||||
|
"alice-bob", "alice-carol", "bob-carol", "alice-dave",
|
||||||
|
"alice-eve",
|
||||||
|
}
|
||||||
|
aliceChans := []*lnrpc.ChannelPoint{
|
||||||
|
chanPointBob, chanPointCarol, chanPointBobCarol, chanPointDave,
|
||||||
|
chanPointEve,
|
||||||
|
}
|
||||||
|
for i, chanPoint := range aliceChans {
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("timed out waiting for channel open %s: %v",
|
||||||
|
chanNames[i], err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the channels are open, we'll take down Eve's node.
|
||||||
|
shutdownAndAssert(net, t, eve)
|
||||||
|
|
||||||
|
// Create an invoice for Alice that will populate the routing hints.
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "routing hints",
|
||||||
|
Value: int64(chanAmt / 4),
|
||||||
|
Private: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Due to the way the channels were set up above, the channel between
|
||||||
|
// Alice and Bob should be the only channel used as a routing hint.
|
||||||
|
var predErr error
|
||||||
|
var decoded *lnrpc.PayReq
|
||||||
|
err := wait.Predicate(func() bool {
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := net.Alice.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to add invoice: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll decode the invoice's payment request to determine which
|
||||||
|
// channels were used as routing hints.
|
||||||
|
payReq := &lnrpc.PayReqString{
|
||||||
|
PayReq: resp.PaymentRequest,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
decoded, err = net.Alice.DecodePayReq(ctxt, payReq)
|
||||||
|
if err != nil {
|
||||||
|
predErr = fmt.Errorf("unable to decode payment "+
|
||||||
|
"request: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(decoded.RouteHints) != 1 {
|
||||||
|
predErr = fmt.Errorf("expected one route hint, got %d",
|
||||||
|
len(decoded.RouteHints))
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}, defaultTimeout)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf(predErr.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
hops := decoded.RouteHints[0].HopHints
|
||||||
|
if len(hops) != 1 {
|
||||||
|
t.Fatalf("expected one hop in route hint, got %d", len(hops))
|
||||||
|
}
|
||||||
|
chanID := hops[0].ChanId
|
||||||
|
|
||||||
|
// We'll need the short channel ID of the channel between Alice and Bob
|
||||||
|
// to make sure the routing hint is for this channel.
|
||||||
|
listReq := &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
listResp, err := net.Alice.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve alice's channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliceBobChanID uint64
|
||||||
|
for _, channel := range listResp.Channels {
|
||||||
|
if channel.RemotePubkey == net.Bob.PubKeyStr {
|
||||||
|
aliceBobChanID = channel.ChanId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliceBobChanID == 0 {
|
||||||
|
t.Fatalf("channel between alice and bob not found")
|
||||||
|
}
|
||||||
|
|
||||||
|
if chanID != aliceBobChanID {
|
||||||
|
t.Fatalf("expected channel ID %d, got %d", aliceBobChanID,
|
||||||
|
chanID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've confirmed the routing hints were added correctly, we
|
||||||
|
// can close all the channels and shut down all the nodes created.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointBob, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointCarol, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobCarol, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointDave, false)
|
||||||
|
|
||||||
|
// The channel between Alice and Eve should be force closed since Eve
|
||||||
|
// is offline.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointEve, true)
|
||||||
|
|
||||||
|
// Cleanup by mining the force close and sweep transaction.
|
||||||
|
cleanupForceClose(t, net, net.Alice, chanPointEve)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMultiHopOverPrivateChannels tests that private channels can be used as
|
||||||
|
// intermediate hops in a route for payments.
|
||||||
|
func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// We'll test that multi-hop payments over private channels work as
|
||||||
|
// intended. To do so, we'll create the following topology:
|
||||||
|
// private public private
|
||||||
|
// Alice <--100k--> Bob <--100k--> Carol <--100k--> Dave
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// First, we'll open a private channel between Alice and Bob with Alice
|
||||||
|
// being the funder.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the channel alice <-> bob before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't see the channel alice <-> bob before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve Alice's funding outpoint.
|
||||||
|
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
aliceFundPoint := wire.OutPoint{
|
||||||
|
Hash: *aliceChanTXID,
|
||||||
|
Index: chanPointAlice.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create Carol's node and open a public channel between
|
||||||
|
// her and Bob with Bob being the funder.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
||||||
|
t.Fatalf("unable to connect bob to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("bob didn't see the channel bob <-> carol before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't see the channel bob <-> carol before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("alice didn't see the channel bob <-> carol before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve Bob's funding outpoint.
|
||||||
|
bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
bobFundPoint := wire.OutPoint{
|
||||||
|
Hash: *bobChanTXID,
|
||||||
|
Index: chanPointBob.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll create Dave's node and open a private channel between him
|
||||||
|
// and Carol with Carol being the funder.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
Private: true,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("carol didn't see the channel carol <-> dave before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dave didn't see the channel carol <-> dave before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("dave didn't see the channel bob <-> carol before "+
|
||||||
|
"timeout: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve Carol's funding point.
|
||||||
|
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
carolFundPoint := wire.OutPoint{
|
||||||
|
Hash: *carolChanTXID,
|
||||||
|
Index: chanPointCarol.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that all the channels are set up according to the topology from
|
||||||
|
// above, we can proceed to test payments. We'll create an invoice for
|
||||||
|
// Dave of 20k satoshis and pay it with Alice. Since there is no public
|
||||||
|
// route from Alice to Dave, we'll need to use the private channel
|
||||||
|
// between Carol and Dave as a routing hint encoded in the invoice.
|
||||||
|
const paymentAmt = 20000
|
||||||
|
|
||||||
|
// Create the invoice for Dave.
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Memo: "two hopz!",
|
||||||
|
Value: paymentAmt,
|
||||||
|
Private: true,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := dave.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to add invoice for dave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let Alice pay the invoice.
|
||||||
|
payReqs := []string{resp.PaymentRequest}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = completePaymentRequests(
|
||||||
|
ctxt, net.Alice, net.Alice.RouterClient, payReqs, true,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to send payments from alice to dave: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// When asserting the amount of satoshis moved, we'll factor in the
|
||||||
|
// default base fee, as we didn't modify the fee structure when opening
|
||||||
|
// the channels.
|
||||||
|
const baseFee = 1
|
||||||
|
|
||||||
|
// Dave should have received 20k satoshis from Carol.
|
||||||
|
assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)",
|
||||||
|
dave, carolFundPoint, 0, paymentAmt)
|
||||||
|
|
||||||
|
// Carol should have sent 20k satoshis to Dave.
|
||||||
|
assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)",
|
||||||
|
carol, carolFundPoint, paymentAmt, 0)
|
||||||
|
|
||||||
|
// Carol should have received 20k satoshis + fee for one hop from Bob.
|
||||||
|
assertAmountPaid(t, "Bob(local) => Carol(remote)",
|
||||||
|
carol, bobFundPoint, 0, paymentAmt+baseFee)
|
||||||
|
|
||||||
|
// Bob should have sent 20k satoshis + fee for one hop to Carol.
|
||||||
|
assertAmountPaid(t, "Bob(local) => Carol(remote)",
|
||||||
|
net.Bob, bobFundPoint, paymentAmt+baseFee, 0)
|
||||||
|
|
||||||
|
// Bob should have received 20k satoshis + fee for two hops from Alice.
|
||||||
|
assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Bob,
|
||||||
|
aliceFundPoint, 0, paymentAmt+baseFee*2)
|
||||||
|
|
||||||
|
// Alice should have sent 20k satoshis + fee for two hops to Bob.
|
||||||
|
assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Alice,
|
||||||
|
aliceFundPoint, paymentAmt+baseFee*2, 0)
|
||||||
|
|
||||||
|
// At this point, the payment was successful. We can now close all the
|
||||||
|
// channels and shutdown the nodes created throughout this test.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// computeFee calculates the payment fee as specified in BOLT07
|
||||||
|
func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
|
||||||
|
return baseFee + amt*feeRate/1000000
|
||||||
|
}
|
||||||
|
|
||||||
|
// testQueryRoutes checks the response of queryroutes.
|
||||||
|
// We'll create the following network topology:
|
||||||
|
// Alice --> Bob --> Carol --> Dave
|
||||||
|
// and query the daemon for routes from Alice to Dave.
|
||||||
|
func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
var networkChans []*lnrpc.ChannelPoint
|
||||||
|
|
||||||
|
// Open a channel between Alice and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAlice := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointAlice)
|
||||||
|
|
||||||
|
// Create Carol and establish a channel from Bob.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, net.Bob); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to bob: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, net.Bob)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointBob)
|
||||||
|
|
||||||
|
// Create Dave and establish a channel from Carol.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, dave, carol); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to carol: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
networkChans = append(networkChans, chanPointCarol)
|
||||||
|
|
||||||
|
// Wait for all nodes to have seen all channels.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
||||||
|
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
point := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d): timeout waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, point, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query for routes to pay from Alice to Dave.
|
||||||
|
const paymentAmt = 1000
|
||||||
|
routesReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: dave.PubKeyStr,
|
||||||
|
Amt: paymentAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
routesRes, err := net.Alice.QueryRoutes(ctxt, routesReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get route: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
const mSat = 1000
|
||||||
|
feePerHopMSat := computeFee(1000, 1, paymentAmt*mSat)
|
||||||
|
|
||||||
|
for i, route := range routesRes.Routes {
|
||||||
|
expectedTotalFeesMSat :=
|
||||||
|
lnwire.MilliSatoshi(len(route.Hops)-1) * feePerHopMSat
|
||||||
|
expectedTotalAmtMSat := (paymentAmt * mSat) + expectedTotalFeesMSat
|
||||||
|
|
||||||
|
if route.TotalFees != route.TotalFeesMsat/mSat { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v: total fees %v (msat) does not "+
|
||||||
|
"round down to %v (sat)",
|
||||||
|
i, route.TotalFeesMsat, route.TotalFees) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
if route.TotalFeesMsat != int64(expectedTotalFeesMSat) {
|
||||||
|
t.Fatalf("route %v: total fees in msat expected %v got %v",
|
||||||
|
i, expectedTotalFeesMSat, route.TotalFeesMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if route.TotalAmt != route.TotalAmtMsat/mSat { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v: total amt %v (msat) does not "+
|
||||||
|
"round down to %v (sat)",
|
||||||
|
i, route.TotalAmtMsat, route.TotalAmt) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
if route.TotalAmtMsat != int64(expectedTotalAmtMSat) {
|
||||||
|
t.Fatalf("route %v: total amt in msat expected %v got %v",
|
||||||
|
i, expectedTotalAmtMSat, route.TotalAmtMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
// For all hops except the last, we check that fee equals feePerHop
|
||||||
|
// and amount to forward deducts feePerHop on each hop.
|
||||||
|
expectedAmtToForwardMSat := expectedTotalAmtMSat
|
||||||
|
for j, hop := range route.Hops[:len(route.Hops)-1] {
|
||||||
|
expectedAmtToForwardMSat -= feePerHopMSat
|
||||||
|
|
||||||
|
if hop.Fee != hop.FeeMsat/mSat { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v hop %v: fee %v (msat) does not "+
|
||||||
|
"round down to %v (sat)",
|
||||||
|
i, j, hop.FeeMsat, hop.Fee) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
if hop.FeeMsat != int64(feePerHopMSat) {
|
||||||
|
t.Fatalf("route %v hop %v: fee in msat expected %v got %v",
|
||||||
|
i, j, feePerHopMSat, hop.FeeMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+
|
||||||
|
"round down to %v (sat)",
|
||||||
|
i, j, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
if hop.AmtToForwardMsat != int64(expectedAmtToForwardMSat) {
|
||||||
|
t.Fatalf("route %v hop %v: amt to forward in msat "+
|
||||||
|
"expected %v got %v",
|
||||||
|
i, j, expectedAmtToForwardMSat, hop.AmtToForwardMsat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Last hop should have zero fee and amount to forward should equal
|
||||||
|
// payment amount.
|
||||||
|
hop := route.Hops[len(route.Hops)-1]
|
||||||
|
|
||||||
|
if hop.Fee != 0 || hop.FeeMsat != 0 { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v hop %v: fee expected 0 got %v (sat) %v (msat)",
|
||||||
|
i, len(route.Hops)-1, hop.Fee, hop.FeeMsat) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
|
||||||
|
if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck
|
||||||
|
t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+
|
||||||
|
"round down to %v (sat)",
|
||||||
|
i, len(route.Hops)-1, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck
|
||||||
|
}
|
||||||
|
if hop.AmtToForwardMsat != paymentAmt*mSat {
|
||||||
|
t.Fatalf("route %v hop %v: amt to forward in msat "+
|
||||||
|
"expected %v got %v",
|
||||||
|
i, len(route.Hops)-1, paymentAmt*mSat, hop.AmtToForwardMsat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// While we're here, we test updating mission control's config values
|
||||||
|
// and assert that they are correctly updated and check that our mission
|
||||||
|
// control import function updates appropriately.
|
||||||
|
testMissionControlCfg(t.t, net.Alice)
|
||||||
|
testMissionControlImport(
|
||||||
|
t.t, net.Alice, net.Bob.PubKey[:], carol.PubKey[:],
|
||||||
|
)
|
||||||
|
|
||||||
|
// We clean up the test case by closing channels that were created for
|
||||||
|
// the duration of the tests.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMissionControlCfg tests getting and setting of a node's mission control
|
||||||
|
// config, resetting to the original values after testing so that no other
|
||||||
|
// tests are affected.
|
||||||
|
func testMissionControlCfg(t *testing.T, node *lntest.HarnessNode) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
startCfg, err := node.RouterClient.GetMissionControlConfig(
|
||||||
|
ctxb, &routerrpc.GetMissionControlConfigRequest{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
cfg := &routerrpc.MissionControlConfig{
|
||||||
|
HalfLifeSeconds: 8000,
|
||||||
|
HopProbability: 0.8,
|
||||||
|
Weight: 0.3,
|
||||||
|
MaximumPaymentResults: 30,
|
||||||
|
MinimumFailureRelaxInterval: 60,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = node.RouterClient.SetMissionControlConfig(
|
||||||
|
ctxb, &routerrpc.SetMissionControlConfigRequest{
|
||||||
|
Config: cfg,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resp, err := node.RouterClient.GetMissionControlConfig(
|
||||||
|
ctxb, &routerrpc.GetMissionControlConfigRequest{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.True(t, proto.Equal(cfg, resp.Config))
|
||||||
|
|
||||||
|
_, err = node.RouterClient.SetMissionControlConfig(
|
||||||
|
ctxb, &routerrpc.SetMissionControlConfigRequest{
|
||||||
|
Config: startCfg.Config,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// testMissionControlImport tests import of mission control results from an
|
||||||
|
// external source.
|
||||||
|
func testMissionControlImport(t *testing.T, node *lntest.HarnessNode,
|
||||||
|
fromNode, toNode []byte) {
|
||||||
|
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// Reset mission control so that our query will return the default
|
||||||
|
// probability for our first request.
|
||||||
|
_, err := node.RouterClient.ResetMissionControl(
|
||||||
|
ctxb, &routerrpc.ResetMissionControlRequest{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "could not reset mission control")
|
||||||
|
|
||||||
|
// Get our baseline probability for a 10 msat hop between our target
|
||||||
|
// nodes.
|
||||||
|
var amount int64 = 10
|
||||||
|
probReq := &routerrpc.QueryProbabilityRequest{
|
||||||
|
FromNode: fromNode,
|
||||||
|
ToNode: toNode,
|
||||||
|
AmtMsat: amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
importHistory := &routerrpc.PairData{
|
||||||
|
FailTime: time.Now().Unix(),
|
||||||
|
FailAmtMsat: amount,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that our history is not already equal to the value we want to
|
||||||
|
// set. This should not happen because we have just cleared our state.
|
||||||
|
resp1, err := node.RouterClient.QueryProbability(ctxb, probReq)
|
||||||
|
require.NoError(t, err, "query probability failed")
|
||||||
|
require.Zero(t, resp1.History.FailTime)
|
||||||
|
require.Zero(t, resp1.History.FailAmtMsat)
|
||||||
|
|
||||||
|
// Now, we import a single entry which tracks a failure of the amount
|
||||||
|
// we want to query between our nodes.
|
||||||
|
req := &routerrpc.XImportMissionControlRequest{
|
||||||
|
Pairs: []*routerrpc.PairHistory{
|
||||||
|
{
|
||||||
|
NodeFrom: fromNode,
|
||||||
|
NodeTo: toNode,
|
||||||
|
History: importHistory,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = node.RouterClient.XImportMissionControl(ctxb, req)
|
||||||
|
require.NoError(t, err, "could not import config")
|
||||||
|
|
||||||
|
resp2, err := node.RouterClient.QueryProbability(ctxb, probReq)
|
||||||
|
require.NoError(t, err, "query probability failed")
|
||||||
|
require.Equal(t, importHistory.FailTime, resp2.History.FailTime)
|
||||||
|
require.Equal(t, importHistory.FailAmtMsat, resp2.History.FailAmtMsat)
|
||||||
|
|
||||||
|
// Finally, check that we will fail if inconsistent sat/msat values are
|
||||||
|
// set.
|
||||||
|
importHistory.FailAmtSat = amount * 2
|
||||||
|
_, err = node.RouterClient.XImportMissionControl(ctxb, req)
|
||||||
|
require.Error(t, err, "mismatched import amounts succeeded")
|
||||||
|
}
|
||||||
|
|
||||||
|
// testRouteFeeCutoff tests that we are able to prevent querying routes and
|
||||||
|
// sending payments that incur a fee higher than the fee limit.
|
||||||
|
func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// For this test, we'll create the following topology:
|
||||||
|
//
|
||||||
|
// --- Bob ---
|
||||||
|
// / \
|
||||||
|
// Alice ---- ---- Dave
|
||||||
|
// \ /
|
||||||
|
// -- Carol --
|
||||||
|
//
|
||||||
|
// Alice will attempt to send payments to Dave that should not incur a
|
||||||
|
// fee greater than the fee limit expressed as a percentage of the
|
||||||
|
// amount and as a fixed amount of satoshis.
|
||||||
|
const chanAmt = btcutil.Amount(100000)
|
||||||
|
|
||||||
|
// Open a channel between Alice and Bob.
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAliceBob := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, net.Bob,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create Carol's node and open a channel between her and Alice with
|
||||||
|
// Alice being the funder.
|
||||||
|
carol := net.NewNode(t.t, "Carol", nil)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to alice: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointAliceCarol := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Alice, carol,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create Dave's node and open a channel between him and Bob with Bob
|
||||||
|
// being the funder.
|
||||||
|
dave := net.NewNode(t.t, "Dave", nil)
|
||||||
|
defer shutdownAndAssert(net, t, dave)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, dave, net.Bob); err != nil {
|
||||||
|
t.Fatalf("unable to connect dave to bob: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointBobDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, net.Bob, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Open a channel between Carol and Dave.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
||||||
|
t.Fatalf("unable to connect carol to dave: %v", err)
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
||||||
|
chanPointCarolDave := openChannelAndAssert(
|
||||||
|
ctxt, t, net, carol, dave,
|
||||||
|
lntest.OpenChannelParams{
|
||||||
|
Amt: chanAmt,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Now that all the channels were set up, we'll wait for all the nodes
|
||||||
|
// to have seen all the channels.
|
||||||
|
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
||||||
|
nodeNames := []string{"alice", "bob", "carol", "dave"}
|
||||||
|
networkChans := []*lnrpc.ChannelPoint{
|
||||||
|
chanPointAliceBob, chanPointAliceCarol, chanPointBobDave,
|
||||||
|
chanPointCarolDave,
|
||||||
|
}
|
||||||
|
for _, chanPoint := range networkChans {
|
||||||
|
for i, node := range nodes {
|
||||||
|
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get txid: %v", err)
|
||||||
|
}
|
||||||
|
outpoint := wire.OutPoint{
|
||||||
|
Hash: *txid,
|
||||||
|
Index: chanPoint.OutputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%s(%d) timed out waiting for "+
|
||||||
|
"channel(%s) open: %v", nodeNames[i],
|
||||||
|
node.NodeID, outpoint, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The payments should only be successful across the route:
|
||||||
|
// Alice -> Bob -> Dave
|
||||||
|
// Therefore, we'll update the fee policy on Carol's side for the
|
||||||
|
// channel between her and Dave to invalidate the route:
|
||||||
|
// Alice -> Carol -> Dave
|
||||||
|
baseFee := int64(10000)
|
||||||
|
feeRate := int64(5)
|
||||||
|
timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
||||||
|
maxHtlc := calculateMaxHtlc(chanAmt)
|
||||||
|
|
||||||
|
expectedPolicy := &lnrpc.RoutingPolicy{
|
||||||
|
FeeBaseMsat: baseFee,
|
||||||
|
FeeRateMilliMsat: testFeeBase * feeRate,
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
MinHtlc: 1000, // default value
|
||||||
|
MaxHtlcMsat: maxHtlc,
|
||||||
|
}
|
||||||
|
|
||||||
|
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
||||||
|
BaseFeeMsat: baseFee,
|
||||||
|
FeeRate: float64(feeRate),
|
||||||
|
TimeLockDelta: timeLockDelta,
|
||||||
|
MaxHtlcMsat: maxHtlc,
|
||||||
|
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
||||||
|
ChanPoint: chanPointCarolDave,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
if _, err := carol.UpdateChannelPolicy(ctxt, updateFeeReq); err != nil {
|
||||||
|
t.Fatalf("unable to update chan policy: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for Alice to receive the channel update from Carol.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
aliceSub := subscribeGraphNotifications(ctxt, t, net.Alice)
|
||||||
|
defer close(aliceSub.quit)
|
||||||
|
|
||||||
|
waitForChannelUpdate(
|
||||||
|
t, aliceSub,
|
||||||
|
[]expectedChanUpdate{
|
||||||
|
{carol.PubKeyStr, expectedPolicy, chanPointCarolDave},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// We'll also need the channel IDs for Bob's channels in order to
|
||||||
|
// confirm the route of the payments.
|
||||||
|
listReq := &lnrpc.ListChannelsRequest{}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
listResp, err := net.Bob.ListChannels(ctxt, listReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to retrieve bob's channels: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliceBobChanID, bobDaveChanID uint64
|
||||||
|
for _, channel := range listResp.Channels {
|
||||||
|
switch channel.RemotePubkey {
|
||||||
|
case net.Alice.PubKeyStr:
|
||||||
|
aliceBobChanID = channel.ChanId
|
||||||
|
case dave.PubKeyStr:
|
||||||
|
bobDaveChanID = channel.ChanId
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if aliceBobChanID == 0 {
|
||||||
|
t.Fatalf("channel between alice and bob not found")
|
||||||
|
}
|
||||||
|
if bobDaveChanID == 0 {
|
||||||
|
t.Fatalf("channel between bob and dave not found")
|
||||||
|
}
|
||||||
|
hopChanIDs := []uint64{aliceBobChanID, bobDaveChanID}
|
||||||
|
|
||||||
|
// checkRoute is a helper closure to ensure the route contains the
|
||||||
|
// correct intermediate hops.
|
||||||
|
checkRoute := func(route *lnrpc.Route) {
|
||||||
|
if len(route.Hops) != 2 {
|
||||||
|
t.Fatalf("expected two hops, got %d", len(route.Hops))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, hop := range route.Hops {
|
||||||
|
if hop.ChanId != hopChanIDs[i] {
|
||||||
|
t.Fatalf("expected chan id %d, got %d",
|
||||||
|
hopChanIDs[i], hop.ChanId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll be attempting to send two payments from Alice to Dave. One will
|
||||||
|
// have a fee cutoff expressed as a percentage of the amount and the
|
||||||
|
// other will have it expressed as a fixed amount of satoshis.
|
||||||
|
const paymentAmt = 100
|
||||||
|
carolFee := computeFee(lnwire.MilliSatoshi(baseFee), 1, paymentAmt)
|
||||||
|
|
||||||
|
// testFeeCutoff is a helper closure that will ensure the different
|
||||||
|
// types of fee limits work as intended when querying routes and sending
|
||||||
|
// payments.
|
||||||
|
testFeeCutoff := func(feeLimit *lnrpc.FeeLimit) {
|
||||||
|
queryRoutesReq := &lnrpc.QueryRoutesRequest{
|
||||||
|
PubKey: dave.PubKeyStr,
|
||||||
|
Amt: paymentAmt,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
routesResp, err := net.Alice.QueryRoutes(ctxt, queryRoutesReq)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get routes: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkRoute(routesResp.Routes[0])
|
||||||
|
|
||||||
|
invoice := &lnrpc.Invoice{Value: paymentAmt}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
invoiceResp, err := dave.AddInvoice(ctxt, invoice)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to create invoice: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sendReq := &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: invoiceResp.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
}
|
||||||
|
switch limit := feeLimit.Limit.(type) {
|
||||||
|
case *lnrpc.FeeLimit_Fixed:
|
||||||
|
sendReq.FeeLimitMsat = 1000 * limit.Fixed
|
||||||
|
case *lnrpc.FeeLimit_Percent:
|
||||||
|
sendReq.FeeLimitMsat = 1000 * paymentAmt * limit.Percent / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
result := sendAndAssertSuccess(ctxt, t, net.Alice, sendReq)
|
||||||
|
|
||||||
|
checkRoute(result.Htlcs[0].Route)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll start off using percentages first. Since the fee along the
|
||||||
|
// route using Carol as an intermediate hop is 10% of the payment's
|
||||||
|
// amount, we'll use a lower percentage in order to invalid that route.
|
||||||
|
feeLimitPercent := &lnrpc.FeeLimit{
|
||||||
|
Limit: &lnrpc.FeeLimit_Percent{
|
||||||
|
Percent: baseFee/1000 - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testFeeCutoff(feeLimitPercent)
|
||||||
|
|
||||||
|
// Now we'll test using fixed fee limit amounts. Since we computed the
|
||||||
|
// fee for the route using Carol as an intermediate hop earlier, we can
|
||||||
|
// use a smaller value in order to invalidate that route.
|
||||||
|
feeLimitFixed := &lnrpc.FeeLimit{
|
||||||
|
Limit: &lnrpc.FeeLimit_Fixed{
|
||||||
|
Fixed: int64(carolFee.ToSatoshis()) - 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
testFeeCutoff(feeLimitFixed)
|
||||||
|
|
||||||
|
// Once we're done, close the channels and shut down the nodes created
|
||||||
|
// throughout this test.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceBob, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceCarol, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobDave, false)
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarolDave, false)
|
||||||
|
}
|
@ -232,7 +232,7 @@ func closeChannelAndAssertType(ctx context.Context, t *harnessTest,
|
|||||||
// updates before initiating the channel closure.
|
// updates before initiating the channel closure.
|
||||||
var graphSub *graphSubscription
|
var graphSub *graphSubscription
|
||||||
if expectDisable {
|
if expectDisable {
|
||||||
sub := subscribeGraphNotifications(t, ctx, node)
|
sub := subscribeGraphNotifications(ctx, t, node)
|
||||||
graphSub = &sub
|
graphSub = &sub
|
||||||
defer close(graphSub.quit)
|
defer close(graphSub.quit)
|
||||||
}
|
}
|
||||||
@ -1568,9 +1568,9 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// Launch notification clients for all nodes, such that we can
|
// Launch notification clients for all nodes, such that we can
|
||||||
// get notified when they discover new channels and updates in the
|
// get notified when they discover new channels and updates in the
|
||||||
// graph.
|
// graph.
|
||||||
aliceSub := subscribeGraphNotifications(t, ctxb, net.Alice)
|
aliceSub := subscribeGraphNotifications(ctxb, t, net.Alice)
|
||||||
defer close(aliceSub.quit)
|
defer close(aliceSub.quit)
|
||||||
bobSub := subscribeGraphNotifications(t, ctxb, net.Bob)
|
bobSub := subscribeGraphNotifications(ctxb, t, net.Bob)
|
||||||
defer close(bobSub.quit)
|
defer close(bobSub.quit)
|
||||||
|
|
||||||
chanAmt := funding.MaxBtcFundingAmount
|
chanAmt := funding.MaxBtcFundingAmount
|
||||||
@ -1643,7 +1643,7 @@ func testUpdateChannelPolicy(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// Clean up carol's node when the test finishes.
|
// Clean up carol's node when the test finishes.
|
||||||
defer shutdownAndAssert(net, t, carol)
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
carolSub := subscribeGraphNotifications(t, ctxb, carol)
|
carolSub := subscribeGraphNotifications(ctxb, t, carol)
|
||||||
defer close(carolSub.quit)
|
defer close(carolSub.quit)
|
||||||
|
|
||||||
graphSubs = append(graphSubs, carolSub)
|
graphSubs = append(graphSubs, carolSub)
|
||||||
@ -4888,7 +4888,7 @@ func testUpdateChanStatus(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
t.Fatalf("unable to connect bob to carol: %v", err)
|
t.Fatalf("unable to connect bob to carol: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
carolSub := subscribeGraphNotifications(t, ctxb, carol)
|
carolSub := subscribeGraphNotifications(ctxb, t, carol)
|
||||||
defer close(carolSub.quit)
|
defer close(carolSub.quit)
|
||||||
|
|
||||||
// sendReq sends an UpdateChanStatus request to the given node.
|
// sendReq sends an UpdateChanStatus request to the given node.
|
||||||
@ -5347,7 +5347,7 @@ func updateChannelPolicy(t *harnessTest, node *lntest.HarnessNode,
|
|||||||
|
|
||||||
// Wait for listener node to receive the channel update from node.
|
// Wait for listener node to receive the channel update from node.
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
graphSub := subscribeGraphNotifications(t, ctxt, listenerNode)
|
graphSub := subscribeGraphNotifications(ctxt, t, listenerNode)
|
||||||
defer close(graphSub.quit)
|
defer close(graphSub.quit)
|
||||||
|
|
||||||
waitForChannelUpdate(
|
waitForChannelUpdate(
|
||||||
@ -5358,720 +5358,6 @@ func updateChannelPolicy(t *harnessTest, node *lntest.HarnessNode,
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
type singleHopSendToRouteCase struct {
|
|
||||||
name string
|
|
||||||
|
|
||||||
// streaming tests streaming SendToRoute if true, otherwise tests
|
|
||||||
// synchronous SenToRoute.
|
|
||||||
streaming bool
|
|
||||||
|
|
||||||
// routerrpc submits the request to the routerrpc subserver if true,
|
|
||||||
// otherwise submits to the main rpc server.
|
|
||||||
routerrpc bool
|
|
||||||
}
|
|
||||||
|
|
||||||
var singleHopSendToRouteCases = []singleHopSendToRouteCase{
|
|
||||||
{
|
|
||||||
name: "regular main sync",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "regular main stream",
|
|
||||||
streaming: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "regular routerrpc sync",
|
|
||||||
routerrpc: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mpp main sync",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mpp main stream",
|
|
||||||
streaming: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "mpp routerrpc sync",
|
|
||||||
routerrpc: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSingleHopSendToRoute tests that payments are properly processed through a
|
|
||||||
// provided route with a single hop. We'll create the following network
|
|
||||||
// topology:
|
|
||||||
// Carol --100k--> Dave
|
|
||||||
// We'll query the daemon for routes from Carol to Dave and then send payments
|
|
||||||
// by feeding the route back into the various SendToRoute RPC methods. Here we
|
|
||||||
// test all three SendToRoute endpoints, forcing each to perform both a regular
|
|
||||||
// payment and an MPP payment.
|
|
||||||
func testSingleHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
for _, test := range singleHopSendToRouteCases {
|
|
||||||
test := test
|
|
||||||
|
|
||||||
t.t.Run(test.name, func(t1 *testing.T) {
|
|
||||||
ht := newHarnessTest(t1, t.lndHarness)
|
|
||||||
ht.RunTestCase(&testCase{
|
|
||||||
name: test.name,
|
|
||||||
test: func(_ *lntest.NetworkHarness, tt *harnessTest) {
|
|
||||||
testSingleHopSendToRouteCase(net, tt, test)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func testSingleHopSendToRouteCase(net *lntest.NetworkHarness, t *harnessTest,
|
|
||||||
test singleHopSendToRouteCase) {
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
const paymentAmtSat = 1000
|
|
||||||
const numPayments = 5
|
|
||||||
const amountPaid = int64(numPayments * paymentAmtSat)
|
|
||||||
|
|
||||||
ctxb := context.Background()
|
|
||||||
var networkChans []*lnrpc.ChannelPoint
|
|
||||||
|
|
||||||
// Create Carol and Dave, then establish a channel between them. Carol
|
|
||||||
// is the sole funder of the channel with 100k satoshis. The network
|
|
||||||
// topology should look like:
|
|
||||||
// Carol -> 100k -> Dave
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Carol and Dave with Carol
|
|
||||||
// being the sole funder of the channel.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointCarol)
|
|
||||||
|
|
||||||
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
carolFundPoint := wire.OutPoint{
|
|
||||||
Hash: *carolChanTXID,
|
|
||||||
Index: chanPointCarol.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all nodes to have seen all channels.
|
|
||||||
nodes := []*lntest.HarnessNode{carol, dave}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for _, node := range nodes {
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
point := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d): timeout waiting for "+
|
|
||||||
"channel(%s) open: %v", node.Name(),
|
|
||||||
node.NodeID, point, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create invoices for Dave, which expect a payment from Carol.
|
|
||||||
payReqs, rHashes, _, err := createPayReqs(
|
|
||||||
dave, paymentAmtSat, numPayments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reconstruct payment addresses.
|
|
||||||
var payAddrs [][]byte
|
|
||||||
for _, payReq := range payReqs {
|
|
||||||
ctx, _ := context.WithTimeout(
|
|
||||||
context.Background(), defaultTimeout,
|
|
||||||
)
|
|
||||||
resp, err := dave.DecodePayReq(
|
|
||||||
ctx,
|
|
||||||
&lnrpc.PayReqString{PayReq: payReq},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("decode pay req: %v", err)
|
|
||||||
}
|
|
||||||
payAddrs = append(payAddrs, resp.PaymentAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert Carol and Dave are synced to the chain before proceeding, to
|
|
||||||
// ensure the queried route will have a valid final CLTV once the HTLC
|
|
||||||
// reaches Dave.
|
|
||||||
_, minerHeight, err := net.Miner.Client.GetBestBlock()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get best height: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
defer cancel()
|
|
||||||
require.NoError(t.t, waitForNodeBlockHeight(ctxt, carol, minerHeight))
|
|
||||||
require.NoError(t.t, waitForNodeBlockHeight(ctxt, dave, minerHeight))
|
|
||||||
|
|
||||||
// Query for routes to pay from Carol to Dave using the default CLTV
|
|
||||||
// config.
|
|
||||||
routesReq := &lnrpc.QueryRoutesRequest{
|
|
||||||
PubKey: dave.PubKeyStr,
|
|
||||||
Amt: paymentAmtSat,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
routes, err := carol.QueryRoutes(ctxt, routesReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get route from %s: %v",
|
|
||||||
carol.Name(), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// There should only be one route to try, so take the first item.
|
|
||||||
r := routes.Routes[0]
|
|
||||||
|
|
||||||
// Construct a closure that will set MPP fields on the route, which
|
|
||||||
// allows us to test MPP payments.
|
|
||||||
setMPPFields := func(i int) {
|
|
||||||
hop := r.Hops[len(r.Hops)-1]
|
|
||||||
hop.TlvPayload = true
|
|
||||||
hop.MppRecord = &lnrpc.MPPRecord{
|
|
||||||
PaymentAddr: payAddrs[i],
|
|
||||||
TotalAmtMsat: paymentAmtSat * 1000,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct closures for each of the payment types covered:
|
|
||||||
// - main rpc server sync
|
|
||||||
// - main rpc server streaming
|
|
||||||
// - routerrpc server sync
|
|
||||||
sendToRouteSync := func() {
|
|
||||||
for i, rHash := range rHashes {
|
|
||||||
setMPPFields(i)
|
|
||||||
|
|
||||||
sendReq := &lnrpc.SendToRouteRequest{
|
|
||||||
PaymentHash: rHash,
|
|
||||||
Route: r,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := carol.SendToRouteSync(
|
|
||||||
ctxt, sendReq,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send to route for "+
|
|
||||||
"%s: %v", carol.Name(), err)
|
|
||||||
}
|
|
||||||
if resp.PaymentError != "" {
|
|
||||||
t.Fatalf("received payment error from %s: %v",
|
|
||||||
carol.Name(), resp.PaymentError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendToRouteStream := func() {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
alicePayStream, err := carol.SendToRoute(ctxt) // nolint:staticcheck
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create payment stream for "+
|
|
||||||
"carol: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, rHash := range rHashes {
|
|
||||||
setMPPFields(i)
|
|
||||||
|
|
||||||
sendReq := &lnrpc.SendToRouteRequest{
|
|
||||||
PaymentHash: rHash,
|
|
||||||
Route: routes.Routes[0],
|
|
||||||
}
|
|
||||||
err := alicePayStream.Send(sendReq)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := alicePayStream.Recv()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
if resp.PaymentError != "" {
|
|
||||||
t.Fatalf("received payment error: %v",
|
|
||||||
resp.PaymentError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sendToRouteRouterRPC := func() {
|
|
||||||
for i, rHash := range rHashes {
|
|
||||||
setMPPFields(i)
|
|
||||||
|
|
||||||
sendReq := &routerrpc.SendToRouteRequest{
|
|
||||||
PaymentHash: rHash,
|
|
||||||
Route: r,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := carol.RouterClient.SendToRouteV2(
|
|
||||||
ctxt, sendReq,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send to route for "+
|
|
||||||
"%s: %v", carol.Name(), err)
|
|
||||||
}
|
|
||||||
if resp.Failure != nil {
|
|
||||||
t.Fatalf("received payment error from %s: %v",
|
|
||||||
carol.Name(), resp.Failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using Carol as the node as the source, send the payments
|
|
||||||
// synchronously via the the routerrpc's SendToRoute, or via the main RPC
|
|
||||||
// server's SendToRoute streaming or sync calls.
|
|
||||||
switch {
|
|
||||||
case !test.routerrpc && test.streaming:
|
|
||||||
sendToRouteStream()
|
|
||||||
case !test.routerrpc && !test.streaming:
|
|
||||||
sendToRouteSync()
|
|
||||||
case test.routerrpc && !test.streaming:
|
|
||||||
sendToRouteRouterRPC()
|
|
||||||
default:
|
|
||||||
t.Fatalf("routerrpc does not support streaming send_to_route")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the payment's from Carol's PoV have the correct payment
|
|
||||||
// hash and amount.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
|
|
||||||
paymentsResp, err := carol.ListPayments(
|
|
||||||
ctxt, &lnrpc.ListPaymentsRequest{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error when obtaining %s payments: %v",
|
|
||||||
carol.Name(), err)
|
|
||||||
}
|
|
||||||
if len(paymentsResp.Payments) != numPayments {
|
|
||||||
t.Fatalf("incorrect number of payments, got %v, want %v",
|
|
||||||
len(paymentsResp.Payments), numPayments)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, p := range paymentsResp.Payments {
|
|
||||||
// Assert that the payment hashes for each payment match up.
|
|
||||||
rHashHex := hex.EncodeToString(rHashes[i])
|
|
||||||
if p.PaymentHash != rHashHex {
|
|
||||||
t.Fatalf("incorrect payment hash for payment %d, "+
|
|
||||||
"want: %s got: %s",
|
|
||||||
i, rHashHex, p.PaymentHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that each payment has no invoice since the payment was
|
|
||||||
// completed using SendToRoute.
|
|
||||||
if p.PaymentRequest != "" {
|
|
||||||
t.Fatalf("incorrect payment request for payment: %d, "+
|
|
||||||
"want: \"\", got: %s",
|
|
||||||
i, p.PaymentRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the payment amount is correct.
|
|
||||||
if p.ValueSat != paymentAmtSat {
|
|
||||||
t.Fatalf("incorrect payment amt for payment %d, "+
|
|
||||||
"want: %d, got: %d",
|
|
||||||
i, paymentAmtSat, p.ValueSat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert exactly one htlc was made.
|
|
||||||
if len(p.Htlcs) != 1 {
|
|
||||||
t.Fatalf("expected 1 htlc for payment %d, got: %d",
|
|
||||||
i, len(p.Htlcs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the htlc's route is populated.
|
|
||||||
htlc := p.Htlcs[0]
|
|
||||||
if htlc.Route == nil {
|
|
||||||
t.Fatalf("expected route for payment %d", i)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert the hop has exactly one hop.
|
|
||||||
if len(htlc.Route.Hops) != 1 {
|
|
||||||
t.Fatalf("expected 1 hop for payment %d, got: %d",
|
|
||||||
i, len(htlc.Route.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If this is an MPP test, assert the MPP record's fields are
|
|
||||||
// properly populated. Otherwise the hop should not have an MPP
|
|
||||||
// record.
|
|
||||||
hop := htlc.Route.Hops[0]
|
|
||||||
if hop.MppRecord == nil {
|
|
||||||
t.Fatalf("expected mpp record for mpp payment")
|
|
||||||
}
|
|
||||||
|
|
||||||
if hop.MppRecord.TotalAmtMsat != paymentAmtSat*1000 {
|
|
||||||
t.Fatalf("incorrect mpp total msat for payment %d "+
|
|
||||||
"want: %d, got: %d",
|
|
||||||
i, paymentAmtSat*1000,
|
|
||||||
hop.MppRecord.TotalAmtMsat)
|
|
||||||
}
|
|
||||||
|
|
||||||
expAddr := payAddrs[i]
|
|
||||||
if !bytes.Equal(hop.MppRecord.PaymentAddr, expAddr) {
|
|
||||||
t.Fatalf("incorrect mpp payment addr for payment %d "+
|
|
||||||
"want: %x, got: %x",
|
|
||||||
i, expAddr, hop.MppRecord.PaymentAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Verify that the invoices's from Dave's PoV have the correct payment
|
|
||||||
// hash and amount.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxt, defaultTimeout)
|
|
||||||
invoicesResp, err := dave.ListInvoices(
|
|
||||||
ctxt, &lnrpc.ListInvoiceRequest{},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error when obtaining %s payments: %v",
|
|
||||||
dave.Name(), err)
|
|
||||||
}
|
|
||||||
if len(invoicesResp.Invoices) != numPayments {
|
|
||||||
t.Fatalf("incorrect number of invoices, got %v, want %v",
|
|
||||||
len(invoicesResp.Invoices), numPayments)
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, inv := range invoicesResp.Invoices {
|
|
||||||
// Assert that the payment hashes match up.
|
|
||||||
if !bytes.Equal(inv.RHash, rHashes[i]) {
|
|
||||||
t.Fatalf("incorrect payment hash for invoice %d, "+
|
|
||||||
"want: %x got: %x",
|
|
||||||
i, rHashes[i], inv.RHash)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that the amount paid to the invoice is correct.
|
|
||||||
if inv.AmtPaidSat != paymentAmtSat {
|
|
||||||
t.Fatalf("incorrect payment amt for invoice %d, "+
|
|
||||||
"want: %d, got %d",
|
|
||||||
i, paymentAmtSat, inv.AmtPaidSat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this point all the channels within our proto network should be
|
|
||||||
// shifted by 5k satoshis in the direction of Dave, the sink within the
|
|
||||||
// payment flow generated above. The order of asserts corresponds to
|
|
||||||
// increasing of time is needed to embed the HTLC in commitment
|
|
||||||
// transaction, in channel Carol->Dave, order is Dave and then Carol.
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
|
||||||
carolFundPoint, int64(0), amountPaid)
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
|
||||||
carolFundPoint, amountPaid, int64(0))
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMultiHopSendToRoute tests that payments are properly processed
|
|
||||||
// through a provided route. We'll create the following network topology:
|
|
||||||
// Alice --100k--> Bob --100k--> Carol
|
|
||||||
// We'll query the daemon for routes from Alice to Carol and then
|
|
||||||
// send payments through the routes.
|
|
||||||
func testMultiHopSendToRoute(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
var networkChans []*lnrpc.ChannelPoint
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
|
||||||
// being the sole funder of the channel.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointAlice)
|
|
||||||
|
|
||||||
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
aliceFundPoint := wire.OutPoint{
|
|
||||||
Hash: *aliceChanTXID,
|
|
||||||
Index: chanPointAlice.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Carol and establish a channel from Bob. Bob is the sole funder
|
|
||||||
// of the channel with 100k satoshis. The network topology should look like:
|
|
||||||
// Alice -> Bob -> Carol
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, net.Bob); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to alice: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, net.Bob)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointBob)
|
|
||||||
bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
bobFundPoint := wire.OutPoint{
|
|
||||||
Hash: *bobChanTXID,
|
|
||||||
Index: chanPointBob.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all nodes to have seen all channels.
|
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol}
|
|
||||||
nodeNames := []string{"Alice", "Bob", "Carol"}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for i, node := range nodes {
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
point := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d): timeout waiting for "+
|
|
||||||
"channel(%s) open: %v", nodeNames[i],
|
|
||||||
node.NodeID, point, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 5 invoices for Carol, which expect a payment from Alice for 1k
|
|
||||||
// satoshis with a different preimage each time.
|
|
||||||
const (
|
|
||||||
numPayments = 5
|
|
||||||
paymentAmt = 1000
|
|
||||||
)
|
|
||||||
_, rHashes, invoices, err := createPayReqs(
|
|
||||||
carol, paymentAmt, numPayments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Construct a route from Alice to Carol for each of the invoices
|
|
||||||
// created above. We set FinalCltvDelta to 40 since by default
|
|
||||||
// QueryRoutes returns the last hop with a final cltv delta of 9 where
|
|
||||||
// as the default in htlcswitch is 40.
|
|
||||||
routesReq := &lnrpc.QueryRoutesRequest{
|
|
||||||
PubKey: carol.PubKeyStr,
|
|
||||||
Amt: paymentAmt,
|
|
||||||
FinalCltvDelta: chainreg.DefaultBitcoinTimeLockDelta,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
routes, err := net.Alice.QueryRoutes(ctxt, routesReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll wait for all parties to recognize the new channels within the
|
|
||||||
// network.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't advertise his channel in time: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
|
|
||||||
// Using Alice as the source, pay to the 5 invoices from Carol created
|
|
||||||
// above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
|
|
||||||
for i, rHash := range rHashes {
|
|
||||||
// Manually set the MPP payload a new for each payment since
|
|
||||||
// the payment addr will change with each invoice, although we
|
|
||||||
// can re-use the route itself.
|
|
||||||
route := *routes.Routes[0]
|
|
||||||
route.Hops[len(route.Hops)-1].TlvPayload = true
|
|
||||||
route.Hops[len(route.Hops)-1].MppRecord = &lnrpc.MPPRecord{
|
|
||||||
PaymentAddr: invoices[i].PaymentAddr,
|
|
||||||
TotalAmtMsat: int64(
|
|
||||||
lnwire.NewMSatFromSatoshis(paymentAmt),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReq := &routerrpc.SendToRouteRequest{
|
|
||||||
PaymentHash: rHash,
|
|
||||||
Route: &route,
|
|
||||||
}
|
|
||||||
resp, err := net.Alice.RouterClient.SendToRouteV2(ctxt, sendReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Failure != nil {
|
|
||||||
t.Fatalf("received payment error: %v", resp.Failure)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// When asserting the amount of satoshis moved, we'll factor in the
|
|
||||||
// default base fee, as we didn't modify the fee structure when
|
|
||||||
// creating the seed nodes in the network.
|
|
||||||
const baseFee = 1
|
|
||||||
|
|
||||||
// At this point all the channels within our proto network should be
|
|
||||||
// shifted by 5k satoshis in the direction of Carol, the sink within the
|
|
||||||
// payment flow generated above. The order of asserts corresponds to
|
|
||||||
// increasing of time is needed to embed the HTLC in commitment
|
|
||||||
// transaction, in channel Alice->Bob->Carol, order is Carol, Bob,
|
|
||||||
// Alice.
|
|
||||||
const amountPaid = int64(5000)
|
|
||||||
assertAmountPaid(t, "Bob(local) => Carol(remote)", carol,
|
|
||||||
bobFundPoint, int64(0), amountPaid)
|
|
||||||
assertAmountPaid(t, "Bob(local) => Carol(remote)", net.Bob,
|
|
||||||
bobFundPoint, amountPaid, int64(0))
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
|
||||||
aliceFundPoint, int64(0), amountPaid+(baseFee*numPayments))
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
|
||||||
aliceFundPoint, amountPaid+(baseFee*numPayments), int64(0))
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointBob, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSendToRouteErrorPropagation tests propagation of errors that occur
|
|
||||||
// while processing a multi-hop payment through an unknown route.
|
|
||||||
func testSendToRouteErrorPropagation(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// Open a channel with 100k satoshis between Alice and Bob with Alice
|
|
||||||
// being the sole funder of the channel.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't advertise her channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new nodes (Carol and Charlie), load her with some funds,
|
|
||||||
// then establish a connection between Carol and Charlie with a channel
|
|
||||||
// that has identical capacity to the one created above.Then we will
|
|
||||||
// get route via queryroutes call which will be fake route for Alice ->
|
|
||||||
// Bob graph.
|
|
||||||
//
|
|
||||||
// The network topology should now look like: Alice -> Bob; Carol -> Charlie.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
charlie := net.NewNode(t.t, "Charlie", nil)
|
|
||||||
defer shutdownAndAssert(net, t, charlie)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, charlie)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, charlie); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to alice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, charlie,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("carol didn't advertise her channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query routes from Carol to Charlie which will be an invalid route
|
|
||||||
// for Alice -> Bob.
|
|
||||||
fakeReq := &lnrpc.QueryRoutesRequest{
|
|
||||||
PubKey: charlie.PubKeyStr,
|
|
||||||
Amt: int64(1),
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
fakeRoute, err := carol.QueryRoutes(ctxt, fakeReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable get fake route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create 1 invoices for Bob, which expect a payment from Alice for 1k
|
|
||||||
// satoshis
|
|
||||||
const paymentAmt = 1000
|
|
||||||
|
|
||||||
invoice := &lnrpc.Invoice{
|
|
||||||
Memo: "testing",
|
|
||||||
Value: paymentAmt,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := net.Bob.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
rHash := resp.RHash
|
|
||||||
|
|
||||||
// Using Alice as the source, pay to the 5 invoices from Bob created above.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
alicePayStream, err := net.Alice.SendToRoute(ctxt)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create payment stream for alice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReq := &lnrpc.SendToRouteRequest{
|
|
||||||
PaymentHash: rHash,
|
|
||||||
Route: fakeRoute.Routes[0],
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := alicePayStream.Send(sendReq); err != nil {
|
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// At this place we should get an rpc error with notification
|
|
||||||
// that edge is not found on hop(0)
|
|
||||||
if _, err := alicePayStream.Recv(); err != nil && strings.Contains(err.Error(),
|
|
||||||
"edge not found") {
|
|
||||||
|
|
||||||
} else if err != nil {
|
|
||||||
t.Fatalf("payment stream has been closed but fake route has consumed: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testUnannouncedChannels checks unannounced channels are not returned by
|
// testUnannouncedChannels checks unannounced channels are not returned by
|
||||||
// describeGraph RPC request unless explicitly asked for.
|
// describeGraph RPC request unless explicitly asked for.
|
||||||
func testUnannouncedChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
func testUnannouncedChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
@ -6179,765 +5465,6 @@ func testUnannouncedChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, fundingChanPoint, false)
|
closeChannelAndAssert(ctxt, t, net, net.Alice, fundingChanPoint, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testPrivateChannels tests that a private channel can be used for
|
|
||||||
// routing by the two endpoints of the channel, but is not known by
|
|
||||||
// the rest of the nodes in the graph.
|
|
||||||
func testPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
var networkChans []*lnrpc.ChannelPoint
|
|
||||||
|
|
||||||
// We create the following topology:
|
|
||||||
//
|
|
||||||
// Dave --100k--> Alice --200k--> Bob
|
|
||||||
// ^ ^
|
|
||||||
// | |
|
|
||||||
// 100k 100k
|
|
||||||
// | |
|
|
||||||
// +---- Carol ----+
|
|
||||||
//
|
|
||||||
// where the 100k channel between Carol and Alice is private.
|
|
||||||
|
|
||||||
// Open a channel with 200k satoshis between Alice and Bob.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt * 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointAlice)
|
|
||||||
|
|
||||||
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
aliceFundPoint := wire.OutPoint{
|
|
||||||
Hash: *aliceChanTXID,
|
|
||||||
Index: chanPointAlice.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create Dave, and a channel to Alice of 100k.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, dave, net.Alice); err != nil {
|
|
||||||
t.Fatalf("unable to connect dave to alice: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointDave := openChannelAndAssert(
|
|
||||||
ctxt, t, net, dave, net.Alice,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointDave)
|
|
||||||
daveChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointDave)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
daveFundPoint := wire.OutPoint{
|
|
||||||
Hash: *daveChanTXID,
|
|
||||||
Index: chanPointDave.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create Carol and establish a channel from her to
|
|
||||||
// Dave of 100k.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointCarol)
|
|
||||||
|
|
||||||
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
carolFundPoint := wire.OutPoint{
|
|
||||||
Hash: *carolChanTXID,
|
|
||||||
Index: chanPointCarol.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for all nodes to have seen all these channels, as they
|
|
||||||
// are all public.
|
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
|
||||||
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for i, node := range nodes {
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
point := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d): timeout waiting for "+
|
|
||||||
"channel(%s) open: %v", nodeNames[i],
|
|
||||||
node.NodeID, point, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Now create a _private_ channel directly between Carol and
|
|
||||||
// Alice of 100k.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil {
|
|
||||||
t.Fatalf("unable to connect dave to alice: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanOpenUpdate := openChannelStream(
|
|
||||||
ctxt, t, net, carol, net.Alice,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to open channel: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// One block is enough to make the channel ready for use, since the
|
|
||||||
// nodes have defaultNumConfs=1 set.
|
|
||||||
block := mineBlocks(t, net, 1, 1)[0]
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
chanPointPrivate, err := net.WaitForChannelOpen(ctxt, chanOpenUpdate)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("error while waiting for channel open: %v", err)
|
|
||||||
}
|
|
||||||
fundingTxID, err := lnrpc.GetChanPointFundingTxid(chanPointPrivate)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
assertTxInBlock(t, block, fundingTxID)
|
|
||||||
|
|
||||||
// The channel should be listed in the peer information returned by
|
|
||||||
// both peers.
|
|
||||||
privateFundPoint := wire.OutPoint{
|
|
||||||
Hash: *fundingTxID,
|
|
||||||
Index: chanPointPrivate.OutputIndex,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.AssertChannelExists(ctxt, carol, &privateFundPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to assert channel existence: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.AssertChannelExists(ctxt, net.Alice, &privateFundPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to assert channel existence: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The channel should be available for payments between Carol and Alice.
|
|
||||||
// We check this by sending payments from Carol to Bob, that
|
|
||||||
// collectively would deplete at least one of Carol's channels.
|
|
||||||
|
|
||||||
// Create 2 invoices for Bob, each of 70k satoshis. Since each of
|
|
||||||
// Carol's channels is of size 100k, these payments cannot succeed
|
|
||||||
// by only using one of the channels.
|
|
||||||
const numPayments = 2
|
|
||||||
const paymentAmt = 70000
|
|
||||||
payReqs, _, _, err := createPayReqs(
|
|
||||||
net.Bob, paymentAmt, numPayments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
|
|
||||||
// Let Carol pay the invoices.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, carol, carol.RouterClient, payReqs, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When asserting the amount of satoshis moved, we'll factor in the
|
|
||||||
// default base fee, as we didn't modify the fee structure when
|
|
||||||
// creating the seed nodes in the network.
|
|
||||||
const baseFee = 1
|
|
||||||
|
|
||||||
// Bob should have received 140k satoshis from Alice.
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Bob,
|
|
||||||
aliceFundPoint, int64(0), 2*paymentAmt)
|
|
||||||
|
|
||||||
// Alice sent 140k to Bob.
|
|
||||||
assertAmountPaid(t, "Alice(local) => Bob(remote)", net.Alice,
|
|
||||||
aliceFundPoint, 2*paymentAmt, int64(0))
|
|
||||||
|
|
||||||
// Alice received 70k + fee from Dave.
|
|
||||||
assertAmountPaid(t, "Dave(local) => Alice(remote)", net.Alice,
|
|
||||||
daveFundPoint, int64(0), paymentAmt+baseFee)
|
|
||||||
|
|
||||||
// Dave sent 70k+fee to Alice.
|
|
||||||
assertAmountPaid(t, "Dave(local) => Alice(remote)", dave,
|
|
||||||
daveFundPoint, paymentAmt+baseFee, int64(0))
|
|
||||||
|
|
||||||
// Dave received 70k+fee of two hops from Carol.
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", dave,
|
|
||||||
carolFundPoint, int64(0), paymentAmt+baseFee*2)
|
|
||||||
|
|
||||||
// Carol sent 70k+fee of two hops to Dave.
|
|
||||||
assertAmountPaid(t, "Carol(local) => Dave(remote)", carol,
|
|
||||||
carolFundPoint, paymentAmt+baseFee*2, int64(0))
|
|
||||||
|
|
||||||
// Alice received 70k+fee from Carol.
|
|
||||||
assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)",
|
|
||||||
net.Alice, privateFundPoint, int64(0), paymentAmt+baseFee)
|
|
||||||
|
|
||||||
// Carol sent 70k+fee to Alice.
|
|
||||||
assertAmountPaid(t, "Carol(local) [private=>] Alice(remote)",
|
|
||||||
carol, privateFundPoint, paymentAmt+baseFee, int64(0))
|
|
||||||
|
|
||||||
// Alice should also be able to route payments using this channel,
|
|
||||||
// so send two payments of 60k back to Carol.
|
|
||||||
const paymentAmt60k = 60000
|
|
||||||
payReqs, _, _, err = createPayReqs(
|
|
||||||
carol, paymentAmt60k, numPayments,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create pay reqs: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
time.Sleep(time.Millisecond * 50)
|
|
||||||
|
|
||||||
// Let Bob pay the invoices.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, net.Alice, net.Alice.RouterClient, payReqs, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we make sure Dave and Bob does not know about the
|
|
||||||
// private channel between Carol and Alice. We first mine
|
|
||||||
// plenty of blocks, such that the channel would have been
|
|
||||||
// announced in case it was public.
|
|
||||||
mineBlocks(t, net, 10, 0)
|
|
||||||
|
|
||||||
// We create a helper method to check how many edges each of the
|
|
||||||
// nodes know about. Carol and Alice should know about 4, while
|
|
||||||
// Bob and Dave should only know about 3, since one channel is
|
|
||||||
// private.
|
|
||||||
numChannels := func(node *lntest.HarnessNode, includeUnannounced bool) int {
|
|
||||||
req := &lnrpc.ChannelGraphRequest{
|
|
||||||
IncludeUnannounced: includeUnannounced,
|
|
||||||
}
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
chanGraph, err := node.DescribeGraph(ctxt, req)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable go describegraph: %v", err)
|
|
||||||
}
|
|
||||||
return len(chanGraph.Edges)
|
|
||||||
}
|
|
||||||
|
|
||||||
var predErr error
|
|
||||||
err = wait.Predicate(func() bool {
|
|
||||||
aliceChans := numChannels(net.Alice, true)
|
|
||||||
if aliceChans != 4 {
|
|
||||||
predErr = fmt.Errorf("expected Alice to know 4 edges, "+
|
|
||||||
"had %v", aliceChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
alicePubChans := numChannels(net.Alice, false)
|
|
||||||
if alicePubChans != 3 {
|
|
||||||
predErr = fmt.Errorf("expected Alice to know 3 public edges, "+
|
|
||||||
"had %v", alicePubChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
bobChans := numChannels(net.Bob, true)
|
|
||||||
if bobChans != 3 {
|
|
||||||
predErr = fmt.Errorf("expected Bob to know 3 edges, "+
|
|
||||||
"had %v", bobChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
carolChans := numChannels(carol, true)
|
|
||||||
if carolChans != 4 {
|
|
||||||
predErr = fmt.Errorf("expected Carol to know 4 edges, "+
|
|
||||||
"had %v", carolChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
carolPubChans := numChannels(carol, false)
|
|
||||||
if carolPubChans != 3 {
|
|
||||||
predErr = fmt.Errorf("expected Carol to know 3 public edges, "+
|
|
||||||
"had %v", carolPubChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
daveChans := numChannels(dave, true)
|
|
||||||
if daveChans != 3 {
|
|
||||||
predErr = fmt.Errorf("expected Dave to know 3 edges, "+
|
|
||||||
"had %v", daveChans)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%v", predErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close all channels.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointPrivate, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testInvoiceRoutingHints tests that the routing hints for an invoice are
|
|
||||||
// created properly.
|
|
||||||
func testInvoiceRoutingHints(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// Throughout this test, we'll be opening a channel between Alice and
|
|
||||||
// several other parties.
|
|
||||||
//
|
|
||||||
// First, we'll create a private channel between Alice and Bob. This
|
|
||||||
// will be the only channel that will be considered as a routing hint
|
|
||||||
// throughout this test. We'll include a push amount since we currently
|
|
||||||
// require channels to have enough remote balance to cover the invoice's
|
|
||||||
// payment.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: chanAmt / 2,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Then, we'll create Carol's node and open a public channel between her
|
|
||||||
// and Alice. This channel will not be considered as a routing hint due
|
|
||||||
// to it being public.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Alice, carol); err != nil {
|
|
||||||
t.Fatalf("unable to connect alice to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: chanAmt / 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// We'll also create a public channel between Bob and Carol to ensure
|
|
||||||
// that Bob gets selected as the only routing hint. We do this as
|
|
||||||
// we should only include routing hints for nodes that are publicly
|
|
||||||
// advertised, otherwise we'd end up leaking information about nodes
|
|
||||||
// that wish to stay unadvertised.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
|
||||||
t.Fatalf("unable to connect alice to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBobCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: chanAmt / 2,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Then, we'll create Dave's node and open a private channel between him
|
|
||||||
// and Alice. We will not include a push amount in order to not consider
|
|
||||||
// this channel as a routing hint as it will not have enough remote
|
|
||||||
// balance for the invoice's amount.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Alice, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect alice to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointDave := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Finally, we'll create Eve's node and open a private channel between
|
|
||||||
// her and Alice. This time though, we'll take Eve's node down after the
|
|
||||||
// channel has been created to avoid populating routing hints for
|
|
||||||
// inactive channels.
|
|
||||||
eve := net.NewNode(t.t, "Eve", nil)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Alice, eve); err != nil {
|
|
||||||
t.Fatalf("unable to connect alice to eve: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointEve := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, eve,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
PushAmt: chanAmt / 2,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Make sure all the channels have been opened.
|
|
||||||
chanNames := []string{
|
|
||||||
"alice-bob", "alice-carol", "bob-carol", "alice-dave",
|
|
||||||
"alice-eve",
|
|
||||||
}
|
|
||||||
aliceChans := []*lnrpc.ChannelPoint{
|
|
||||||
chanPointBob, chanPointCarol, chanPointBobCarol, chanPointDave,
|
|
||||||
chanPointEve,
|
|
||||||
}
|
|
||||||
for i, chanPoint := range aliceChans {
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("timed out waiting for channel open %s: %v",
|
|
||||||
chanNames[i], err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that the channels are open, we'll take down Eve's node.
|
|
||||||
shutdownAndAssert(net, t, eve)
|
|
||||||
|
|
||||||
// Create an invoice for Alice that will populate the routing hints.
|
|
||||||
invoice := &lnrpc.Invoice{
|
|
||||||
Memo: "routing hints",
|
|
||||||
Value: int64(chanAmt / 4),
|
|
||||||
Private: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Due to the way the channels were set up above, the channel between
|
|
||||||
// Alice and Bob should be the only channel used as a routing hint.
|
|
||||||
var predErr error
|
|
||||||
var decoded *lnrpc.PayReq
|
|
||||||
err := wait.Predicate(func() bool {
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := net.Alice.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to add invoice: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll decode the invoice's payment request to determine which
|
|
||||||
// channels were used as routing hints.
|
|
||||||
payReq := &lnrpc.PayReqString{
|
|
||||||
PayReq: resp.PaymentRequest,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
decoded, err = net.Alice.DecodePayReq(ctxt, payReq)
|
|
||||||
if err != nil {
|
|
||||||
predErr = fmt.Errorf("unable to decode payment "+
|
|
||||||
"request: %v", err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(decoded.RouteHints) != 1 {
|
|
||||||
predErr = fmt.Errorf("expected one route hint, got %d",
|
|
||||||
len(decoded.RouteHints))
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}, defaultTimeout)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf(predErr.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
hops := decoded.RouteHints[0].HopHints
|
|
||||||
if len(hops) != 1 {
|
|
||||||
t.Fatalf("expected one hop in route hint, got %d", len(hops))
|
|
||||||
}
|
|
||||||
chanID := hops[0].ChanId
|
|
||||||
|
|
||||||
// We'll need the short channel ID of the channel between Alice and Bob
|
|
||||||
// to make sure the routing hint is for this channel.
|
|
||||||
listReq := &lnrpc.ListChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
listResp, err := net.Alice.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to retrieve alice's channels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliceBobChanID uint64
|
|
||||||
for _, channel := range listResp.Channels {
|
|
||||||
if channel.RemotePubkey == net.Bob.PubKeyStr {
|
|
||||||
aliceBobChanID = channel.ChanId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliceBobChanID == 0 {
|
|
||||||
t.Fatalf("channel between alice and bob not found")
|
|
||||||
}
|
|
||||||
|
|
||||||
if chanID != aliceBobChanID {
|
|
||||||
t.Fatalf("expected channel ID %d, got %d", aliceBobChanID,
|
|
||||||
chanID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that we've confirmed the routing hints were added correctly, we
|
|
||||||
// can close all the channels and shut down all the nodes created.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointBob, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointCarol, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobCarol, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointDave, false)
|
|
||||||
|
|
||||||
// The channel between Alice and Eve should be force closed since Eve
|
|
||||||
// is offline.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointEve, true)
|
|
||||||
|
|
||||||
// Cleanup by mining the force close and sweep transaction.
|
|
||||||
cleanupForceClose(t, net, net.Alice, chanPointEve)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMultiHopOverPrivateChannels tests that private channels can be used as
|
|
||||||
// intermediate hops in a route for payments.
|
|
||||||
func testMultiHopOverPrivateChannels(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// We'll test that multi-hop payments over private channels work as
|
|
||||||
// intended. To do so, we'll create the following topology:
|
|
||||||
// private public private
|
|
||||||
// Alice <--100k--> Bob <--100k--> Carol <--100k--> Dave
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// First, we'll open a private channel between Alice and Bob with Alice
|
|
||||||
// being the funder.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err := net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the channel alice <-> bob before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't see the channel alice <-> bob before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve Alice's funding outpoint.
|
|
||||||
aliceChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointAlice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
aliceFundPoint := wire.OutPoint{
|
|
||||||
Hash: *aliceChanTXID,
|
|
||||||
Index: chanPointAlice.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create Carol's node and open a public channel between
|
|
||||||
// her and Bob with Bob being the funder.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, net.Bob, carol); err != nil {
|
|
||||||
t.Fatalf("unable to connect bob to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Bob.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("bob didn't see the channel bob <-> carol before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("carol didn't see the channel bob <-> carol before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = net.Alice.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("alice didn't see the channel bob <-> carol before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve Bob's funding outpoint.
|
|
||||||
bobChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
bobFundPoint := wire.OutPoint{
|
|
||||||
Hash: *bobChanTXID,
|
|
||||||
Index: chanPointBob.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, we'll create Dave's node and open a private channel between him
|
|
||||||
// and Carol with Carol being the funder.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
Private: true,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = carol.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("carol didn't see the channel carol <-> dave before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dave didn't see the channel carol <-> dave before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = dave.WaitForNetworkChannelOpen(ctxt, chanPointBob)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("dave didn't see the channel bob <-> carol before "+
|
|
||||||
"timeout: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retrieve Carol's funding point.
|
|
||||||
carolChanTXID, err := lnrpc.GetChanPointFundingTxid(chanPointCarol)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
carolFundPoint := wire.OutPoint{
|
|
||||||
Hash: *carolChanTXID,
|
|
||||||
Index: chanPointCarol.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now that all the channels are set up according to the topology from
|
|
||||||
// above, we can proceed to test payments. We'll create an invoice for
|
|
||||||
// Dave of 20k satoshis and pay it with Alice. Since there is no public
|
|
||||||
// route from Alice to Dave, we'll need to use the private channel
|
|
||||||
// between Carol and Dave as a routing hint encoded in the invoice.
|
|
||||||
const paymentAmt = 20000
|
|
||||||
|
|
||||||
// Create the invoice for Dave.
|
|
||||||
invoice := &lnrpc.Invoice{
|
|
||||||
Memo: "two hopz!",
|
|
||||||
Value: paymentAmt,
|
|
||||||
Private: true,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
resp, err := dave.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to add invoice for dave: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Let Alice pay the invoice.
|
|
||||||
payReqs := []string{resp.PaymentRequest}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = completePaymentRequests(
|
|
||||||
ctxt, net.Alice, net.Alice.RouterClient, payReqs, true,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to send payments from alice to dave: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// When asserting the amount of satoshis moved, we'll factor in the
|
|
||||||
// default base fee, as we didn't modify the fee structure when opening
|
|
||||||
// the channels.
|
|
||||||
const baseFee = 1
|
|
||||||
|
|
||||||
// Dave should have received 20k satoshis from Carol.
|
|
||||||
assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)",
|
|
||||||
dave, carolFundPoint, 0, paymentAmt)
|
|
||||||
|
|
||||||
// Carol should have sent 20k satoshis to Dave.
|
|
||||||
assertAmountPaid(t, "Carol(local) [private=>] Dave(remote)",
|
|
||||||
carol, carolFundPoint, paymentAmt, 0)
|
|
||||||
|
|
||||||
// Carol should have received 20k satoshis + fee for one hop from Bob.
|
|
||||||
assertAmountPaid(t, "Bob(local) => Carol(remote)",
|
|
||||||
carol, bobFundPoint, 0, paymentAmt+baseFee)
|
|
||||||
|
|
||||||
// Bob should have sent 20k satoshis + fee for one hop to Carol.
|
|
||||||
assertAmountPaid(t, "Bob(local) => Carol(remote)",
|
|
||||||
net.Bob, bobFundPoint, paymentAmt+baseFee, 0)
|
|
||||||
|
|
||||||
// Bob should have received 20k satoshis + fee for two hops from Alice.
|
|
||||||
assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Bob,
|
|
||||||
aliceFundPoint, 0, paymentAmt+baseFee*2)
|
|
||||||
|
|
||||||
// Alice should have sent 20k satoshis + fee for two hops to Bob.
|
|
||||||
assertAmountPaid(t, "Alice(local) [private=>] Bob(remote)", net.Alice,
|
|
||||||
aliceFundPoint, paymentAmt+baseFee*2, 0)
|
|
||||||
|
|
||||||
// At this point, the payment was successful. We can now close all the
|
|
||||||
// channels and shutdown the nodes created throughout this test.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
ctxb := context.Background()
|
ctxb := context.Background()
|
||||||
|
|
||||||
@ -6996,7 +5523,7 @@ func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
// The invoice update should exactly match the invoice created
|
// The invoice update should exactly match the invoice created
|
||||||
// above, but should now be settled and have SettleDate
|
// above, but should now be settled and have SettleDate
|
||||||
if !invoiceUpdate.Settled {
|
if !invoiceUpdate.Settled { // nolint:staticcheck
|
||||||
t.Fatalf("invoice not settled but should be")
|
t.Fatalf("invoice not settled but should be")
|
||||||
}
|
}
|
||||||
if invoiceUpdate.SettleDate == 0 {
|
if invoiceUpdate.SettleDate == 0 {
|
||||||
@ -7094,11 +5621,11 @@ func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
// We should now get the ith invoice we added, as they should
|
// We should now get the ith invoice we added, as they should
|
||||||
// be returned in order.
|
// be returned in order.
|
||||||
if invoiceUpdate.Settled {
|
if invoiceUpdate.Settled { // nolint:staticcheck
|
||||||
t.Fatalf("should have only received add events")
|
t.Fatalf("should have only received add events")
|
||||||
}
|
}
|
||||||
originalInvoice := newInvoices[i]
|
originalInvoice := newInvoices[i]
|
||||||
rHash := sha256.Sum256(originalInvoice.RPreimage[:])
|
rHash := sha256.Sum256(originalInvoice.RPreimage)
|
||||||
if !bytes.Equal(invoiceUpdate.RHash, rHash[:]) {
|
if !bytes.Equal(invoiceUpdate.RHash, rHash[:]) {
|
||||||
t.Fatalf("invoices have mismatched payment hashes: "+
|
t.Fatalf("invoices have mismatched payment hashes: "+
|
||||||
"expected %x, got %x", rHash[:],
|
"expected %x, got %x", rHash[:],
|
||||||
@ -7138,7 +5665,7 @@ func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// we'll use a map to assert that the proper set has been settled.
|
// we'll use a map to assert that the proper set has been settled.
|
||||||
settledInvoices := make(map[[32]byte]struct{})
|
settledInvoices := make(map[[32]byte]struct{})
|
||||||
for _, invoice := range newInvoices {
|
for _, invoice := range newInvoices {
|
||||||
rHash := sha256.Sum256(invoice.RPreimage[:])
|
rHash := sha256.Sum256(invoice.RPreimage)
|
||||||
settledInvoices[rHash] = struct{}{}
|
settledInvoices[rHash] = struct{}{}
|
||||||
}
|
}
|
||||||
for i := 0; i < numInvoices; i++ {
|
for i := 0; i < numInvoices; i++ {
|
||||||
@ -7149,7 +5676,7 @@ func testInvoiceSubscriptions(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
|
|
||||||
// We should now get the ith invoice we added, as they should
|
// We should now get the ith invoice we added, as they should
|
||||||
// be returned in order.
|
// be returned in order.
|
||||||
if !invoiceUpdate.Settled {
|
if !invoiceUpdate.Settled { // nolint:staticcheck
|
||||||
t.Fatalf("should have only received settle events")
|
t.Fatalf("should have only received settle events")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -8009,11 +6536,9 @@ func testGarbageCollectLinkNodes(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
predErr = checkNumForceClosedChannels(pendingChanResp, 0)
|
||||||
if predErr != nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return predErr == nil
|
||||||
|
|
||||||
}, defaultTimeout)
|
}, defaultTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("channels not marked as fully resolved: %v", predErr)
|
t.Fatalf("channels not marked as fully resolved: %v", predErr)
|
||||||
@ -8421,7 +6946,7 @@ func testRevokedCloseRetributionZeroValueRemoteOutput(net *lntest.NetworkHarness
|
|||||||
}
|
}
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
carolChan, err = getChanInfo(ctxt, carol)
|
_, err = getChanInfo(ctxt, carol)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to get carol chan info: %v", err)
|
t.Fatalf("unable to get carol chan info: %v", err)
|
||||||
}
|
}
|
||||||
@ -8453,13 +6978,14 @@ func testRevokedCloseRetributionZeroValueRemoteOutput(net *lntest.NetworkHarness
|
|||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
var (
|
var (
|
||||||
closeUpdates lnrpc.Lightning_CloseChannelClient
|
closeUpdates lnrpc.Lightning_CloseChannelClient
|
||||||
closeTxId *chainhash.Hash
|
closeTxID *chainhash.Hash
|
||||||
closeErr error
|
closeErr error
|
||||||
force bool = true
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
force := true
|
||||||
err = wait.Predicate(func() bool {
|
err = wait.Predicate(func() bool {
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelCloseTimeout)
|
ctxt, _ := context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
closeUpdates, closeTxId, closeErr = net.CloseChannel(
|
closeUpdates, closeTxID, closeErr = net.CloseChannel(
|
||||||
ctxt, carol, chanPoint, force,
|
ctxt, carol, chanPoint, force,
|
||||||
)
|
)
|
||||||
return closeErr == nil
|
return closeErr == nil
|
||||||
@ -8475,9 +7001,9 @@ func testRevokedCloseRetributionZeroValueRemoteOutput(net *lntest.NetworkHarness
|
|||||||
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
if *txid != *closeTxId {
|
if *txid != *closeTxID {
|
||||||
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
||||||
closeTxId, txid)
|
closeTxID, txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, generate a single block, wait for the final close status
|
// Finally, generate a single block, wait for the final close status
|
||||||
@ -8783,7 +7309,7 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
|
|||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
force := true
|
force := true
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
closeUpdates, closeTxId, err := net.CloseChannel(ctxt, carol,
|
closeUpdates, closeTxID, err := net.CloseChannel(ctxt, carol,
|
||||||
chanPoint, force)
|
chanPoint, force)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to close channel: %v", err)
|
t.Fatalf("unable to close channel: %v", err)
|
||||||
@ -8796,9 +7322,9 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
|
|||||||
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
if *txid != *closeTxId {
|
if *txid != *closeTxID {
|
||||||
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
||||||
closeTxId, txid)
|
closeTxID, txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Generate a single block to mine the breach transaction.
|
// Generate a single block to mine the breach transaction.
|
||||||
@ -8817,9 +7343,9 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("error while waiting for channel close: %v", err)
|
t.Fatalf("error while waiting for channel close: %v", err)
|
||||||
}
|
}
|
||||||
if *breachTXID != *closeTxId {
|
if *breachTXID != *closeTxID {
|
||||||
t.Fatalf("expected breach ID(%v) to be equal to close ID (%v)",
|
t.Fatalf("expected breach ID(%v) to be equal to close ID (%v)",
|
||||||
breachTXID, closeTxId)
|
breachTXID, closeTxID)
|
||||||
}
|
}
|
||||||
assertTxInBlock(t, block, breachTXID)
|
assertTxInBlock(t, block, breachTXID)
|
||||||
|
|
||||||
@ -8916,11 +7442,8 @@ func testRevokedCloseRetributionRemoteHodl(net *lntest.NetworkHarness,
|
|||||||
|
|
||||||
// The sole input should be spending from the commit tx.
|
// The sole input should be spending from the commit tx.
|
||||||
txIn := secondLevel.MsgTx().TxIn[0]
|
txIn := secondLevel.MsgTx().TxIn[0]
|
||||||
if !bytes.Equal(txIn.PreviousOutPoint.Hash[:], commitTxid[:]) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
return bytes.Equal(txIn.PreviousOutPoint.Hash[:], commitTxid[:])
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check that all the inputs of this transaction are spending outputs
|
// Check that all the inputs of this transaction are spending outputs
|
||||||
@ -9237,7 +7760,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(
|
|||||||
// broadcasting his current channel state. This is actually the
|
// broadcasting his current channel state. This is actually the
|
||||||
// commitment transaction of a prior *revoked* state, so he'll soon
|
// commitment transaction of a prior *revoked* state, so he'll soon
|
||||||
// feel the wrath of Dave's retribution.
|
// feel the wrath of Dave's retribution.
|
||||||
closeUpdates, closeTxId, err := net.CloseChannel(
|
closeUpdates, closeTxID, err := net.CloseChannel(
|
||||||
ctxb, carol, chanPoint, true,
|
ctxb, carol, chanPoint, true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -9251,9 +7774,9 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(
|
|||||||
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
t.Fatalf("unable to find Carol's force close tx in mempool: %v",
|
||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
if *txid != *closeTxId {
|
if *txid != *closeTxID {
|
||||||
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
t.Fatalf("expected closeTx(%v) in mempool, instead found %v",
|
||||||
closeTxId, txid)
|
closeTxID, txid)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Finally, generate a single block, wait for the final close status
|
// Finally, generate a single block, wait for the final close status
|
||||||
@ -9636,7 +8159,7 @@ func testDataLossProtection(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
// node that Carol will pay to in order to advance the state of
|
// node that Carol will pay to in order to advance the state of
|
||||||
// the channel.
|
// the channel.
|
||||||
// TODO(halseth): have dangling HTLCs on the commitment, able to
|
// TODO(halseth): have dangling HTLCs on the commitment, able to
|
||||||
// retrive funds?
|
// retrieve funds?
|
||||||
payReqs, _, _, err := createPayReqs(
|
payReqs, _, _, err := createPayReqs(
|
||||||
node, paymentAmt, numInvoices,
|
node, paymentAmt, numInvoices,
|
||||||
)
|
)
|
||||||
@ -10093,7 +8616,7 @@ type graphSubscription struct {
|
|||||||
|
|
||||||
// subscribeGraphNotifications subscribes to channel graph updates and launches
|
// subscribeGraphNotifications subscribes to channel graph updates and launches
|
||||||
// a goroutine that forwards these to the returned channel.
|
// a goroutine that forwards these to the returned channel.
|
||||||
func subscribeGraphNotifications(t *harnessTest, ctxb context.Context,
|
func subscribeGraphNotifications(ctxb context.Context, t *harnessTest,
|
||||||
node *lntest.HarnessNode) graphSubscription {
|
node *lntest.HarnessNode) graphSubscription {
|
||||||
|
|
||||||
// We'll first start by establishing a notification client which will
|
// We'll first start by establishing a notification client which will
|
||||||
@ -10251,9 +8774,7 @@ func testGraphTopologyNtfns(net *lntest.NetworkHarness, t *harnessTest, pinned b
|
|||||||
waitForGraphSync(t, alice)
|
waitForGraphSync(t, alice)
|
||||||
|
|
||||||
// Let Alice subscribe to graph notifications.
|
// Let Alice subscribe to graph notifications.
|
||||||
graphSub := subscribeGraphNotifications(
|
graphSub := subscribeGraphNotifications(ctxb, t, alice)
|
||||||
t, ctxb, alice,
|
|
||||||
)
|
|
||||||
defer close(graphSub.quit)
|
defer close(graphSub.quit)
|
||||||
|
|
||||||
// Open a new channel between Alice and Bob.
|
// Open a new channel between Alice and Bob.
|
||||||
@ -10473,7 +8994,7 @@ out:
|
|||||||
func testNodeAnnouncement(net *lntest.NetworkHarness, t *harnessTest) {
|
func testNodeAnnouncement(net *lntest.NetworkHarness, t *harnessTest) {
|
||||||
ctxb := context.Background()
|
ctxb := context.Background()
|
||||||
|
|
||||||
aliceSub := subscribeGraphNotifications(t, ctxb, net.Alice)
|
aliceSub := subscribeGraphNotifications(ctxb, t, net.Alice)
|
||||||
defer close(aliceSub.quit)
|
defer close(aliceSub.quit)
|
||||||
|
|
||||||
advertisedAddrs := []string{
|
advertisedAddrs := []string{
|
||||||
@ -10540,7 +9061,7 @@ func testNodeAnnouncement(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
for _, update := range graphUpdate.NodeUpdates {
|
for _, update := range graphUpdate.NodeUpdates {
|
||||||
if update.IdentityKey == nodePubKey {
|
if update.IdentityKey == nodePubKey {
|
||||||
assertAddrs(
|
assertAddrs(
|
||||||
update.Addresses,
|
update.Addresses, // nolint:staticcheck
|
||||||
targetAddrs...,
|
targetAddrs...,
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
@ -10790,7 +9311,7 @@ func testAsyncPayments(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Log("\tBenchmark info: Elapsed time: ", timeTaken)
|
t.Log("\tBenchmark info: Elapsed time: ", timeTaken)
|
||||||
t.Log("\tBenchmark info: TPS: ", float64(numInvoices)/float64(timeTaken.Seconds()))
|
t.Log("\tBenchmark info: TPS: ", float64(numInvoices)/timeTaken.Seconds())
|
||||||
|
|
||||||
// Finally, immediately close the channel. This function will also
|
// Finally, immediately close the channel. This function will also
|
||||||
// block until the channel is closed and will additionally assert the
|
// block until the channel is closed and will additionally assert the
|
||||||
@ -11925,10 +10446,7 @@ func testSwitchOfflineDeliveryPersistence(net *lntest.NetworkHarness, t *harness
|
|||||||
var predErr error
|
var predErr error
|
||||||
err = wait.Predicate(func() bool {
|
err = wait.Predicate(func() bool {
|
||||||
predErr = assertNumActiveHtlcs(nodes, numPayments)
|
predErr = assertNumActiveHtlcs(nodes, numPayments)
|
||||||
if predErr != nil {
|
return predErr == nil
|
||||||
return false
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
|
|
||||||
}, defaultTimeout)
|
}, defaultTimeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -12346,574 +10864,6 @@ func testSwitchOfflineDeliveryOutgoingOffline(
|
|||||||
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
closeChannelAndAssert(ctxt, t, net, dave, chanPointDave, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
// computeFee calculates the payment fee as specified in BOLT07
|
|
||||||
func computeFee(baseFee, feeRate, amt lnwire.MilliSatoshi) lnwire.MilliSatoshi {
|
|
||||||
return baseFee + amt*feeRate/1000000
|
|
||||||
}
|
|
||||||
|
|
||||||
// testQueryRoutes checks the response of queryroutes.
|
|
||||||
// We'll create the following network topology:
|
|
||||||
// Alice --> Bob --> Carol --> Dave
|
|
||||||
// and query the daemon for routes from Alice to Dave.
|
|
||||||
func testQueryRoutes(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
var networkChans []*lnrpc.ChannelPoint
|
|
||||||
|
|
||||||
// Open a channel between Alice and Bob.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAlice := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointAlice)
|
|
||||||
|
|
||||||
// Create Carol and establish a channel from Bob.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, net.Bob); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to bob: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, net.Bob)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointBob)
|
|
||||||
|
|
||||||
// Create Dave and establish a channel from Carol.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, dave, carol); err != nil {
|
|
||||||
t.Fatalf("unable to connect dave to carol: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
networkChans = append(networkChans, chanPointCarol)
|
|
||||||
|
|
||||||
// Wait for all nodes to have seen all channels.
|
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
|
||||||
nodeNames := []string{"Alice", "Bob", "Carol", "Dave"}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for i, node := range nodes {
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
point := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d): timeout waiting for "+
|
|
||||||
"channel(%s) open: %v", nodeNames[i],
|
|
||||||
node.NodeID, point, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Query for routes to pay from Alice to Dave.
|
|
||||||
const paymentAmt = 1000
|
|
||||||
routesReq := &lnrpc.QueryRoutesRequest{
|
|
||||||
PubKey: dave.PubKeyStr,
|
|
||||||
Amt: paymentAmt,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
routesRes, err := net.Alice.QueryRoutes(ctxt, routesReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const mSat = 1000
|
|
||||||
feePerHopMSat := computeFee(1000, 1, paymentAmt*mSat)
|
|
||||||
|
|
||||||
for i, route := range routesRes.Routes {
|
|
||||||
expectedTotalFeesMSat :=
|
|
||||||
lnwire.MilliSatoshi(len(route.Hops)-1) * feePerHopMSat
|
|
||||||
expectedTotalAmtMSat := (paymentAmt * mSat) + expectedTotalFeesMSat
|
|
||||||
|
|
||||||
if route.TotalFees != route.TotalFeesMsat/mSat { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v: total fees %v (msat) does not "+
|
|
||||||
"round down to %v (sat)",
|
|
||||||
i, route.TotalFeesMsat, route.TotalFees) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
if route.TotalFeesMsat != int64(expectedTotalFeesMSat) {
|
|
||||||
t.Fatalf("route %v: total fees in msat expected %v got %v",
|
|
||||||
i, expectedTotalFeesMSat, route.TotalFeesMsat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.TotalAmt != route.TotalAmtMsat/mSat { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v: total amt %v (msat) does not "+
|
|
||||||
"round down to %v (sat)",
|
|
||||||
i, route.TotalAmtMsat, route.TotalAmt) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
if route.TotalAmtMsat != int64(expectedTotalAmtMSat) {
|
|
||||||
t.Fatalf("route %v: total amt in msat expected %v got %v",
|
|
||||||
i, expectedTotalAmtMSat, route.TotalAmtMsat)
|
|
||||||
}
|
|
||||||
|
|
||||||
// For all hops except the last, we check that fee equals feePerHop
|
|
||||||
// and amount to forward deducts feePerHop on each hop.
|
|
||||||
expectedAmtToForwardMSat := expectedTotalAmtMSat
|
|
||||||
for j, hop := range route.Hops[:len(route.Hops)-1] {
|
|
||||||
expectedAmtToForwardMSat -= feePerHopMSat
|
|
||||||
|
|
||||||
if hop.Fee != hop.FeeMsat/mSat { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v hop %v: fee %v (msat) does not "+
|
|
||||||
"round down to %v (sat)",
|
|
||||||
i, j, hop.FeeMsat, hop.Fee) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
if hop.FeeMsat != int64(feePerHopMSat) {
|
|
||||||
t.Fatalf("route %v hop %v: fee in msat expected %v got %v",
|
|
||||||
i, j, feePerHopMSat, hop.FeeMsat)
|
|
||||||
}
|
|
||||||
|
|
||||||
if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+
|
|
||||||
"round down to %v (sat)",
|
|
||||||
i, j, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
if hop.AmtToForwardMsat != int64(expectedAmtToForwardMSat) {
|
|
||||||
t.Fatalf("route %v hop %v: amt to forward in msat "+
|
|
||||||
"expected %v got %v",
|
|
||||||
i, j, expectedAmtToForwardMSat, hop.AmtToForwardMsat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Last hop should have zero fee and amount to forward should equal
|
|
||||||
// payment amount.
|
|
||||||
hop := route.Hops[len(route.Hops)-1]
|
|
||||||
|
|
||||||
if hop.Fee != 0 || hop.FeeMsat != 0 { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v hop %v: fee expected 0 got %v (sat) %v (msat)",
|
|
||||||
i, len(route.Hops)-1, hop.Fee, hop.FeeMsat) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
|
|
||||||
if hop.AmtToForward != hop.AmtToForwardMsat/mSat { // nolint:staticcheck
|
|
||||||
t.Fatalf("route %v hop %v: amt to forward %v (msat) does not "+
|
|
||||||
"round down to %v (sat)",
|
|
||||||
i, len(route.Hops)-1, hop.AmtToForwardMsat, hop.AmtToForward) // nolint:staticcheck
|
|
||||||
}
|
|
||||||
if hop.AmtToForwardMsat != paymentAmt*mSat {
|
|
||||||
t.Fatalf("route %v hop %v: amt to forward in msat "+
|
|
||||||
"expected %v got %v",
|
|
||||||
i, len(route.Hops)-1, paymentAmt*mSat, hop.AmtToForwardMsat)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// While we're here, we test updating mission control's config values
|
|
||||||
// and assert that they are correctly updated and check that our mission
|
|
||||||
// control import function updates appropriately.
|
|
||||||
testMissionControlCfg(t.t, net.Alice)
|
|
||||||
testMissionControlImport(
|
|
||||||
t.t, net.Alice, net.Bob.PubKey[:], carol.PubKey[:],
|
|
||||||
)
|
|
||||||
|
|
||||||
// We clean up the test case by closing channels that were created for
|
|
||||||
// the duration of the tests.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAlice, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBob, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarol, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMissionControlCfg tests getting and setting of a node's mission control
|
|
||||||
// config, resetting to the original values after testing so that no other
|
|
||||||
// tests are affected.
|
|
||||||
func testMissionControlCfg(t *testing.T, node *lntest.HarnessNode) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
startCfg, err := node.RouterClient.GetMissionControlConfig(
|
|
||||||
ctxb, &routerrpc.GetMissionControlConfigRequest{},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
cfg := &routerrpc.MissionControlConfig{
|
|
||||||
HalfLifeSeconds: 8000,
|
|
||||||
HopProbability: 0.8,
|
|
||||||
Weight: 0.3,
|
|
||||||
MaximumPaymentResults: 30,
|
|
||||||
MinimumFailureRelaxInterval: 60,
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = node.RouterClient.SetMissionControlConfig(
|
|
||||||
ctxb, &routerrpc.SetMissionControlConfigRequest{
|
|
||||||
Config: cfg,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
resp, err := node.RouterClient.GetMissionControlConfig(
|
|
||||||
ctxb, &routerrpc.GetMissionControlConfigRequest{},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.True(t, proto.Equal(cfg, resp.Config))
|
|
||||||
|
|
||||||
_, err = node.RouterClient.SetMissionControlConfig(
|
|
||||||
ctxb, &routerrpc.SetMissionControlConfigRequest{
|
|
||||||
Config: startCfg.Config,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testMissionControlImport tests import of mission control results from an
|
|
||||||
// external source.
|
|
||||||
func testMissionControlImport(t *testing.T, node *lntest.HarnessNode,
|
|
||||||
fromNode, toNode []byte) {
|
|
||||||
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// Reset mission control so that our query will return the default
|
|
||||||
// probability for our first request.
|
|
||||||
_, err := node.RouterClient.ResetMissionControl(
|
|
||||||
ctxb, &routerrpc.ResetMissionControlRequest{},
|
|
||||||
)
|
|
||||||
require.NoError(t, err, "could not reset mission control")
|
|
||||||
|
|
||||||
// Get our baseline probability for a 10 msat hop between our target
|
|
||||||
// nodes.
|
|
||||||
var amount int64 = 10
|
|
||||||
probReq := &routerrpc.QueryProbabilityRequest{
|
|
||||||
FromNode: fromNode,
|
|
||||||
ToNode: toNode,
|
|
||||||
AmtMsat: amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
importHistory := &routerrpc.PairData{
|
|
||||||
FailTime: time.Now().Unix(),
|
|
||||||
FailAmtMsat: amount,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that our history is not already equal to the value we want to
|
|
||||||
// set. This should not happen because we have just cleared our state.
|
|
||||||
resp1, err := node.RouterClient.QueryProbability(ctxb, probReq)
|
|
||||||
require.NoError(t, err, "query probability failed")
|
|
||||||
require.Zero(t, resp1.History.FailTime)
|
|
||||||
require.Zero(t, resp1.History.FailAmtMsat)
|
|
||||||
|
|
||||||
// Now, we import a single entry which tracks a failure of the amount
|
|
||||||
// we want to query between our nodes.
|
|
||||||
req := &routerrpc.XImportMissionControlRequest{
|
|
||||||
Pairs: []*routerrpc.PairHistory{
|
|
||||||
{
|
|
||||||
NodeFrom: fromNode,
|
|
||||||
NodeTo: toNode,
|
|
||||||
History: importHistory,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = node.RouterClient.XImportMissionControl(ctxb, req)
|
|
||||||
require.NoError(t, err, "could not import config")
|
|
||||||
|
|
||||||
resp2, err := node.RouterClient.QueryProbability(ctxb, probReq)
|
|
||||||
require.NoError(t, err, "query probability failed")
|
|
||||||
require.Equal(t, importHistory.FailTime, resp2.History.FailTime)
|
|
||||||
require.Equal(t, importHistory.FailAmtMsat, resp2.History.FailAmtMsat)
|
|
||||||
|
|
||||||
// Finally, check that we will fail if inconsistent sat/msat values are
|
|
||||||
// set.
|
|
||||||
importHistory.FailAmtSat = amount * 2
|
|
||||||
_, err = node.RouterClient.XImportMissionControl(ctxb, req)
|
|
||||||
require.Error(t, err, "mismatched import amounts succeeded")
|
|
||||||
}
|
|
||||||
|
|
||||||
// testRouteFeeCutoff tests that we are able to prevent querying routes and
|
|
||||||
// sending payments that incur a fee higher than the fee limit.
|
|
||||||
func testRouteFeeCutoff(net *lntest.NetworkHarness, t *harnessTest) {
|
|
||||||
ctxb := context.Background()
|
|
||||||
|
|
||||||
// For this test, we'll create the following topology:
|
|
||||||
//
|
|
||||||
// --- Bob ---
|
|
||||||
// / \
|
|
||||||
// Alice ---- ---- Dave
|
|
||||||
// \ /
|
|
||||||
// -- Carol --
|
|
||||||
//
|
|
||||||
// Alice will attempt to send payments to Dave that should not incur a
|
|
||||||
// fee greater than the fee limit expressed as a percentage of the
|
|
||||||
// amount and as a fixed amount of satoshis.
|
|
||||||
const chanAmt = btcutil.Amount(100000)
|
|
||||||
|
|
||||||
// Open a channel between Alice and Bob.
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAliceBob := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, net.Bob,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create Carol's node and open a channel between her and Alice with
|
|
||||||
// Alice being the funder.
|
|
||||||
carol := net.NewNode(t.t, "Carol", nil)
|
|
||||||
defer shutdownAndAssert(net, t, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, net.Alice); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to alice: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
net.SendCoins(ctxt, t.t, btcutil.SatoshiPerBitcoin, carol)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointAliceCarol := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Alice, carol,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create Dave's node and open a channel between him and Bob with Bob
|
|
||||||
// being the funder.
|
|
||||||
dave := net.NewNode(t.t, "Dave", nil)
|
|
||||||
defer shutdownAndAssert(net, t, dave)
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, dave, net.Bob); err != nil {
|
|
||||||
t.Fatalf("unable to connect dave to bob: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointBobDave := openChannelAndAssert(
|
|
||||||
ctxt, t, net, net.Bob, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Open a channel between Carol and Dave.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if err := net.ConnectNodes(ctxt, carol, dave); err != nil {
|
|
||||||
t.Fatalf("unable to connect carol to dave: %v", err)
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelOpenTimeout)
|
|
||||||
chanPointCarolDave := openChannelAndAssert(
|
|
||||||
ctxt, t, net, carol, dave,
|
|
||||||
lntest.OpenChannelParams{
|
|
||||||
Amt: chanAmt,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// Now that all the channels were set up, we'll wait for all the nodes
|
|
||||||
// to have seen all the channels.
|
|
||||||
nodes := []*lntest.HarnessNode{net.Alice, net.Bob, carol, dave}
|
|
||||||
nodeNames := []string{"alice", "bob", "carol", "dave"}
|
|
||||||
networkChans := []*lnrpc.ChannelPoint{
|
|
||||||
chanPointAliceBob, chanPointAliceCarol, chanPointBobDave,
|
|
||||||
chanPointCarolDave,
|
|
||||||
}
|
|
||||||
for _, chanPoint := range networkChans {
|
|
||||||
for i, node := range nodes {
|
|
||||||
txid, err := lnrpc.GetChanPointFundingTxid(chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get txid: %v", err)
|
|
||||||
}
|
|
||||||
outpoint := wire.OutPoint{
|
|
||||||
Hash: *txid,
|
|
||||||
Index: chanPoint.OutputIndex,
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
err = node.WaitForNetworkChannelOpen(ctxt, chanPoint)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("%s(%d) timed out waiting for "+
|
|
||||||
"channel(%s) open: %v", nodeNames[i],
|
|
||||||
node.NodeID, outpoint, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The payments should only be successful across the route:
|
|
||||||
// Alice -> Bob -> Dave
|
|
||||||
// Therefore, we'll update the fee policy on Carol's side for the
|
|
||||||
// channel between her and Dave to invalidate the route:
|
|
||||||
// Alice -> Carol -> Dave
|
|
||||||
baseFee := int64(10000)
|
|
||||||
feeRate := int64(5)
|
|
||||||
timeLockDelta := uint32(chainreg.DefaultBitcoinTimeLockDelta)
|
|
||||||
maxHtlc := calculateMaxHtlc(chanAmt)
|
|
||||||
|
|
||||||
expectedPolicy := &lnrpc.RoutingPolicy{
|
|
||||||
FeeBaseMsat: baseFee,
|
|
||||||
FeeRateMilliMsat: testFeeBase * feeRate,
|
|
||||||
TimeLockDelta: timeLockDelta,
|
|
||||||
MinHtlc: 1000, // default value
|
|
||||||
MaxHtlcMsat: maxHtlc,
|
|
||||||
}
|
|
||||||
|
|
||||||
updateFeeReq := &lnrpc.PolicyUpdateRequest{
|
|
||||||
BaseFeeMsat: baseFee,
|
|
||||||
FeeRate: float64(feeRate),
|
|
||||||
TimeLockDelta: timeLockDelta,
|
|
||||||
MaxHtlcMsat: maxHtlc,
|
|
||||||
Scope: &lnrpc.PolicyUpdateRequest_ChanPoint{
|
|
||||||
ChanPoint: chanPointCarolDave,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
if _, err := carol.UpdateChannelPolicy(ctxt, updateFeeReq); err != nil {
|
|
||||||
t.Fatalf("unable to update chan policy: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wait for Alice to receive the channel update from Carol.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
aliceSub := subscribeGraphNotifications(t, ctxt, net.Alice)
|
|
||||||
defer close(aliceSub.quit)
|
|
||||||
|
|
||||||
waitForChannelUpdate(
|
|
||||||
t, aliceSub,
|
|
||||||
[]expectedChanUpdate{
|
|
||||||
{carol.PubKeyStr, expectedPolicy, chanPointCarolDave},
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
// We'll also need the channel IDs for Bob's channels in order to
|
|
||||||
// confirm the route of the payments.
|
|
||||||
listReq := &lnrpc.ListChannelsRequest{}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
listResp, err := net.Bob.ListChannels(ctxt, listReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to retrieve bob's channels: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var aliceBobChanID, bobDaveChanID uint64
|
|
||||||
for _, channel := range listResp.Channels {
|
|
||||||
switch channel.RemotePubkey {
|
|
||||||
case net.Alice.PubKeyStr:
|
|
||||||
aliceBobChanID = channel.ChanId
|
|
||||||
case dave.PubKeyStr:
|
|
||||||
bobDaveChanID = channel.ChanId
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if aliceBobChanID == 0 {
|
|
||||||
t.Fatalf("channel between alice and bob not found")
|
|
||||||
}
|
|
||||||
if bobDaveChanID == 0 {
|
|
||||||
t.Fatalf("channel between bob and dave not found")
|
|
||||||
}
|
|
||||||
hopChanIDs := []uint64{aliceBobChanID, bobDaveChanID}
|
|
||||||
|
|
||||||
// checkRoute is a helper closure to ensure the route contains the
|
|
||||||
// correct intermediate hops.
|
|
||||||
checkRoute := func(route *lnrpc.Route) {
|
|
||||||
if len(route.Hops) != 2 {
|
|
||||||
t.Fatalf("expected two hops, got %d", len(route.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, hop := range route.Hops {
|
|
||||||
if hop.ChanId != hopChanIDs[i] {
|
|
||||||
t.Fatalf("expected chan id %d, got %d",
|
|
||||||
hopChanIDs[i], hop.ChanId)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll be attempting to send two payments from Alice to Dave. One will
|
|
||||||
// have a fee cutoff expressed as a percentage of the amount and the
|
|
||||||
// other will have it expressed as a fixed amount of satoshis.
|
|
||||||
const paymentAmt = 100
|
|
||||||
carolFee := computeFee(lnwire.MilliSatoshi(baseFee), 1, paymentAmt)
|
|
||||||
|
|
||||||
// testFeeCutoff is a helper closure that will ensure the different
|
|
||||||
// types of fee limits work as intended when querying routes and sending
|
|
||||||
// payments.
|
|
||||||
testFeeCutoff := func(feeLimit *lnrpc.FeeLimit) {
|
|
||||||
queryRoutesReq := &lnrpc.QueryRoutesRequest{
|
|
||||||
PubKey: dave.PubKeyStr,
|
|
||||||
Amt: paymentAmt,
|
|
||||||
FeeLimit: feeLimit,
|
|
||||||
}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
routesResp, err := net.Alice.QueryRoutes(ctxt, queryRoutesReq)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to get routes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
checkRoute(routesResp.Routes[0])
|
|
||||||
|
|
||||||
invoice := &lnrpc.Invoice{Value: paymentAmt}
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
invoiceResp, err := dave.AddInvoice(ctxt, invoice)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create invoice: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sendReq := &routerrpc.SendPaymentRequest{
|
|
||||||
PaymentRequest: invoiceResp.PaymentRequest,
|
|
||||||
TimeoutSeconds: 60,
|
|
||||||
FeeLimitMsat: noFeeLimitMsat,
|
|
||||||
}
|
|
||||||
switch limit := feeLimit.Limit.(type) {
|
|
||||||
case *lnrpc.FeeLimit_Fixed:
|
|
||||||
sendReq.FeeLimitMsat = 1000 * limit.Fixed
|
|
||||||
case *lnrpc.FeeLimit_Percent:
|
|
||||||
sendReq.FeeLimitMsat = 1000 * paymentAmt * limit.Percent / 100
|
|
||||||
}
|
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
|
||||||
result := sendAndAssertSuccess(ctxt, t, net.Alice, sendReq)
|
|
||||||
|
|
||||||
checkRoute(result.Htlcs[0].Route)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll start off using percentages first. Since the fee along the
|
|
||||||
// route using Carol as an intermediate hop is 10% of the payment's
|
|
||||||
// amount, we'll use a lower percentage in order to invalid that route.
|
|
||||||
feeLimitPercent := &lnrpc.FeeLimit{
|
|
||||||
Limit: &lnrpc.FeeLimit_Percent{
|
|
||||||
Percent: baseFee/1000 - 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testFeeCutoff(feeLimitPercent)
|
|
||||||
|
|
||||||
// Now we'll test using fixed fee limit amounts. Since we computed the
|
|
||||||
// fee for the route using Carol as an intermediate hop earlier, we can
|
|
||||||
// use a smaller value in order to invalidate that route.
|
|
||||||
feeLimitFixed := &lnrpc.FeeLimit{
|
|
||||||
Limit: &lnrpc.FeeLimit_Fixed{
|
|
||||||
Fixed: int64(carolFee.ToSatoshis()) - 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
testFeeCutoff(feeLimitFixed)
|
|
||||||
|
|
||||||
// Once we're done, close the channels and shut down the nodes created
|
|
||||||
// throughout this test.
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceBob, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Alice, chanPointAliceCarol, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, net.Bob, chanPointBobDave, false)
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
|
||||||
closeChannelAndAssert(ctxt, t, net, carol, chanPointCarolDave, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// testSendUpdateDisableChannel ensures that a channel update with the disable
|
// testSendUpdateDisableChannel ensures that a channel update with the disable
|
||||||
// flag set is sent once a channel has been either unilaterally or cooperatively
|
// flag set is sent once a channel has been either unilaterally or cooperatively
|
||||||
// closed.
|
// closed.
|
||||||
@ -13001,7 +10951,7 @@ func testSendUpdateDisableChannel(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
t.Fatalf("unable to connect bob to dave: %v", err)
|
t.Fatalf("unable to connect bob to dave: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
daveSub := subscribeGraphNotifications(t, ctxb, dave)
|
daveSub := subscribeGraphNotifications(ctxb, t, dave)
|
||||||
defer close(daveSub.quit)
|
defer close(daveSub.quit)
|
||||||
|
|
||||||
// We should expect to see a channel update with the default routing
|
// We should expect to see a channel update with the default routing
|
||||||
|
@ -107,6 +107,11 @@ var allTestCases = []*testCase{
|
|||||||
name: "private channels",
|
name: "private channels",
|
||||||
test: testPrivateChannels,
|
test: testPrivateChannels,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "private channel update policy",
|
||||||
|
test: testUpdateChannelPolicyForPrivateChannel,
|
||||||
|
},
|
||||||
|
|
||||||
{
|
{
|
||||||
name: "invoice routing hints",
|
name: "invoice routing hints",
|
||||||
test: testInvoiceRoutingHints,
|
test: testInvoiceRoutingHints,
|
||||||
|
@ -283,3 +283,4 @@
|
|||||||
<time> [ERR] HSWC: ChannelLink(<chan>): failing link: process hodl queue: unable to update commitment: link shutting down with error: internal error
|
<time> [ERR] HSWC: ChannelLink(<chan>): failing link: process hodl queue: unable to update commitment: link shutting down with error: internal error
|
||||||
<time> [ERR] INVC: SettleHodlInvoice with preimage <hex>: invoice already canceled
|
<time> [ERR] INVC: SettleHodlInvoice with preimage <hex>: invoice already canceled
|
||||||
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SettleInvoice]: invoice already canceled
|
<time> [ERR] RPCS: [/invoicesrpc.Invoices/SettleInvoice]: invoice already canceled
|
||||||
|
<time> [ERR] HSWC: ChannelLink(<chan>): outgoing htlc(<hex>) has insufficient fee: expected 33000, got 1020
|
||||||
|
@ -4,25 +4,27 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockPaymentAttemptDispatcher struct {
|
type mockPaymentAttemptDispatcherOld struct {
|
||||||
onPayment func(firstHop lnwire.ShortChannelID) ([32]byte, error)
|
onPayment func(firstHop lnwire.ShortChannelID) ([32]byte, error)
|
||||||
results map[uint64]*htlcswitch.PaymentResult
|
results map[uint64]*htlcswitch.PaymentResult
|
||||||
|
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil)
|
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcherOld)(nil)
|
||||||
|
|
||||||
func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
func (m *mockPaymentAttemptDispatcherOld) SendHTLC(
|
||||||
pid uint64,
|
firstHop lnwire.ShortChannelID, pid uint64,
|
||||||
_ *lnwire.UpdateAddHTLC) error {
|
_ *lnwire.UpdateAddHTLC) error {
|
||||||
|
|
||||||
if m.onPayment == nil {
|
if m.onPayment == nil {
|
||||||
@ -54,7 +56,7 @@ func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPaymentAttemptDispatcher) GetPaymentResult(paymentID uint64,
|
func (m *mockPaymentAttemptDispatcherOld) GetPaymentResult(paymentID uint64,
|
||||||
_ lntypes.Hash, _ htlcswitch.ErrorDecrypter) (
|
_ lntypes.Hash, _ htlcswitch.ErrorDecrypter) (
|
||||||
<-chan *htlcswitch.PaymentResult, error) {
|
<-chan *htlcswitch.PaymentResult, error) {
|
||||||
|
|
||||||
@ -72,48 +74,51 @@ func (m *mockPaymentAttemptDispatcher) GetPaymentResult(paymentID uint64,
|
|||||||
return c, nil
|
return c, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
func (m *mockPaymentAttemptDispatcher) CleanStore(map[uint64]struct{}) error {
|
func (m *mockPaymentAttemptDispatcherOld) CleanStore(
|
||||||
|
map[uint64]struct{}) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPaymentAttemptDispatcher) setPaymentResult(
|
func (m *mockPaymentAttemptDispatcherOld) setPaymentResult(
|
||||||
f func(firstHop lnwire.ShortChannelID) ([32]byte, error)) {
|
f func(firstHop lnwire.ShortChannelID) ([32]byte, error)) {
|
||||||
|
|
||||||
m.onPayment = f
|
m.onPayment = f
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockPaymentSessionSource struct {
|
type mockPaymentSessionSourceOld struct {
|
||||||
routes []*route.Route
|
routes []*route.Route
|
||||||
routeRelease chan struct{}
|
routeRelease chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
var _ PaymentSessionSource = (*mockPaymentSessionSourceOld)(nil)
|
||||||
|
|
||||||
func (m *mockPaymentSessionSource) NewPaymentSession(
|
func (m *mockPaymentSessionSourceOld) NewPaymentSession(
|
||||||
_ *LightningPayment) (PaymentSession, error) {
|
_ *LightningPayment) (PaymentSession, error) {
|
||||||
|
|
||||||
return &mockPaymentSession{
|
return &mockPaymentSessionOld{
|
||||||
routes: m.routes,
|
routes: m.routes,
|
||||||
release: m.routeRelease,
|
release: m.routeRelease,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
func (m *mockPaymentSessionSourceOld) NewPaymentSessionForRoute(
|
||||||
preBuiltRoute *route.Route) PaymentSession {
|
preBuiltRoute *route.Route) PaymentSession {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
|
func (m *mockPaymentSessionSourceOld) NewPaymentSessionEmpty() PaymentSession {
|
||||||
return &mockPaymentSession{}
|
return &mockPaymentSessionOld{}
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockMissionControl struct {
|
type mockMissionControlOld struct {
|
||||||
MissionControl
|
MissionControl
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ MissionController = (*mockMissionControl)(nil)
|
var _ MissionController = (*mockMissionControlOld)(nil)
|
||||||
|
|
||||||
func (m *mockMissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route,
|
func (m *mockMissionControlOld) ReportPaymentFail(
|
||||||
|
paymentID uint64, rt *route.Route,
|
||||||
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
||||||
*channeldb.FailureReason, error) {
|
*channeldb.FailureReason, error) {
|
||||||
|
|
||||||
@ -127,19 +132,19 @@ func (m *mockMissionControl) ReportPaymentFail(paymentID uint64, rt *route.Route
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64,
|
func (m *mockMissionControlOld) ReportPaymentSuccess(paymentID uint64,
|
||||||
rt *route.Route) error {
|
rt *route.Route) error {
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
|
func (m *mockMissionControlOld) GetProbability(fromNode, toNode route.Vertex,
|
||||||
amt lnwire.MilliSatoshi) float64 {
|
amt lnwire.MilliSatoshi) float64 {
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockPaymentSession struct {
|
type mockPaymentSessionOld struct {
|
||||||
routes []*route.Route
|
routes []*route.Route
|
||||||
|
|
||||||
// release is a channel that optionally blocks requesting a route
|
// release is a channel that optionally blocks requesting a route
|
||||||
@ -148,9 +153,9 @@ type mockPaymentSession struct {
|
|||||||
release chan struct{}
|
release chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentSession = (*mockPaymentSession)(nil)
|
var _ PaymentSession = (*mockPaymentSessionOld)(nil)
|
||||||
|
|
||||||
func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
func (m *mockPaymentSessionOld) RequestRoute(_, _ lnwire.MilliSatoshi,
|
||||||
_, height uint32) (*route.Route, error) {
|
_, height uint32) (*route.Route, error) {
|
||||||
|
|
||||||
if m.release != nil {
|
if m.release != nil {
|
||||||
@ -167,15 +172,27 @@ func (m *mockPaymentSession) RequestRoute(_, _ lnwire.MilliSatoshi,
|
|||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockPayer struct {
|
func (m *mockPaymentSessionOld) UpdateAdditionalEdge(_ *lnwire.ChannelUpdate,
|
||||||
|
_ *btcec.PublicKey, _ *channeldb.ChannelEdgePolicy) bool {
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentSessionOld) GetAdditionalEdgePolicy(_ *btcec.PublicKey,
|
||||||
|
_ uint64) *channeldb.ChannelEdgePolicy {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPayerOld struct {
|
||||||
sendResult chan error
|
sendResult chan error
|
||||||
paymentResult chan *htlcswitch.PaymentResult
|
paymentResult chan *htlcswitch.PaymentResult
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ PaymentAttemptDispatcher = (*mockPayer)(nil)
|
var _ PaymentAttemptDispatcher = (*mockPayerOld)(nil)
|
||||||
|
|
||||||
func (m *mockPayer) SendHTLC(_ lnwire.ShortChannelID,
|
func (m *mockPayerOld) SendHTLC(_ lnwire.ShortChannelID,
|
||||||
paymentID uint64,
|
paymentID uint64,
|
||||||
_ *lnwire.UpdateAddHTLC) error {
|
_ *lnwire.UpdateAddHTLC) error {
|
||||||
|
|
||||||
@ -188,7 +205,7 @@ func (m *mockPayer) SendHTLC(_ lnwire.ShortChannelID,
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPayer) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
func (m *mockPayerOld) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
||||||
_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
|
_ htlcswitch.ErrorDecrypter) (<-chan *htlcswitch.PaymentResult, error) {
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@ -207,7 +224,7 @@ func (m *mockPayer) GetPaymentResult(paymentID uint64, _ lntypes.Hash,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockPayer) CleanStore(pids map[uint64]struct{}) error {
|
func (m *mockPayerOld) CleanStore(pids map[uint64]struct{}) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -236,7 +253,7 @@ type testPayment struct {
|
|||||||
attempts []channeldb.HTLCAttempt
|
attempts []channeldb.HTLCAttempt
|
||||||
}
|
}
|
||||||
|
|
||||||
type mockControlTower struct {
|
type mockControlTowerOld struct {
|
||||||
payments map[lntypes.Hash]*testPayment
|
payments map[lntypes.Hash]*testPayment
|
||||||
successful map[lntypes.Hash]struct{}
|
successful map[lntypes.Hash]struct{}
|
||||||
failed map[lntypes.Hash]channeldb.FailureReason
|
failed map[lntypes.Hash]channeldb.FailureReason
|
||||||
@ -251,17 +268,17 @@ type mockControlTower struct {
|
|||||||
sync.Mutex
|
sync.Mutex
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ ControlTower = (*mockControlTower)(nil)
|
var _ ControlTower = (*mockControlTowerOld)(nil)
|
||||||
|
|
||||||
func makeMockControlTower() *mockControlTower {
|
func makeMockControlTower() *mockControlTowerOld {
|
||||||
return &mockControlTower{
|
return &mockControlTowerOld{
|
||||||
payments: make(map[lntypes.Hash]*testPayment),
|
payments: make(map[lntypes.Hash]*testPayment),
|
||||||
successful: make(map[lntypes.Hash]struct{}),
|
successful: make(map[lntypes.Hash]struct{}),
|
||||||
failed: make(map[lntypes.Hash]channeldb.FailureReason),
|
failed: make(map[lntypes.Hash]channeldb.FailureReason),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
func (m *mockControlTowerOld) InitPayment(phash lntypes.Hash,
|
||||||
c *channeldb.PaymentCreationInfo) error {
|
c *channeldb.PaymentCreationInfo) error {
|
||||||
|
|
||||||
if m.init != nil {
|
if m.init != nil {
|
||||||
@ -292,7 +309,7 @@ func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
func (m *mockControlTowerOld) RegisterAttempt(phash lntypes.Hash,
|
||||||
a *channeldb.HTLCAttemptInfo) error {
|
a *channeldb.HTLCAttemptInfo) error {
|
||||||
|
|
||||||
if m.registerAttempt != nil {
|
if m.registerAttempt != nil {
|
||||||
@ -346,7 +363,7 @@ func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
func (m *mockControlTowerOld) SettleAttempt(phash lntypes.Hash,
|
||||||
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
||||||
*channeldb.HTLCAttempt, error) {
|
*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
@ -388,7 +405,7 @@ func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
|||||||
return nil, fmt.Errorf("pid not found")
|
return nil, fmt.Errorf("pid not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
func (m *mockControlTowerOld) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||||
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
if m.failAttempt != nil {
|
if m.failAttempt != nil {
|
||||||
@ -426,7 +443,7 @@ func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
|||||||
return nil, fmt.Errorf("pid not found")
|
return nil, fmt.Errorf("pid not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) Fail(phash lntypes.Hash,
|
func (m *mockControlTowerOld) Fail(phash lntypes.Hash,
|
||||||
reason channeldb.FailureReason) error {
|
reason channeldb.FailureReason) error {
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
@ -446,7 +463,7 @@ func (m *mockControlTower) Fail(phash lntypes.Hash,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
func (m *mockControlTowerOld) FetchPayment(phash lntypes.Hash) (
|
||||||
*channeldb.MPPayment, error) {
|
*channeldb.MPPayment, error) {
|
||||||
|
|
||||||
m.Lock()
|
m.Lock()
|
||||||
@ -455,7 +472,7 @@ func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
|||||||
return m.fetchPayment(phash)
|
return m.fetchPayment(phash)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) fetchPayment(phash lntypes.Hash) (
|
func (m *mockControlTowerOld) fetchPayment(phash lntypes.Hash) (
|
||||||
*channeldb.MPPayment, error) {
|
*channeldb.MPPayment, error) {
|
||||||
|
|
||||||
p, ok := m.payments[phash]
|
p, ok := m.payments[phash]
|
||||||
@ -477,7 +494,7 @@ func (m *mockControlTower) fetchPayment(phash lntypes.Hash) (
|
|||||||
return mp, nil
|
return mp, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) FetchInFlightPayments() (
|
func (m *mockControlTowerOld) FetchInFlightPayments() (
|
||||||
[]*channeldb.MPPayment, error) {
|
[]*channeldb.MPPayment, error) {
|
||||||
|
|
||||||
if m.fetchInFlight != nil {
|
if m.fetchInFlight != nil {
|
||||||
@ -508,8 +525,215 @@ func (m *mockControlTower) FetchInFlightPayments() (
|
|||||||
return fl, nil
|
return fl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockControlTower) SubscribePayment(paymentHash lntypes.Hash) (
|
func (m *mockControlTowerOld) SubscribePayment(paymentHash lntypes.Hash) (
|
||||||
*ControlTowerSubscriber, error) {
|
*ControlTowerSubscriber, error) {
|
||||||
|
|
||||||
return nil, errors.New("not implemented")
|
return nil, errors.New("not implemented")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockPaymentAttemptDispatcher struct {
|
||||||
|
mock.Mock
|
||||||
|
|
||||||
|
resultChan chan *htlcswitch.PaymentResult
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil)
|
||||||
|
|
||||||
|
func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID,
|
||||||
|
pid uint64, htlcAdd *lnwire.UpdateAddHTLC) error {
|
||||||
|
|
||||||
|
args := m.Called(firstHop, pid, htlcAdd)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentAttemptDispatcher) GetPaymentResult(attemptID uint64,
|
||||||
|
paymentHash lntypes.Hash, deobfuscator htlcswitch.ErrorDecrypter) (
|
||||||
|
<-chan *htlcswitch.PaymentResult, error) {
|
||||||
|
|
||||||
|
m.Called(attemptID, paymentHash, deobfuscator)
|
||||||
|
|
||||||
|
// Instead of returning the mocked returned values, we need to return
|
||||||
|
// the chan resultChan so it can be converted into a read-only chan.
|
||||||
|
return m.resultChan, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentAttemptDispatcher) CleanStore(
|
||||||
|
keepPids map[uint64]struct{}) error {
|
||||||
|
|
||||||
|
args := m.Called(keepPids)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPaymentSessionSource struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PaymentSessionSource = (*mockPaymentSessionSource)(nil)
|
||||||
|
|
||||||
|
func (m *mockPaymentSessionSource) NewPaymentSession(
|
||||||
|
payment *LightningPayment) (PaymentSession, error) {
|
||||||
|
|
||||||
|
args := m.Called(payment)
|
||||||
|
return args.Get(0).(PaymentSession), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentSessionSource) NewPaymentSessionForRoute(
|
||||||
|
preBuiltRoute *route.Route) PaymentSession {
|
||||||
|
|
||||||
|
args := m.Called(preBuiltRoute)
|
||||||
|
return args.Get(0).(PaymentSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentSessionSource) NewPaymentSessionEmpty() PaymentSession {
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).(PaymentSession)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockMissionControl struct {
|
||||||
|
mock.Mock
|
||||||
|
|
||||||
|
failReason *channeldb.FailureReason
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ MissionController = (*mockMissionControl)(nil)
|
||||||
|
|
||||||
|
func (m *mockMissionControl) ReportPaymentFail(
|
||||||
|
paymentID uint64, rt *route.Route,
|
||||||
|
failureSourceIdx *int, failure lnwire.FailureMessage) (
|
||||||
|
*channeldb.FailureReason, error) {
|
||||||
|
|
||||||
|
args := m.Called(paymentID, rt, failureSourceIdx, failure)
|
||||||
|
return m.failReason, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMissionControl) ReportPaymentSuccess(paymentID uint64,
|
||||||
|
rt *route.Route) error {
|
||||||
|
|
||||||
|
args := m.Called(paymentID, rt)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockMissionControl) GetProbability(fromNode, toNode route.Vertex,
|
||||||
|
amt lnwire.MilliSatoshi) float64 {
|
||||||
|
|
||||||
|
args := m.Called(fromNode, toNode, amt)
|
||||||
|
return args.Get(0).(float64)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockPaymentSession struct {
|
||||||
|
mock.Mock
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ PaymentSession = (*mockPaymentSession)(nil)
|
||||||
|
|
||||||
|
func (m *mockPaymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||||
|
activeShards, height uint32) (*route.Route, error) {
|
||||||
|
args := m.Called(maxAmt, feeLimit, activeShards, height)
|
||||||
|
return args.Get(0).(*route.Route), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate,
|
||||||
|
pubKey *btcec.PublicKey, policy *channeldb.ChannelEdgePolicy) bool {
|
||||||
|
|
||||||
|
args := m.Called(msg, pubKey, policy)
|
||||||
|
return args.Bool(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockPaymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||||
|
channelID uint64) *channeldb.ChannelEdgePolicy {
|
||||||
|
|
||||||
|
args := m.Called(pubKey, channelID)
|
||||||
|
return args.Get(0).(*channeldb.ChannelEdgePolicy)
|
||||||
|
}
|
||||||
|
|
||||||
|
type mockControlTower struct {
|
||||||
|
mock.Mock
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ ControlTower = (*mockControlTower)(nil)
|
||||||
|
|
||||||
|
func (m *mockControlTower) InitPayment(phash lntypes.Hash,
|
||||||
|
c *channeldb.PaymentCreationInfo) error {
|
||||||
|
|
||||||
|
args := m.Called(phash, c)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash,
|
||||||
|
a *channeldb.HTLCAttemptInfo) error {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
args := m.Called(phash, a)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) SettleAttempt(phash lntypes.Hash,
|
||||||
|
pid uint64, settleInfo *channeldb.HTLCSettleInfo) (
|
||||||
|
*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
args := m.Called(phash, pid, settleInfo)
|
||||||
|
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) FailAttempt(phash lntypes.Hash, pid uint64,
|
||||||
|
failInfo *channeldb.HTLCFailInfo) (*channeldb.HTLCAttempt, error) {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
args := m.Called(phash, pid, failInfo)
|
||||||
|
return args.Get(0).(*channeldb.HTLCAttempt), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) Fail(phash lntypes.Hash,
|
||||||
|
reason channeldb.FailureReason) error {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
args := m.Called(phash, reason)
|
||||||
|
return args.Error(0)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) FetchPayment(phash lntypes.Hash) (
|
||||||
|
*channeldb.MPPayment, error) {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
args := m.Called(phash)
|
||||||
|
|
||||||
|
// Type assertion on nil will fail, so we check and return here.
|
||||||
|
if args.Get(0) == nil {
|
||||||
|
return nil, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make a copy of the payment here to avoid data race.
|
||||||
|
p := args.Get(0).(*channeldb.MPPayment)
|
||||||
|
payment := &channeldb.MPPayment{
|
||||||
|
FailureReason: p.FailureReason,
|
||||||
|
}
|
||||||
|
payment.HTLCs = make([]channeldb.HTLCAttempt, len(p.HTLCs))
|
||||||
|
copy(payment.HTLCs, p.HTLCs)
|
||||||
|
|
||||||
|
return payment, args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) FetchInFlightPayments() (
|
||||||
|
[]*channeldb.MPPayment, error) {
|
||||||
|
|
||||||
|
args := m.Called()
|
||||||
|
return args.Get(0).([]*channeldb.MPPayment), args.Error(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockControlTower) SubscribePayment(paymentHash lntypes.Hash) (
|
||||||
|
*ControlTowerSubscriber, error) {
|
||||||
|
|
||||||
|
args := m.Called(paymentHash)
|
||||||
|
return args.Get(0).(*ControlTowerSubscriber), args.Error(1)
|
||||||
|
}
|
||||||
|
@ -352,11 +352,8 @@ func (m *mockChainView) Stop() error {
|
|||||||
func TestEdgeUpdateNotification(t *testing.T) {
|
func TestEdgeUpdateNotification(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(0)
|
ctx, cleanUp := createTestCtxSingleNode(t, 0)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First we'll create the utxo for the channel to be "closed"
|
// First we'll create the utxo for the channel to be "closed"
|
||||||
const chanValue = 10000
|
const chanValue = 10000
|
||||||
@ -546,11 +543,8 @@ func TestNodeUpdateNotification(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We only accept node announcements from nodes having a known channel,
|
// We only accept node announcements from nodes having a known channel,
|
||||||
// so create one now.
|
// so create one now.
|
||||||
@ -739,11 +733,8 @@ func TestNotificationCancellation(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a new client to receive notifications.
|
// Create a new client to receive notifications.
|
||||||
ntfnClient, err := ctx.router.SubscribeTopology()
|
ntfnClient, err := ctx.router.SubscribeTopology()
|
||||||
@ -831,11 +822,8 @@ func TestChannelCloseNotification(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// First we'll create the utxo for the channel to be "closed"
|
// First we'll create the utxo for the channel to be "closed"
|
||||||
const chanValue = 10000
|
const chanValue = 10000
|
||||||
|
@ -118,11 +118,14 @@ type testGraph struct {
|
|||||||
|
|
||||||
// testNode represents a node within the test graph above. We skip certain
|
// testNode represents a node within the test graph above. We skip certain
|
||||||
// information such as the node's IP address as that information isn't needed
|
// information such as the node's IP address as that information isn't needed
|
||||||
// for our tests.
|
// for our tests. Private keys are optional. If set, they should be consistent
|
||||||
|
// with the public key. The private key is used to sign error messages
|
||||||
|
// sent from the node.
|
||||||
type testNode struct {
|
type testNode struct {
|
||||||
Source bool `json:"source"`
|
Source bool `json:"source"`
|
||||||
PubKey string `json:"pubkey"`
|
PubKey string `json:"pubkey"`
|
||||||
Alias string `json:"alias"`
|
PrivKey string `json:"privkey"`
|
||||||
|
Alias string `json:"alias"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// testChan represents the JSON version of a payment channel. This struct
|
// testChan represents the JSON version of a payment channel. This struct
|
||||||
@ -200,6 +203,8 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
aliasMap := make(map[string]route.Vertex)
|
aliasMap := make(map[string]route.Vertex)
|
||||||
|
privKeyMap := make(map[string]*btcec.PrivateKey)
|
||||||
|
channelIDs := make(map[route.Vertex]map[route.Vertex]uint64)
|
||||||
var source *channeldb.LightningNode
|
var source *channeldb.LightningNode
|
||||||
|
|
||||||
// First we insert all the nodes within the graph as vertexes.
|
// First we insert all the nodes within the graph as vertexes.
|
||||||
@ -230,6 +235,33 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
|||||||
// alias map for easy lookup.
|
// alias map for easy lookup.
|
||||||
aliasMap[node.Alias] = dbNode.PubKeyBytes
|
aliasMap[node.Alias] = dbNode.PubKeyBytes
|
||||||
|
|
||||||
|
// private keys are needed for signing error messages. If set
|
||||||
|
// check the consistency with the public key.
|
||||||
|
privBytes, err := hex.DecodeString(node.PrivKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(privBytes) > 0 {
|
||||||
|
key, derivedPub := btcec.PrivKeyFromBytes(
|
||||||
|
btcec.S256(), privBytes,
|
||||||
|
)
|
||||||
|
|
||||||
|
if !bytes.Equal(
|
||||||
|
pubBytes, derivedPub.SerializeCompressed(),
|
||||||
|
) {
|
||||||
|
|
||||||
|
return nil, fmt.Errorf("%s public key and "+
|
||||||
|
"private key are inconsistent\n"+
|
||||||
|
"got %x\nwant %x\n",
|
||||||
|
node.Alias,
|
||||||
|
derivedPub.SerializeCompressed(),
|
||||||
|
pubBytes,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
privKeyMap[node.Alias] = key
|
||||||
|
}
|
||||||
|
|
||||||
// If the node is tagged as the source, then we create a
|
// If the node is tagged as the source, then we create a
|
||||||
// pointer to is so we can mark the source in the graph
|
// pointer to is so we can mark the source in the graph
|
||||||
// properly.
|
// properly.
|
||||||
@ -240,7 +272,8 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
|||||||
// node can be the source in the graph.
|
// node can be the source in the graph.
|
||||||
if source != nil {
|
if source != nil {
|
||||||
return nil, errors.New("JSON is invalid " +
|
return nil, errors.New("JSON is invalid " +
|
||||||
"multiple nodes are tagged as the source")
|
"multiple nodes are tagged as the " +
|
||||||
|
"source")
|
||||||
}
|
}
|
||||||
|
|
||||||
source = dbNode
|
source = dbNode
|
||||||
@ -324,12 +357,35 @@ func parseTestGraph(path string) (*testGraphInstance, error) {
|
|||||||
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
if err := graph.UpdateEdgePolicy(edgePolicy); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We also store the channel IDs info for each of the node.
|
||||||
|
node1Vertex, err := route.NewVertexFromBytes(node1Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
node2Vertex, err := route.NewVertexFromBytes(node2Bytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := channelIDs[node1Vertex]; !ok {
|
||||||
|
channelIDs[node1Vertex] = map[route.Vertex]uint64{}
|
||||||
|
}
|
||||||
|
channelIDs[node1Vertex][node2Vertex] = edge.ChannelID
|
||||||
|
|
||||||
|
if _, ok := channelIDs[node2Vertex]; !ok {
|
||||||
|
channelIDs[node2Vertex] = map[route.Vertex]uint64{}
|
||||||
|
}
|
||||||
|
channelIDs[node2Vertex][node1Vertex] = edge.ChannelID
|
||||||
}
|
}
|
||||||
|
|
||||||
return &testGraphInstance{
|
return &testGraphInstance{
|
||||||
graph: graph,
|
graph: graph,
|
||||||
cleanUp: cleanUp,
|
cleanUp: cleanUp,
|
||||||
aliasMap: aliasMap,
|
aliasMap: aliasMap,
|
||||||
|
privKeyMap: privKeyMap,
|
||||||
|
channelIDs: channelIDs,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -402,6 +458,9 @@ type testGraphInstance struct {
|
|||||||
// privKeyMap maps a node alias to its private key. This is used to be
|
// privKeyMap maps a node alias to its private key. This is used to be
|
||||||
// able to mock a remote node's signing behaviour.
|
// able to mock a remote node's signing behaviour.
|
||||||
privKeyMap map[string]*btcec.PrivateKey
|
privKeyMap map[string]*btcec.PrivateKey
|
||||||
|
|
||||||
|
// channelIDs stores the channel ID for each node.
|
||||||
|
channelIDs map[route.Vertex]map[route.Vertex]uint64
|
||||||
}
|
}
|
||||||
|
|
||||||
// createTestGraphFromChannels returns a fully populated ChannelGraph based on a set of
|
// createTestGraphFromChannels returns a fully populated ChannelGraph based on a set of
|
||||||
@ -2052,10 +2111,9 @@ func TestPathFindSpecExample(t *testing.T) {
|
|||||||
// we'll pass that in to ensure that the router uses 100 as the current
|
// we'll pass that in to ensure that the router uses 100 as the current
|
||||||
// height.
|
// height.
|
||||||
const startingHeight = 100
|
const startingHeight = 100
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingHeight, specExampleFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingHeight, specExampleFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// We'll first exercise the scenario of a direct payment from Bob to
|
// We'll first exercise the scenario of a direct payment from Bob to
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
sphinx "github.com/lightningnetwork/lightning-onion"
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
@ -37,21 +38,53 @@ type paymentState struct {
|
|||||||
numShardsInFlight int
|
numShardsInFlight int
|
||||||
remainingAmt lnwire.MilliSatoshi
|
remainingAmt lnwire.MilliSatoshi
|
||||||
remainingFees lnwire.MilliSatoshi
|
remainingFees lnwire.MilliSatoshi
|
||||||
terminate bool
|
|
||||||
|
// terminate indicates the payment is in its final stage and no more
|
||||||
|
// shards should be launched. This value is true if we have an HTLC
|
||||||
|
// settled or the payment has an error.
|
||||||
|
terminate bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// paymentState uses the passed payment to find the latest information we need
|
// terminated returns a bool to indicate there are no further actions needed
|
||||||
// to act on every iteration of the payment loop.
|
// and we should return what we have, either the payment preimage or the
|
||||||
func (p *paymentLifecycle) paymentState(payment *channeldb.MPPayment) (
|
// payment error.
|
||||||
|
func (ps paymentState) terminated() bool {
|
||||||
|
// If the payment is in final stage and we have no in flight shards to
|
||||||
|
// wait result for, we consider the whole action terminated.
|
||||||
|
return ps.terminate && ps.numShardsInFlight == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// needWaitForShards returns a bool to specify whether we need to wait for the
|
||||||
|
// outcome of the shanrdHandler.
|
||||||
|
func (ps paymentState) needWaitForShards() bool {
|
||||||
|
// If we have in flight shards and the payment is in final stage, we
|
||||||
|
// need to wait for the outcomes from the shards. Or if we have no more
|
||||||
|
// money to be sent, we need to wait for the already launched shards.
|
||||||
|
if ps.numShardsInFlight == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return ps.terminate || ps.remainingAmt == 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// updatePaymentState will fetch db for the payment to find the latest
|
||||||
|
// information we need to act on every iteration of the payment loop and update
|
||||||
|
// the paymentState.
|
||||||
|
func (p *paymentLifecycle) updatePaymentState() (*channeldb.MPPayment,
|
||||||
*paymentState, error) {
|
*paymentState, error) {
|
||||||
|
|
||||||
|
// Fetch the latest payment from db.
|
||||||
|
payment, err := p.router.cfg.Control.FetchPayment(p.identifier)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// Fetch the total amount and fees that has already been sent in
|
// Fetch the total amount and fees that has already been sent in
|
||||||
// settled and still in-flight shards.
|
// settled and still in-flight shards.
|
||||||
sentAmt, fees := payment.SentAmt()
|
sentAmt, fees := payment.SentAmt()
|
||||||
|
|
||||||
// Sanity check we haven't sent a value larger than the payment amount.
|
// Sanity check we haven't sent a value larger than the payment amount.
|
||||||
if sentAmt > p.totalAmount {
|
if sentAmt > p.totalAmount {
|
||||||
return nil, fmt.Errorf("amount sent %v exceeds "+
|
return nil, nil, fmt.Errorf("amount sent %v exceeds "+
|
||||||
"total amount %v", sentAmt, p.totalAmount)
|
"total amount %v", sentAmt, p.totalAmount)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,13 +106,15 @@ func (p *paymentLifecycle) paymentState(payment *channeldb.MPPayment) (
|
|||||||
// have returned with a result.
|
// have returned with a result.
|
||||||
terminate := settle != nil || failure != nil
|
terminate := settle != nil || failure != nil
|
||||||
|
|
||||||
activeShards := payment.InFlightHTLCs()
|
// Update the payment state.
|
||||||
return &paymentState{
|
state := &paymentState{
|
||||||
numShardsInFlight: len(activeShards),
|
numShardsInFlight: len(payment.InFlightHTLCs()),
|
||||||
remainingAmt: p.totalAmount - sentAmt,
|
remainingAmt: p.totalAmount - sentAmt,
|
||||||
remainingFees: feeBudget,
|
remainingFees: feeBudget,
|
||||||
terminate: terminate,
|
terminate: terminate,
|
||||||
}, nil
|
}
|
||||||
|
|
||||||
|
return payment, state, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resumePayment resumes the paymentLifecycle from the current state.
|
// resumePayment resumes the paymentLifecycle from the current state.
|
||||||
@ -90,6 +125,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
shardTracker: p.shardTracker,
|
shardTracker: p.shardTracker,
|
||||||
shardErrors: make(chan error),
|
shardErrors: make(chan error),
|
||||||
quit: make(chan struct{}),
|
quit: make(chan struct{}),
|
||||||
|
paySession: p.paySession,
|
||||||
}
|
}
|
||||||
|
|
||||||
// When the payment lifecycle loop exits, we make sure to signal any
|
// When the payment lifecycle loop exits, we make sure to signal any
|
||||||
@ -100,9 +136,7 @@ func (p *paymentLifecycle) resumePayment() ([32]byte, *route.Route, error) {
|
|||||||
// If we had any existing attempts outstanding, we'll start by spinning
|
// If we had any existing attempts outstanding, we'll start by spinning
|
||||||
// up goroutines that'll collect their results and deliver them to the
|
// up goroutines that'll collect their results and deliver them to the
|
||||||
// lifecycle loop below.
|
// lifecycle loop below.
|
||||||
payment, err := p.router.cfg.Control.FetchPayment(
|
payment, _, err := p.updatePaymentState()
|
||||||
p.identifier,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
@ -126,34 +160,30 @@ lifecycle:
|
|||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// We start every iteration by fetching the lastest state of
|
// We update the payment state on every iteration. Since the
|
||||||
// the payment from the ControlTower. This ensures that we will
|
// payment state is affected by multiple goroutines (ie,
|
||||||
// act on the latest available information, whether we are
|
// collectResultAsync), it is NOT guaranteed that we always
|
||||||
// resuming an existing payment or just sent a new attempt.
|
// have the latest state here. This is fine as long as the
|
||||||
payment, err := p.router.cfg.Control.FetchPayment(
|
// state is consistent as a whole.
|
||||||
p.identifier,
|
payment, currentState, err := p.updatePaymentState()
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return [32]byte{}, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Using this latest state of the payment, calculate
|
|
||||||
// information about our active shards and terminal conditions.
|
|
||||||
state, err := p.paymentState(payment)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Payment %v in state terminate=%v, "+
|
log.Debugf("Payment %v in state terminate=%v, "+
|
||||||
"active_shards=%v, rem_value=%v, fee_limit=%v",
|
"active_shards=%v, rem_value=%v, fee_limit=%v",
|
||||||
p.identifier, state.terminate, state.numShardsInFlight,
|
p.identifier, currentState.terminate,
|
||||||
state.remainingAmt, state.remainingFees)
|
currentState.numShardsInFlight,
|
||||||
|
currentState.remainingAmt, currentState.remainingFees,
|
||||||
|
)
|
||||||
|
|
||||||
|
// TODO(yy): sanity check all the states to make sure
|
||||||
|
// everything is expected.
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
// We have a terminal condition and no active shards, we are
|
// We have a terminal condition and no active shards, we are
|
||||||
// ready to exit.
|
// ready to exit.
|
||||||
case state.terminate && state.numShardsInFlight == 0:
|
case currentState.terminated():
|
||||||
// Find the first successful shard and return
|
// Find the first successful shard and return
|
||||||
// the preimage and route.
|
// the preimage and route.
|
||||||
for _, a := range payment.HTLCs {
|
for _, a := range payment.HTLCs {
|
||||||
@ -168,7 +198,7 @@ lifecycle:
|
|||||||
// If we either reached a terminal error condition (but had
|
// If we either reached a terminal error condition (but had
|
||||||
// active shards still) or there is no remaining value to send,
|
// active shards still) or there is no remaining value to send,
|
||||||
// we'll wait for a shard outcome.
|
// we'll wait for a shard outcome.
|
||||||
case state.terminate || state.remainingAmt == 0:
|
case currentState.needWaitForShards():
|
||||||
// We still have outstanding shards, so wait for a new
|
// We still have outstanding shards, so wait for a new
|
||||||
// outcome to be available before re-evaluating our
|
// outcome to be available before re-evaluating our
|
||||||
// state.
|
// state.
|
||||||
@ -210,8 +240,9 @@ lifecycle:
|
|||||||
|
|
||||||
// Create a new payment attempt from the given payment session.
|
// Create a new payment attempt from the given payment session.
|
||||||
rt, err := p.paySession.RequestRoute(
|
rt, err := p.paySession.RequestRoute(
|
||||||
state.remainingAmt, state.remainingFees,
|
currentState.remainingAmt, currentState.remainingFees,
|
||||||
uint32(state.numShardsInFlight), uint32(p.currentHeight),
|
uint32(currentState.numShardsInFlight),
|
||||||
|
uint32(p.currentHeight),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Warnf("Failed to find route for payment %v: %v",
|
log.Warnf("Failed to find route for payment %v: %v",
|
||||||
@ -225,7 +256,7 @@ lifecycle:
|
|||||||
// There is no route to try, and we have no active
|
// There is no route to try, and we have no active
|
||||||
// shards. This means that there is no way for us to
|
// shards. This means that there is no way for us to
|
||||||
// send the payment, so mark it failed with no route.
|
// send the payment, so mark it failed with no route.
|
||||||
if state.numShardsInFlight == 0 {
|
if currentState.numShardsInFlight == 0 {
|
||||||
failureCode := routeErr.FailureReason()
|
failureCode := routeErr.FailureReason()
|
||||||
log.Debugf("Marking payment %v permanently "+
|
log.Debugf("Marking payment %v permanently "+
|
||||||
"failed with no route: %v",
|
"failed with no route: %v",
|
||||||
@ -251,22 +282,11 @@ lifecycle:
|
|||||||
|
|
||||||
// If this route will consume the last remeining amount to send
|
// If this route will consume the last remeining amount to send
|
||||||
// to the receiver, this will be our last shard (for now).
|
// to the receiver, this will be our last shard (for now).
|
||||||
lastShard := rt.ReceiverAmt() == state.remainingAmt
|
lastShard := rt.ReceiverAmt() == currentState.remainingAmt
|
||||||
|
|
||||||
// We found a route to try, launch a new shard.
|
// We found a route to try, launch a new shard.
|
||||||
attempt, outcome, err := shardHandler.launchShard(rt, lastShard)
|
attempt, outcome, err := shardHandler.launchShard(rt, lastShard)
|
||||||
switch {
|
if err != nil {
|
||||||
// We may get a terminal error if we've processed a shard with
|
|
||||||
// a terminal state (settled or permanent failure), while we
|
|
||||||
// were pathfinding. We know we're in a terminal state here,
|
|
||||||
// so we can continue and wait for our last shards to return.
|
|
||||||
case err == channeldb.ErrPaymentTerminal:
|
|
||||||
log.Infof("Payment %v in terminal state, abandoning "+
|
|
||||||
"shard", p.identifier)
|
|
||||||
|
|
||||||
continue lifecycle
|
|
||||||
|
|
||||||
case err != nil:
|
|
||||||
return [32]byte{}, nil, err
|
return [32]byte{}, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -295,6 +315,7 @@ lifecycle:
|
|||||||
// Now that the shard was successfully sent, launch a go
|
// Now that the shard was successfully sent, launch a go
|
||||||
// routine that will handle its result when its back.
|
// routine that will handle its result when its back.
|
||||||
shardHandler.collectResultAsync(attempt)
|
shardHandler.collectResultAsync(attempt)
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,6 +325,7 @@ type shardHandler struct {
|
|||||||
identifier lntypes.Hash
|
identifier lntypes.Hash
|
||||||
router *ChannelRouter
|
router *ChannelRouter
|
||||||
shardTracker shards.ShardTracker
|
shardTracker shards.ShardTracker
|
||||||
|
paySession PaymentSession
|
||||||
|
|
||||||
// shardErrors is a channel where errors collected by calling
|
// shardErrors is a channel where errors collected by calling
|
||||||
// collectResultAsync will be delivered. These results are meant to be
|
// collectResultAsync will be delivered. These results are meant to be
|
||||||
@ -434,12 +456,30 @@ type shardResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// collectResultAsync launches a goroutine that will wait for the result of the
|
// collectResultAsync launches a goroutine that will wait for the result of the
|
||||||
// given HTLC attempt to be available then handle its result. Note that it will
|
// given HTLC attempt to be available then handle its result. It will fail the
|
||||||
// fail the payment with the control tower if a terminal error is encountered.
|
// payment with the control tower if a terminal error is encountered.
|
||||||
func (p *shardHandler) collectResultAsync(attempt *channeldb.HTLCAttemptInfo) {
|
func (p *shardHandler) collectResultAsync(attempt *channeldb.HTLCAttemptInfo) {
|
||||||
|
|
||||||
|
// errToSend is the error to be sent to sh.shardErrors.
|
||||||
|
var errToSend error
|
||||||
|
|
||||||
|
// handleResultErr is a function closure must be called using defer. It
|
||||||
|
// finishes collecting result by updating the payment state and send
|
||||||
|
// the error (or nil) to sh.shardErrors.
|
||||||
|
handleResultErr := func() {
|
||||||
|
// Send the error or quit.
|
||||||
|
select {
|
||||||
|
case p.shardErrors <- errToSend:
|
||||||
|
case <-p.router.quit:
|
||||||
|
case <-p.quit:
|
||||||
|
}
|
||||||
|
|
||||||
|
p.wg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
p.wg.Add(1)
|
p.wg.Add(1)
|
||||||
go func() {
|
go func() {
|
||||||
defer p.wg.Done()
|
defer handleResultErr()
|
||||||
|
|
||||||
// Block until the result is available.
|
// Block until the result is available.
|
||||||
result, err := p.collectResult(attempt)
|
result, err := p.collectResult(attempt)
|
||||||
@ -453,32 +493,18 @@ func (p *shardHandler) collectResultAsync(attempt *channeldb.HTLCAttemptInfo) {
|
|||||||
attempt.AttemptID, p.identifier, err)
|
attempt.AttemptID, p.identifier, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
select {
|
// Overwrite errToSend and return.
|
||||||
case p.shardErrors <- err:
|
errToSend = err
|
||||||
case <-p.router.quit:
|
|
||||||
case <-p.quit:
|
|
||||||
}
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// If a non-critical error was encountered handle it and mark
|
// If a non-critical error was encountered handle it and mark
|
||||||
// the payment failed if the failure was terminal.
|
// the payment failed if the failure was terminal.
|
||||||
if result.err != nil {
|
if result.err != nil {
|
||||||
err := p.handleSendError(attempt, result.err)
|
// Overwrite errToSend and return. Notice that the
|
||||||
if err != nil {
|
// errToSend could be nil here.
|
||||||
select {
|
errToSend = p.handleSendError(attempt, result.err)
|
||||||
case p.shardErrors <- err:
|
return
|
||||||
case <-p.router.quit:
|
|
||||||
case <-p.quit:
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case p.shardErrors <- nil:
|
|
||||||
case <-p.router.quit:
|
|
||||||
case <-p.quit:
|
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
@ -721,25 +747,175 @@ func (p *shardHandler) sendPaymentAttempt(
|
|||||||
// handleSendError inspects the given error from the Switch and determines
|
// handleSendError inspects the given error from the Switch and determines
|
||||||
// whether we should make another payment attempt, or if it should be
|
// whether we should make another payment attempt, or if it should be
|
||||||
// considered a terminal error. Terminal errors will be recorded with the
|
// considered a terminal error. Terminal errors will be recorded with the
|
||||||
// control tower.
|
// control tower. It analyzes the sendErr for the payment attempt received from
|
||||||
|
// the switch and updates mission control and/or channel policies. Depending on
|
||||||
|
// the error type, the error is either the final outcome of the payment or we
|
||||||
|
// need to continue with an alternative route. A final outcome is indicated by
|
||||||
|
// a non-nil reason value.
|
||||||
func (p *shardHandler) handleSendError(attempt *channeldb.HTLCAttemptInfo,
|
func (p *shardHandler) handleSendError(attempt *channeldb.HTLCAttemptInfo,
|
||||||
sendErr error) error {
|
sendErr error) error {
|
||||||
|
|
||||||
reason := p.router.processSendError(
|
internalErrorReason := channeldb.FailureReasonError
|
||||||
attempt.AttemptID, &attempt.Route, sendErr,
|
|
||||||
|
// failPayment is a helper closure that fails the payment via the
|
||||||
|
// router's control tower, which marks the payment as failed in db.
|
||||||
|
failPayment := func(reason *channeldb.FailureReason,
|
||||||
|
sendErr error) error {
|
||||||
|
|
||||||
|
log.Infof("Payment %v failed: final_outcome=%v, raw_err=%v",
|
||||||
|
p.identifier, *reason, sendErr)
|
||||||
|
|
||||||
|
// Fail the payment via control tower.
|
||||||
|
if err := p.router.cfg.Control.Fail(
|
||||||
|
p.identifier, *reason); err != nil {
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return *reason
|
||||||
|
}
|
||||||
|
|
||||||
|
// reportFail is a helper closure that reports the failure to the
|
||||||
|
// mission control, which helps us to decide whether we want to retry
|
||||||
|
// the payment or not. If a non nil reason is returned from mission
|
||||||
|
// control, it will further fail the payment via control tower.
|
||||||
|
reportFail := func(srcIdx *int, msg lnwire.FailureMessage) error {
|
||||||
|
// Report outcome to mission control.
|
||||||
|
reason, err := p.router.cfg.MissionControl.ReportPaymentFail(
|
||||||
|
attempt.AttemptID, &attempt.Route, srcIdx, msg,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("Error reporting payment result to mc: %v",
|
||||||
|
err)
|
||||||
|
|
||||||
|
reason = &internalErrorReason
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit early if there's no reason.
|
||||||
|
if reason == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return failPayment(reason, sendErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
if sendErr == htlcswitch.ErrUnreadableFailureMessage {
|
||||||
|
log.Tracef("Unreadable failure when sending htlc")
|
||||||
|
|
||||||
|
return reportFail(nil, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the error is a ClearTextError, we have received a valid wire
|
||||||
|
// failure message, either from our own outgoing link or from a node
|
||||||
|
// down the route. If the error is not related to the propagation of
|
||||||
|
// our payment, we can stop trying because an internal error has
|
||||||
|
// occurred.
|
||||||
|
rtErr, ok := sendErr.(htlcswitch.ClearTextError)
|
||||||
|
if !ok {
|
||||||
|
return failPayment(&internalErrorReason, sendErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
// failureSourceIdx is the index of the node that the failure occurred
|
||||||
|
// at. If the ClearTextError received is not a ForwardingError the
|
||||||
|
// payment error occurred at our node, so we leave this value as 0
|
||||||
|
// to indicate that the failure occurred locally. If the error is a
|
||||||
|
// ForwardingError, it did not originate at our node, so we set
|
||||||
|
// failureSourceIdx to the index of the node where the failure occurred.
|
||||||
|
failureSourceIdx := 0
|
||||||
|
source, ok := rtErr.(*htlcswitch.ForwardingError)
|
||||||
|
if ok {
|
||||||
|
failureSourceIdx = source.FailureSourceIdx
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract the wire failure and apply channel update if it contains one.
|
||||||
|
// If we received an unknown failure message from a node along the
|
||||||
|
// route, the failure message will be nil.
|
||||||
|
failureMessage := rtErr.WireMessage()
|
||||||
|
err := p.handleFailureMessage(
|
||||||
|
&attempt.Route, failureSourceIdx, failureMessage,
|
||||||
)
|
)
|
||||||
if reason == nil {
|
if err != nil {
|
||||||
|
return failPayment(&internalErrorReason, sendErr)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Tracef("Node=%v reported failure when sending htlc",
|
||||||
|
failureSourceIdx)
|
||||||
|
|
||||||
|
return reportFail(&failureSourceIdx, failureMessage)
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleFailureMessage tries to apply a channel update present in the failure
|
||||||
|
// message if any.
|
||||||
|
func (p *shardHandler) handleFailureMessage(rt *route.Route,
|
||||||
|
errorSourceIdx int, failure lnwire.FailureMessage) error {
|
||||||
|
|
||||||
|
if failure == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("Payment %v failed: final_outcome=%v, raw_err=%v",
|
// It makes no sense to apply our own channel updates.
|
||||||
p.identifier, *reason, sendErr)
|
if errorSourceIdx == 0 {
|
||||||
|
log.Errorf("Channel update of ourselves received")
|
||||||
|
|
||||||
err := p.router.cfg.Control.Fail(p.identifier, *reason)
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract channel update if the error contains one.
|
||||||
|
update := p.router.extractChannelUpdate(failure)
|
||||||
|
if update == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse pubkey to allow validation of the channel update. This should
|
||||||
|
// always succeed, otherwise there is something wrong in our
|
||||||
|
// implementation. Therefore return an error.
|
||||||
|
errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
|
||||||
|
errSource, err := btcec.ParsePubKey(
|
||||||
|
errVertex[:], btcec.S256(),
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
log.Errorf("Cannot parse pubkey: idx=%v, pubkey=%v",
|
||||||
|
errorSourceIdx, errVertex)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
isAdditionalEdge bool
|
||||||
|
policy *channeldb.ChannelEdgePolicy
|
||||||
|
)
|
||||||
|
|
||||||
|
// Before we apply the channel update, we need to decide whether the
|
||||||
|
// update is for additional (ephemeral) edge or normal edge stored in
|
||||||
|
// db.
|
||||||
|
//
|
||||||
|
// Note: the p.paySession might be nil here if it's called inside
|
||||||
|
// SendToRoute where there's no payment lifecycle.
|
||||||
|
if p.paySession != nil {
|
||||||
|
policy = p.paySession.GetAdditionalEdgePolicy(
|
||||||
|
errSource, update.ShortChannelID.ToUint64(),
|
||||||
|
)
|
||||||
|
if policy != nil {
|
||||||
|
isAdditionalEdge = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply channel update to additional edge policy.
|
||||||
|
if isAdditionalEdge {
|
||||||
|
if !p.paySession.UpdateAdditionalEdge(
|
||||||
|
update, errSource, policy) {
|
||||||
|
|
||||||
|
log.Debugf("Invalid channel update received: node=%v",
|
||||||
|
errVertex)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply channel update to the channel edge policy in our db.
|
||||||
|
if !p.router.applyChannelUpdate(update, errSource) {
|
||||||
|
log.Debugf("Invalid channel update received: node=%v",
|
||||||
|
errVertex)
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ package routing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
|
"fmt"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
@ -194,14 +195,6 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
t.Fatalf("unable to create route: %v", err)
|
t.Fatalf("unable to create route: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
halfShard, err := createTestRoute(paymentAmt/2, testGraph.aliasMap)
|
|
||||||
require.NoError(t, err, "unable to create half route")
|
|
||||||
|
|
||||||
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []paymentLifecycleTestCase{
|
tests := []paymentLifecycleTestCase{
|
||||||
{
|
{
|
||||||
// Tests a normal payment flow that succeeds.
|
// Tests a normal payment flow that succeeds.
|
||||||
@ -424,280 +417,6 @@ func TestRouterPaymentStateMachine(t *testing.T) {
|
|||||||
routes: []*route.Route{rt},
|
routes: []*route.Route{rt},
|
||||||
paymentErr: channeldb.FailureReasonNoRoute,
|
paymentErr: channeldb.FailureReasonNoRoute,
|
||||||
},
|
},
|
||||||
|
|
||||||
// =====================================
|
|
||||||
// || MPP scenarios ||
|
|
||||||
// =====================================
|
|
||||||
{
|
|
||||||
// Tests a simple successful MP payment of 4 shards.
|
|
||||||
name: "MP success",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 1
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 2
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 3
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// All shards succeed.
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
|
|
||||||
// Router should settle them all.
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
|
|
||||||
// And the final result is obviously
|
|
||||||
// successful.
|
|
||||||
paymentSuccess,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{shard, shard, shard, shard},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// An MP payment scenario where we need several extra
|
|
||||||
// attempts before the payment finally settle.
|
|
||||||
name: "MP failed shards",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 1
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 2
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 3
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// First two shards fail, two new ones are sent.
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
|
||||||
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// The four shards settle.
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
routerSettleAttempt,
|
|
||||||
|
|
||||||
// Overall payment succeeds.
|
|
||||||
paymentSuccess,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{
|
|
||||||
shard, shard, shard, shard, shard, shard,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// An MP payment scenario where one of the shards fails,
|
|
||||||
// but we still receive a single success shard.
|
|
||||||
name: "MP one shard success",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 1
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 0 fails, and should be failed by the
|
|
||||||
// router.
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
|
|
||||||
// We will try one more shard because we haven't
|
|
||||||
// sent the full payment amount.
|
|
||||||
routeRelease,
|
|
||||||
|
|
||||||
// The second shard succeed against all odds,
|
|
||||||
// making the overall payment succeed.
|
|
||||||
getPaymentResultSuccess,
|
|
||||||
routerSettleAttempt,
|
|
||||||
paymentSuccess,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{halfShard, halfShard},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// An MP payment scenario a shard fail with a terminal
|
|
||||||
// error, causing the router to stop attempting.
|
|
||||||
name: "MP terminal",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 1
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 2
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 3
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// The first shard fail with a terminal error.
|
|
||||||
getPaymentResultTerminalFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailPayment,
|
|
||||||
|
|
||||||
// Remaining 3 shards fail.
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
|
||||||
|
|
||||||
// Payment fails.
|
|
||||||
paymentError,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{
|
|
||||||
shard, shard, shard, shard, shard, shard,
|
|
||||||
},
|
|
||||||
paymentErr: channeldb.FailureReasonPaymentDetails,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// A MP payment scenario when our path finding returns
|
|
||||||
// after we've just received a terminal failure, and
|
|
||||||
// attempts to dispatch a new shard. Testing that we
|
|
||||||
// correctly abandon the shard and conclude the payment.
|
|
||||||
name: "MP path found after failure",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// The first shard fail with a terminal error.
|
|
||||||
getPaymentResultTerminalFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailPayment,
|
|
||||||
|
|
||||||
// shard 1 fails because we've had a terminal
|
|
||||||
// failure.
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
|
|
||||||
// Payment fails.
|
|
||||||
paymentError,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{
|
|
||||||
shard, shard,
|
|
||||||
},
|
|
||||||
paymentErr: channeldb.FailureReasonPaymentDetails,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
// A MP payment scenario when our path finding returns
|
|
||||||
// after we've just received a terminal failure, and
|
|
||||||
// we have another shard still in flight.
|
|
||||||
name: "MP shard in flight after terminal",
|
|
||||||
|
|
||||||
steps: []string{
|
|
||||||
routerInitPayment,
|
|
||||||
|
|
||||||
// shard 0
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 1
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// shard 2
|
|
||||||
routeRelease,
|
|
||||||
routerRegisterAttempt,
|
|
||||||
sendToSwitchSuccess,
|
|
||||||
|
|
||||||
// We find a path for another shard.
|
|
||||||
routeRelease,
|
|
||||||
|
|
||||||
// shard 0 fails with a terminal error.
|
|
||||||
getPaymentResultTerminalFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailPayment,
|
|
||||||
|
|
||||||
// We try to register our final shard after
|
|
||||||
// processing a terminal failure.
|
|
||||||
routerRegisterAttempt,
|
|
||||||
|
|
||||||
// Our in-flight shards fail.
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
getPaymentResultTempFailure,
|
|
||||||
routerFailAttempt,
|
|
||||||
routerFailAttempt,
|
|
||||||
|
|
||||||
// Payment fails.
|
|
||||||
paymentError,
|
|
||||||
},
|
|
||||||
routes: []*route.Route{
|
|
||||||
shard, shard, shard, shard,
|
|
||||||
},
|
|
||||||
paymentErr: channeldb.FailureReasonPaymentDetails,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
@ -738,7 +457,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
sendResult := make(chan error)
|
sendResult := make(chan error)
|
||||||
paymentResult := make(chan *htlcswitch.PaymentResult)
|
paymentResult := make(chan *htlcswitch.PaymentResult)
|
||||||
|
|
||||||
payer := &mockPayer{
|
payer := &mockPayerOld{
|
||||||
sendResult: sendResult,
|
sendResult: sendResult,
|
||||||
paymentResult: paymentResult,
|
paymentResult: paymentResult,
|
||||||
}
|
}
|
||||||
@ -748,8 +467,8 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
Chain: chain,
|
Chain: chain,
|
||||||
ChainView: chainView,
|
ChainView: chainView,
|
||||||
Control: control,
|
Control: control,
|
||||||
SessionSource: &mockPaymentSessionSource{},
|
SessionSource: &mockPaymentSessionSourceOld{},
|
||||||
MissionControl: &mockMissionControl{},
|
MissionControl: &mockMissionControlOld{},
|
||||||
Payer: payer,
|
Payer: payer,
|
||||||
ChannelPruneExpiry: time.Hour * 24,
|
ChannelPruneExpiry: time.Hour * 24,
|
||||||
GraphPruneInterval: time.Hour * 2,
|
GraphPruneInterval: time.Hour * 2,
|
||||||
@ -821,12 +540,12 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
// Setup our payment session source to block on release of
|
// Setup our payment session source to block on release of
|
||||||
// routes.
|
// routes.
|
||||||
routeChan := make(chan struct{})
|
routeChan := make(chan struct{})
|
||||||
router.cfg.SessionSource = &mockPaymentSessionSource{
|
router.cfg.SessionSource = &mockPaymentSessionSourceOld{
|
||||||
routes: test.routes,
|
routes: test.routes,
|
||||||
routeRelease: routeChan,
|
routeRelease: routeChan,
|
||||||
}
|
}
|
||||||
|
|
||||||
router.cfg.MissionControl = &mockMissionControl{}
|
router.cfg.MissionControl = &mockMissionControlOld{}
|
||||||
|
|
||||||
// Send the payment. Since this is new payment hash, the
|
// Send the payment. Since this is new payment hash, the
|
||||||
// information should be registered with the ControlTower.
|
// information should be registered with the ControlTower.
|
||||||
@ -839,7 +558,20 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
var resendResult chan error
|
var resendResult chan error
|
||||||
for _, step := range test.steps {
|
for i, step := range test.steps {
|
||||||
|
i, step := i, step
|
||||||
|
|
||||||
|
// fatal is a helper closure that wraps the step info.
|
||||||
|
fatal := func(err string, args ...interface{}) {
|
||||||
|
if args != nil {
|
||||||
|
err = fmt.Sprintf(err, args)
|
||||||
|
}
|
||||||
|
t.Fatalf(
|
||||||
|
"test case: %s failed on step [%v:%s], err: %s",
|
||||||
|
test.name, i, step, err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
switch step {
|
switch step {
|
||||||
|
|
||||||
case routerInitPayment:
|
case routerInitPayment:
|
||||||
@ -847,19 +579,18 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case args = <-control.init:
|
case args = <-control.init:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("no init payment with control")
|
fatal("no init payment with control")
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.c == nil {
|
if args.c == nil {
|
||||||
t.Fatalf("expected non-nil CreationInfo")
|
fatal("expected non-nil CreationInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
case routeRelease:
|
case routeRelease:
|
||||||
select {
|
select {
|
||||||
case <-routeChan:
|
case <-routeChan:
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("no route requested")
|
fatal("no route requested")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we expect the router to make a call to
|
// In this step we expect the router to make a call to
|
||||||
@ -869,12 +600,11 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case args = <-control.registerAttempt:
|
case args = <-control.registerAttempt:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("attempt not registered " +
|
fatal("attempt not registered with control")
|
||||||
"with control")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.a == nil {
|
if args.a == nil {
|
||||||
t.Fatalf("expected non-nil AttemptInfo")
|
fatal("expected non-nil AttemptInfo")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we expect the router to call the
|
// In this step we expect the router to call the
|
||||||
@ -883,7 +613,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case <-control.settleAttempt:
|
case <-control.settleAttempt:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("attempt settle not " +
|
fatal("attempt settle not " +
|
||||||
"registered with control")
|
"registered with control")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -894,7 +624,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case <-control.failAttempt:
|
case <-control.failAttempt:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("attempt fail not " +
|
fatal("attempt fail not " +
|
||||||
"registered with control")
|
"registered with control")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -905,7 +635,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case <-control.failPayment:
|
case <-control.failPayment:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("payment fail not " +
|
fatal("payment fail not " +
|
||||||
"registered with control")
|
"registered with control")
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -915,7 +645,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
select {
|
select {
|
||||||
case sendResult <- nil:
|
case sendResult <- nil:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("unable to send result")
|
fatal("unable to send result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we expect the SendToSwitch method to be
|
// In this step we expect the SendToSwitch method to be
|
||||||
@ -927,7 +657,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
1,
|
1,
|
||||||
):
|
):
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("unable to send result")
|
fatal("unable to send result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we expect the GetPaymentResult method
|
// In this step we expect the GetPaymentResult method
|
||||||
@ -939,7 +669,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
Preimage: preImage,
|
Preimage: preImage,
|
||||||
}:
|
}:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("unable to send result")
|
fatal("unable to send result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this state we expect the GetPaymentResult method
|
// In this state we expect the GetPaymentResult method
|
||||||
@ -956,7 +686,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
Error: failure,
|
Error: failure,
|
||||||
}:
|
}:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("unable to get result")
|
fatal("unable to get result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this state we expect the router to call the
|
// In this state we expect the router to call the
|
||||||
@ -974,7 +704,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
Error: failure,
|
Error: failure,
|
||||||
}:
|
}:
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("unable to get result")
|
fatal("unable to get result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we manually try to resend the same
|
// In this step we manually try to resend the same
|
||||||
@ -994,7 +724,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
close(getPaymentResult)
|
close(getPaymentResult)
|
||||||
|
|
||||||
if err := router.Stop(); err != nil {
|
if err := router.Stop(); err != nil {
|
||||||
t.Fatalf("unable to restart: %v", err)
|
fatal("unable to restart: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this step we manually start the router.
|
// In this step we manually start the router.
|
||||||
@ -1012,7 +742,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
require.Equal(t, test.paymentErr, err)
|
require.Equal(t, test.paymentErr, err)
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("got no payment result")
|
fatal("got no payment result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this state we expect the original payment to
|
// In this state we expect the original payment to
|
||||||
@ -1028,7 +758,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("got no payment result")
|
fatal("got no payment result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this state we expect to receive an error for the
|
// In this state we expect to receive an error for the
|
||||||
@ -1041,7 +771,7 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("got no payment result")
|
fatal("got no payment result")
|
||||||
}
|
}
|
||||||
|
|
||||||
// In this state we expect the resent payment to
|
// In this state we expect the resent payment to
|
||||||
@ -1054,11 +784,11 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
}
|
}
|
||||||
|
|
||||||
case <-time.After(stepTimeout):
|
case <-time.After(stepTimeout):
|
||||||
t.Fatalf("got no payment result")
|
fatal("got no payment result")
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
t.Fatalf("unknown step %v", step)
|
fatal("unknown step %v", step)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1068,3 +798,343 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
|
|||||||
t.Fatalf("SendPayment didn't exit")
|
t.Fatalf("SendPayment didn't exit")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestPaymentState tests that the logics implemented on paymentState struct
|
||||||
|
// are as expected. In particular, that the method terminated and
|
||||||
|
// needWaitForShards return the right values.
|
||||||
|
func TestPaymentState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
|
||||||
|
// Use the following three params, each is equivalent to a bool
|
||||||
|
// statement, to construct 8 test cases so that we can
|
||||||
|
// exhaustively catch all possible states.
|
||||||
|
numShardsInFlight int
|
||||||
|
remainingAmt lnwire.MilliSatoshi
|
||||||
|
terminate bool
|
||||||
|
|
||||||
|
expectedTerminated bool
|
||||||
|
expectedNeedWaitForShards bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// If we have active shards and terminate is marked
|
||||||
|
// false, the state is not terminated. Since the
|
||||||
|
// remaining amount is zero, we need to wait for shards
|
||||||
|
// to be finished and launch no more shards.
|
||||||
|
name: "state 100",
|
||||||
|
numShardsInFlight: 1,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(0),
|
||||||
|
terminate: false,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have active shards while terminate is marked
|
||||||
|
// true, the state is not terminated, and we need to
|
||||||
|
// wait for shards to be finished and launch no more
|
||||||
|
// shards.
|
||||||
|
name: "state 101",
|
||||||
|
numShardsInFlight: 1,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(0),
|
||||||
|
terminate: true,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: true,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
// If we have active shards and terminate is marked
|
||||||
|
// false, the state is not terminated. Since the
|
||||||
|
// remaining amount is not zero, we don't need to wait
|
||||||
|
// for shards outcomes and should launch more shards.
|
||||||
|
name: "state 110",
|
||||||
|
numShardsInFlight: 1,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(1),
|
||||||
|
terminate: false,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have active shards and terminate is marked
|
||||||
|
// true, the state is not terminated. Even the
|
||||||
|
// remaining amount is not zero, we need to wait for
|
||||||
|
// shards outcomes because state is terminated.
|
||||||
|
name: "state 111",
|
||||||
|
numShardsInFlight: 1,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(1),
|
||||||
|
terminate: true,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have no active shards while terminate is marked
|
||||||
|
// false, the state is not terminated, and we don't
|
||||||
|
// need to wait for more shard outcomes because there
|
||||||
|
// are no active shards.
|
||||||
|
name: "state 000",
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(0),
|
||||||
|
terminate: false,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have no active shards while terminate is marked
|
||||||
|
// true, the state is terminated, and we don't need to
|
||||||
|
// wait for shards to be finished.
|
||||||
|
name: "state 001",
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(0),
|
||||||
|
terminate: true,
|
||||||
|
expectedTerminated: true,
|
||||||
|
expectedNeedWaitForShards: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have no active shards while terminate is marked
|
||||||
|
// false, the state is not terminated. Since the
|
||||||
|
// remaining amount is not zero, we don't need to wait
|
||||||
|
// for shards outcomes and should launch more shards.
|
||||||
|
name: "state 010",
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(1),
|
||||||
|
terminate: false,
|
||||||
|
expectedTerminated: false,
|
||||||
|
expectedNeedWaitForShards: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// If we have no active shards while terminate is marked
|
||||||
|
// true, the state is terminated, and we don't need to
|
||||||
|
// wait for shards outcomes.
|
||||||
|
name: "state 011",
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: lnwire.MilliSatoshi(1),
|
||||||
|
terminate: true,
|
||||||
|
expectedTerminated: true,
|
||||||
|
expectedNeedWaitForShards: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ps := &paymentState{
|
||||||
|
numShardsInFlight: tc.numShardsInFlight,
|
||||||
|
remainingAmt: tc.remainingAmt,
|
||||||
|
terminate: tc.terminate,
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
t, tc.expectedTerminated, ps.terminated(),
|
||||||
|
"terminated returned wrong value",
|
||||||
|
)
|
||||||
|
require.Equal(
|
||||||
|
t, tc.expectedNeedWaitForShards,
|
||||||
|
ps.needWaitForShards(),
|
||||||
|
"needWaitForShards returned wrong value",
|
||||||
|
)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestUpdatePaymentState checks that the method updatePaymentState updates the
|
||||||
|
// paymentState as expected.
|
||||||
|
func TestUpdatePaymentState(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// paymentHash is the identifier on paymentLifecycle.
|
||||||
|
paymentHash := lntypes.Hash{}
|
||||||
|
|
||||||
|
// TODO(yy): make MPPayment into an interface so we can mock it. The
|
||||||
|
// current design implicitly tests the methods SendAmt, TerminalInfo,
|
||||||
|
// and InFlightHTLCs on channeldb.MPPayment, which is not good. Once
|
||||||
|
// MPPayment becomes an interface, we can then mock these methods here.
|
||||||
|
|
||||||
|
// SentAmt returns 90, 10
|
||||||
|
// TerminalInfo returns non-nil, nil
|
||||||
|
// InFlightHTLCs returns 0
|
||||||
|
var preimage lntypes.Preimage
|
||||||
|
paymentSettled := &channeldb.MPPayment{
|
||||||
|
HTLCs: []channeldb.HTLCAttempt{
|
||||||
|
makeSettledAttempt(100, 10, preimage),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// SentAmt returns 0, 0
|
||||||
|
// TerminalInfo returns nil, non-nil
|
||||||
|
// InFlightHTLCs returns 0
|
||||||
|
reason := channeldb.FailureReasonError
|
||||||
|
paymentFailed := &channeldb.MPPayment{
|
||||||
|
FailureReason: &reason,
|
||||||
|
}
|
||||||
|
|
||||||
|
// SentAmt returns 90, 10
|
||||||
|
// TerminalInfo returns nil, nil
|
||||||
|
// InFlightHTLCs returns 1
|
||||||
|
paymentActive := &channeldb.MPPayment{
|
||||||
|
HTLCs: []channeldb.HTLCAttempt{
|
||||||
|
makeActiveAttempt(100, 10),
|
||||||
|
makeFailedAttempt(100, 10),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
payment *channeldb.MPPayment
|
||||||
|
totalAmt int
|
||||||
|
feeLimit int
|
||||||
|
|
||||||
|
expectedState *paymentState
|
||||||
|
shouldReturnError bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
// Test that the error returned from FetchPayment is
|
||||||
|
// handled properly. We use a nil payment to indicate
|
||||||
|
// we want to return an error.
|
||||||
|
name: "fetch payment error",
|
||||||
|
payment: nil,
|
||||||
|
shouldReturnError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Test that when the sentAmt exceeds totalAmount, the
|
||||||
|
// error is returned.
|
||||||
|
name: "amount exceeded error",
|
||||||
|
payment: paymentSettled,
|
||||||
|
totalAmt: 1,
|
||||||
|
shouldReturnError: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Test that when the fee budget is reached, the
|
||||||
|
// remaining fee should be zero.
|
||||||
|
name: "fee budget reached",
|
||||||
|
payment: paymentActive,
|
||||||
|
totalAmt: 1000,
|
||||||
|
feeLimit: 1,
|
||||||
|
expectedState: &paymentState{
|
||||||
|
numShardsInFlight: 1,
|
||||||
|
remainingAmt: 1000 - 90,
|
||||||
|
remainingFees: 0,
|
||||||
|
terminate: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Test when the payment is settled, the state should
|
||||||
|
// be marked as terminated.
|
||||||
|
name: "payment settled",
|
||||||
|
payment: paymentSettled,
|
||||||
|
totalAmt: 1000,
|
||||||
|
feeLimit: 100,
|
||||||
|
expectedState: &paymentState{
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: 1000 - 90,
|
||||||
|
remainingFees: 100 - 10,
|
||||||
|
terminate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// Test when the payment is failed, the state should be
|
||||||
|
// marked as terminated.
|
||||||
|
name: "payment failed",
|
||||||
|
payment: paymentFailed,
|
||||||
|
totalAmt: 1000,
|
||||||
|
feeLimit: 100,
|
||||||
|
expectedState: &paymentState{
|
||||||
|
numShardsInFlight: 0,
|
||||||
|
remainingAmt: 1000,
|
||||||
|
remainingFees: 100,
|
||||||
|
terminate: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
tc := tc
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create mock control tower and assign it to router.
|
||||||
|
// We will then use the router and the paymentHash
|
||||||
|
// above to create our paymentLifecycle for this test.
|
||||||
|
ct := &mockControlTower{}
|
||||||
|
rt := &ChannelRouter{cfg: &Config{Control: ct}}
|
||||||
|
pl := &paymentLifecycle{
|
||||||
|
router: rt,
|
||||||
|
identifier: paymentHash,
|
||||||
|
totalAmount: lnwire.MilliSatoshi(tc.totalAmt),
|
||||||
|
feeLimit: lnwire.MilliSatoshi(tc.feeLimit),
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.payment == nil {
|
||||||
|
// A nil payment indicates we want to test an
|
||||||
|
// error returned from FetchPayment.
|
||||||
|
dummyErr := errors.New("dummy")
|
||||||
|
ct.On("FetchPayment", paymentHash).Return(
|
||||||
|
nil, dummyErr,
|
||||||
|
)
|
||||||
|
|
||||||
|
} else {
|
||||||
|
// Otherwise we will return the payment.
|
||||||
|
ct.On("FetchPayment", paymentHash).Return(
|
||||||
|
tc.payment, nil,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call the method that updates the payment state.
|
||||||
|
_, state, err := pl.updatePaymentState()
|
||||||
|
|
||||||
|
// Assert that the mock method is called as
|
||||||
|
// intended.
|
||||||
|
ct.AssertExpectations(t)
|
||||||
|
|
||||||
|
if tc.shouldReturnError {
|
||||||
|
require.Error(t, err, "expect an error")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
require.NoError(t, err, "unexpected error")
|
||||||
|
require.Equal(
|
||||||
|
t, tc.expectedState, state,
|
||||||
|
"state not updated as expected",
|
||||||
|
)
|
||||||
|
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeActiveAttempt(total, fee int) channeldb.HTLCAttempt {
|
||||||
|
return channeldb.HTLCAttempt{
|
||||||
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeSettledAttempt(total, fee int,
|
||||||
|
preimage lntypes.Preimage) channeldb.HTLCAttempt {
|
||||||
|
|
||||||
|
return channeldb.HTLCAttempt{
|
||||||
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||||
|
Settle: &channeldb.HTLCSettleInfo{Preimage: preimage},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeFailedAttempt(total, fee int) channeldb.HTLCAttempt {
|
||||||
|
return channeldb.HTLCAttempt{
|
||||||
|
HTLCAttemptInfo: makeAttemptInfo(total, total-fee),
|
||||||
|
Failure: &channeldb.HTLCFailInfo{
|
||||||
|
Reason: channeldb.HTLCFailInternal,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeAttemptInfo(total, amtForwarded int) channeldb.HTLCAttemptInfo {
|
||||||
|
hop := &route.Hop{AmtToForward: lnwire.MilliSatoshi(amtForwarded)}
|
||||||
|
return channeldb.HTLCAttemptInfo{
|
||||||
|
Route: route.Route{
|
||||||
|
TotalAmount: lnwire.MilliSatoshi(total),
|
||||||
|
Hops: []*route.Hop{hop},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,7 +3,9 @@ package routing
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btclog"
|
"github.com/btcsuite/btclog"
|
||||||
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/build"
|
"github.com/lightningnetwork/lnd/build"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -136,6 +138,19 @@ type PaymentSession interface {
|
|||||||
// during path finding.
|
// during path finding.
|
||||||
RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
||||||
activeShards, height uint32) (*route.Route, error)
|
activeShards, height uint32) (*route.Route, error)
|
||||||
|
|
||||||
|
// UpdateAdditionalEdge takes an additional channel edge policy
|
||||||
|
// (private channels) and applies the update from the message. Returns
|
||||||
|
// a boolean to indicate whether the update has been applied without
|
||||||
|
// error.
|
||||||
|
UpdateAdditionalEdge(msg *lnwire.ChannelUpdate, pubKey *btcec.PublicKey,
|
||||||
|
policy *channeldb.ChannelEdgePolicy) bool
|
||||||
|
|
||||||
|
// GetAdditionalEdgePolicy uses the public key and channel ID to query
|
||||||
|
// the ephemeral channel edge policy for additional edges. Returns a nil
|
||||||
|
// if nothing found.
|
||||||
|
GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||||
|
channelID uint64) *channeldb.ChannelEdgePolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
// paymentSession is used during an HTLC routings session to prune the local
|
// paymentSession is used during an HTLC routings session to prune the local
|
||||||
@ -382,3 +397,53 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
|
|||||||
return route, err
|
return route, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateAdditionalEdge updates the channel edge policy for a private edge. It
|
||||||
|
// validates the message signature and checks it's up to date, then applies the
|
||||||
|
// updates to the supplied policy. It returns a boolean to indicate whether
|
||||||
|
// there's an error when applying the updates.
|
||||||
|
func (p *paymentSession) UpdateAdditionalEdge(msg *lnwire.ChannelUpdate,
|
||||||
|
pubKey *btcec.PublicKey, policy *channeldb.ChannelEdgePolicy) bool {
|
||||||
|
|
||||||
|
// Validate the message signature.
|
||||||
|
if err := VerifyChannelUpdateSignature(msg, pubKey); err != nil {
|
||||||
|
log.Errorf(
|
||||||
|
"Unable to validate channel update signature: %v", err,
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update channel policy for the additional edge.
|
||||||
|
policy.TimeLockDelta = msg.TimeLockDelta
|
||||||
|
policy.FeeBaseMSat = lnwire.MilliSatoshi(msg.BaseFee)
|
||||||
|
policy.FeeProportionalMillionths = lnwire.MilliSatoshi(msg.FeeRate)
|
||||||
|
|
||||||
|
log.Debugf("New private channel update applied: %v",
|
||||||
|
newLogClosure(func() string { return spew.Sdump(msg) }))
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAdditionalEdgePolicy uses the public key and channel ID to query the
|
||||||
|
// ephemeral channel edge policy for additional edges. Returns a nil if nothing
|
||||||
|
// found.
|
||||||
|
func (p *paymentSession) GetAdditionalEdgePolicy(pubKey *btcec.PublicKey,
|
||||||
|
channelID uint64) *channeldb.ChannelEdgePolicy {
|
||||||
|
|
||||||
|
target := route.NewVertex(pubKey)
|
||||||
|
|
||||||
|
edges, ok := p.additionalEdges[target]
|
||||||
|
if !ok {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, edge := range edges {
|
||||||
|
if edge.ChannelID != channelID {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return edge
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
@ -2,10 +2,13 @@ package routing
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -70,6 +73,109 @@ func TestValidateCLTVLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestUpdateAdditionalEdge checks that we can update the additional edges as
|
||||||
|
// expected.
|
||||||
|
func TestUpdateAdditionalEdge(t *testing.T) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
testChannelID = uint64(12345)
|
||||||
|
oldFeeBaseMSat = uint32(1000)
|
||||||
|
newFeeBaseMSat = uint32(1100)
|
||||||
|
oldExpiryDelta = uint16(100)
|
||||||
|
newExpiryDelta = uint16(120)
|
||||||
|
|
||||||
|
payHash lntypes.Hash
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create a minimal test node using the private key priv1.
|
||||||
|
pub := priv1.PubKey().SerializeCompressed()
|
||||||
|
testNode := &channeldb.LightningNode{}
|
||||||
|
copy(testNode.PubKeyBytes[:], pub)
|
||||||
|
|
||||||
|
nodeID, err := testNode.PubKey()
|
||||||
|
require.NoError(t, err, "failed to get node id")
|
||||||
|
|
||||||
|
// Create a payment with a route hint.
|
||||||
|
payment := &LightningPayment{
|
||||||
|
Target: testNode.PubKeyBytes,
|
||||||
|
Amount: 1000,
|
||||||
|
RouteHints: [][]zpay32.HopHint{{
|
||||||
|
zpay32.HopHint{
|
||||||
|
// The nodeID is actually the target itself. It
|
||||||
|
// doesn't matter as we are not doing routing
|
||||||
|
// in this test.
|
||||||
|
NodeID: nodeID,
|
||||||
|
ChannelID: testChannelID,
|
||||||
|
FeeBaseMSat: oldFeeBaseMSat,
|
||||||
|
CLTVExpiryDelta: oldExpiryDelta,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
paymentHash: &payHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the paymentsession.
|
||||||
|
session, err := newPaymentSession(
|
||||||
|
payment,
|
||||||
|
func() (map[uint64]lnwire.MilliSatoshi,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
return nil, nil
|
||||||
|
},
|
||||||
|
func() (routingGraph, func(), error) {
|
||||||
|
return &sessionGraph{}, func() {}, nil
|
||||||
|
},
|
||||||
|
&MissionControl{},
|
||||||
|
PathFindingConfig{},
|
||||||
|
)
|
||||||
|
require.NoError(t, err, "failed to create payment session")
|
||||||
|
|
||||||
|
// We should have 1 additional edge.
|
||||||
|
require.Equal(t, 1, len(session.additionalEdges))
|
||||||
|
|
||||||
|
// The edge should use nodeID as key, and its value should have 1 edge
|
||||||
|
// policy.
|
||||||
|
vertex := route.NewVertex(nodeID)
|
||||||
|
policies, ok := session.additionalEdges[vertex]
|
||||||
|
require.True(t, ok, "cannot find policy")
|
||||||
|
require.Equal(t, 1, len(policies), "should have 1 edge policy")
|
||||||
|
|
||||||
|
// Check that the policy has been created as expected.
|
||||||
|
policy := policies[0]
|
||||||
|
require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch")
|
||||||
|
require.Equal(t,
|
||||||
|
oldExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch",
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
lnwire.MilliSatoshi(oldFeeBaseMSat),
|
||||||
|
policy.FeeBaseMSat, "fee base msat mismatch",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Create the channel update message and sign.
|
||||||
|
msg := &lnwire.ChannelUpdate{
|
||||||
|
ShortChannelID: lnwire.NewShortChanIDFromInt(testChannelID),
|
||||||
|
Timestamp: uint32(time.Now().Unix()),
|
||||||
|
BaseFee: newFeeBaseMSat,
|
||||||
|
TimeLockDelta: newExpiryDelta,
|
||||||
|
}
|
||||||
|
signErrChanUpdate(t, priv1, msg)
|
||||||
|
|
||||||
|
// Apply the update.
|
||||||
|
require.True(t,
|
||||||
|
session.UpdateAdditionalEdge(msg, nodeID, policy),
|
||||||
|
"failed to update additional edge",
|
||||||
|
)
|
||||||
|
|
||||||
|
// Check that the policy has been updated as expected.
|
||||||
|
require.Equal(t, testChannelID, policy.ChannelID, "channel ID mismatch")
|
||||||
|
require.Equal(t,
|
||||||
|
newExpiryDelta, policy.TimeLockDelta, "timelock delta mismatch",
|
||||||
|
)
|
||||||
|
require.Equal(t,
|
||||||
|
lnwire.MilliSatoshi(newFeeBaseMSat),
|
||||||
|
policy.FeeBaseMSat, "fee base msat mismatch",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
func TestRequestRoute(t *testing.T) {
|
func TestRequestRoute(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
height = 10
|
height = 10
|
||||||
|
@ -2163,15 +2163,13 @@ func (r *ChannelRouter) SendToRoute(htlcHash lntypes.Hash, rt *route.Route) (
|
|||||||
// mark the payment failed with the control tower immediately. Process
|
// mark the payment failed with the control tower immediately. Process
|
||||||
// the error to check if it maps into a terminal error code, if not use
|
// the error to check if it maps into a terminal error code, if not use
|
||||||
// a generic NO_ROUTE error.
|
// a generic NO_ROUTE error.
|
||||||
reason := r.processSendError(
|
if err := sh.handleSendError(attempt, shardError); err != nil {
|
||||||
attempt.AttemptID, &attempt.Route, shardError,
|
return nil, err
|
||||||
)
|
|
||||||
if reason == nil {
|
|
||||||
r := channeldb.FailureReasonNoRoute
|
|
||||||
reason = &r
|
|
||||||
}
|
}
|
||||||
|
|
||||||
err = r.cfg.Control.Fail(paymentIdentifier, *reason)
|
err = r.cfg.Control.Fail(
|
||||||
|
paymentIdentifier, channeldb.FailureReasonNoRoute,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -2230,121 +2228,6 @@ func (r *ChannelRouter) sendPayment(
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryApplyChannelUpdate tries to apply a channel update present in the failure
|
|
||||||
// message if any.
|
|
||||||
func (r *ChannelRouter) tryApplyChannelUpdate(rt *route.Route,
|
|
||||||
errorSourceIdx int, failure lnwire.FailureMessage) error {
|
|
||||||
|
|
||||||
// It makes no sense to apply our own channel updates.
|
|
||||||
if errorSourceIdx == 0 {
|
|
||||||
log.Errorf("Channel update of ourselves received")
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract channel update if the error contains one.
|
|
||||||
update := r.extractChannelUpdate(failure)
|
|
||||||
if update == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse pubkey to allow validation of the channel update. This should
|
|
||||||
// always succeed, otherwise there is something wrong in our
|
|
||||||
// implementation. Therefore return an error.
|
|
||||||
errVertex := rt.Hops[errorSourceIdx-1].PubKeyBytes
|
|
||||||
errSource, err := btcec.ParsePubKey(
|
|
||||||
errVertex[:], btcec.S256(),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Cannot parse pubkey: idx=%v, pubkey=%v",
|
|
||||||
errorSourceIdx, errVertex)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply channel update.
|
|
||||||
if !r.applyChannelUpdate(update, errSource) {
|
|
||||||
log.Debugf("Invalid channel update received: node=%v",
|
|
||||||
errVertex)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// processSendError analyzes the error for the payment attempt received from the
|
|
||||||
// switch and updates mission control and/or channel policies. Depending on the
|
|
||||||
// error type, this error is either the final outcome of the payment or we need
|
|
||||||
// to continue with an alternative route. A final outcome is indicated by a
|
|
||||||
// non-nil return value.
|
|
||||||
func (r *ChannelRouter) processSendError(attemptID uint64, rt *route.Route,
|
|
||||||
sendErr error) *channeldb.FailureReason {
|
|
||||||
|
|
||||||
internalErrorReason := channeldb.FailureReasonError
|
|
||||||
|
|
||||||
reportFail := func(srcIdx *int,
|
|
||||||
msg lnwire.FailureMessage) *channeldb.FailureReason {
|
|
||||||
|
|
||||||
// Report outcome to mission control.
|
|
||||||
reason, err := r.cfg.MissionControl.ReportPaymentFail(
|
|
||||||
attemptID, rt, srcIdx, msg,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
log.Errorf("Error reporting payment result to mc: %v",
|
|
||||||
err)
|
|
||||||
|
|
||||||
return &internalErrorReason
|
|
||||||
}
|
|
||||||
|
|
||||||
return reason
|
|
||||||
}
|
|
||||||
|
|
||||||
if sendErr == htlcswitch.ErrUnreadableFailureMessage {
|
|
||||||
log.Tracef("Unreadable failure when sending htlc")
|
|
||||||
|
|
||||||
return reportFail(nil, nil)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the error is a ClearTextError, we have received a valid wire
|
|
||||||
// failure message, either from our own outgoing link or from a node
|
|
||||||
// down the route. If the error is not related to the propagation of
|
|
||||||
// our payment, we can stop trying because an internal error has
|
|
||||||
// occurred.
|
|
||||||
rtErr, ok := sendErr.(htlcswitch.ClearTextError)
|
|
||||||
if !ok {
|
|
||||||
return &internalErrorReason
|
|
||||||
}
|
|
||||||
|
|
||||||
// failureSourceIdx is the index of the node that the failure occurred
|
|
||||||
// at. If the ClearTextError received is not a ForwardingError the
|
|
||||||
// payment error occurred at our node, so we leave this value as 0
|
|
||||||
// to indicate that the failure occurred locally. If the error is a
|
|
||||||
// ForwardingError, it did not originate at our node, so we set
|
|
||||||
// failureSourceIdx to the index of the node where the failure occurred.
|
|
||||||
failureSourceIdx := 0
|
|
||||||
source, ok := rtErr.(*htlcswitch.ForwardingError)
|
|
||||||
if ok {
|
|
||||||
failureSourceIdx = source.FailureSourceIdx
|
|
||||||
}
|
|
||||||
|
|
||||||
// Extract the wire failure and apply channel update if it contains one.
|
|
||||||
// If we received an unknown failure message from a node along the
|
|
||||||
// route, the failure message will be nil.
|
|
||||||
failureMessage := rtErr.WireMessage()
|
|
||||||
if failureMessage != nil {
|
|
||||||
err := r.tryApplyChannelUpdate(
|
|
||||||
rt, failureSourceIdx, failureMessage,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return &internalErrorReason
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Tracef("Node=%v reported failure when sending htlc",
|
|
||||||
failureSourceIdx)
|
|
||||||
|
|
||||||
return reportFail(&failureSourceIdx, failureMessage)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractChannelUpdate examines the error and extracts the channel update.
|
// extractChannelUpdate examines the error and extracts the channel update.
|
||||||
func (r *ChannelRouter) extractChannelUpdate(
|
func (r *ChannelRouter) extractChannelUpdate(
|
||||||
failure lnwire.FailureMessage) *lnwire.ChannelUpdate {
|
failure lnwire.FailureMessage) *lnwire.ChannelUpdate {
|
||||||
|
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
@ -24,6 +25,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
)
|
)
|
||||||
|
|
||||||
var uniquePaymentID uint64 = 1 // to be used atomically
|
var uniquePaymentID uint64 = 1 // to be used atomically
|
||||||
@ -35,12 +37,32 @@ type testCtx struct {
|
|||||||
|
|
||||||
aliases map[string]route.Vertex
|
aliases map[string]route.Vertex
|
||||||
|
|
||||||
|
privKeys map[string]*btcec.PrivateKey
|
||||||
|
|
||||||
|
channelIDs map[route.Vertex]map[route.Vertex]uint64
|
||||||
|
|
||||||
chain *mockChain
|
chain *mockChain
|
||||||
|
|
||||||
chainView *mockChainView
|
chainView *mockChainView
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *testCtx) RestartRouter() error {
|
func (c *testCtx) getChannelIDFromAlias(t *testing.T, a, b string) uint64 {
|
||||||
|
vertexA, ok := c.aliases[a]
|
||||||
|
require.True(t, ok, "cannot find aliases for %s", a)
|
||||||
|
|
||||||
|
vertexB, ok := c.aliases[b]
|
||||||
|
require.True(t, ok, "cannot find aliases for %s", b)
|
||||||
|
|
||||||
|
channelIDMap, ok := c.channelIDs[vertexA]
|
||||||
|
require.True(t, ok, "cannot find channelID map %s(%s)", vertexA, a)
|
||||||
|
|
||||||
|
channelID, ok := channelIDMap[vertexB]
|
||||||
|
require.True(t, ok, "cannot find channelID using %s(%s)", vertexB, b)
|
||||||
|
|
||||||
|
return channelID
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *testCtx) RestartRouter(t *testing.T) {
|
||||||
// First, we'll reset the chainView's state as it doesn't persist the
|
// First, we'll reset the chainView's state as it doesn't persist the
|
||||||
// filter between restarts.
|
// filter between restarts.
|
||||||
c.chainView.Reset()
|
c.chainView.Reset()
|
||||||
@ -51,35 +73,31 @@ func (c *testCtx) RestartRouter() error {
|
|||||||
Graph: c.graph,
|
Graph: c.graph,
|
||||||
Chain: c.chain,
|
Chain: c.chain,
|
||||||
ChainView: c.chainView,
|
ChainView: c.chainView,
|
||||||
Payer: &mockPaymentAttemptDispatcher{},
|
Payer: &mockPaymentAttemptDispatcherOld{},
|
||||||
Control: makeMockControlTower(),
|
Control: makeMockControlTower(),
|
||||||
ChannelPruneExpiry: time.Hour * 24,
|
ChannelPruneExpiry: time.Hour * 24,
|
||||||
GraphPruneInterval: time.Hour * 2,
|
GraphPruneInterval: time.Hour * 2,
|
||||||
})
|
})
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to create router")
|
||||||
return fmt.Errorf("unable to create router %v", err)
|
require.NoError(t, router.Start(), "unable to start router")
|
||||||
}
|
|
||||||
if err := router.Start(); err != nil {
|
|
||||||
return fmt.Errorf("unable to start router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we'll swap out the pointer in the testCtx with this fresh
|
// Finally, we'll swap out the pointer in the testCtx with this fresh
|
||||||
// instance of the router.
|
// instance of the router.
|
||||||
c.router = router
|
c.router = router
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGraphInstance,
|
func createTestCtxFromGraphInstance(t *testing.T,
|
||||||
strictPruning bool) (*testCtx, func(), error) {
|
startingHeight uint32, graphInstance *testGraphInstance,
|
||||||
|
strictPruning bool) (*testCtx, func()) {
|
||||||
|
|
||||||
return createTestCtxFromGraphInstanceAssumeValid(
|
return createTestCtxFromGraphInstanceAssumeValid(
|
||||||
startingHeight, graphInstance, false, strictPruning,
|
t, startingHeight, graphInstance, false, strictPruning,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
|
||||||
graphInstance *testGraphInstance, assumeValid bool,
|
startingHeight uint32, graphInstance *testGraphInstance,
|
||||||
strictPruning bool) (*testCtx, func(), error) {
|
assumeValid bool, strictPruning bool) (*testCtx, func()) {
|
||||||
|
|
||||||
// We'll initialize an instance of the channel router with mock
|
// We'll initialize an instance of the channel router with mock
|
||||||
// versions of the chain and channel notifier. As we don't need to test
|
// versions of the chain and channel notifier. As we don't need to test
|
||||||
@ -105,13 +123,13 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
|||||||
graphInstance.graph.Database(), route.Vertex{},
|
graphInstance.graph.Database(), route.Vertex{},
|
||||||
mcConfig,
|
mcConfig,
|
||||||
)
|
)
|
||||||
if err != nil {
|
require.NoError(t, err, "failed to create missioncontrol")
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
sessionSource := &SessionSource{
|
sessionSource := &SessionSource{
|
||||||
Graph: graphInstance.graph,
|
Graph: graphInstance.graph,
|
||||||
QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
|
QueryBandwidth: func(
|
||||||
|
e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
|
||||||
|
|
||||||
return lnwire.NewMSatFromSatoshis(e.Capacity)
|
return lnwire.NewMSatFromSatoshis(e.Capacity)
|
||||||
},
|
},
|
||||||
PathFindingConfig: pathFindingConfig,
|
PathFindingConfig: pathFindingConfig,
|
||||||
@ -122,13 +140,15 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
|||||||
Graph: graphInstance.graph,
|
Graph: graphInstance.graph,
|
||||||
Chain: chain,
|
Chain: chain,
|
||||||
ChainView: chainView,
|
ChainView: chainView,
|
||||||
Payer: &mockPaymentAttemptDispatcher{},
|
Payer: &mockPaymentAttemptDispatcherOld{},
|
||||||
Control: makeMockControlTower(),
|
Control: makeMockControlTower(),
|
||||||
MissionControl: mc,
|
MissionControl: mc,
|
||||||
SessionSource: sessionSource,
|
SessionSource: sessionSource,
|
||||||
ChannelPruneExpiry: time.Hour * 24,
|
ChannelPruneExpiry: time.Hour * 24,
|
||||||
GraphPruneInterval: time.Hour * 2,
|
GraphPruneInterval: time.Hour * 2,
|
||||||
QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
|
QueryBandwidth: func(
|
||||||
|
e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
|
||||||
|
|
||||||
return lnwire.NewMSatFromSatoshis(e.Capacity)
|
return lnwire.NewMSatFromSatoshis(e.Capacity)
|
||||||
},
|
},
|
||||||
NextPaymentID: func() (uint64, error) {
|
NextPaymentID: func() (uint64, error) {
|
||||||
@ -140,19 +160,17 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
|||||||
AssumeChannelValid: assumeValid,
|
AssumeChannelValid: assumeValid,
|
||||||
StrictZombiePruning: strictPruning,
|
StrictZombiePruning: strictPruning,
|
||||||
})
|
})
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to create router")
|
||||||
return nil, nil, fmt.Errorf("unable to create router %v", err)
|
require.NoError(t, router.Start(), "unable to start router")
|
||||||
}
|
|
||||||
if err := router.Start(); err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("unable to start router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := &testCtx{
|
ctx := &testCtx{
|
||||||
router: router,
|
router: router,
|
||||||
graph: graphInstance.graph,
|
graph: graphInstance.graph,
|
||||||
aliases: graphInstance.aliasMap,
|
aliases: graphInstance.aliasMap,
|
||||||
chain: chain,
|
privKeys: graphInstance.privKeyMap,
|
||||||
chainView: chainView,
|
channelIDs: graphInstance.channelIDs,
|
||||||
|
chain: chain,
|
||||||
|
chainView: chainView,
|
||||||
}
|
}
|
||||||
|
|
||||||
cleanUp := func() {
|
cleanUp := func() {
|
||||||
@ -160,10 +178,12 @@ func createTestCtxFromGraphInstanceAssumeValid(startingHeight uint32,
|
|||||||
graphInstance.cleanUp()
|
graphInstance.cleanUp()
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx, cleanUp, nil
|
return ctx, cleanUp
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCtxSingleNode(startingHeight uint32) (*testCtx, func(), error) {
|
func createTestCtxSingleNode(t *testing.T,
|
||||||
|
startingHeight uint32) (*testCtx, func()) {
|
||||||
|
|
||||||
var (
|
var (
|
||||||
graph *channeldb.ChannelGraph
|
graph *channeldb.ChannelGraph
|
||||||
sourceNode *channeldb.LightningNode
|
sourceNode *channeldb.LightningNode
|
||||||
@ -172,35 +192,52 @@ func createTestCtxSingleNode(startingHeight uint32) (*testCtx, func(), error) {
|
|||||||
)
|
)
|
||||||
|
|
||||||
graph, cleanup, err = makeTestGraph()
|
graph, cleanup, err = makeTestGraph()
|
||||||
if err != nil {
|
require.NoError(t, err, "failed to make test graph")
|
||||||
return nil, nil, fmt.Errorf("unable to create test graph: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
sourceNode, err = createTestNode()
|
sourceNode, err = createTestNode()
|
||||||
if err != nil {
|
require.NoError(t, err, "failed to create test node")
|
||||||
return nil, nil, fmt.Errorf("unable to create source node: %v", err)
|
|
||||||
}
|
require.NoError(t,
|
||||||
if err = graph.SetSourceNode(sourceNode); err != nil {
|
graph.SetSourceNode(sourceNode), "failed to set source node",
|
||||||
return nil, nil, fmt.Errorf("unable to set source node: %v", err)
|
)
|
||||||
}
|
|
||||||
|
|
||||||
graphInstance := &testGraphInstance{
|
graphInstance := &testGraphInstance{
|
||||||
graph: graph,
|
graph: graph,
|
||||||
cleanUp: cleanup,
|
cleanUp: cleanup,
|
||||||
}
|
}
|
||||||
|
|
||||||
return createTestCtxFromGraphInstance(startingHeight, graphInstance, false)
|
return createTestCtxFromGraphInstance(
|
||||||
|
t, startingHeight, graphInstance, false,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, func(), error) {
|
func createTestCtxFromFile(t *testing.T,
|
||||||
|
startingHeight uint32, testGraph string) (*testCtx, func()) {
|
||||||
|
|
||||||
// We'll attempt to locate and parse out the file
|
// We'll attempt to locate and parse out the file
|
||||||
// that encodes the graph that our tests should be run against.
|
// that encodes the graph that our tests should be run against.
|
||||||
graphInstance, err := parseTestGraph(testGraph)
|
graphInstance, err := parseTestGraph(testGraph)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to create test graph")
|
||||||
return nil, nil, fmt.Errorf("unable to create test graph: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return createTestCtxFromGraphInstance(startingHeight, graphInstance, false)
|
return createTestCtxFromGraphInstance(
|
||||||
|
t, startingHeight, graphInstance, false,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add valid signature to channel update simulated as error received from the
|
||||||
|
// network.
|
||||||
|
func signErrChanUpdate(t *testing.T, key *btcec.PrivateKey,
|
||||||
|
errChanUpdate *lnwire.ChannelUpdate) {
|
||||||
|
|
||||||
|
chanUpdateMsg, err := errChanUpdate.DataToSign()
|
||||||
|
require.NoError(t, err, "failed to retrieve data to sign")
|
||||||
|
|
||||||
|
digest := chainhash.DoubleHashB(chanUpdateMsg)
|
||||||
|
sig, err := key.Sign(digest)
|
||||||
|
require.NoError(t, err, "failed to sign msg")
|
||||||
|
|
||||||
|
errChanUpdate.Signature, err = lnwire.NewSigFromSignature(sig)
|
||||||
|
require.NoError(t, err, "failed to create new signature")
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
|
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
|
||||||
@ -210,12 +247,9 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
startingBlockHeight, basicGraphFilePath,
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// This test will attempt to find routes from roasbeef to sophon for 100
|
// This test will attempt to find routes from roasbeef to sophon for 100
|
||||||
@ -238,25 +272,21 @@ func TestFindRoutesWithFeeLimit(t *testing.T) {
|
|||||||
target, paymentAmt, restrictions, nil, nil,
|
target, paymentAmt, restrictions, nil, nil,
|
||||||
MinCLTVDelta,
|
MinCLTVDelta,
|
||||||
)
|
)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to find any routes")
|
||||||
t.Fatalf("unable to find any routes: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if route.TotalFees() > restrictions.FeeLimit {
|
require.Falsef(t,
|
||||||
t.Fatalf("route exceeded fee limit: %v", spew.Sdump(route))
|
route.TotalFees() > restrictions.FeeLimit,
|
||||||
}
|
"route exceeded fee limit: %v", spew.Sdump(route),
|
||||||
|
)
|
||||||
|
|
||||||
hops := route.Hops
|
hops := route.Hops
|
||||||
if len(hops) != 2 {
|
require.Equal(t, 2, len(hops), "expected 2 hops")
|
||||||
t.Fatalf("expected 2 hops, got %d", len(hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
if hops[0].PubKeyBytes != ctx.aliases["songoku"] {
|
require.Equalf(t,
|
||||||
|
ctx.aliases["songoku"], hops[0].PubKeyBytes,
|
||||||
t.Fatalf("expected first hop through songoku, got %s",
|
"expected first hop through songoku, got %s",
|
||||||
getAliasFromPubKey(hops[0].PubKeyBytes,
|
getAliasFromPubKey(hops[0].PubKeyBytes, ctx.aliases),
|
||||||
ctx.aliases))
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
|
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
|
||||||
@ -267,10 +297,9 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
@ -287,14 +316,18 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
|
|||||||
var preImage [32]byte
|
var preImage [32]byte
|
||||||
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
||||||
|
|
||||||
|
// Get the channel ID.
|
||||||
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
|
||||||
|
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
|
||||||
|
)
|
||||||
|
|
||||||
// We'll modify the SendToSwitch method that's been set within the
|
// We'll modify the SendToSwitch method that's been set within the
|
||||||
// router's configuration to ignore the path that has son goku as the
|
// router's configuration to ignore the path that has son goku as the
|
||||||
// first hop. This should force the router to instead take the
|
// first hop. This should force the router to instead take the
|
||||||
// the more costly path (through pham nuwen).
|
// the more costly path (through pham nuwen).
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
roasbeefSongoku := lnwire.NewShortChanIDFromInt(12345)
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
return [32]byte{}, htlcswitch.NewForwardingError(
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
// TODO(roasbeef): temp node failure
|
// TODO(roasbeef): temp node failure
|
||||||
@ -310,15 +343,10 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
|
|||||||
// Send off the payment request to the router, route through pham nuwen
|
// Send off the payment request to the router, route through pham nuwen
|
||||||
// should've been selected as a fall back and succeeded correctly.
|
// should've been selected as a fall back and succeeded correctly.
|
||||||
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to send payment")
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The route selected should have two hops
|
// The route selected should have two hops
|
||||||
if len(route.Hops) != 2 {
|
require.Equal(t, 2, len(route.Hops), "incorrect route length")
|
||||||
t.Fatalf("incorrect route length: expected %v got %v", 2,
|
|
||||||
len(route.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The preimage should match up with the once created above.
|
// The preimage should match up with the once created above.
|
||||||
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
|
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
|
||||||
@ -327,13 +355,12 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// The route should have pham nuwen as the first hop.
|
// The route should have pham nuwen as the first hop.
|
||||||
if route.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] {
|
require.Equalf(t,
|
||||||
|
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
|
||||||
t.Fatalf("route should go through phamnuwen as first hop, "+
|
"route should go through phamnuwen as first hop, instead "+
|
||||||
"instead passes through: %v",
|
"passes through: %v",
|
||||||
getAliasFromPubKey(route.Hops[0].PubKeyBytes,
|
getAliasFromPubKey(route.Hops[0].PubKeyBytes, ctx.aliases),
|
||||||
ctx.aliases))
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestChannelUpdateValidation tests that a failed payment with an associated
|
// TestChannelUpdateValidation tests that a failed payment with an associated
|
||||||
@ -344,55 +371,46 @@ func TestChannelUpdateValidation(t *testing.T) {
|
|||||||
|
|
||||||
// Setup a three node network.
|
// Setup a three node network.
|
||||||
chanCapSat := btcutil.Amount(100000)
|
chanCapSat := btcutil.Amount(100000)
|
||||||
|
feeRate := lnwire.MilliSatoshi(400)
|
||||||
testChannels := []*testChannel{
|
testChannels := []*testChannel{
|
||||||
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
|
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
|
||||||
Expiry: 144,
|
Expiry: 144,
|
||||||
FeeRate: 400,
|
FeeRate: feeRate,
|
||||||
MinHTLC: 1,
|
MinHTLC: 1,
|
||||||
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
}, 1),
|
}, 1),
|
||||||
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
||||||
Expiry: 144,
|
Expiry: 144,
|
||||||
FeeRate: 400,
|
FeeRate: feeRate,
|
||||||
MinHTLC: 1,
|
MinHTLC: 1,
|
||||||
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
}, 2),
|
}, 2),
|
||||||
}
|
}
|
||||||
|
|
||||||
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
||||||
|
require.NoError(t, err, "unable to create graph")
|
||||||
defer testGraph.cleanUp()
|
defer testGraph.cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create graph: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
t, startingBlockHeight, testGraph, true,
|
||||||
startingBlockHeight, testGraph, true,
|
|
||||||
)
|
)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Assert that the initially configured fee is retrieved correctly.
|
// Assert that the initially configured fee is retrieved correctly.
|
||||||
_, policy, _, err := ctx.router.GetChannelByID(
|
_, policy, _, err := ctx.router.GetChannelByID(
|
||||||
lnwire.NewShortChanIDFromInt(1))
|
lnwire.NewShortChanIDFromInt(1))
|
||||||
if err != nil {
|
require.NoError(t, err, "cannot retrieve channel")
|
||||||
t.Fatalf("cannot retrieve channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.FeeProportionalMillionths != 400 {
|
require.Equal(t,
|
||||||
t.Fatalf("invalid fee")
|
feeRate, policy.FeeProportionalMillionths, "invalid fee",
|
||||||
}
|
)
|
||||||
|
|
||||||
// Setup a route from source a to destination c. The route will be used
|
// Setup a route from source a to destination c. The route will be used
|
||||||
// in a call to SendToRoute. SendToRoute also applies channel updates,
|
// in a call to SendToRoute. SendToRoute also applies channel updates,
|
||||||
// but it saves us from including RequestRoute in the test scope too.
|
// but it saves us from including RequestRoute in the test scope too.
|
||||||
hop1 := ctx.aliases["b"]
|
hop1 := ctx.aliases["b"]
|
||||||
|
|
||||||
hop2 := ctx.aliases["c"]
|
hop2 := ctx.aliases["c"]
|
||||||
|
|
||||||
hops := []*route.Hop{
|
hops := []*route.Hop{
|
||||||
{
|
{
|
||||||
ChannelID: 1,
|
ChannelID: 1,
|
||||||
@ -410,9 +428,7 @@ func TestChannelUpdateValidation(t *testing.T) {
|
|||||||
lnwire.MilliSatoshi(10000), 100,
|
lnwire.MilliSatoshi(10000), 100,
|
||||||
ctx.aliases["a"], hops,
|
ctx.aliases["a"], hops,
|
||||||
)
|
)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to create route")
|
||||||
t.Fatalf("unable to create route: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set up a channel update message with an invalid signature to be
|
// Set up a channel update message with an invalid signature to be
|
||||||
// returned to the sender.
|
// returned to the sender.
|
||||||
@ -427,7 +443,7 @@ func TestChannelUpdateValidation(t *testing.T) {
|
|||||||
// We'll modify the SendToSwitch method so that it simulates a failed
|
// We'll modify the SendToSwitch method so that it simulates a failed
|
||||||
// payment with an error originating from the first hop of the route.
|
// payment with an error originating from the first hop of the route.
|
||||||
// The unsigned channel update is attached to the failure message.
|
// The unsigned channel update is attached to the failure message.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
return [32]byte{}, htlcswitch.NewForwardingError(
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
&lnwire.FailFeeInsufficient{
|
&lnwire.FailFeeInsufficient{
|
||||||
@ -445,36 +461,19 @@ func TestChannelUpdateValidation(t *testing.T) {
|
|||||||
// should be attempted and the channel update should be received by
|
// should be attempted and the channel update should be received by
|
||||||
// router and ignored because it is missing a valid signature.
|
// router and ignored because it is missing a valid signature.
|
||||||
_, err = ctx.router.SendToRoute(payment, rt)
|
_, err = ctx.router.SendToRoute(payment, rt)
|
||||||
if err == nil {
|
require.Error(t, err, "expected route to fail with channel update")
|
||||||
t.Fatalf("expected route to fail with channel update")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, policy, _, err = ctx.router.GetChannelByID(
|
_, policy, _, err = ctx.router.GetChannelByID(
|
||||||
lnwire.NewShortChanIDFromInt(1))
|
lnwire.NewShortChanIDFromInt(1))
|
||||||
if err != nil {
|
require.NoError(t, err, "cannot retrieve channel")
|
||||||
t.Fatalf("cannot retrieve channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.FeeProportionalMillionths != 400 {
|
require.Equal(t,
|
||||||
t.Fatalf("fee updated without valid signature")
|
feeRate, policy.FeeProportionalMillionths,
|
||||||
}
|
"fee updated without valid signature",
|
||||||
|
)
|
||||||
|
|
||||||
// Next, add a signature to the channel update.
|
// Next, add a signature to the channel update.
|
||||||
chanUpdateMsg, err := errChanUpdate.DataToSign()
|
signErrChanUpdate(t, testGraph.privKeyMap["b"], &errChanUpdate)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
digest := chainhash.DoubleHashB(chanUpdateMsg)
|
|
||||||
sig, err := testGraph.privKeyMap["b"].Sign(digest)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
errChanUpdate.Signature, err = lnwire.NewSigFromSignature(sig)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Retry the payment using the same route as before.
|
// Retry the payment using the same route as before.
|
||||||
_, err = ctx.router.SendToRoute(payment, rt)
|
_, err = ctx.router.SendToRoute(payment, rt)
|
||||||
@ -486,13 +485,12 @@ func TestChannelUpdateValidation(t *testing.T) {
|
|||||||
// have been applied to the graph.
|
// have been applied to the graph.
|
||||||
_, policy, _, err = ctx.router.GetChannelByID(
|
_, policy, _, err = ctx.router.GetChannelByID(
|
||||||
lnwire.NewShortChanIDFromInt(1))
|
lnwire.NewShortChanIDFromInt(1))
|
||||||
if err != nil {
|
require.NoError(t, err, "cannot retrieve channel")
|
||||||
t.Fatalf("cannot retrieve channel")
|
|
||||||
}
|
|
||||||
|
|
||||||
if policy.FeeProportionalMillionths != 500 {
|
require.Equal(t,
|
||||||
t.Fatalf("fee not updated even though signature is valid")
|
lnwire.MilliSatoshi(500), policy.FeeProportionalMillionths,
|
||||||
}
|
"fee not updated even though signature is valid",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSendPaymentErrorRepeatedFeeInsufficient tests that if we receive
|
// TestSendPaymentErrorRepeatedFeeInsufficient tests that if we receive
|
||||||
@ -502,14 +500,21 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
|
// Get the channel ID.
|
||||||
|
roasbeefSongokuChanID := ctx.getChannelIDFromAlias(
|
||||||
|
t, "roasbeef", "songoku",
|
||||||
|
)
|
||||||
|
songokuSophonChanID := ctx.getChannelIDFromAlias(
|
||||||
|
t, "songoku", "sophon",
|
||||||
|
)
|
||||||
|
|
||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
// to luo ji for 100 satoshis.
|
// to sophon for 1000 satoshis.
|
||||||
var payHash lntypes.Hash
|
var payHash lntypes.Hash
|
||||||
amt := lnwire.NewMSatFromSatoshis(1000)
|
amt := lnwire.NewMSatFromSatoshis(1000)
|
||||||
payment := LightningPayment{
|
payment := LightningPayment{
|
||||||
@ -522,17 +527,18 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||||||
var preImage [32]byte
|
var preImage [32]byte
|
||||||
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
||||||
|
|
||||||
// We'll also fetch the first outgoing channel edge from roasbeef to
|
// We'll also fetch the first outgoing channel edge from son goku
|
||||||
// son goku. We'll obtain this as we'll need to to generate the
|
// to sophon. We'll obtain this as we'll need to to generate the
|
||||||
// FeeInsufficient error that we'll send back.
|
// FeeInsufficient error that we'll send back.
|
||||||
chanID := uint64(12345)
|
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(
|
||||||
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID)
|
songokuSophonChanID,
|
||||||
if err != nil {
|
)
|
||||||
t.Fatalf("unable to fetch chan id: %v", err)
|
require.NoError(t, err, "unable to fetch chan id")
|
||||||
}
|
|
||||||
|
|
||||||
errChanUpdate := lnwire.ChannelUpdate{
|
errChanUpdate := lnwire.ChannelUpdate{
|
||||||
ShortChannelID: lnwire.NewShortChanIDFromInt(chanID),
|
ShortChannelID: lnwire.NewShortChanIDFromInt(
|
||||||
|
songokuSophonChanID,
|
||||||
|
),
|
||||||
Timestamp: uint32(edgeUpdateToFail.LastUpdate.Unix()),
|
Timestamp: uint32(edgeUpdateToFail.LastUpdate.Unix()),
|
||||||
MessageFlags: edgeUpdateToFail.MessageFlags,
|
MessageFlags: edgeUpdateToFail.MessageFlags,
|
||||||
ChannelFlags: edgeUpdateToFail.ChannelFlags,
|
ChannelFlags: edgeUpdateToFail.ChannelFlags,
|
||||||
@ -543,13 +549,17 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||||||
FeeRate: uint32(edgeUpdateToFail.FeeProportionalMillionths),
|
FeeRate: uint32(edgeUpdateToFail.FeeProportionalMillionths),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)
|
||||||
|
|
||||||
// We'll now modify the SendToSwitch method to return an error for the
|
// We'll now modify the SendToSwitch method to return an error for the
|
||||||
// outgoing channel to Son goku. This will be a fee related error, so
|
// outgoing channel to Son goku. This will be a fee related error, so
|
||||||
// it should only cause the edge to be pruned after the second attempt.
|
// it should only cause the edge to be pruned after the second attempt.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
|
||||||
|
roasbeefSongokuChanID,
|
||||||
|
)
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
return [32]byte{}, htlcswitch.NewForwardingError(
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
// Within our error, we'll add a
|
// Within our error, we'll add a
|
||||||
@ -565,33 +575,285 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
|
|||||||
return preImage, nil
|
return preImage, nil
|
||||||
})
|
})
|
||||||
|
|
||||||
// Send off the payment request to the router, route through satoshi
|
// Send off the payment request to the router, route through phamnuwen
|
||||||
// should've been selected as a fall back and succeeded correctly.
|
// should've been selected as a fall back and succeeded correctly.
|
||||||
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to send payment")
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// The route selected should have two hops
|
// The route selected should have two hops
|
||||||
if len(route.Hops) != 2 {
|
require.Equal(t, 2, len(route.Hops), "incorrect route length")
|
||||||
t.Fatalf("incorrect route length: expected %v got %v", 2,
|
|
||||||
len(route.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The preimage should match up with the once created above.
|
// The preimage should match up with the once created above.
|
||||||
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
|
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
|
||||||
t.Fatalf("incorrect preimage used: expected %x got %x",
|
|
||||||
preImage[:], paymentPreImage[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// The route should have pham nuwen as the first hop.
|
// The route should have pham nuwen as the first hop.
|
||||||
if route.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] {
|
require.Equalf(t,
|
||||||
|
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
|
||||||
t.Fatalf("route should go through satoshi as first hop, "+
|
"route should go through pham nuwen as first hop, "+
|
||||||
"instead passes through: %v",
|
"instead passes through: %v",
|
||||||
getAliasFromPubKey(route.Hops[0].PubKeyBytes,
|
getAliasFromPubKey(route.Hops[0].PubKeyBytes, ctx.aliases),
|
||||||
ctx.aliases))
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendPaymentErrorFeeInsufficientPrivateEdge tests that if we receive
|
||||||
|
// a fee related error from a private channel that we're attempting to route
|
||||||
|
// through, then we'll update the fees in the route hints and successfully
|
||||||
|
// route through the private channel in the second attempt.
|
||||||
|
//
|
||||||
|
// The test will send a payment from roasbeef to elst, available paths are,
|
||||||
|
// path1: roasbeef -> songoku -> sophon -> elst, total fee: 210k
|
||||||
|
// path2: roasbeef -> phamnuwen -> sophon -> elst, total fee: 220k
|
||||||
|
// path3: roasbeef -> songoku ->(private channel) elst
|
||||||
|
// We will setup the path3 to have the lowest fee so it's always the preferred
|
||||||
|
// path.
|
||||||
|
func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
|
)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
// Get the channel ID.
|
||||||
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
|
||||||
|
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
payHash lntypes.Hash
|
||||||
|
preImage [32]byte
|
||||||
|
amt = lnwire.NewMSatFromSatoshis(1000)
|
||||||
|
privateChannelID = uint64(55555)
|
||||||
|
feeBaseMSat = uint32(15)
|
||||||
|
expiryDelta = uint16(32)
|
||||||
|
sgNode = ctx.aliases["songoku"]
|
||||||
|
)
|
||||||
|
|
||||||
|
sgNodeID, err := btcec.ParsePubKey(sgNode[:], btcec.S256())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
|
// to elst, through a private channel between songoku and elst for
|
||||||
|
// 1000 satoshis. This route has lowest fees compared with the rest.
|
||||||
|
// This also holds when the private channel fee is updated to a higher
|
||||||
|
// value.
|
||||||
|
payment := LightningPayment{
|
||||||
|
Target: ctx.aliases["elst"],
|
||||||
|
Amount: amt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
|
paymentHash: &payHash,
|
||||||
|
RouteHints: [][]zpay32.HopHint{{
|
||||||
|
// Add a private channel between songoku and elst.
|
||||||
|
zpay32.HopHint{
|
||||||
|
NodeID: sgNodeID,
|
||||||
|
ChannelID: privateChannelID,
|
||||||
|
FeeBaseMSat: feeBaseMSat,
|
||||||
|
CLTVExpiryDelta: expiryDelta,
|
||||||
|
},
|
||||||
|
}},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Prepare an error update for the private channel, with twice the
|
||||||
|
// original fee.
|
||||||
|
updatedFeeBaseMSat := feeBaseMSat * 2
|
||||||
|
errChanUpdate := lnwire.ChannelUpdate{
|
||||||
|
ShortChannelID: lnwire.NewShortChanIDFromInt(privateChannelID),
|
||||||
|
Timestamp: uint32(testTime.Add(time.Minute).Unix()),
|
||||||
|
BaseFee: updatedFeeBaseMSat,
|
||||||
|
TimeLockDelta: expiryDelta,
|
||||||
|
}
|
||||||
|
signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)
|
||||||
|
|
||||||
|
// We'll now modify the SendHTLC method to return an error for the
|
||||||
|
// outgoing channel to songoku.
|
||||||
|
errorReturned := false
|
||||||
|
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
||||||
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
|
if firstHop != roasbeefSongoku || errorReturned {
|
||||||
|
return preImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errorReturned = true
|
||||||
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
|
// Within our error, we'll add a
|
||||||
|
// channel update which is meant to
|
||||||
|
// reflect the new fee schedule for the
|
||||||
|
// node/channel.
|
||||||
|
&lnwire.FailFeeInsufficient{
|
||||||
|
Update: errChanUpdate,
|
||||||
|
}, 1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send off the payment request to the router, route through son
|
||||||
|
// goku and then across the private channel to elst.
|
||||||
|
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
||||||
|
require.NoError(t, err, "unable to send payment")
|
||||||
|
|
||||||
|
require.True(t, errorReturned,
|
||||||
|
"failed to simulate error in the first payment attempt",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route selected should have two hops. Make sure that,
|
||||||
|
// path: roasbeef -> son goku -> sophon -> elst
|
||||||
|
// path: roasbeef -> pham nuwen -> sophon -> elst
|
||||||
|
// are not selected instead.
|
||||||
|
require.Equal(t, 2, len(route.Hops), "incorrect route length")
|
||||||
|
|
||||||
|
// The preimage should match up with the one created above.
|
||||||
|
require.Equal(t,
|
||||||
|
paymentPreImage[:], preImage[:], "incorrect preimage used",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route should have son goku as the first hop.
|
||||||
|
require.Equal(t, route.Hops[0].PubKeyBytes, ctx.aliases["songoku"],
|
||||||
|
"route should go through son goku as first hop",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route should pass via the private channel.
|
||||||
|
require.Equal(t,
|
||||||
|
privateChannelID, route.FinalHop().ChannelID,
|
||||||
|
"route did not pass through private channel "+
|
||||||
|
"between pham nuwen and elst",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route should have the updated fee.
|
||||||
|
require.Equal(t,
|
||||||
|
lnwire.MilliSatoshi(updatedFeeBaseMSat).String(),
|
||||||
|
route.HopFee(0).String(),
|
||||||
|
"fee to forward to the private channel not matched",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit tests that upon receiving a
|
||||||
|
// ChannelUpdate in a fee related error from the private channel, we won't
|
||||||
|
// choose the route in our second attempt if the updated fee exceeds our fee
|
||||||
|
// limit specified in the payment.
|
||||||
|
//
|
||||||
|
// The test will send a payment from roasbeef to elst, available paths are,
|
||||||
|
// path1: roasbeef -> songoku -> sophon -> elst, total fee: 210k
|
||||||
|
// path2: roasbeef -> phamnuwen -> sophon -> elst, total fee: 220k
|
||||||
|
// path3: roasbeef -> songoku ->(private channel) elst
|
||||||
|
// We will setup the path3 to have the lowest fee and then update it with a fee
|
||||||
|
// exceeds our fee limit, thus this route won't be chosen.
|
||||||
|
func TestSendPaymentPrivateEdgeUpdateFeeExceedsLimit(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
|
)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
// Get the channel ID.
|
||||||
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
|
||||||
|
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
payHash lntypes.Hash
|
||||||
|
preImage [32]byte
|
||||||
|
amt = lnwire.NewMSatFromSatoshis(1000)
|
||||||
|
privateChannelID = uint64(55555)
|
||||||
|
feeBaseMSat = uint32(15)
|
||||||
|
expiryDelta = uint16(32)
|
||||||
|
sgNode = ctx.aliases["songoku"]
|
||||||
|
feeLimit = lnwire.MilliSatoshi(500000)
|
||||||
|
)
|
||||||
|
|
||||||
|
sgNodeID, err := btcec.ParsePubKey(sgNode[:], btcec.S256())
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
|
// to elst, through a private channel between songoku and elst for
|
||||||
|
// 1000 satoshis. This route has lowest fees compared with the rest.
|
||||||
|
payment := LightningPayment{
|
||||||
|
Target: ctx.aliases["elst"],
|
||||||
|
Amount: amt,
|
||||||
|
FeeLimit: feeLimit,
|
||||||
|
paymentHash: &payHash,
|
||||||
|
RouteHints: [][]zpay32.HopHint{{
|
||||||
|
// Add a private channel between songoku and elst.
|
||||||
|
zpay32.HopHint{
|
||||||
|
NodeID: sgNodeID,
|
||||||
|
ChannelID: privateChannelID,
|
||||||
|
FeeBaseMSat: feeBaseMSat,
|
||||||
|
CLTVExpiryDelta: expiryDelta,
|
||||||
|
},
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Prepare an error update for the private channel. The updated fee
|
||||||
|
// will exceeds the feeLimit.
|
||||||
|
updatedFeeBaseMSat := feeBaseMSat + uint32(feeLimit)
|
||||||
|
errChanUpdate := lnwire.ChannelUpdate{
|
||||||
|
ShortChannelID: lnwire.NewShortChanIDFromInt(privateChannelID),
|
||||||
|
Timestamp: uint32(testTime.Add(time.Minute).Unix()),
|
||||||
|
BaseFee: updatedFeeBaseMSat,
|
||||||
|
TimeLockDelta: expiryDelta,
|
||||||
|
}
|
||||||
|
signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)
|
||||||
|
|
||||||
|
// We'll now modify the SendHTLC method to return an error for the
|
||||||
|
// outgoing channel to songoku.
|
||||||
|
errorReturned := false
|
||||||
|
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
||||||
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
|
if firstHop != roasbeefSongoku || errorReturned {
|
||||||
|
return preImage, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
errorReturned = true
|
||||||
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
|
// Within our error, we'll add a
|
||||||
|
// channel update which is meant to
|
||||||
|
// reflect the new fee schedule for the
|
||||||
|
// node/channel.
|
||||||
|
&lnwire.FailFeeInsufficient{
|
||||||
|
Update: errChanUpdate,
|
||||||
|
}, 1,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Send off the payment request to the router, route through son
|
||||||
|
// goku and then across the private channel to elst.
|
||||||
|
paymentPreImage, route, err := ctx.router.SendPayment(&payment)
|
||||||
|
require.NoError(t, err, "unable to send payment")
|
||||||
|
|
||||||
|
require.True(t, errorReturned,
|
||||||
|
"failed to simulate error in the first payment attempt",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route selected should have three hops. Make sure that,
|
||||||
|
// path1: roasbeef -> son goku -> sophon -> elst
|
||||||
|
// path2: roasbeef -> pham nuwen -> sophon -> elst
|
||||||
|
// path3: roasbeef -> sophon -> (private channel) else
|
||||||
|
// path1 is selected.
|
||||||
|
require.Equal(t, 3, len(route.Hops), "incorrect route length")
|
||||||
|
|
||||||
|
// The preimage should match up with the one created above.
|
||||||
|
require.Equal(t,
|
||||||
|
paymentPreImage[:], preImage[:], "incorrect preimage used",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route should have son goku as the first hop.
|
||||||
|
require.Equal(t, route.Hops[0].PubKeyBytes, ctx.aliases["songoku"],
|
||||||
|
"route should go through son goku as the first hop",
|
||||||
|
)
|
||||||
|
|
||||||
|
// The route should have sophon as the first hop.
|
||||||
|
require.Equal(t, route.Hops[1].PubKeyBytes, ctx.aliases["sophon"],
|
||||||
|
"route should go through sophon as the second hop",
|
||||||
|
)
|
||||||
|
// The route should pass via the public channel.
|
||||||
|
require.Equal(t, route.FinalHop().PubKeyBytes, ctx.aliases["elst"],
|
||||||
|
"route should go through elst as the final hop",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSendPaymentErrorNonFinalTimeLockErrors tests that if we receive either
|
// TestSendPaymentErrorNonFinalTimeLockErrors tests that if we receive either
|
||||||
@ -603,10 +865,9 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
@ -627,12 +888,11 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
// son goku. This edge will be included in the time lock related expiry
|
// son goku. This edge will be included in the time lock related expiry
|
||||||
// errors that we'll get back due to disagrements in what the current
|
// errors that we'll get back due to disagrements in what the current
|
||||||
// block height is.
|
// block height is.
|
||||||
chanID := uint64(12345)
|
chanID := ctx.getChannelIDFromAlias(t, "roasbeef", "songoku")
|
||||||
roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)
|
||||||
|
|
||||||
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID)
|
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to fetch chan id")
|
||||||
t.Fatalf("unable to fetch chan id: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
errChanUpdate := lnwire.ChannelUpdate{
|
errChanUpdate := lnwire.ChannelUpdate{
|
||||||
ShortChannelID: lnwire.NewShortChanIDFromInt(chanID),
|
ShortChannelID: lnwire.NewShortChanIDFromInt(chanID),
|
||||||
@ -650,7 +910,7 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
// outgoing channel to son goku. Since this is a time lock related
|
// outgoing channel to son goku. Since this is a time lock related
|
||||||
// error, we should fail the payment flow all together, as Goku is the
|
// error, we should fail the payment flow all together, as Goku is the
|
||||||
// only channel to Sophon.
|
// only channel to Sophon.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
@ -669,41 +929,36 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
// graph.
|
// graph.
|
||||||
assertExpectedPath := func(retPreImage [32]byte, route *route.Route) {
|
assertExpectedPath := func(retPreImage [32]byte, route *route.Route) {
|
||||||
// The route selected should have two hops
|
// The route selected should have two hops
|
||||||
if len(route.Hops) != 2 {
|
require.Equal(t, 2, len(route.Hops), "incorrect route length")
|
||||||
t.Fatalf("incorrect route length: expected %v got %v", 2,
|
|
||||||
len(route.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The preimage should match up with the once created above.
|
// The preimage should match up with the once created above.
|
||||||
if !bytes.Equal(retPreImage[:], preImage[:]) {
|
require.Equal(t,
|
||||||
t.Fatalf("incorrect preimage used: expected %x got %x",
|
preImage[:], retPreImage[:], "incorrect preimage used",
|
||||||
preImage[:], retPreImage[:])
|
)
|
||||||
}
|
|
||||||
|
|
||||||
// The route should have satoshi as the first hop.
|
// The route should have satoshi as the first hop.
|
||||||
if route.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] {
|
require.Equalf(t,
|
||||||
|
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
|
||||||
t.Fatalf("route should go through phamnuwen as first hop, "+
|
"route should go through phamnuwen as first hop, "+
|
||||||
"instead passes through: %v",
|
"instead passes through: %v",
|
||||||
getAliasFromPubKey(route.Hops[0].PubKeyBytes,
|
getAliasFromPubKey(
|
||||||
ctx.aliases))
|
route.Hops[0].PubKeyBytes, ctx.aliases,
|
||||||
}
|
),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send off the payment request to the router, this payment should
|
// Send off the payment request to the router, this payment should
|
||||||
// succeed as we should actually go through Pham Nuwen in order to get
|
// succeed as we should actually go through Pham Nuwen in order to get
|
||||||
// to Sophon, even though he has higher fees.
|
// to Sophon, even though he has higher fees.
|
||||||
paymentPreImage, rt, err := ctx.router.SendPayment(&payment)
|
paymentPreImage, rt, err := ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to send payment")
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertExpectedPath(paymentPreImage, rt)
|
assertExpectedPath(paymentPreImage, rt)
|
||||||
|
|
||||||
// We'll now modify the error return an IncorrectCltvExpiry error
|
// We'll now modify the error return an IncorrectCltvExpiry error
|
||||||
// instead, this should result in the same behavior of roasbeef routing
|
// instead, this should result in the same behavior of roasbeef routing
|
||||||
// around the faulty Son Goku node.
|
// around the faulty Son Goku node.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
@ -722,9 +977,7 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
|
|||||||
// flip a bit in the payment hash to allow resending this payment.
|
// flip a bit in the payment hash to allow resending this payment.
|
||||||
payment.paymentHash[1] ^= 1
|
payment.paymentHash[1] ^= 1
|
||||||
paymentPreImage, rt, err = ctx.router.SendPayment(&payment)
|
paymentPreImage, rt, err = ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable to send payment")
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
assertExpectedPath(paymentPreImage, rt)
|
assertExpectedPath(paymentPreImage, rt)
|
||||||
}
|
}
|
||||||
@ -736,10 +989,9 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
// Craft a LightningPayment struct that'll send a payment from roasbeef
|
||||||
@ -756,13 +1008,17 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
var preImage [32]byte
|
var preImage [32]byte
|
||||||
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
|
||||||
|
|
||||||
roasbeefSongoku := lnwire.NewShortChanIDFromInt(12345)
|
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
|
||||||
roasbeefPhanNuwen := lnwire.NewShortChanIDFromInt(999991)
|
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
|
||||||
|
)
|
||||||
|
roasbeefPhanNuwen := lnwire.NewShortChanIDFromInt(
|
||||||
|
ctx.getChannelIDFromAlias(t, "roasbeef", "phamnuwen"),
|
||||||
|
)
|
||||||
|
|
||||||
// First, we'll modify the SendToSwitch method to return an error
|
// First, we'll modify the SendToSwitch method to return an error
|
||||||
// indicating that the channel from roasbeef to son goku is not operable
|
// indicating that the channel from roasbeef to son goku is not operable
|
||||||
// with an UnknownNextPeer.
|
// with an UnknownNextPeer.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
@ -791,44 +1047,35 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
|
|
||||||
// When we try to dispatch that payment, we should receive an error as
|
// When we try to dispatch that payment, we should receive an error as
|
||||||
// both attempts should fail and cause both routes to be pruned.
|
// both attempts should fail and cause both routes to be pruned.
|
||||||
_, _, err = ctx.router.SendPayment(&payment)
|
_, _, err := ctx.router.SendPayment(&payment)
|
||||||
if err == nil {
|
require.Error(t, err, "payment didn't return error")
|
||||||
t.Fatalf("payment didn't return error")
|
|
||||||
}
|
|
||||||
|
|
||||||
// The final error returned should also indicate that the peer wasn't
|
// The final error returned should also indicate that the peer wasn't
|
||||||
// online (the last error we returned).
|
// online (the last error we returned).
|
||||||
if err != channeldb.FailureReasonNoRoute {
|
require.Equal(t, channeldb.FailureReasonNoRoute, err)
|
||||||
t.Fatalf("expected no route instead got: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Inspect the two attempts that were made before the payment failed.
|
// Inspect the two attempts that were made before the payment failed.
|
||||||
p, err := ctx.router.cfg.Control.FetchPayment(payHash)
|
p, err := ctx.router.cfg.Control.FetchPayment(payHash)
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(p.HTLCs) != 2 {
|
require.Equal(t, 2, len(p.HTLCs), "expected two attempts")
|
||||||
t.Fatalf("expected two attempts got %v", len(p.HTLCs))
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect the first attempt to have failed with a
|
// We expect the first attempt to have failed with a
|
||||||
// TemporaryChannelFailure, the second with UnknownNextPeer.
|
// TemporaryChannelFailure, the second with UnknownNextPeer.
|
||||||
msg := p.HTLCs[0].Failure.Message
|
msg := p.HTLCs[0].Failure.Message
|
||||||
if _, ok := msg.(*lnwire.FailTemporaryChannelFailure); !ok {
|
_, ok := msg.(*lnwire.FailTemporaryChannelFailure)
|
||||||
t.Fatalf("unexpected fail message: %T", msg)
|
require.True(t, ok, "unexpected fail message")
|
||||||
}
|
|
||||||
|
|
||||||
msg = p.HTLCs[1].Failure.Message
|
msg = p.HTLCs[1].Failure.Message
|
||||||
if _, ok := msg.(*lnwire.FailUnknownNextPeer); !ok {
|
_, ok = msg.(*lnwire.FailUnknownNextPeer)
|
||||||
t.Fatalf("unexpected fail message: %T", msg)
|
require.True(t, ok, "unexpected fail message")
|
||||||
}
|
|
||||||
|
|
||||||
ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
|
err = ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
|
||||||
|
require.NoError(t, err, "reset history failed")
|
||||||
|
|
||||||
// Next, we'll modify the SendToSwitch method to indicate that the
|
// Next, we'll modify the SendToSwitch method to indicate that the
|
||||||
// connection between songoku and isn't up.
|
// connection between songoku and isn't up.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
@ -845,33 +1092,24 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
// the pham nuwen channel based on the assumption that there might be an
|
// the pham nuwen channel based on the assumption that there might be an
|
||||||
// intermittent issue with the songoku <-> sophon channel.
|
// intermittent issue with the songoku <-> sophon channel.
|
||||||
paymentPreImage, rt, err := ctx.router.SendPayment(&payment)
|
paymentPreImage, rt, err := ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable send payment")
|
||||||
t.Fatalf("unable send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This path should go: roasbeef -> pham nuwen -> sophon
|
// This path should go: roasbeef -> pham nuwen -> sophon
|
||||||
if len(rt.Hops) != 2 {
|
require.Equal(t, 2, len(rt.Hops), "incorrect route length")
|
||||||
t.Fatalf("incorrect route length: expected %v got %v", 2,
|
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
|
||||||
len(rt.Hops))
|
require.Equalf(t,
|
||||||
}
|
ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
|
||||||
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
|
"route should go through phamnuwen as first hop, "+
|
||||||
t.Fatalf("incorrect preimage used: expected %x got %x",
|
|
||||||
preImage[:], paymentPreImage[:])
|
|
||||||
}
|
|
||||||
if rt.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] {
|
|
||||||
|
|
||||||
t.Fatalf("route should go through phamnuwen as first hop, "+
|
|
||||||
"instead passes through: %v",
|
"instead passes through: %v",
|
||||||
getAliasFromPubKey(rt.Hops[0].PubKeyBytes,
|
getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
|
||||||
ctx.aliases))
|
)
|
||||||
}
|
|
||||||
|
|
||||||
ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
|
ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
|
||||||
|
|
||||||
// Finally, we'll modify the SendToSwitch function to indicate that the
|
// Finally, we'll modify the SendToSwitch function to indicate that the
|
||||||
// roasbeef -> luoji channel has insufficient capacity. This should
|
// roasbeef -> luoji channel has insufficient capacity. This should
|
||||||
// again cause us to instead go via the satoshi route.
|
// again cause us to instead go via the satoshi route.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
if firstHop == roasbeefSongoku {
|
if firstHop == roasbeefSongoku {
|
||||||
@ -889,31 +1127,22 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
// We flip a bit in the payment hash to allow resending this payment.
|
// We flip a bit in the payment hash to allow resending this payment.
|
||||||
payment.paymentHash[1] ^= 1
|
payment.paymentHash[1] ^= 1
|
||||||
paymentPreImage, rt, err = ctx.router.SendPayment(&payment)
|
paymentPreImage, rt, err = ctx.router.SendPayment(&payment)
|
||||||
if err != nil {
|
require.NoError(t, err, "unable send payment")
|
||||||
t.Fatalf("unable to send payment: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// This should succeed finally. The route selected should have two
|
// This should succeed finally. The route selected should have two
|
||||||
// hops.
|
// hops.
|
||||||
if len(rt.Hops) != 2 {
|
require.Equal(t, 2, len(rt.Hops), "incorrect route length")
|
||||||
t.Fatalf("incorrect route length: expected %v got %v", 2,
|
|
||||||
len(rt.Hops))
|
|
||||||
}
|
|
||||||
|
|
||||||
// The preimage should match up with the once created above.
|
// The preimage should match up with the once created above.
|
||||||
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
|
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
|
||||||
t.Fatalf("incorrect preimage used: expected %x got %x",
|
|
||||||
preImage[:], paymentPreImage[:])
|
|
||||||
}
|
|
||||||
|
|
||||||
// The route should have satoshi as the first hop.
|
// The route should have satoshi as the first hop.
|
||||||
if rt.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] {
|
require.Equalf(t,
|
||||||
|
ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
|
||||||
t.Fatalf("route should go through phamnuwen as first hop, "+
|
"route should go through phamnuwen as first hop, "+
|
||||||
"instead passes through: %v",
|
"instead passes through: %v",
|
||||||
getAliasFromPubKey(rt.Hops[0].PubKeyBytes,
|
getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
|
||||||
ctx.aliases))
|
)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAddProof checks that we can update the channel proof after channel
|
// TestAddProof checks that we can update the channel proof after channel
|
||||||
@ -921,10 +1150,7 @@ func TestSendPaymentErrorPathPruning(t *testing.T) {
|
|||||||
func TestAddProof(t *testing.T) {
|
func TestAddProof(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cleanup, err := createTestCtxSingleNode(0)
|
ctx, cleanup := createTestCtxSingleNode(t, 0)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// Before creating out edge, we'll create two new nodes within the
|
// Before creating out edge, we'll create two new nodes within the
|
||||||
@ -987,11 +1213,9 @@ func TestIgnoreNodeAnnouncement(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight,
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
basicGraphFilePath)
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
if err != nil {
|
)
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
pub := priv1.PubKey()
|
pub := priv1.PubKey()
|
||||||
@ -1006,7 +1230,7 @@ func TestIgnoreNodeAnnouncement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
copy(node.PubKeyBytes[:], pub.SerializeCompressed())
|
copy(node.PubKeyBytes[:], pub.SerializeCompressed())
|
||||||
|
|
||||||
err = ctx.router.AddNode(node)
|
err := ctx.router.AddNode(node)
|
||||||
if !IsError(err, ErrIgnored) {
|
if !IsError(err, ErrIgnored) {
|
||||||
t.Fatalf("expected to get ErrIgnore, instead got: %v", err)
|
t.Fatalf("expected to get ErrIgnore, instead got: %v", err)
|
||||||
}
|
}
|
||||||
@ -1029,12 +1253,9 @@ func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) {
|
|||||||
}
|
}
|
||||||
defer testGraph.cleanUp()
|
defer testGraph.cleanUp()
|
||||||
|
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
startingBlockHeight, testGraph, false,
|
t, startingBlockHeight, testGraph, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
var pub1 [33]byte
|
var pub1 [33]byte
|
||||||
@ -1102,12 +1323,9 @@ func TestAddEdgeUnknownVertexes(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
startingBlockHeight, basicGraphFilePath,
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
var pub1 [33]byte
|
var pub1 [33]byte
|
||||||
@ -1373,10 +1591,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
const chanValue = 10000
|
const chanValue = 10000
|
||||||
@ -1539,7 +1754,7 @@ func TestWakeUpOnStaleBranch(t *testing.T) {
|
|||||||
Graph: ctx.graph,
|
Graph: ctx.graph,
|
||||||
Chain: ctx.chain,
|
Chain: ctx.chain,
|
||||||
ChainView: ctx.chainView,
|
ChainView: ctx.chainView,
|
||||||
Payer: &mockPaymentAttemptDispatcher{},
|
Payer: &mockPaymentAttemptDispatcherOld{},
|
||||||
Control: makeMockControlTower(),
|
Control: makeMockControlTower(),
|
||||||
ChannelPruneExpiry: time.Hour * 24,
|
ChannelPruneExpiry: time.Hour * 24,
|
||||||
GraphPruneInterval: time.Hour * 2,
|
GraphPruneInterval: time.Hour * 2,
|
||||||
@ -1588,10 +1803,7 @@ func TestDisconnectedBlocks(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
const chanValue = 10000
|
const chanValue = 10000
|
||||||
@ -1789,10 +2001,7 @@ func TestRouterChansClosedOfflinePruneGraph(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
const chanValue = 10000
|
const chanValue = 10000
|
||||||
@ -1924,7 +2133,7 @@ func TestRouterChansClosedOfflinePruneGraph(t *testing.T) {
|
|||||||
// Now we'll re-start the ChannelRouter. It should recognize that it's
|
// Now we'll re-start the ChannelRouter. It should recognize that it's
|
||||||
// behind the main chain and prune all the blocks that it missed while
|
// behind the main chain and prune all the blocks that it missed while
|
||||||
// it was down.
|
// it was down.
|
||||||
ctx.RestartRouter()
|
ctx.RestartRouter(t)
|
||||||
|
|
||||||
// At this point, the channel that was pruned should no longer be known
|
// At this point, the channel that was pruned should no longer be known
|
||||||
// by the router.
|
// by the router.
|
||||||
@ -2040,12 +2249,9 @@ func TestPruneChannelGraphStaleEdges(t *testing.T) {
|
|||||||
defer testGraph.cleanUp()
|
defer testGraph.cleanUp()
|
||||||
|
|
||||||
const startingHeight = 100
|
const startingHeight = 100
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
startingHeight, testGraph, strictPruning,
|
t, startingHeight, testGraph, strictPruning,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create test context: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// All of the channels should exist before pruning them.
|
// All of the channels should exist before pruning them.
|
||||||
@ -2173,12 +2379,9 @@ func testPruneChannelGraphDoubleDisabled(t *testing.T, assumeValid bool) {
|
|||||||
defer testGraph.cleanUp()
|
defer testGraph.cleanUp()
|
||||||
|
|
||||||
const startingHeight = 100
|
const startingHeight = 100
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstanceAssumeValid(
|
ctx, cleanUp := createTestCtxFromGraphInstanceAssumeValid(
|
||||||
startingHeight, testGraph, assumeValid, false,
|
t, startingHeight, testGraph, assumeValid, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create test context: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// All the channels should exist within the graph before pruning them
|
// All the channels should exist within the graph before pruning them
|
||||||
@ -2217,10 +2420,9 @@ func TestFindPathFeeWeighting(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath)
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
if err != nil {
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
t.Fatalf("unable to create router: %v", err)
|
)
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
var preImage [32]byte
|
var preImage [32]byte
|
||||||
@ -2264,10 +2466,7 @@ func TestIsStaleNode(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Before we can insert a node in to the database, we need to create a
|
// Before we can insert a node in to the database, we need to create a
|
||||||
@ -2346,10 +2545,7 @@ func TestIsKnownEdge(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxSingleNode(startingBlockHeight)
|
ctx, cleanUp := createTestCtxSingleNode(t, startingBlockHeight)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// First, we'll create a new channel edge (just the info) and insert it
|
// First, we'll create a new channel edge (just the info) and insert it
|
||||||
@ -2398,11 +2594,9 @@ func TestIsStaleEdgePolicy(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight,
|
ctx, cleanUp := createTestCtxFromFile(
|
||||||
basicGraphFilePath)
|
t, startingBlockHeight, basicGraphFilePath,
|
||||||
if err != nil {
|
)
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// First, we'll create a new channel edge (just the info) and insert it
|
// First, we'll create a new channel edge (just the info) and insert it
|
||||||
@ -2555,14 +2749,10 @@ func TestUnknownErrorSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
t, startingBlockHeight, testGraph, false,
|
||||||
startingBlockHeight, testGraph, false,
|
|
||||||
)
|
)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a payment to node c.
|
// Create a payment to node c.
|
||||||
var payHash lntypes.Hash
|
var payHash lntypes.Hash
|
||||||
@ -2576,7 +2766,7 @@ func TestUnknownErrorSource(t *testing.T) {
|
|||||||
// We'll modify the SendToSwitch method so that it simulates hop b as a
|
// We'll modify the SendToSwitch method so that it simulates hop b as a
|
||||||
// node that returns an unparsable failure if approached via the a->b
|
// node that returns an unparsable failure if approached via the a->b
|
||||||
// channel.
|
// channel.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
// If channel a->b is used, return an error without
|
// If channel a->b is used, return an error without
|
||||||
@ -2601,7 +2791,7 @@ func TestUnknownErrorSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Next we modify payment result to return an unknown failure.
|
// Next we modify payment result to return an unknown failure.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
// If channel a->b is used, simulate that the failure
|
// If channel a->b is used, simulate that the failure
|
||||||
@ -2695,19 +2885,15 @@ func TestSendToRouteStructuredError(t *testing.T) {
|
|||||||
defer testGraph.cleanUp()
|
defer testGraph.cleanUp()
|
||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
t, startingBlockHeight, testGraph, false,
|
||||||
startingBlockHeight, testGraph, false,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Set up an init channel for the control tower, such that we can make
|
// Set up an init channel for the control tower, such that we can make
|
||||||
// sure the payment is initiated correctly.
|
// sure the payment is initiated correctly.
|
||||||
init := make(chan initArgs, 1)
|
init := make(chan initArgs, 1)
|
||||||
ctx.router.cfg.Control.(*mockControlTower).init = init
|
ctx.router.cfg.Control.(*mockControlTowerOld).init = init
|
||||||
|
|
||||||
// Setup a route from source a to destination c. The route will be used
|
// Setup a route from source a to destination c. The route will be used
|
||||||
// in a call to SendToRoute. SendToRoute also applies channel updates,
|
// in a call to SendToRoute. SendToRoute also applies channel updates,
|
||||||
@ -2738,7 +2924,7 @@ func TestSendToRouteStructuredError(t *testing.T) {
|
|||||||
// We'll modify the SendToSwitch method so that it simulates a failed
|
// We'll modify the SendToSwitch method so that it simulates a failed
|
||||||
// payment with an error originating from the first hop of the route.
|
// payment with an error originating from the first hop of the route.
|
||||||
// The unsigned channel update is attached to the failure message.
|
// The unsigned channel update is attached to the failure message.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
return [32]byte{}, htlcswitch.NewForwardingError(
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
&lnwire.FailFeeInsufficient{
|
&lnwire.FailFeeInsufficient{
|
||||||
@ -2781,10 +2967,7 @@ func TestSendToRouteStructuredError(t *testing.T) {
|
|||||||
func TestSendToRouteMultiShardSend(t *testing.T) {
|
func TestSendToRouteMultiShardSend(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cleanup, err := createTestCtxSingleNode(0)
|
ctx, cleanup := createTestCtxSingleNode(t, 0)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
const numShards = 3
|
const numShards = 3
|
||||||
@ -2818,7 +3001,7 @@ func TestSendToRouteMultiShardSend(t *testing.T) {
|
|||||||
|
|
||||||
// The first shard we send we'll fail immediately, to check that we are
|
// The first shard we send we'll fail immediately, to check that we are
|
||||||
// still allowed to retry with other shards after a failed one.
|
// still allowed to retry with other shards after a failed one.
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
return [32]byte{}, htlcswitch.NewForwardingError(
|
return [32]byte{}, htlcswitch.NewForwardingError(
|
||||||
&lnwire.FailFeeInsufficient{
|
&lnwire.FailFeeInsufficient{
|
||||||
@ -2845,7 +3028,7 @@ func TestSendToRouteMultiShardSend(t *testing.T) {
|
|||||||
waitForResultSignal := make(chan struct{}, numShards)
|
waitForResultSignal := make(chan struct{}, numShards)
|
||||||
results := make(chan lntypes.Preimage, numShards)
|
results := make(chan lntypes.Preimage, numShards)
|
||||||
|
|
||||||
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
|
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
|
||||||
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
|
||||||
|
|
||||||
// Signal that the shard has been initiated and is
|
// Signal that the shard has been initiated and is
|
||||||
@ -2932,12 +3115,9 @@ func TestSendToRouteMaxHops(t *testing.T) {
|
|||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
startingBlockHeight, testGraph, false,
|
t, startingBlockHeight, testGraph, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
// Create a 30 hop route that exceeds the maximum hop limit.
|
// Create a 30 hop route that exceeds the maximum hop limit.
|
||||||
@ -3046,12 +3226,9 @@ func TestBuildRoute(t *testing.T) {
|
|||||||
|
|
||||||
const startingBlockHeight = 101
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
ctx, cleanUp, err := createTestCtxFromGraphInstance(
|
ctx, cleanUp := createTestCtxFromGraphInstance(
|
||||||
startingBlockHeight, testGraph, false,
|
t, startingBlockHeight, testGraph, false,
|
||||||
)
|
)
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to create router: %v", err)
|
|
||||||
}
|
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
|
|
||||||
checkHops := func(rt *route.Route, expected []uint64,
|
checkHops := func(rt *route.Route, expected []uint64,
|
||||||
@ -3233,10 +3410,7 @@ func assertChanChainRejection(t *testing.T, ctx *testCtx,
|
|||||||
func TestChannelOnChainRejectionZombie(t *testing.T) {
|
func TestChannelOnChainRejectionZombie(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, cleanup, err := createTestCtxSingleNode(0)
|
ctx, cleanup := createTestCtxSingleNode(t, 0)
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
// To start, we'll make an edge for the channel, but we won't add the
|
// To start, we'll make an edge for the channel, but we won't add the
|
||||||
@ -3264,3 +3438,796 @@ func TestChannelOnChainRejectionZombie(t *testing.T) {
|
|||||||
require.Nil(t, err)
|
require.Nil(t, err)
|
||||||
assertChanChainRejection(t, ctx, edge, ErrInvalidFundingOutput)
|
assertChanChainRejection(t, ctx, edge, ErrInvalidFundingOutput)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createDummyTestGraph(t *testing.T) *testGraphInstance {
|
||||||
|
// Setup two simple channels such that we can mock sending along this
|
||||||
|
// route.
|
||||||
|
chanCapSat := btcutil.Amount(100000)
|
||||||
|
testChannels := []*testChannel{
|
||||||
|
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
|
||||||
|
Expiry: 144,
|
||||||
|
FeeRate: 400,
|
||||||
|
MinHTLC: 1,
|
||||||
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
|
}, 1),
|
||||||
|
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
|
||||||
|
Expiry: 144,
|
||||||
|
FeeRate: 400,
|
||||||
|
MinHTLC: 1,
|
||||||
|
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
|
||||||
|
}, 2),
|
||||||
|
}
|
||||||
|
|
||||||
|
testGraph, err := createTestGraphFromChannels(testChannels, "a")
|
||||||
|
require.NoError(t, err, "failed to create graph")
|
||||||
|
return testGraph
|
||||||
|
}
|
||||||
|
|
||||||
|
func createDummyLightningPayment(t *testing.T,
|
||||||
|
target route.Vertex, amt lnwire.MilliSatoshi) *LightningPayment {
|
||||||
|
|
||||||
|
var preImage lntypes.Preimage
|
||||||
|
_, err := rand.Read(preImage[:])
|
||||||
|
require.NoError(t, err, "unable to generate preimage")
|
||||||
|
|
||||||
|
payHash := preImage.Hash()
|
||||||
|
|
||||||
|
return &LightningPayment{
|
||||||
|
Target: target,
|
||||||
|
Amount: amt,
|
||||||
|
FeeLimit: noFeeLimit,
|
||||||
|
paymentHash: &payHash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendMPPaymentSucceed tests that we can successfully send a MPPayment via
|
||||||
|
// router.SendPayment. This test mainly focuses on testing the logic of the
|
||||||
|
// method resumePayment is implemented as expected.
|
||||||
|
func TestSendMPPaymentSucceed(t *testing.T) {
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
|
// Create mockers to initialize the router.
|
||||||
|
controlTower := &mockControlTower{}
|
||||||
|
sessionSource := &mockPaymentSessionSource{}
|
||||||
|
missionControl := &mockMissionControl{}
|
||||||
|
payer := &mockPaymentAttemptDispatcher{}
|
||||||
|
chain := newMockChain(startingBlockHeight)
|
||||||
|
chainView := newMockChainView(chain)
|
||||||
|
testGraph := createDummyTestGraph(t)
|
||||||
|
|
||||||
|
// Define the behavior of the mockers to the point where we can
|
||||||
|
// successfully start the router.
|
||||||
|
controlTower.On("FetchInFlightPayments").Return(
|
||||||
|
[]*channeldb.MPPayment{}, nil,
|
||||||
|
)
|
||||||
|
payer.On("CleanStore", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Create and start the router.
|
||||||
|
router, err := New(Config{
|
||||||
|
Control: controlTower,
|
||||||
|
SessionSource: sessionSource,
|
||||||
|
MissionControl: missionControl,
|
||||||
|
Payer: payer,
|
||||||
|
|
||||||
|
// TODO(yy): create new mocks for the chain and chainview.
|
||||||
|
Chain: chain,
|
||||||
|
ChainView: chainView,
|
||||||
|
|
||||||
|
// TODO(yy): mock the graph once it's changed into interface.
|
||||||
|
Graph: testGraph.graph,
|
||||||
|
|
||||||
|
Clock: clock.NewTestClock(time.Unix(1, 0)),
|
||||||
|
GraphPruneInterval: time.Hour * 2,
|
||||||
|
NextPaymentID: func() (uint64, error) {
|
||||||
|
next := atomic.AddUint64(&uniquePaymentID, 1)
|
||||||
|
return next, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
// Make sure the router can start and stop without error.
|
||||||
|
require.NoError(t, router.Start(), "router failed to start")
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, router.Stop(), "router failed to stop")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Once the router is started, check that the mocked methods are called
|
||||||
|
// as expected.
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Mock the methods to the point where we are inside the function
|
||||||
|
// resumePayment.
|
||||||
|
paymentAmt := lnwire.MilliSatoshi(10000)
|
||||||
|
req := createDummyLightningPayment(
|
||||||
|
t, testGraph.aliasMap["c"], paymentAmt,
|
||||||
|
)
|
||||||
|
identifier := lntypes.Hash(req.Identifier())
|
||||||
|
session := &mockPaymentSession{}
|
||||||
|
sessionSource.On("NewPaymentSession", req).Return(session, nil)
|
||||||
|
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// The following mocked methods are called inside resumePayment. Note
|
||||||
|
// that the payment object below will determine the state of the
|
||||||
|
// paymentLifecycle.
|
||||||
|
payment := &channeldb.MPPayment{}
|
||||||
|
controlTower.On("FetchPayment", identifier).Return(payment, nil)
|
||||||
|
|
||||||
|
// Create a route that can send 1/4 of the total amount. This value
|
||||||
|
// will be returned by calling RequestRoute.
|
||||||
|
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
||||||
|
require.NoError(t, err, "failed to create route")
|
||||||
|
session.On("RequestRoute",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(shard, nil)
|
||||||
|
|
||||||
|
// Make a new htlc attempt with zero fee and append it to the payment's
|
||||||
|
// HTLCs when calling RegisterAttempt.
|
||||||
|
activeAttempt := makeActiveAttempt(int(paymentAmt/4), 0)
|
||||||
|
controlTower.On("RegisterAttempt",
|
||||||
|
identifier, mock.Anything,
|
||||||
|
).Return(nil).Run(func(args mock.Arguments) {
|
||||||
|
payment.HTLCs = append(payment.HTLCs, activeAttempt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a buffered chan and it will be returned by GetPaymentResult.
|
||||||
|
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
|
||||||
|
payer.On("GetPaymentResult",
|
||||||
|
mock.Anything, identifier, mock.Anything,
|
||||||
|
).Run(func(args mock.Arguments) {
|
||||||
|
// Before the mock method is returned, we send the result to
|
||||||
|
// the read-only chan.
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simple mocking the rest.
|
||||||
|
payer.On("SendHTLC",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
missionControl.On("ReportPaymentSuccess",
|
||||||
|
mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Mock SettleAttempt by changing one of the HTLCs to be settled.
|
||||||
|
preimage := lntypes.Preimage{1, 2, 3}
|
||||||
|
settledAttempt := makeSettledAttempt(
|
||||||
|
int(paymentAmt/4), 0, preimage,
|
||||||
|
)
|
||||||
|
controlTower.On("SettleAttempt",
|
||||||
|
identifier, mock.Anything, mock.Anything,
|
||||||
|
).Return(&settledAttempt, nil).Run(func(args mock.Arguments) {
|
||||||
|
// Whenever this method is invoked, we will mark the first
|
||||||
|
// active attempt settled and exit.
|
||||||
|
for i, attempt := range payment.HTLCs {
|
||||||
|
if attempt.Settle == nil {
|
||||||
|
attempt.Settle = &channeldb.HTLCSettleInfo{
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
|
payment.HTLCs[i] = attempt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call the actual method SendPayment on router. This is place inside a
|
||||||
|
// goroutine so we can set a timeout for the whole test, in case
|
||||||
|
// anything goes wrong and the test never finishes.
|
||||||
|
done := make(chan struct{})
|
||||||
|
var p lntypes.Hash
|
||||||
|
go func() {
|
||||||
|
p, _, err = router.SendPayment(req)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatalf("SendPayment didn't exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, validate the returned values and check that the mock
|
||||||
|
// methods are called as expected.
|
||||||
|
require.NoError(t, err, "send payment failed")
|
||||||
|
require.EqualValues(t, preimage, p, "preimage not match")
|
||||||
|
|
||||||
|
// Note that we also implicitly check the methods such as FailAttempt,
|
||||||
|
// ReportPaymentFail, etc, are not called because we never mocked them
|
||||||
|
// in this test. If any of the unexpected methods was called, the test
|
||||||
|
// would fail.
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
sessionSource.AssertExpectations(t)
|
||||||
|
session.AssertExpectations(t)
|
||||||
|
missionControl.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendMPPaymentSucceedOnExtraShards tests that we need extra attempts if
|
||||||
|
// there are failed ones,so that a payment is successfully sent. This test
|
||||||
|
// mainly focuses on testing the logic of the method resumePayment is
|
||||||
|
// implemented as expected.
|
||||||
|
func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
|
// Create mockers to initialize the router.
|
||||||
|
controlTower := &mockControlTower{}
|
||||||
|
sessionSource := &mockPaymentSessionSource{}
|
||||||
|
missionControl := &mockMissionControl{}
|
||||||
|
payer := &mockPaymentAttemptDispatcher{}
|
||||||
|
chain := newMockChain(startingBlockHeight)
|
||||||
|
chainView := newMockChainView(chain)
|
||||||
|
testGraph := createDummyTestGraph(t)
|
||||||
|
|
||||||
|
// Define the behavior of the mockers to the point where we can
|
||||||
|
// successfully start the router.
|
||||||
|
controlTower.On("FetchInFlightPayments").Return(
|
||||||
|
[]*channeldb.MPPayment{}, nil,
|
||||||
|
)
|
||||||
|
payer.On("CleanStore", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Create and start the router.
|
||||||
|
router, err := New(Config{
|
||||||
|
Control: controlTower,
|
||||||
|
SessionSource: sessionSource,
|
||||||
|
MissionControl: missionControl,
|
||||||
|
Payer: payer,
|
||||||
|
|
||||||
|
// TODO(yy): create new mocks for the chain and chainview.
|
||||||
|
Chain: chain,
|
||||||
|
ChainView: chainView,
|
||||||
|
|
||||||
|
// TODO(yy): mock the graph once it's changed into interface.
|
||||||
|
Graph: testGraph.graph,
|
||||||
|
|
||||||
|
Clock: clock.NewTestClock(time.Unix(1, 0)),
|
||||||
|
GraphPruneInterval: time.Hour * 2,
|
||||||
|
NextPaymentID: func() (uint64, error) {
|
||||||
|
next := atomic.AddUint64(&uniquePaymentID, 1)
|
||||||
|
return next, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
// Make sure the router can start and stop without error.
|
||||||
|
require.NoError(t, router.Start(), "router failed to start")
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, router.Stop(), "router failed to stop")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Once the router is started, check that the mocked methods are called
|
||||||
|
// as expected.
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Mock the methods to the point where we are inside the function
|
||||||
|
// resumePayment.
|
||||||
|
paymentAmt := lnwire.MilliSatoshi(20000)
|
||||||
|
req := createDummyLightningPayment(
|
||||||
|
t, testGraph.aliasMap["c"], paymentAmt,
|
||||||
|
)
|
||||||
|
identifier := lntypes.Hash(req.Identifier())
|
||||||
|
session := &mockPaymentSession{}
|
||||||
|
sessionSource.On("NewPaymentSession", req).Return(session, nil)
|
||||||
|
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// The following mocked methods are called inside resumePayment. Note
|
||||||
|
// that the payment object below will determine the state of the
|
||||||
|
// paymentLifecycle.
|
||||||
|
payment := &channeldb.MPPayment{}
|
||||||
|
controlTower.On("FetchPayment", identifier).Return(payment, nil)
|
||||||
|
|
||||||
|
// Create a route that can send 1/4 of the total amount. This value
|
||||||
|
// will be returned by calling RequestRoute.
|
||||||
|
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
||||||
|
require.NoError(t, err, "failed to create route")
|
||||||
|
session.On("RequestRoute",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(shard, nil)
|
||||||
|
|
||||||
|
// Make a new htlc attempt with zero fee and append it to the payment's
|
||||||
|
// HTLCs when calling RegisterAttempt.
|
||||||
|
activeAttempt := makeActiveAttempt(int(paymentAmt/4), 0)
|
||||||
|
controlTower.On("RegisterAttempt",
|
||||||
|
identifier, mock.Anything,
|
||||||
|
).Return(nil).Run(func(args mock.Arguments) {
|
||||||
|
payment.HTLCs = append(payment.HTLCs, activeAttempt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a buffered chan and it will be returned by GetPaymentResult.
|
||||||
|
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
|
||||||
|
|
||||||
|
// We use the failAttemptCount to track how many attempts we want to
|
||||||
|
// fail. Each time the following mock method is called, the count gets
|
||||||
|
// updated.
|
||||||
|
failAttemptCount := 0
|
||||||
|
payer.On("GetPaymentResult",
|
||||||
|
mock.Anything, identifier, mock.Anything,
|
||||||
|
).Run(func(args mock.Arguments) {
|
||||||
|
// Before the mock method is returned, we send the result to
|
||||||
|
// the read-only chan.
|
||||||
|
|
||||||
|
// Update the counter.
|
||||||
|
failAttemptCount++
|
||||||
|
|
||||||
|
// We will make the first two attempts failed with temporary
|
||||||
|
// error.
|
||||||
|
if failAttemptCount <= 2 {
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{
|
||||||
|
Error: htlcswitch.NewForwardingError(
|
||||||
|
&lnwire.FailTemporaryChannelFailure{},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we will mark the attempt succeeded.
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock the FailAttempt method to fail one of the attempts.
|
||||||
|
var failedAttempt channeldb.HTLCAttempt
|
||||||
|
controlTower.On("FailAttempt",
|
||||||
|
identifier, mock.Anything, mock.Anything,
|
||||||
|
).Return(&failedAttempt, nil).Run(func(args mock.Arguments) {
|
||||||
|
// Whenever this method is invoked, we will mark the first
|
||||||
|
// active attempt as failed and exit.
|
||||||
|
for i, attempt := range payment.HTLCs {
|
||||||
|
if attempt.Settle != nil || attempt.Failure != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt.Failure = &channeldb.HTLCFailInfo{}
|
||||||
|
failedAttempt = attempt
|
||||||
|
payment.HTLCs[i] = attempt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup ReportPaymentFail to return nil reason and error so the
|
||||||
|
// payment won't fail.
|
||||||
|
missionControl.On("ReportPaymentFail",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil, nil)
|
||||||
|
|
||||||
|
// Simple mocking the rest.
|
||||||
|
payer.On("SendHTLC",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
missionControl.On("ReportPaymentSuccess",
|
||||||
|
mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Mock SettleAttempt by changing one of the HTLCs to be settled.
|
||||||
|
preimage := lntypes.Preimage{1, 2, 3}
|
||||||
|
settledAttempt := makeSettledAttempt(
|
||||||
|
int(paymentAmt/4), 0, preimage,
|
||||||
|
)
|
||||||
|
controlTower.On("SettleAttempt",
|
||||||
|
identifier, mock.Anything, mock.Anything,
|
||||||
|
).Return(&settledAttempt, nil).Run(func(args mock.Arguments) {
|
||||||
|
// Whenever this method is invoked, we will mark the first
|
||||||
|
// active attempt settled and exit.
|
||||||
|
for i, attempt := range payment.HTLCs {
|
||||||
|
if attempt.Settle != nil || attempt.Failure != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt.Settle = &channeldb.HTLCSettleInfo{
|
||||||
|
Preimage: preimage,
|
||||||
|
}
|
||||||
|
payment.HTLCs[i] = attempt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Call the actual method SendPayment on router. This is place inside a
|
||||||
|
// goroutine so we can set a timeout for the whole test, in case
|
||||||
|
// anything goes wrong and the test never finishes.
|
||||||
|
done := make(chan struct{})
|
||||||
|
var p lntypes.Hash
|
||||||
|
go func() {
|
||||||
|
p, _, err = router.SendPayment(req)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatalf("SendPayment didn't exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, validate the returned values and check that the mock
|
||||||
|
// methods are called as expected.
|
||||||
|
require.NoError(t, err, "send payment failed")
|
||||||
|
require.EqualValues(t, preimage, p, "preimage not match")
|
||||||
|
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
sessionSource.AssertExpectations(t)
|
||||||
|
session.AssertExpectations(t)
|
||||||
|
missionControl.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendMPPaymentFailed tests that when one of the shard fails with a
|
||||||
|
// terminal error, the router will stop attempting and the payment will fail.
|
||||||
|
// This test mainly focuses on testing the logic of the method resumePayment
|
||||||
|
// is implemented as expected.
|
||||||
|
func TestSendMPPaymentFailed(t *testing.T) {
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
|
// Create mockers to initialize the router.
|
||||||
|
controlTower := &mockControlTower{}
|
||||||
|
sessionSource := &mockPaymentSessionSource{}
|
||||||
|
missionControl := &mockMissionControl{}
|
||||||
|
payer := &mockPaymentAttemptDispatcher{}
|
||||||
|
chain := newMockChain(startingBlockHeight)
|
||||||
|
chainView := newMockChainView(chain)
|
||||||
|
testGraph := createDummyTestGraph(t)
|
||||||
|
|
||||||
|
// Define the behavior of the mockers to the point where we can
|
||||||
|
// successfully start the router.
|
||||||
|
controlTower.On("FetchInFlightPayments").Return(
|
||||||
|
[]*channeldb.MPPayment{}, nil,
|
||||||
|
)
|
||||||
|
payer.On("CleanStore", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Create and start the router.
|
||||||
|
router, err := New(Config{
|
||||||
|
Control: controlTower,
|
||||||
|
SessionSource: sessionSource,
|
||||||
|
MissionControl: missionControl,
|
||||||
|
Payer: payer,
|
||||||
|
|
||||||
|
// TODO(yy): create new mocks for the chain and chainview.
|
||||||
|
Chain: chain,
|
||||||
|
ChainView: chainView,
|
||||||
|
|
||||||
|
// TODO(yy): mock the graph once it's changed into interface.
|
||||||
|
Graph: testGraph.graph,
|
||||||
|
|
||||||
|
Clock: clock.NewTestClock(time.Unix(1, 0)),
|
||||||
|
GraphPruneInterval: time.Hour * 2,
|
||||||
|
NextPaymentID: func() (uint64, error) {
|
||||||
|
next := atomic.AddUint64(&uniquePaymentID, 1)
|
||||||
|
return next, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
// Make sure the router can start and stop without error.
|
||||||
|
require.NoError(t, router.Start(), "router failed to start")
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, router.Stop(), "router failed to stop")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Once the router is started, check that the mocked methods are called
|
||||||
|
// as expected.
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Mock the methods to the point where we are inside the function
|
||||||
|
// resumePayment.
|
||||||
|
paymentAmt := lnwire.MilliSatoshi(10000)
|
||||||
|
req := createDummyLightningPayment(
|
||||||
|
t, testGraph.aliasMap["c"], paymentAmt,
|
||||||
|
)
|
||||||
|
identifier := lntypes.Hash(req.Identifier())
|
||||||
|
session := &mockPaymentSession{}
|
||||||
|
sessionSource.On("NewPaymentSession", req).Return(session, nil)
|
||||||
|
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// The following mocked methods are called inside resumePayment. Note
|
||||||
|
// that the payment object below will determine the state of the
|
||||||
|
// paymentLifecycle.
|
||||||
|
payment := &channeldb.MPPayment{}
|
||||||
|
controlTower.On("FetchPayment", identifier).Return(payment, nil)
|
||||||
|
|
||||||
|
// Create a route that can send 1/4 of the total amount. This value
|
||||||
|
// will be returned by calling RequestRoute.
|
||||||
|
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
||||||
|
require.NoError(t, err, "failed to create route")
|
||||||
|
session.On("RequestRoute",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(shard, nil)
|
||||||
|
|
||||||
|
// Make a new htlc attempt with zero fee and append it to the payment's
|
||||||
|
// HTLCs when calling RegisterAttempt.
|
||||||
|
activeAttempt := makeActiveAttempt(int(paymentAmt/4), 0)
|
||||||
|
controlTower.On("RegisterAttempt",
|
||||||
|
identifier, mock.Anything,
|
||||||
|
).Return(nil).Run(func(args mock.Arguments) {
|
||||||
|
payment.HTLCs = append(payment.HTLCs, activeAttempt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a buffered chan and it will be returned by GetPaymentResult.
|
||||||
|
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
|
||||||
|
|
||||||
|
// We use the failAttemptCount to track how many attempts we want to
|
||||||
|
// fail. Each time the following mock method is called, the count gets
|
||||||
|
// updated.
|
||||||
|
failAttemptCount := 0
|
||||||
|
payer.On("GetPaymentResult",
|
||||||
|
mock.Anything, identifier, mock.Anything,
|
||||||
|
).Run(func(args mock.Arguments) {
|
||||||
|
// Before the mock method is returned, we send the result to
|
||||||
|
// the read-only chan.
|
||||||
|
|
||||||
|
// Update the counter.
|
||||||
|
failAttemptCount++
|
||||||
|
|
||||||
|
// We fail the first attempt with terminal error.
|
||||||
|
if failAttemptCount == 1 {
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{
|
||||||
|
Error: htlcswitch.NewForwardingError(
|
||||||
|
&lnwire.FailIncorrectDetails{},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// We will make the rest attempts failed with temporary error.
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{
|
||||||
|
Error: htlcswitch.NewForwardingError(
|
||||||
|
&lnwire.FailTemporaryChannelFailure{},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock the FailAttempt method to fail one of the attempts.
|
||||||
|
var failedAttempt channeldb.HTLCAttempt
|
||||||
|
controlTower.On("FailAttempt",
|
||||||
|
identifier, mock.Anything, mock.Anything,
|
||||||
|
).Return(&failedAttempt, nil).Run(func(args mock.Arguments) {
|
||||||
|
// Whenever this method is invoked, we will mark the first
|
||||||
|
// active attempt as failed and exit.
|
||||||
|
for i, attempt := range payment.HTLCs {
|
||||||
|
if attempt.Settle != nil || attempt.Failure != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
attempt.Failure = &channeldb.HTLCFailInfo{}
|
||||||
|
failedAttempt = attempt
|
||||||
|
payment.HTLCs[i] = attempt
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
// Setup ReportPaymentFail to return nil reason and error so the
|
||||||
|
// payment won't fail.
|
||||||
|
var called bool
|
||||||
|
failureReason := channeldb.FailureReasonPaymentDetails
|
||||||
|
missionControl.On("ReportPaymentFail",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil, nil).Run(func(args mock.Arguments) {
|
||||||
|
// We only return the terminal error once, thus when the method
|
||||||
|
// is called, we will return it with a nil error.
|
||||||
|
if called {
|
||||||
|
missionControl.failReason = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's the first time calling this method, we will return a
|
||||||
|
// terminal error.
|
||||||
|
missionControl.failReason = &failureReason
|
||||||
|
payment.FailureReason = &failureReason
|
||||||
|
called = true
|
||||||
|
})
|
||||||
|
|
||||||
|
// Simple mocking the rest.
|
||||||
|
controlTower.On("Fail", identifier, failureReason).Return(nil)
|
||||||
|
payer.On("SendHTLC",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Call the actual method SendPayment on router. This is place inside a
|
||||||
|
// goroutine so we can set a timeout for the whole test, in case
|
||||||
|
// anything goes wrong and the test never finishes.
|
||||||
|
done := make(chan struct{})
|
||||||
|
var p lntypes.Hash
|
||||||
|
go func() {
|
||||||
|
p, _, err = router.SendPayment(req)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatalf("SendPayment didn't exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, validate the returned values and check that the mock
|
||||||
|
// methods are called as expected.
|
||||||
|
require.Error(t, err, "expected send payment error")
|
||||||
|
require.EqualValues(t, [32]byte{}, p, "preimage not match")
|
||||||
|
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
sessionSource.AssertExpectations(t)
|
||||||
|
session.AssertExpectations(t)
|
||||||
|
missionControl.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestSendMPPaymentFailedWithShardsInFlight tests that when the payment is in
|
||||||
|
// terminal state, even if we have shards in flight, we still fail the payment
|
||||||
|
// and exit. This test mainly focuses on testing the logic of the method
|
||||||
|
// resumePayment is implemented as expected.
|
||||||
|
func TestSendMPPaymentFailedWithShardsInFlight(t *testing.T) {
|
||||||
|
const startingBlockHeight = 101
|
||||||
|
|
||||||
|
// Create mockers to initialize the router.
|
||||||
|
controlTower := &mockControlTower{}
|
||||||
|
sessionSource := &mockPaymentSessionSource{}
|
||||||
|
missionControl := &mockMissionControl{}
|
||||||
|
payer := &mockPaymentAttemptDispatcher{}
|
||||||
|
chain := newMockChain(startingBlockHeight)
|
||||||
|
chainView := newMockChainView(chain)
|
||||||
|
testGraph := createDummyTestGraph(t)
|
||||||
|
|
||||||
|
// Define the behavior of the mockers to the point where we can
|
||||||
|
// successfully start the router.
|
||||||
|
controlTower.On("FetchInFlightPayments").Return(
|
||||||
|
[]*channeldb.MPPayment{}, nil,
|
||||||
|
)
|
||||||
|
payer.On("CleanStore", mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// Create and start the router.
|
||||||
|
router, err := New(Config{
|
||||||
|
Control: controlTower,
|
||||||
|
SessionSource: sessionSource,
|
||||||
|
MissionControl: missionControl,
|
||||||
|
Payer: payer,
|
||||||
|
|
||||||
|
// TODO(yy): create new mocks for the chain and chainview.
|
||||||
|
Chain: chain,
|
||||||
|
ChainView: chainView,
|
||||||
|
|
||||||
|
// TODO(yy): mock the graph once it's changed into interface.
|
||||||
|
Graph: testGraph.graph,
|
||||||
|
|
||||||
|
Clock: clock.NewTestClock(time.Unix(1, 0)),
|
||||||
|
GraphPruneInterval: time.Hour * 2,
|
||||||
|
NextPaymentID: func() (uint64, error) {
|
||||||
|
next := atomic.AddUint64(&uniquePaymentID, 1)
|
||||||
|
return next, nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.NoError(t, err, "failed to create router")
|
||||||
|
|
||||||
|
// Make sure the router can start and stop without error.
|
||||||
|
require.NoError(t, router.Start(), "router failed to start")
|
||||||
|
defer func() {
|
||||||
|
require.NoError(t, router.Stop(), "router failed to stop")
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Once the router is started, check that the mocked methods are called
|
||||||
|
// as expected.
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
|
||||||
|
// Mock the methods to the point where we are inside the function
|
||||||
|
// resumePayment.
|
||||||
|
paymentAmt := lnwire.MilliSatoshi(10000)
|
||||||
|
req := createDummyLightningPayment(
|
||||||
|
t, testGraph.aliasMap["c"], paymentAmt,
|
||||||
|
)
|
||||||
|
identifier := lntypes.Hash(req.Identifier())
|
||||||
|
session := &mockPaymentSession{}
|
||||||
|
sessionSource.On("NewPaymentSession", req).Return(session, nil)
|
||||||
|
controlTower.On("InitPayment", identifier, mock.Anything).Return(nil)
|
||||||
|
|
||||||
|
// The following mocked methods are called inside resumePayment. Note
|
||||||
|
// that the payment object below will determine the state of the
|
||||||
|
// paymentLifecycle.
|
||||||
|
payment := &channeldb.MPPayment{}
|
||||||
|
controlTower.On("FetchPayment", identifier).Return(payment, nil)
|
||||||
|
|
||||||
|
// Create a route that can send 1/4 of the total amount. This value
|
||||||
|
// will be returned by calling RequestRoute.
|
||||||
|
shard, err := createTestRoute(paymentAmt/4, testGraph.aliasMap)
|
||||||
|
require.NoError(t, err, "failed to create route")
|
||||||
|
session.On("RequestRoute",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(shard, nil)
|
||||||
|
|
||||||
|
// Make a new htlc attempt with zero fee and append it to the payment's
|
||||||
|
// HTLCs when calling RegisterAttempt.
|
||||||
|
activeAttempt := makeActiveAttempt(int(paymentAmt/4), 0)
|
||||||
|
controlTower.On("RegisterAttempt",
|
||||||
|
identifier, mock.Anything,
|
||||||
|
).Return(nil).Run(func(args mock.Arguments) {
|
||||||
|
payment.HTLCs = append(payment.HTLCs, activeAttempt)
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create a buffered chan and it will be returned by GetPaymentResult.
|
||||||
|
payer.resultChan = make(chan *htlcswitch.PaymentResult, 10)
|
||||||
|
|
||||||
|
// We use the failAttemptCount to track how many attempts we want to
|
||||||
|
// fail. Each time the following mock method is called, the count gets
|
||||||
|
// updated.
|
||||||
|
failAttemptCount := 0
|
||||||
|
payer.On("GetPaymentResult",
|
||||||
|
mock.Anything, identifier, mock.Anything,
|
||||||
|
).Run(func(args mock.Arguments) {
|
||||||
|
// Before the mock method is returned, we send the result to
|
||||||
|
// the read-only chan.
|
||||||
|
|
||||||
|
// Update the counter.
|
||||||
|
failAttemptCount++
|
||||||
|
|
||||||
|
// We fail the first attempt with terminal error.
|
||||||
|
if failAttemptCount == 1 {
|
||||||
|
payer.resultChan <- &htlcswitch.PaymentResult{
|
||||||
|
Error: htlcswitch.NewForwardingError(
|
||||||
|
&lnwire.FailIncorrectDetails{},
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
return
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// For the rest attempts we will NOT send anything to the
|
||||||
|
// resultChan, thus making all the shards in active state,
|
||||||
|
// neither settled or failed.
|
||||||
|
})
|
||||||
|
|
||||||
|
// Mock the FailAttempt method to fail EXACTLY once.
|
||||||
|
var failedAttempt channeldb.HTLCAttempt
|
||||||
|
controlTower.On("FailAttempt",
|
||||||
|
identifier, mock.Anything, mock.Anything,
|
||||||
|
).Return(&failedAttempt, nil).Run(func(args mock.Arguments) {
|
||||||
|
// Whenever this method is invoked, we will mark the first
|
||||||
|
// active attempt as failed and exit.
|
||||||
|
failedAttempt = payment.HTLCs[0]
|
||||||
|
failedAttempt.Failure = &channeldb.HTLCFailInfo{}
|
||||||
|
payment.HTLCs[0] = failedAttempt
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
// Setup ReportPaymentFail to return nil reason and error so the
|
||||||
|
// payment won't fail.
|
||||||
|
failureReason := channeldb.FailureReasonPaymentDetails
|
||||||
|
missionControl.On("ReportPaymentFail",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(failureReason, nil).Run(func(args mock.Arguments) {
|
||||||
|
missionControl.failReason = &failureReason
|
||||||
|
payment.FailureReason = &failureReason
|
||||||
|
}).Once()
|
||||||
|
|
||||||
|
// Simple mocking the rest.
|
||||||
|
controlTower.On("Fail", identifier, failureReason).Return(nil).Once()
|
||||||
|
payer.On("SendHTLC",
|
||||||
|
mock.Anything, mock.Anything, mock.Anything,
|
||||||
|
).Return(nil)
|
||||||
|
|
||||||
|
// Call the actual method SendPayment on router. This is place inside a
|
||||||
|
// goroutine so we can set a timeout for the whole test, in case
|
||||||
|
// anything goes wrong and the test never finishes.
|
||||||
|
done := make(chan struct{})
|
||||||
|
var p lntypes.Hash
|
||||||
|
go func() {
|
||||||
|
p, _, err = router.SendPayment(req)
|
||||||
|
close(done)
|
||||||
|
}()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-done:
|
||||||
|
case <-time.After(testTimeout):
|
||||||
|
t.Fatalf("SendPayment didn't exit")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, validate the returned values and check that the mock
|
||||||
|
// methods are called as expected.
|
||||||
|
require.Error(t, err, "expected send payment error")
|
||||||
|
require.EqualValues(t, [32]byte{}, p, "preimage not match")
|
||||||
|
|
||||||
|
controlTower.AssertExpectations(t)
|
||||||
|
payer.AssertExpectations(t)
|
||||||
|
sessionSource.AssertExpectations(t)
|
||||||
|
session.AssertExpectations(t)
|
||||||
|
missionControl.AssertExpectations(t)
|
||||||
|
}
|
||||||
|
11
routing/testdata/basic_graph.json
vendored
11
routing/testdata/basic_graph.json
vendored
@ -39,7 +39,8 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"source": false,
|
"source": false,
|
||||||
"pubkey": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
"pubkey": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||||
|
"privkey": "82b266f659bd83a976bac11b2cc442baec5508e84e61085d7ec2b0fc52156c87",
|
||||||
"alias": "songoku"
|
"alias": "songoku"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@ -154,7 +155,7 @@
|
|||||||
"capacity": 120000
|
"capacity": 120000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||||
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
||||||
"channel_id": 12345,
|
"channel_id": 12345,
|
||||||
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
||||||
@ -168,7 +169,7 @@
|
|||||||
"capacity": 100000
|
"capacity": 100000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||||
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
"node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6",
|
||||||
"channel_id": 12345,
|
"channel_id": 12345,
|
||||||
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
"channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0",
|
||||||
@ -182,7 +183,7 @@
|
|||||||
"capacity": 100000
|
"capacity": 100000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||||
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
||||||
"channel_id": 3495345,
|
"channel_id": 3495345,
|
||||||
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
||||||
@ -196,7 +197,7 @@
|
|||||||
"capacity": 110000
|
"capacity": 110000
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add",
|
"node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318",
|
||||||
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
"node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb",
|
||||||
"channel_id": 3495345,
|
"channel_id": 3495345,
|
||||||
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
"channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",
|
||||||
|
Loading…
Reference in New Issue
Block a user