lnd/routing/router_test.go
Joost Jager e0a080454a routing: track unifiedPolicyEdge
Preparation so that we can have the inbound fee available
in addition to the outgoing policy.
2024-03-31 18:12:28 +02:00

3999 lines
121 KiB
Go

package routing
import (
"bytes"
"fmt"
"image/color"
"math"
"math/rand"
"sync/atomic"
"testing"
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/htlcswitch"
lnmock "github.com/lightningnetwork/lnd/lntest/mock"
"github.com/lightningnetwork/lnd/lntest/wait"
"github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/lightningnetwork/lnd/zpay32"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var uniquePaymentID uint64 = 1 // to be used atomically
type testCtx struct {
router *ChannelRouter
graph *channeldb.ChannelGraph
aliases map[string]route.Vertex
privKeys map[string]*btcec.PrivateKey
channelIDs map[route.Vertex]map[route.Vertex]uint64
chain *mockChain
chainView *mockChainView
notifier *lnmock.ChainNotifier
}
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
// filter between restarts.
c.chainView.Reset()
// With the chainView reset, we'll now re-create the router itself, and
// start it.
router, err := New(Config{
Graph: c.graph,
Chain: c.chain,
ChainView: c.chainView,
Payer: &mockPaymentAttemptDispatcherOld{},
Control: makeMockControlTower(),
ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "unable to create router")
require.NoError(t, router.Start(), "unable to start router")
// Finally, we'll swap out the pointer in the testCtx with this fresh
// instance of the router.
c.router = router
}
func createTestCtxFromGraphInstance(t *testing.T,
startingHeight uint32, graphInstance *testGraphInstance,
strictPruning bool) *testCtx {
return createTestCtxFromGraphInstanceAssumeValid(
t, startingHeight, graphInstance, false, strictPruning,
)
}
func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
startingHeight uint32, graphInstance *testGraphInstance,
assumeValid bool, strictPruning bool) *testCtx {
// 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
// any p2p functionality, the peer send and switch send messages won't
// be populated.
chain := newMockChain(startingHeight)
chainView := newMockChainView(chain)
pathFindingConfig := PathFindingConfig{
MinProbability: 0.01,
AttemptCost: 100,
}
aCfg := AprioriConfig{
PenaltyHalfLife: time.Hour,
AprioriHopProbability: 0.9,
AprioriWeight: 0.5,
CapacityFraction: testCapacityFraction,
}
estimator, err := NewAprioriEstimator(aCfg)
require.NoError(t, err)
mcConfig := &MissionControlConfig{Estimator: estimator}
mc, err := NewMissionControl(
graphInstance.graphBackend, route.Vertex{}, mcConfig,
)
require.NoError(t, err, "failed to create missioncontrol")
sourceNode, err := graphInstance.graph.SourceNode()
require.NoError(t, err)
sessionSource := &SessionSource{
Graph: graphInstance.graph,
SourceNode: sourceNode,
GetLink: graphInstance.getLink,
PathFindingConfig: pathFindingConfig,
MissionControl: mc,
}
notifier := &lnmock.ChainNotifier{
EpochChan: make(chan *chainntnfs.BlockEpoch),
SpendChan: make(chan *chainntnfs.SpendDetail),
ConfChan: make(chan *chainntnfs.TxConfirmation),
}
router, err := New(Config{
Graph: graphInstance.graph,
Chain: chain,
ChainView: chainView,
Payer: &mockPaymentAttemptDispatcherOld{},
Notifier: notifier,
Control: makeMockControlTower(),
MissionControl: mc,
SessionSource: sessionSource,
ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2,
GetLink: graphInstance.getLink,
NextPaymentID: func() (uint64, error) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
PathFindingConfig: pathFindingConfig,
Clock: clock.NewTestClock(time.Unix(1, 0)),
AssumeChannelValid: assumeValid,
StrictZombiePruning: strictPruning,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "unable to create router")
require.NoError(t, router.Start(), "unable to start router")
ctx := &testCtx{
router: router,
graph: graphInstance.graph,
aliases: graphInstance.aliasMap,
privKeys: graphInstance.privKeyMap,
channelIDs: graphInstance.channelIDs,
chain: chain,
chainView: chainView,
notifier: notifier,
}
t.Cleanup(func() {
ctx.router.Stop()
})
return ctx
}
func createTestCtxSingleNode(t *testing.T,
startingHeight uint32) *testCtx {
graph, graphBackend, err := makeTestGraph(t, true)
require.NoError(t, err, "failed to make test graph")
sourceNode, err := createTestNode()
require.NoError(t, err, "failed to create test node")
require.NoError(t,
graph.SetSourceNode(sourceNode), "failed to set source node",
)
graphInstance := &testGraphInstance{
graph: graph,
graphBackend: graphBackend,
}
return createTestCtxFromGraphInstance(
t, startingHeight, graphInstance, false,
)
}
func createTestCtxFromFile(t *testing.T,
startingHeight uint32, testGraph string) *testCtx {
// We'll attempt to locate and parse out the file
// that encodes the graph that our tests should be run against.
graphInstance, err := parseTestGraph(t, true, testGraph)
require.NoError(t, err, "unable to create test graph")
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 := ecdsa.Sign(key, digest)
errChanUpdate.Signature, err = lnwire.NewSigFromSignature(sig)
require.NoError(t, err, "failed to create new signature")
}
// TestFindRoutesWithFeeLimit asserts that routes found by the FindRoutes method
// within the channel router contain a total fee less than or equal to the fee
// limit.
func TestFindRoutesWithFeeLimit(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// This test will attempt to find routes from roasbeef to sophon for 100
// satoshis with a fee limit of 10 satoshis. There are two routes from
// roasbeef to sophon:
// 1. roasbeef -> songoku -> sophon
// 2. roasbeef -> phamnuwen -> sophon
// The second route violates our fee limit, so we should only expect to
// see the first route.
target := ctx.aliases["sophon"]
paymentAmt := lnwire.NewMSatFromSatoshis(100)
restrictions := &RestrictParams{
FeeLimit: lnwire.NewMSatFromSatoshis(10),
ProbabilitySource: noProbabilitySource,
CltvLimit: math.MaxUint32,
}
req, err := NewRouteRequest(
ctx.router.selfNode.PubKeyBytes, &target, paymentAmt, 0,
restrictions, nil, nil, nil, MinCLTVDelta,
)
require.NoError(t, err, "invalid route request")
route, _, err := ctx.router.FindRoute(req)
require.NoError(t, err, "unable to find any routes")
require.Falsef(t,
route.TotalFees() > restrictions.FeeLimit,
"route exceeded fee limit: %v", spew.Sdump(route),
)
hops := route.Hops
require.Equal(t, 2, len(hops), "expected 2 hops")
require.Equalf(t,
ctx.aliases["songoku"], hops[0].PubKeyBytes,
"expected first hop through songoku, got %s",
getAliasFromPubKey(hops[0].PubKeyBytes, ctx.aliases),
)
}
// TestSendPaymentRouteFailureFallback tests that when sending a payment, if
// one of the target routes is seen as unavailable, then the next route in the
// queue is used instead. This process should continue until either a payment
// succeeds, or all routes have been exhausted.
func TestSendPaymentRouteFailureFallback(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
payment := createDummyLightningPayment(
t, ctx.aliases["sophon"], paymentAmt,
)
var preImage [32]byte
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
// router's configuration to ignore the path that has son goku as the
// first hop. This should force the router to instead take the
// the more costly path (through pham nuwen).
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
return [32]byte{}, htlcswitch.NewForwardingError(
// TODO(roasbeef): temp node failure
// should be?
&lnwire.FailTemporaryChannelFailure{},
1,
)
}
return preImage, nil
})
// Send off the payment request to the router, route through pham nuwen
// should've been selected as a fall back and succeeded correctly.
paymentPreImage, route, err := ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
// The route selected should have two hops
require.Equal(t, 2, len(route.Hops), "incorrect route length")
// The preimage should match up with the once created above.
if !bytes.Equal(paymentPreImage[:], preImage[:]) {
t.Fatalf("incorrect preimage used: expected %x got %x",
preImage[:], paymentPreImage[:])
}
// The route should have pham nuwen as the first hop.
require.Equalf(t,
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
"route should go through phamnuwen as first hop, instead "+
"passes through: %v",
getAliasFromPubKey(route.Hops[0].PubKeyBytes, ctx.aliases),
)
}
// TestSendPaymentRouteInfiniteLoopWithBadHopHint tests that when sending
// a payment with a malformed hop hint in the first hop, the hint is ignored
// and the payment succeeds without an infinite loop of retries.
func TestSendPaymentRouteInfiniteLoopWithBadHopHint(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
source := ctx.aliases["roasbeef"]
sourceNodeID, err := btcec.ParsePubKey(source[:])
require.NoError(t, err)
actualChannelID := ctx.getChannelIDFromAlias(t, "roasbeef", "songoku")
badChannelID := uint64(66666)
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to songoku for 1000 satoshis.
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
payment := createDummyLightningPayment(
t, ctx.aliases["songoku"], paymentAmt,
)
payment.RouteHints = [][]zpay32.HopHint{{
zpay32.HopHint{
NodeID: sourceNodeID,
ChannelID: badChannelID,
FeeBaseMSat: uint32(50),
CLTVExpiryDelta: uint16(200),
},
}}
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
// Mock a payment result that always fails with FailUnknownNextPeer when
// the bad channel is the first hop.
badShortChanID := lnwire.NewShortChanIDFromInt(badChannelID)
newFwdError := htlcswitch.NewForwardingError(
&lnwire.FailUnknownNextPeer{}, 0,
)
payer, ok := ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld)
require.Equal(t, ok, true, "failed Payer cast")
payer.setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
// Returns a FailUnknownNextPeer if it's trying
// to pay an invalid channel.
if firstHop == badShortChanID {
return [32]byte{}, newFwdError
}
return preImage, nil
})
// Send off the payment request to the router, should succeed
// ignoring the bad channel id hint.
paymentPreImage, route, paymentErr := ctx.router.SendPayment(payment)
require.NoErrorf(t, paymentErr, "unable to send payment: %v",
payment.paymentHash)
// The preimage should match up with the one created above.
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
// The route should have songoku as the first hop.
require.Equal(t, actualChannelID, route.Hops[0].ChannelID,
"route should go through the correct channel id",
)
}
// TestChannelUpdateValidation tests that a failed payment with an associated
// channel update will only be applied to the graph when the update contains a
// valid signature.
func TestChannelUpdateValidation(t *testing.T) {
t.Parallel()
// Setup a three node network.
chanCapSat := btcutil.Amount(100000)
feeRate := lnwire.MilliSatoshi(400)
testChannels := []*testChannel{
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: feeRate,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 1),
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: feeRate,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 2),
}
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "a",
)
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, true,
)
// Assert that the initially configured fee is retrieved correctly.
_, e1, e2, err := ctx.router.GetChannelByID(
lnwire.NewShortChanIDFromInt(1),
)
require.NoError(t, err, "cannot retrieve channel")
require.Equal(t, feeRate, e1.FeeProportionalMillionths, "invalid fee")
require.Equal(t, feeRate, e2.FeeProportionalMillionths, "invalid fee")
// Setup a route from source a to destination c. The route will be used
// in a call to SendToRoute. SendToRoute also applies channel updates,
// but it saves us from including RequestRoute in the test scope too.
hop1 := ctx.aliases["b"]
hop2 := ctx.aliases["c"]
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: hop1,
LegacyPayload: true,
},
{
ChannelID: 2,
PubKeyBytes: hop2,
LegacyPayload: true,
},
}
rt, err := route.NewRouteFromHops(
lnwire.MilliSatoshi(10000), 100,
ctx.aliases["a"], hops,
)
require.NoError(t, err, "unable to create route")
// Set up a channel update message with an invalid signature to be
// returned to the sender.
var invalidSignature lnwire.Sig
errChanUpdate := lnwire.ChannelUpdate{
Signature: invalidSignature,
FeeRate: 500,
ShortChannelID: lnwire.NewShortChanIDFromInt(1),
Timestamp: uint32(testTime.Add(time.Minute).Unix()),
MessageFlags: e2.MessageFlags,
ChannelFlags: e2.ChannelFlags,
HtlcMaximumMsat: e2.MaxHTLC,
}
// We'll modify the SendToSwitch method so that it simulates a failed
// payment with an error originating from the first hop of the route.
// The unsigned channel update is attached to the failure message.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailFeeInsufficient{
Update: errChanUpdate,
},
1,
)
})
// The payment parameter is mostly redundant in SendToRoute. Can be left
// empty for this test.
var payment lntypes.Hash
// Send off the payment request to the router. The specified route
// should be attempted and the channel update should be received by
// router and ignored because it is missing a valid signature.
_, err = ctx.router.SendToRoute(payment, rt)
require.Error(t, err, "expected route to fail with channel update")
_, e1, e2, err = ctx.router.GetChannelByID(
lnwire.NewShortChanIDFromInt(1),
)
require.NoError(t, err, "cannot retrieve channel")
require.Equal(t, feeRate, e1.FeeProportionalMillionths,
"fee updated without valid signature")
require.Equal(t, feeRate, e2.FeeProportionalMillionths,
"fee updated without valid signature")
// Next, add a signature to the channel update.
signErrChanUpdate(t, testGraph.privKeyMap["b"], &errChanUpdate)
// Retry the payment using the same route as before.
_, err = ctx.router.SendToRoute(payment, rt)
require.Error(t, err, "expected route to fail with channel update")
// This time a valid signature was supplied and the policy change should
// have been applied to the graph.
_, e1, e2, err = ctx.router.GetChannelByID(
lnwire.NewShortChanIDFromInt(1),
)
require.NoError(t, err, "cannot retrieve channel")
require.Equal(t, feeRate, e1.FeeProportionalMillionths,
"fee should not be updated")
require.EqualValues(t, 500, int(e2.FeeProportionalMillionths),
"fee not updated even though signature is valid")
}
// TestSendPaymentErrorRepeatedFeeInsufficient tests that if we receive
// multiple fee related errors from a channel that we're attempting to route
// through, then we'll prune the channel after the second attempt.
func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// 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
// to sophon for 1000 satoshis.
amt := lnwire.NewMSatFromSatoshis(1000)
payment := createDummyLightningPayment(
t, ctx.aliases["sophon"], amt,
)
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
// We'll also fetch the first outgoing channel edge from son goku
// to sophon. We'll obtain this as we'll need to to generate the
// FeeInsufficient error that we'll send back.
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(
songokuSophonChanID,
)
require.NoError(t, err, "unable to fetch chan id")
errChanUpdate := lnwire.ChannelUpdate{
ShortChannelID: lnwire.NewShortChanIDFromInt(
songokuSophonChanID,
),
Timestamp: uint32(edgeUpdateToFail.LastUpdate.Unix()),
MessageFlags: edgeUpdateToFail.MessageFlags,
ChannelFlags: edgeUpdateToFail.ChannelFlags,
TimeLockDelta: edgeUpdateToFail.TimeLockDelta,
HtlcMinimumMsat: edgeUpdateToFail.MinHTLC,
HtlcMaximumMsat: edgeUpdateToFail.MaxHTLC,
BaseFee: uint32(edgeUpdateToFail.FeeBaseMSat),
FeeRate: uint32(edgeUpdateToFail.FeeProportionalMillionths),
}
signErrChanUpdate(t, ctx.privKeys["songoku"], &errChanUpdate)
// 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
// it should only cause the edge to be pruned after the second attempt.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
roasbeefSongokuChanID,
)
if firstHop == roasbeefSongoku {
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,
)
}
return preImage, nil
})
// Send off the payment request to the router, route through phamnuwen
// should've been selected as a fall back and succeeded correctly.
paymentPreImage, route, err := ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
// The route selected should have two hops
require.Equal(t, 2, len(route.Hops), "incorrect route length")
// The preimage should match up with the once created above.
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
// The route should have pham nuwen as the first hop.
require.Equalf(t,
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
"route should go through pham nuwen as first hop, "+
"instead passes through: %v",
getAliasFromPubKey(route.Hops[0].PubKeyBytes, 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 := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// Get the channel ID.
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
)
var (
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[:])
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 := createDummyLightningPayment(
t, ctx.aliases["elst"], amt,
)
payment.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.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
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 := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// Get the channel ID.
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
)
var (
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[:])
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 := createDummyLightningPayment(
t, ctx.aliases["elst"], amt,
)
payment.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.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
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
// an ExpiryTooSoon or a IncorrectCltvExpiry error from a node, then we prune
// that node from the available graph within a mission control session. This
// test ensures that we'll route around errors due to nodes not knowing the
// current block height.
func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(
t, startingBlockHeight, basicGraphFilePath,
)
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to sophon for 1k satoshis.
amt := lnwire.NewMSatFromSatoshis(1000)
payment := createDummyLightningPayment(
t, ctx.aliases["sophon"], amt,
)
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
// We'll also fetch the first outgoing channel edge from roasbeef to
// 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
// block height is.
chanID := ctx.getChannelIDFromAlias(t, "roasbeef", "songoku")
roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)
_, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID)
require.NoError(t, err, "unable to fetch chan id")
errChanUpdate := lnwire.ChannelUpdate{
ShortChannelID: lnwire.NewShortChanIDFromInt(chanID),
Timestamp: uint32(edgeUpdateToFail.LastUpdate.Unix()),
MessageFlags: edgeUpdateToFail.MessageFlags,
ChannelFlags: edgeUpdateToFail.ChannelFlags,
TimeLockDelta: edgeUpdateToFail.TimeLockDelta,
HtlcMinimumMsat: edgeUpdateToFail.MinHTLC,
HtlcMaximumMsat: edgeUpdateToFail.MaxHTLC,
BaseFee: uint32(edgeUpdateToFail.FeeBaseMSat),
FeeRate: uint32(edgeUpdateToFail.FeeProportionalMillionths),
}
// We'll now modify the SendToSwitch method to return an error for the
// 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
// only channel to Sophon.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailExpiryTooSoon{
Update: errChanUpdate,
}, 1,
)
}
return preImage, nil
})
// assertExpectedPath is a helper function that asserts the returned
// route properly routes around the failure we've introduced in the
// graph.
assertExpectedPath := func(retPreImage [32]byte, route *route.Route) {
// The route selected should have two hops
require.Equal(t, 2, len(route.Hops), "incorrect route length")
// The preimage should match up with the once created above.
require.Equal(t,
preImage[:], retPreImage[:], "incorrect preimage used",
)
// The route should have satoshi as the first hop.
require.Equalf(t,
ctx.aliases["phamnuwen"], route.Hops[0].PubKeyBytes,
"route should go through phamnuwen as first hop, "+
"instead passes through: %v",
getAliasFromPubKey(
route.Hops[0].PubKeyBytes, ctx.aliases,
),
)
}
// Send off the payment request to the router, this payment should
// succeed as we should actually go through Pham Nuwen in order to get
// to Sophon, even though he has higher fees.
paymentPreImage, rt, err := ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
assertExpectedPath(paymentPreImage, rt)
// We'll now modify the error return an IncorrectCltvExpiry error
// instead, this should result in the same behavior of roasbeef routing
// around the faulty Son Goku node.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailIncorrectCltvExpiry{
Update: errChanUpdate,
}, 1,
)
}
return preImage, nil
})
// Once again, Roasbeef should route around Goku since they disagree
// w.r.t to the block height, and instead go through Pham Nuwen. We
// flip a bit in the payment hash to allow resending this payment.
payment.paymentHash[1] ^= 1
paymentPreImage, rt, err = ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
assertExpectedPath(paymentPreImage, rt)
}
// TestSendPaymentErrorPathPruning tests that the send of candidate routes
// properly gets pruned in response to ForwardingError response from the
// underlying SendToSwitch function.
func TestSendPaymentErrorPathPruning(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// Craft a LightningPayment struct that'll send a payment from roasbeef
// to luo ji for 1000 satoshis, with a maximum of 1000 satoshis in fees.
paymentAmt := lnwire.NewMSatFromSatoshis(1000)
payment := createDummyLightningPayment(
t, ctx.aliases["sophon"], paymentAmt,
)
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
roasbeefSongoku := lnwire.NewShortChanIDFromInt(
ctx.getChannelIDFromAlias(t, "roasbeef", "songoku"),
)
roasbeefPhanNuwen := lnwire.NewShortChanIDFromInt(
ctx.getChannelIDFromAlias(t, "roasbeef", "phamnuwen"),
)
// First, we'll modify the SendToSwitch method to return an error
// indicating that the channel from roasbeef to son goku is not operable
// with an UnknownNextPeer.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
// We'll first simulate an error from the first
// hop to simulate the channel from songoku to
// sophon not having enough capacity.
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
)
}
// Next, we'll create an error from phan nuwen to
// indicate that the sophon node is not longer online,
// which should prune out the rest of the routes.
if firstHop == roasbeefPhanNuwen {
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailUnknownNextPeer{}, 1,
)
}
return preImage, nil
})
ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
// When we try to dispatch that payment, we should receive an error as
// both attempts should fail and cause both routes to be pruned.
_, _, err := ctx.router.SendPayment(payment)
require.Error(t, err, "payment didn't return error")
// The final error returned should also indicate that the peer wasn't
// online (the last error we returned).
require.Equal(t, channeldb.FailureReasonNoRoute, err)
// Inspect the two attempts that were made before the payment failed.
p, err := ctx.router.cfg.Control.FetchPayment(*payment.paymentHash)
require.NoError(t, err)
htlcs := p.GetHTLCs()
require.Equal(t, 2, len(htlcs), "expected two attempts")
// We expect the first attempt to have failed with a
// TemporaryChannelFailure, the second with UnknownNextPeer.
msg := htlcs[0].Failure.Message
_, ok := msg.(*lnwire.FailTemporaryChannelFailure)
require.True(t, ok, "unexpected fail message")
msg = htlcs[1].Failure.Message
_, ok = msg.(*lnwire.FailUnknownNextPeer)
require.True(t, ok, "unexpected fail message")
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
// connection between songoku and isn't up.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
failure := htlcswitch.NewForwardingError(
&lnwire.FailUnknownNextPeer{}, 1,
)
return [32]byte{}, failure
}
return preImage, nil
})
// This shouldn't return an error, as we'll make a payment attempt via
// the pham nuwen channel based on the assumption that there might be an
// intermittent issue with the songoku <-> sophon channel.
paymentPreImage, rt, err := ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
// This path should go: roasbeef -> pham nuwen -> sophon
require.Equal(t, 2, len(rt.Hops), "incorrect route length")
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
require.Equalf(t,
ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
"route should go through phamnuwen as first hop, "+
"instead passes through: %v",
getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
)
ctx.router.cfg.MissionControl.(*MissionControl).ResetHistory()
// Finally, we'll modify the SendToSwitch function to indicate that the
// roasbeef -> luoji channel has insufficient capacity. This should
// again cause us to instead go via the satoshi route.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku {
// We'll first simulate an error from the first
// outgoing link to simulate the channel from luo ji to
// roasbeef not having enough capacity.
return [32]byte{}, htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{},
1,
)
}
return preImage, nil
})
// We flip a bit in the payment hash to allow resending this payment.
payment.paymentHash[1] ^= 1
paymentPreImage, rt, err = ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
// This should succeed finally. The route selected should have two
// hops.
require.Equal(t, 2, len(rt.Hops), "incorrect route length")
// The preimage should match up with the once created above.
require.Equal(t, preImage[:], paymentPreImage[:], "incorrect preimage")
// The route should have satoshi as the first hop.
require.Equalf(t,
ctx.aliases["phamnuwen"], rt.Hops[0].PubKeyBytes,
"route should go through phamnuwen as first hop, "+
"instead passes through: %v",
getAliasFromPubKey(rt.Hops[0].PubKeyBytes, ctx.aliases),
)
}
// TestAddProof checks that we can update the channel proof after channel
// info was added to the database.
func TestAddProof(t *testing.T) {
t.Parallel()
ctx := createTestCtxSingleNode(t, 0)
// Before creating out edge, we'll create two new nodes within the
// network that the channel will connect.
node1, err := createTestNode()
if err != nil {
t.Fatal(err)
}
node2, err := createTestNode()
if err != nil {
t.Fatal(err)
}
// In order to be able to add the edge we should have a valid funding
// UTXO within the blockchain.
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(), bitcoinKey2.SerializeCompressed(),
100, 0)
require.NoError(t, err, "unable create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
// After utxo was recreated adding the edge without the proof.
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
AuthProof: nil,
}
copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Now we'll attempt to update the proof and check that it has been
// properly updated.
if err := ctx.router.AddProof(*chanID, &testAuthProof); err != nil {
t.Fatalf("unable to add proof: %v", err)
}
info, _, _, err := ctx.router.GetChannelByID(*chanID)
require.NoError(t, err, "unable to get channel")
if info.AuthProof == nil {
t.Fatal("proof have been updated")
}
}
// TestIgnoreNodeAnnouncement tests that adding a node to the router that is
// not known from any channel announcement, leads to the announcement being
// ignored.
func TestIgnoreNodeAnnouncement(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
pub := priv1.PubKey()
node := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
Color: color.RGBA{1, 2, 3, 0},
Alias: "node11",
AuthSigBytes: testSig.Serialize(),
Features: testFeatures,
}
copy(node.PubKeyBytes[:], pub.SerializeCompressed())
err := ctx.router.AddNode(node)
if !IsError(err, ErrIgnored) {
t.Fatalf("expected to get ErrIgnore, instead got: %v", err)
}
}
// TestIgnoreChannelEdgePolicyForUnknownChannel checks that a router will
// ignore a channel policy for a channel not in the graph.
func TestIgnoreChannelEdgePolicyForUnknownChannel(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
// Setup an initially empty network.
testChannels := []*testChannel{}
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "roasbeef",
)
require.NoError(t, err, "unable to create graph")
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, false,
)
var pub1 [33]byte
copy(pub1[:], priv1.PubKey().SerializeCompressed())
var pub2 [33]byte
copy(pub2[:], priv2.PubKey().SerializeCompressed())
// Add the edge between the two unknown nodes to the graph, and check
// that the nodes are found after the fact.
fundingTx, _, chanID, err := createChannelEdge(
ctx, bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(), 10000, 500,
)
require.NoError(t, err, "unable to create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: pub1,
NodeKey2Bytes: pub2,
BitcoinKey1Bytes: pub1,
BitcoinKey2Bytes: pub2,
AuthProof: nil,
}
edgePolicy := &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
// Attempt to update the edge. This should be ignored, since the edge
// is not yet added to the router.
err = ctx.router.UpdateEdge(edgePolicy)
if !IsError(err, ErrIgnored) {
t.Fatalf("expected to get ErrIgnore, instead got: %v", err)
}
// Add the edge.
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("expected to be able to add edge to the channel graph,"+
" even though the vertexes were unknown: %v.", err)
}
// Now updating the edge policy should succeed.
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
}
// TestAddEdgeUnknownVertexes tests that if an edge is added that contains two
// vertexes which we don't know of, the edge should be available for use
// regardless. This is due to the fact that we don't actually need node
// announcements for the channel vertexes to be able to use the channel.
func TestAddEdgeUnknownVertexes(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
var pub1 [33]byte
copy(pub1[:], priv1.PubKey().SerializeCompressed())
var pub2 [33]byte
copy(pub2[:], priv2.PubKey().SerializeCompressed())
// The two nodes we are about to add should not exist yet.
_, exists1, err := ctx.graph.HasLightningNode(pub1)
require.NoError(t, err, "unable to query graph")
if exists1 {
t.Fatalf("node already existed")
}
_, exists2, err := ctx.graph.HasLightningNode(pub2)
require.NoError(t, err, "unable to query graph")
if exists2 {
t.Fatalf("node already existed")
}
// Add the edge between the two unknown nodes to the graph, and check
// that the nodes are found after the fact.
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
10000, 500,
)
require.NoError(t, err, "unable to create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: pub1,
NodeKey2Bytes: pub2,
BitcoinKey1Bytes: pub1,
BitcoinKey2Bytes: pub2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("expected to be able to add edge to the channel graph,"+
" even though the vertexes were unknown: %v.", err)
}
// We must add the edge policy to be able to use the edge for route
// finding.
edgePolicy := &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
ToNode: edge.NodeKey2Bytes,
}
edgePolicy.ChannelFlags = 0
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// Create edge in the other direction as well.
edgePolicy = &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
ToNode: edge.NodeKey1Bytes,
}
edgePolicy.ChannelFlags = 1
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// After adding the edge between the two previously unknown nodes, they
// should have been added to the graph.
_, exists1, err = ctx.graph.HasLightningNode(pub1)
require.NoError(t, err, "unable to query graph")
if !exists1 {
t.Fatalf("node1 was not added to the graph")
}
_, exists2, err = ctx.graph.HasLightningNode(pub2)
require.NoError(t, err, "unable to query graph")
if !exists2 {
t.Fatalf("node2 was not added to the graph")
}
// We will connect node1 to the rest of the test graph, and make sure
// we can find a route to node2, which will use the just added channel
// edge.
// We will connect node 1 to "sophon"
connectNode := ctx.aliases["sophon"]
connectNodeKey, err := btcec.ParsePubKey(connectNode[:])
if err != nil {
t.Fatal(err)
}
var (
pubKey1 *btcec.PublicKey
pubKey2 *btcec.PublicKey
)
node1Bytes := priv1.PubKey().SerializeCompressed()
node2Bytes := connectNode
if bytes.Compare(node1Bytes[:], node2Bytes[:]) == -1 {
pubKey1 = priv1.PubKey()
pubKey2 = connectNodeKey
} else {
pubKey1 = connectNodeKey
pubKey2 = priv1.PubKey()
}
fundingTx, _, chanID, err = createChannelEdge(ctx,
pubKey1.SerializeCompressed(), pubKey2.SerializeCompressed(),
10000, 510)
require.NoError(t, err, "unable to create channel edge")
fundingBlock = &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
edge = &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
AuthProof: nil,
}
copy(edge.NodeKey1Bytes[:], node1Bytes)
edge.NodeKey2Bytes = node2Bytes
copy(edge.BitcoinKey1Bytes[:], node1Bytes)
edge.BitcoinKey2Bytes = node2Bytes
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge to the channel graph: %v.", err)
}
edgePolicy = &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
ToNode: edge.NodeKey2Bytes,
}
edgePolicy.ChannelFlags = 0
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
edgePolicy = &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: testTime,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
ToNode: edge.NodeKey1Bytes,
}
edgePolicy.ChannelFlags = 1
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// We should now be able to find a route to node 2.
paymentAmt := lnwire.NewMSatFromSatoshis(100)
targetNode := priv2.PubKey()
var targetPubKeyBytes route.Vertex
copy(targetPubKeyBytes[:], targetNode.SerializeCompressed())
req, err := NewRouteRequest(
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
)
require.NoError(t, err, "invalid route request")
_, _, err = ctx.router.FindRoute(req)
require.NoError(t, err, "unable to find any routes")
// Now check that we can update the node info for the partial node
// without messing up the channel graph.
n1 := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
Color: color.RGBA{1, 2, 3, 0},
Alias: "node11",
AuthSigBytes: testSig.Serialize(),
Features: testFeatures,
}
copy(n1.PubKeyBytes[:], priv1.PubKey().SerializeCompressed())
if err := ctx.router.AddNode(n1); err != nil {
t.Fatalf("could not add node: %v", err)
}
n2 := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: time.Unix(123, 0),
Addresses: testAddrs,
Color: color.RGBA{1, 2, 3, 0},
Alias: "node22",
AuthSigBytes: testSig.Serialize(),
Features: testFeatures,
}
copy(n2.PubKeyBytes[:], priv2.PubKey().SerializeCompressed())
if err := ctx.router.AddNode(n2); err != nil {
t.Fatalf("could not add node: %v", err)
}
// Should still be able to find the route, and the info should be
// updated.
req, err = NewRouteRequest(
ctx.router.selfNode.PubKeyBytes, &targetPubKeyBytes,
paymentAmt, 0, noRestrictions, nil, nil, nil, MinCLTVDelta,
)
require.NoError(t, err, "invalid route request")
_, _, err = ctx.router.FindRoute(req)
require.NoError(t, err, "unable to find any routes")
copy1, err := ctx.graph.FetchLightningNode(nil, pub1)
require.NoError(t, err, "unable to fetch node")
if copy1.Alias != n1.Alias {
t.Fatalf("fetched node not equal to original")
}
copy2, err := ctx.graph.FetchLightningNode(nil, pub2)
require.NoError(t, err, "unable to fetch node")
if copy2.Alias != n2.Alias {
t.Fatalf("fetched node not equal to original")
}
}
// TestWakeUpOnStaleBranch tests that upon startup of the ChannelRouter, if the
// the chain previously reflected in the channel graph is stale (overtaken by a
// longer chain), the channel router will prune the graph for any channels
// confirmed on the stale chain, and resync to the main chain.
func TestWakeUpOnStaleBranch(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxSingleNode(t, startingBlockHeight)
const chanValue = 10000
// chanID1 will not be reorged out.
var chanID1 uint64
// chanID2 will be reorged out.
var chanID2 uint64
// Create 10 common blocks, confirming chanID1.
for i := uint32(1); i <= 10; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := startingBlockHeight + i
if i == 5 {
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
chanValue, height)
if err != nil {
t.Fatalf("unable create channel edge: %v", err)
}
block.Transactions = append(block.Transactions,
fundingTx)
chanID1 = chanID.ToUint64()
}
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
ctx.chainView.notifyBlock(block.BlockHash(), height,
[]*wire.MsgTx{}, t)
}
// Give time to process new blocks
time.Sleep(time.Millisecond * 500)
_, forkHeight, err := ctx.chain.GetBestBlock()
require.NoError(t, err, "unable to ge best block")
// Create 10 blocks on the minority chain, confirming chanID2.
for i := uint32(1); i <= 10; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := uint32(forkHeight) + i
if i == 5 {
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
chanValue, height)
if err != nil {
t.Fatalf("unable create channel edge: %v", err)
}
block.Transactions = append(block.Transactions,
fundingTx)
chanID2 = chanID.ToUint64()
}
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
ctx.chainView.notifyBlock(block.BlockHash(), height,
[]*wire.MsgTx{}, t)
}
// Give time to process new blocks
time.Sleep(time.Millisecond * 500)
// Now add the two edges to the channel graph, and check that they
// correctly show up in the database.
node1, err := createTestNode()
require.NoError(t, err, "unable to create test node")
node2, err := createTestNode()
require.NoError(t, err, "unable to create test node")
edge1 := &models.ChannelEdgeInfo{
ChannelID: chanID1,
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
AuthProof: &models.ChannelAuthProof{
NodeSig1Bytes: testSig.Serialize(),
NodeSig2Bytes: testSig.Serialize(),
BitcoinSig1Bytes: testSig.Serialize(),
BitcoinSig2Bytes: testSig.Serialize(),
},
}
copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge1); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
edge2 := &models.ChannelEdgeInfo{
ChannelID: chanID2,
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
AuthProof: &models.ChannelAuthProof{
NodeSig1Bytes: testSig.Serialize(),
NodeSig2Bytes: testSig.Serialize(),
BitcoinSig1Bytes: testSig.Serialize(),
BitcoinSig2Bytes: testSig.Serialize(),
},
}
copy(edge2.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge2.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge2); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Check that the fundingTxs are in the graph db.
_, _, has, isZombie, err := ctx.graph.HasChannelEdge(chanID1)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if !has {
t.Fatalf("could not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID2)
}
if !has {
t.Fatalf("could not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
// Stop the router, so we can reorg the chain while its offline.
if err := ctx.router.Stop(); err != nil {
t.Fatalf("unable to stop router: %v", err)
}
// Create a 15 block fork.
for i := uint32(1); i <= 15; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := uint32(forkHeight) + i
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
}
// Give time to process new blocks.
time.Sleep(time.Millisecond * 500)
// Create new router with same graph database.
router, err := New(Config{
Graph: ctx.graph,
Chain: ctx.chain,
ChainView: ctx.chainView,
Payer: &mockPaymentAttemptDispatcherOld{},
Control: makeMockControlTower(),
ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2,
// We'll set the delay to zero to prune immediately.
FirstTimePruneDelay: 0,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
if err != nil {
t.Fatalf("unable to create router %v", err)
}
// It should resync to the longer chain on startup.
if err := router.Start(); err != nil {
t.Fatalf("unable to start router: %v", err)
}
// The channel with chanID2 should not be in the database anymore,
// since it is not confirmed on the longest chain. chanID1 should
// still be.
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID1)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if !has {
t.Fatalf("did not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID2)
}
if has {
t.Fatalf("found edge in graph")
}
if isZombie {
t.Fatal("reorged edge should not be marked as zombie")
}
}
// TestDisconnectedBlocks checks that the router handles a reorg happening when
// it is active.
func TestDisconnectedBlocks(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxSingleNode(t, startingBlockHeight)
const chanValue = 10000
// chanID1 will not be reorged out, while chanID2 will be reorged out.
var chanID1, chanID2 uint64
// Create 10 common blocks, confirming chanID1.
for i := uint32(1); i <= 10; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := startingBlockHeight + i
if i == 5 {
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
chanValue, height)
if err != nil {
t.Fatalf("unable create channel edge: %v", err)
}
block.Transactions = append(block.Transactions,
fundingTx)
chanID1 = chanID.ToUint64()
}
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
ctx.chainView.notifyBlock(block.BlockHash(), height,
[]*wire.MsgTx{}, t)
}
// Give time to process new blocks
time.Sleep(time.Millisecond * 500)
_, forkHeight, err := ctx.chain.GetBestBlock()
require.NoError(t, err, "unable to get best block")
// Create 10 blocks on the minority chain, confirming chanID2.
var minorityChain []*wire.MsgBlock
for i := uint32(1); i <= 10; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := uint32(forkHeight) + i
if i == 5 {
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
chanValue, height)
if err != nil {
t.Fatalf("unable create channel edge: %v", err)
}
block.Transactions = append(block.Transactions,
fundingTx)
chanID2 = chanID.ToUint64()
}
minorityChain = append(minorityChain, block)
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
ctx.chainView.notifyBlock(block.BlockHash(), height,
[]*wire.MsgTx{}, t)
}
// Give time to process new blocks
time.Sleep(time.Millisecond * 500)
// Now add the two edges to the channel graph, and check that they
// correctly show up in the database.
node1, err := createTestNode()
require.NoError(t, err, "unable to create test node")
node2, err := createTestNode()
require.NoError(t, err, "unable to create test node")
edge1 := &models.ChannelEdgeInfo{
ChannelID: chanID1,
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
BitcoinKey1Bytes: node1.PubKeyBytes,
BitcoinKey2Bytes: node2.PubKeyBytes,
AuthProof: &models.ChannelAuthProof{
NodeSig1Bytes: testSig.Serialize(),
NodeSig2Bytes: testSig.Serialize(),
BitcoinSig1Bytes: testSig.Serialize(),
BitcoinSig2Bytes: testSig.Serialize(),
},
}
copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge1); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
edge2 := &models.ChannelEdgeInfo{
ChannelID: chanID2,
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
BitcoinKey1Bytes: node1.PubKeyBytes,
BitcoinKey2Bytes: node2.PubKeyBytes,
AuthProof: &models.ChannelAuthProof{
NodeSig1Bytes: testSig.Serialize(),
NodeSig2Bytes: testSig.Serialize(),
BitcoinSig1Bytes: testSig.Serialize(),
BitcoinSig2Bytes: testSig.Serialize(),
},
}
copy(edge2.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge2.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge2); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Check that the fundingTxs are in the graph db.
_, _, has, isZombie, err := ctx.graph.HasChannelEdge(chanID1)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if !has {
t.Fatalf("could not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID2)
}
if !has {
t.Fatalf("could not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
// Create a 15 block fork. We first let the chainView notify the router
// about stale blocks, before sending the now connected blocks. We do
// this because we expect this order from the chainview.
ctx.chainView.notifyStaleBlockAck = make(chan struct{}, 1)
for i := len(minorityChain) - 1; i >= 0; i-- {
block := minorityChain[i]
height := uint32(forkHeight) + uint32(i) + 1
ctx.chainView.notifyStaleBlock(block.BlockHash(), height,
block.Transactions, t)
<-ctx.chainView.notifyStaleBlockAck
}
time.Sleep(time.Second * 2)
ctx.chainView.notifyBlockAck = make(chan struct{}, 1)
for i := uint32(1); i <= 15; i++ {
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
height := uint32(forkHeight) + i
ctx.chain.addBlock(block, height, rand.Uint32())
ctx.chain.setBestBlock(int32(height))
ctx.chainView.notifyBlock(block.BlockHash(), height,
block.Transactions, t)
<-ctx.chainView.notifyBlockAck
}
time.Sleep(time.Millisecond * 500)
// chanID2 should not be in the database anymore, since it is not
// confirmed on the longest chain. chanID1 should still be.
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID1)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if !has {
t.Fatalf("did not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
_, _, has, isZombie, err = ctx.graph.HasChannelEdge(chanID2)
if err != nil {
t.Fatalf("error looking for edge: %v", chanID2)
}
if has {
t.Fatalf("found edge in graph")
}
if isZombie {
t.Fatal("reorged edge should not be marked as zombie")
}
}
// TestChansClosedOfflinePruneGraph tests that if channels we know of are
// closed while we're offline, then once we resume operation of the
// ChannelRouter, then the channels are properly pruned.
func TestRouterChansClosedOfflinePruneGraph(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxSingleNode(t, startingBlockHeight)
const chanValue = 10000
// First, we'll create a channel, to be mined shortly at height 102.
block102 := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
nextHeight := startingBlockHeight + 1
fundingTx1, chanUTXO, chanID1, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
chanValue, uint32(nextHeight))
require.NoError(t, err, "unable create channel edge")
block102.Transactions = append(block102.Transactions, fundingTx1)
ctx.chain.addBlock(block102, uint32(nextHeight), rand.Uint32())
ctx.chain.setBestBlock(int32(nextHeight))
ctx.chainView.notifyBlock(block102.BlockHash(), uint32(nextHeight),
[]*wire.MsgTx{}, t)
// We'll now create the edges and nodes within the database required
// for the ChannelRouter to properly recognize the channel we added
// above.
node1, err := createTestNode()
require.NoError(t, err, "unable to create test node")
node2, err := createTestNode()
require.NoError(t, err, "unable to create test node")
edge1 := &models.ChannelEdgeInfo{
ChannelID: chanID1.ToUint64(),
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
AuthProof: &models.ChannelAuthProof{
NodeSig1Bytes: testSig.Serialize(),
NodeSig2Bytes: testSig.Serialize(),
BitcoinSig1Bytes: testSig.Serialize(),
BitcoinSig2Bytes: testSig.Serialize(),
},
}
copy(edge1.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge1.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if err := ctx.router.AddEdge(edge1); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// The router should now be aware of the channel we created above.
_, _, hasChan, isZombie, err := ctx.graph.HasChannelEdge(chanID1.ToUint64())
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if !hasChan {
t.Fatalf("could not find edge in graph")
}
if isZombie {
t.Fatal("edge was marked as zombie")
}
// With the transaction included, and the router's database state
// updated, we'll now mine 5 additional blocks on top of it.
for i := 0; i < 5; i++ {
nextHeight++
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
ctx.chain.addBlock(block, uint32(nextHeight), rand.Uint32())
ctx.chain.setBestBlock(int32(nextHeight))
ctx.chainView.notifyBlock(block.BlockHash(), uint32(nextHeight),
[]*wire.MsgTx{}, t)
}
// At this point, our starting height should be 107.
_, chainHeight, err := ctx.chain.GetBestBlock()
require.NoError(t, err, "unable to get best block")
if chainHeight != 107 {
t.Fatalf("incorrect chain height: expected %v, got %v",
107, chainHeight)
}
// Next, we'll "shut down" the router in order to simulate downtime.
if err := ctx.router.Stop(); err != nil {
t.Fatalf("unable to shutdown router: %v", err)
}
// While the router is "offline" we'll mine 5 additional blocks, with
// the second block closing the channel we created above.
for i := 0; i < 5; i++ {
nextHeight++
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
if i == 2 {
// For the second block, we'll add a transaction that
// closes the channel we created above by spending the
// output.
closingTx := wire.NewMsgTx(2)
closingTx.AddTxIn(&wire.TxIn{
PreviousOutPoint: *chanUTXO,
})
block.Transactions = append(block.Transactions,
closingTx)
}
ctx.chain.addBlock(block, uint32(nextHeight), rand.Uint32())
ctx.chain.setBestBlock(int32(nextHeight))
ctx.chainView.notifyBlock(block.BlockHash(), uint32(nextHeight),
[]*wire.MsgTx{}, t)
}
// At this point, our starting height should be 112.
_, chainHeight, err = ctx.chain.GetBestBlock()
require.NoError(t, err, "unable to get best block")
if chainHeight != 112 {
t.Fatalf("incorrect chain height: expected %v, got %v",
112, chainHeight)
}
// 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
// it was down.
ctx.RestartRouter(t)
// At this point, the channel that was pruned should no longer be known
// by the router.
_, _, hasChan, isZombie, err = ctx.graph.HasChannelEdge(chanID1.ToUint64())
if err != nil {
t.Fatalf("error looking for edge: %v", chanID1)
}
if hasChan {
t.Fatalf("channel was found in graph but shouldn't have been")
}
if isZombie {
t.Fatal("closed channel should not be marked as zombie")
}
}
// TestPruneChannelGraphStaleEdges ensures that we properly prune stale edges
// from the channel graph.
func TestPruneChannelGraphStaleEdges(t *testing.T) {
t.Parallel()
freshTimestamp := time.Now()
staleTimestamp := time.Unix(0, 0)
// We'll create the following test graph so that two of the channels
// are pruned.
testChannels := []*testChannel{
// No edges.
{
Node1: &testChannelEnd{Alias: "a"},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 1,
},
// Only one edge with a stale timestamp.
{
Node1: &testChannelEnd{
Alias: "d",
testChannelPolicy: &testChannelPolicy{
LastUpdate: staleTimestamp,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 2,
},
// Only one edge with a stale timestamp, but it's the source
// node so it won't get pruned.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: staleTimestamp,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 3,
},
// Only one edge with a fresh timestamp.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: freshTimestamp,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 4,
},
// One edge fresh, one edge stale. This will be pruned with
// strict pruning activated.
{
Node1: &testChannelEnd{
Alias: "c",
testChannelPolicy: &testChannelPolicy{
LastUpdate: freshTimestamp,
},
},
Node2: &testChannelEnd{
Alias: "d",
testChannelPolicy: &testChannelPolicy{
LastUpdate: staleTimestamp,
},
},
Capacity: 100000,
ChannelID: 5,
},
// Both edges fresh.
symmetricTestChannel("g", "h", 100000, &testChannelPolicy{
LastUpdate: freshTimestamp,
}, 6),
// Both edges stale, only one pruned. This should be pruned for
// both normal and strict pruning.
symmetricTestChannel("e", "f", 100000, &testChannelPolicy{
LastUpdate: staleTimestamp,
}, 7),
}
for _, strictPruning := range []bool{true, false} {
// We'll create our test graph and router backed with these test
// channels we've created.
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "a",
)
if err != nil {
t.Fatalf("unable to create test graph: %v", err)
}
const startingHeight = 100
ctx := createTestCtxFromGraphInstance(
t, startingHeight, testGraph, strictPruning,
)
// All of the channels should exist before pruning them.
assertChannelsPruned(t, ctx.graph, testChannels)
// Proceed to prune the channels - only the last one should be pruned.
if err := ctx.router.pruneZombieChans(); err != nil {
t.Fatalf("unable to prune zombie channels: %v", err)
}
// We expect channels that have either both edges stale, or one edge
// stale with both known.
var prunedChannels []uint64
if strictPruning {
prunedChannels = []uint64{2, 5, 7}
} else {
prunedChannels = []uint64{2, 7}
}
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannels...)
}
}
// TestPruneChannelGraphDoubleDisabled test that we can properly prune channels
// with both edges disabled from our channel graph.
func TestPruneChannelGraphDoubleDisabled(t *testing.T) {
t.Parallel()
t.Run("no_assumechannelvalid", func(t *testing.T) {
testPruneChannelGraphDoubleDisabled(t, false)
})
t.Run("assumechannelvalid", func(t *testing.T) {
testPruneChannelGraphDoubleDisabled(t, true)
})
}
func testPruneChannelGraphDoubleDisabled(t *testing.T, assumeValid bool) {
// We'll create the following test graph so that only the last channel
// is pruned. We'll use a fresh timestamp to ensure they're not pruned
// according to that heuristic.
timestamp := time.Now()
testChannels := []*testChannel{
// Channel from self shouldn't be pruned.
symmetricTestChannel(
"self", "a", 100000, &testChannelPolicy{
LastUpdate: timestamp,
Disabled: true,
}, 99,
),
// No edges.
{
Node1: &testChannelEnd{Alias: "a"},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 1,
},
// Only one edge disabled.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: timestamp,
Disabled: true,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 2,
},
// Only one edge enabled.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: timestamp,
Disabled: false,
},
},
Node2: &testChannelEnd{Alias: "b"},
Capacity: 100000,
ChannelID: 3,
},
// One edge disabled, one edge enabled.
{
Node1: &testChannelEnd{
Alias: "a",
testChannelPolicy: &testChannelPolicy{
LastUpdate: timestamp,
Disabled: true,
},
},
Node2: &testChannelEnd{
Alias: "b",
testChannelPolicy: &testChannelPolicy{
LastUpdate: timestamp,
Disabled: false,
},
},
Capacity: 100000,
ChannelID: 1,
},
// Both edges enabled.
symmetricTestChannel("c", "d", 100000, &testChannelPolicy{
LastUpdate: timestamp,
Disabled: false,
}, 2),
// Both edges disabled, only one pruned.
symmetricTestChannel("e", "f", 100000, &testChannelPolicy{
LastUpdate: timestamp,
Disabled: true,
}, 3),
}
// We'll create our test graph and router backed with these test
// channels we've created.
testGraph, err := createTestGraphFromChannels(
t, true, testChannels, "self",
)
require.NoError(t, err, "unable to create test graph")
const startingHeight = 100
ctx := createTestCtxFromGraphInstanceAssumeValid(
t, startingHeight, testGraph, assumeValid, false,
)
// All the channels should exist within the graph before pruning them
// when not using AssumeChannelValid, otherwise we should have pruned
// the last channel on startup.
if !assumeValid {
assertChannelsPruned(t, ctx.graph, testChannels)
} else {
// Sleep to allow the pruning to finish.
time.Sleep(200 * time.Millisecond)
prunedChannel := testChannels[len(testChannels)-1].ChannelID
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannel)
}
if err := ctx.router.pruneZombieChans(); err != nil {
t.Fatalf("unable to prune zombie channels: %v", err)
}
// If we attempted to prune them without AssumeChannelValid being set,
// none should be pruned. Otherwise the last channel should still be
// pruned.
if !assumeValid {
assertChannelsPruned(t, ctx.graph, testChannels)
} else {
prunedChannel := testChannels[len(testChannels)-1].ChannelID
assertChannelsPruned(t, ctx.graph, testChannels, prunedChannel)
}
}
// TestFindPathFeeWeighting tests that the findPath method will properly prefer
// routes with lower fees over routes with lower time lock values. This is
// meant to exercise the fact that the internal findPath method ranks edges
// with the square of the total fee in order bias towards lower fees.
func TestFindPathFeeWeighting(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
var preImage [32]byte
copy(preImage[:], bytes.Repeat([]byte{9}, 32))
sourceNode, err := ctx.graph.SourceNode()
require.NoError(t, err, "unable to fetch source node")
amt := lnwire.MilliSatoshi(100)
target := ctx.aliases["luoji"]
// We'll now attempt a path finding attempt using this set up. Due to
// the edge weighting, we should select the direct path over the 2 hop
// path even though the direct path has a higher potential time lock.
path, err := dbFindPath(
ctx.graph, nil, &mockBandwidthHints{},
noRestrictions,
testPathFindingConfig,
sourceNode.PubKeyBytes, target, amt, 0, 0,
)
require.NoError(t, err, "unable to find path")
// The route that was chosen should be exactly one hop, and should be
// directly to luoji.
if len(path) != 1 {
t.Fatalf("expected path length of 1, instead was: %v", len(path))
}
if path[0].policy.ToNodePubKey() != ctx.aliases["luoji"] {
t.Fatalf("wrong node: %v", path[0].policy.ToNodePubKey())
}
}
// TestIsStaleNode tests that the IsStaleNode method properly detects stale
// node announcements.
func TestIsStaleNode(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxSingleNode(t, startingBlockHeight)
// Before we can insert a node in to the database, we need to create a
// channel that it's linked to.
var (
pub1 [33]byte
pub2 [33]byte
)
copy(pub1[:], priv1.PubKey().SerializeCompressed())
copy(pub2[:], priv2.PubKey().SerializeCompressed())
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
10000, 500)
require.NoError(t, err, "unable to create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: pub1,
NodeKey2Bytes: pub2,
BitcoinKey1Bytes: pub1,
BitcoinKey2Bytes: pub2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Before we add the node, if we query for staleness, we should get
// false, as we haven't added the full node.
updateTimeStamp := time.Unix(123, 0)
if ctx.router.IsStaleNode(pub1, updateTimeStamp) {
t.Fatalf("incorrectly detected node as stale")
}
// With the node stub in the database, we'll add the fully node
// announcement to the database.
n1 := &channeldb.LightningNode{
HaveNodeAnnouncement: true,
LastUpdate: updateTimeStamp,
Addresses: testAddrs,
Color: color.RGBA{1, 2, 3, 0},
Alias: "node11",
AuthSigBytes: testSig.Serialize(),
Features: testFeatures,
}
copy(n1.PubKeyBytes[:], priv1.PubKey().SerializeCompressed())
if err := ctx.router.AddNode(n1); err != nil {
t.Fatalf("could not add node: %v", err)
}
// If we use the same timestamp and query for staleness, we should get
// true.
if !ctx.router.IsStaleNode(pub1, updateTimeStamp) {
t.Fatalf("failure to detect stale node update")
}
// If we update the timestamp and once again query for staleness, it
// should report false.
newTimeStamp := time.Unix(1234, 0)
if ctx.router.IsStaleNode(pub1, newTimeStamp) {
t.Fatalf("incorrectly detected node as stale")
}
}
// TestIsKnownEdge tests that the IsKnownEdge method properly detects stale
// channel announcements.
func TestIsKnownEdge(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxSingleNode(t, startingBlockHeight)
// First, we'll create a new channel edge (just the info) and insert it
// into the database.
var (
pub1 [33]byte
pub2 [33]byte
)
copy(pub1[:], priv1.PubKey().SerializeCompressed())
copy(pub2[:], priv2.PubKey().SerializeCompressed())
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
10000, 500)
require.NoError(t, err, "unable to create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: pub1,
NodeKey2Bytes: pub2,
BitcoinKey1Bytes: pub1,
BitcoinKey2Bytes: pub2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// Now that the edge has been inserted, query is the router already
// knows of the edge should return true.
if !ctx.router.IsKnownEdge(*chanID) {
t.Fatalf("router should detect edge as known")
}
}
// TestIsStaleEdgePolicy tests that the IsStaleEdgePolicy properly detects
// stale channel edge update announcements.
func TestIsStaleEdgePolicy(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
// First, we'll create a new channel edge (just the info) and insert it
// into the database.
var (
pub1 [33]byte
pub2 [33]byte
)
copy(pub1[:], priv1.PubKey().SerializeCompressed())
copy(pub2[:], priv2.PubKey().SerializeCompressed())
fundingTx, _, chanID, err := createChannelEdge(ctx,
bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(),
10000, 500)
require.NoError(t, err, "unable to create channel edge")
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
// If we query for staleness before adding the edge, we should get
// false.
updateTimeStamp := time.Unix(123, 0)
if ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 0) {
t.Fatalf("router failed to detect fresh edge policy")
}
if ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 1) {
t.Fatalf("router failed to detect fresh edge policy")
}
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: pub1,
NodeKey2Bytes: pub2,
BitcoinKey1Bytes: pub1,
BitcoinKey2Bytes: pub2,
AuthProof: nil,
}
if err := ctx.router.AddEdge(edge); err != nil {
t.Fatalf("unable to add edge: %v", err)
}
// We'll also add two edge policies, one for each direction.
edgePolicy := &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: updateTimeStamp,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.ChannelFlags = 0
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
edgePolicy = &models.ChannelEdgePolicy{
SigBytes: testSig.Serialize(),
ChannelID: edge.ChannelID,
LastUpdate: updateTimeStamp,
TimeLockDelta: 10,
MinHTLC: 1,
FeeBaseMSat: 10,
FeeProportionalMillionths: 10000,
}
edgePolicy.ChannelFlags = 1
if err := ctx.router.UpdateEdge(edgePolicy); err != nil {
t.Fatalf("unable to update edge policy: %v", err)
}
// Now that the edges have been added, an identical (chanID, flag,
// timestamp) tuple for each edge should be detected as a stale edge.
if !ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 0) {
t.Fatalf("router failed to detect stale edge policy")
}
if !ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 1) {
t.Fatalf("router failed to detect stale edge policy")
}
// If we now update the timestamp for both edges, the router should
// detect that this tuple represents a fresh edge.
updateTimeStamp = time.Unix(9999, 0)
if ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 0) {
t.Fatalf("router failed to detect fresh edge policy")
}
if ctx.router.IsStaleEdgePolicy(*chanID, updateTimeStamp, 1) {
t.Fatalf("router failed to detect fresh edge policy")
}
}
// TestEmptyRoutesGenerateSphinxPacket tests that the generateSphinxPacket
// function is able to gracefully handle being passed a nil set of hops for the
// route by the caller.
func TestEmptyRoutesGenerateSphinxPacket(t *testing.T) {
t.Parallel()
sessionKey, _ := btcec.NewPrivateKey()
emptyRoute := &route.Route{}
_, _, err := generateSphinxPacket(emptyRoute, testHash[:], sessionKey)
if err != route.ErrNoRouteHopsProvided {
t.Fatalf("expected empty hops error: instead got: %v", err)
}
}
// TestUnknownErrorSource tests that if the source of an error is unknown, all
// edges along the route will be pruned.
func TestUnknownErrorSource(t *testing.T) {
t.Parallel()
// Setup a network. It contains two paths to c: a->b->c and an
// alternative a->d->c.
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),
}, 3),
symmetricTestChannel("a", "d", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
FeeBaseMsat: 100000,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 2),
symmetricTestChannel("d", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
FeeBaseMsat: 100000,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 4),
}
testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, false,
)
// Create a payment to node c.
payment := createDummyLightningPayment(
t, ctx.aliases["c"], lnwire.NewMSatFromSatoshis(1000),
)
// 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
// channel.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
// If channel a->b is used, return an error without
// source and message. The sender won't know the origin
// of the error.
if firstHop.ToUint64() == 1 {
return [32]byte{},
htlcswitch.ErrUnreadableFailureMessage
}
// Otherwise the payment succeeds.
return lntypes.Preimage{}, nil
})
// Send off the payment request to the router. The expectation is that
// the route a->b->c is tried first. An unreadable faiure is returned
// which should pruning the channel a->b. We expect the payment to
// succeed via a->d.
_, _, err = ctx.router.SendPayment(payment)
require.NoErrorf(t, err, "unable to send payment: %v",
payment.paymentHash)
// Next we modify payment result to return an unknown failure.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
// If channel a->b is used, simulate that the failure
// couldn't be decoded (FailureMessage is nil).
if firstHop.ToUint64() == 2 {
return [32]byte{},
htlcswitch.NewUnknownForwardingError(1)
}
// Otherwise the payment succeeds.
return lntypes.Preimage{}, nil
})
// Send off the payment request to the router. We expect the payment to
// fail because both routes have been pruned.
payment.paymentHash[1] ^= 1
_, _, err = ctx.router.SendPayment(payment)
if err == nil {
t.Fatalf("expected payment to fail")
}
}
// assertChannelsPruned ensures that only the given channels are pruned from the
// graph out of the set of all channels.
func assertChannelsPruned(t *testing.T, graph *channeldb.ChannelGraph,
channels []*testChannel, prunedChanIDs ...uint64) {
t.Helper()
pruned := make(map[uint64]struct{}, len(channels))
for _, chanID := range prunedChanIDs {
pruned[chanID] = struct{}{}
}
for _, channel := range channels {
_, shouldPrune := pruned[channel.ChannelID]
_, _, exists, isZombie, err := graph.HasChannelEdge(
channel.ChannelID,
)
if err != nil {
t.Fatalf("unable to determine existence of "+
"channel=%v in the graph: %v",
channel.ChannelID, err)
}
if !shouldPrune && !exists {
t.Fatalf("expected channel=%v to exist within "+
"the graph", channel.ChannelID)
}
if shouldPrune && exists {
t.Fatalf("expected channel=%v to not exist "+
"within the graph", channel.ChannelID)
}
if !shouldPrune && isZombie {
t.Fatalf("expected channel=%v to not be marked "+
"as zombie", channel.ChannelID)
}
if shouldPrune && !isZombie {
t.Fatalf("expected channel=%v to be marked as "+
"zombie", channel.ChannelID)
}
}
}
// TestSendToRouteStructuredError asserts that SendToRoute returns a structured
// error.
func TestSendToRouteStructuredError(t *testing.T) {
t.Parallel()
// Setup a three node network.
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(t, true, testChannels, "a")
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, false,
)
// Set up an init channel for the control tower, such that we can make
// sure the payment is initiated correctly.
init := make(chan initArgs, 1)
ctx.router.cfg.Control.(*mockControlTowerOld).init = init
// Setup a route from source a to destination c. The route will be used
// in a call to SendToRoute. SendToRoute also applies channel updates,
// but it saves us from including RequestRoute in the test scope too.
const payAmt = lnwire.MilliSatoshi(10000)
hop1 := ctx.aliases["b"]
hop2 := ctx.aliases["c"]
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: hop1,
AmtToForward: payAmt,
LegacyPayload: true,
},
{
ChannelID: 2,
PubKeyBytes: hop2,
AmtToForward: payAmt,
LegacyPayload: true,
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, ctx.aliases["a"], hops)
require.NoError(t, err, "unable to create route")
finalHopIndex := len(hops)
testCases := map[int]lnwire.FailureMessage{
finalHopIndex: lnwire.NewFailIncorrectDetails(payAmt, 100),
1: &lnwire.FailFeeInsufficient{
Update: lnwire.ChannelUpdate{},
},
}
for failIndex, errorType := range testCases {
failIndex := failIndex
errorType := errorType
t.Run(fmt.Sprintf("%T", errorType), func(t *testing.T) {
// We'll modify the SendToSwitch method so that it
// simulates a failed payment with an error originating
// from the final hop in the route.
ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcherOld).setPaymentResult(
func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
return [32]byte{}, htlcswitch.NewForwardingError(
errorType, failIndex,
)
},
)
// The payment parameter is mostly redundant in
// SendToRoute. Can be left empty for this test.
var payment lntypes.Hash
// Send off the payment request to the router. The
// specified route should be attempted and the channel
// update should be received by router and ignored
// because it is missing a valid
// signature.
_, err = ctx.router.SendToRoute(payment, rt)
fErr, ok := err.(*htlcswitch.ForwardingError)
require.True(
t, ok, "expected forwarding error, got: %T", err,
)
require.IsType(
t, errorType, fErr.WireMessage(),
"expected type %T got %T", errorType,
fErr.WireMessage(),
)
// Check that the correct values were used when
// initiating the payment.
select {
case initVal := <-init:
if initVal.c.Value != payAmt {
t.Fatalf("expected %v, got %v", payAmt,
initVal.c.Value)
}
case <-time.After(100 * time.Millisecond):
t.Fatalf("initPayment not called")
}
})
}
}
// TestSendToRouteMaxHops asserts that SendToRoute fails when using a route that
// exceeds the maximum number of hops.
func TestSendToRouteMaxHops(t *testing.T) {
t.Parallel()
// Setup a two node network.
chanCapSat := btcutil.Amount(100000)
testChannels := []*testChannel{
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 400,
MinHTLC: 1,
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 1),
}
testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, false,
)
// Create a 30 hop route that exceeds the maximum hop limit.
const payAmt = lnwire.MilliSatoshi(10000)
hopA := ctx.aliases["a"]
hopB := ctx.aliases["b"]
var hops []*route.Hop
for i := 0; i < 15; i++ {
hops = append(hops, &route.Hop{
ChannelID: 1,
PubKeyBytes: hopB,
AmtToForward: payAmt,
LegacyPayload: true,
})
hops = append(hops, &route.Hop{
ChannelID: 1,
PubKeyBytes: hopA,
AmtToForward: payAmt,
LegacyPayload: true,
})
}
rt, err := route.NewRouteFromHops(payAmt, 100, ctx.aliases["a"], hops)
require.NoError(t, err, "unable to create route")
// Send off the payment request to the router. We expect an error back
// indicating that the route is too long.
var payHash lntypes.Hash
_, err = ctx.router.SendToRoute(payHash, rt)
if err != route.ErrMaxRouteHopsExceeded {
t.Fatalf("expected ErrMaxRouteHopsExceeded, but got %v", err)
}
}
// TestBuildRoute tests whether correct routes are built.
func TestBuildRoute(t *testing.T) {
// Setup a three node network.
chanCapSat := btcutil.Amount(100000)
paymentAddrFeatures := lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(lnwire.PaymentAddrOptional),
lnwire.Features,
)
testChannels := []*testChannel{
// Create two local channels from a. The bandwidth is estimated
// in this test as the channel capacity. For building routes, we
// expected the channel with the largest estimated bandwidth to
// be selected.
symmetricTestChannel("a", "b", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 20000,
MinHTLC: lnwire.NewMSatFromSatoshis(5),
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
}, 1),
symmetricTestChannel("a", "b", chanCapSat/2, &testChannelPolicy{
Expiry: 144,
FeeRate: 20000,
MinHTLC: lnwire.NewMSatFromSatoshis(5),
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat / 2),
}, 6),
// Create two channels from b to c. For building routes, we
// expect the lowest cost channel to be selected. Note that this
// isn't a situation that we are expecting in reality. Routing
// nodes are recommended to keep their channel policies towards
// the same peer identical.
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 50000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.NewMSatFromSatoshis(120),
Features: paymentAddrFeatures,
}, 2),
symmetricTestChannel("b", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 60000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.NewMSatFromSatoshis(120),
Features: paymentAddrFeatures,
}, 7),
symmetricTestChannel("a", "e", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 80000,
MinHTLC: lnwire.NewMSatFromSatoshis(5),
MaxHTLC: lnwire.NewMSatFromSatoshis(10),
Features: paymentAddrFeatures,
}, 5),
symmetricTestChannel("e", "c", chanCapSat, &testChannelPolicy{
Expiry: 144,
FeeRate: 100000,
MinHTLC: lnwire.NewMSatFromSatoshis(20),
MaxHTLC: lnwire.NewMSatFromSatoshis(chanCapSat),
Features: paymentAddrFeatures,
}, 4),
}
testGraph, err := createTestGraphFromChannels(t, true, testChannels, "a")
require.NoError(t, err, "unable to create graph")
const startingBlockHeight = 101
ctx := createTestCtxFromGraphInstance(
t, startingBlockHeight, testGraph, false,
)
checkHops := func(rt *route.Route, expected []uint64,
payAddr [32]byte) {
t.Helper()
if len(rt.Hops) != len(expected) {
t.Fatal("hop count mismatch")
}
for i, hop := range rt.Hops {
if hop.ChannelID != expected[i] {
t.Fatalf("expected channel %v at pos %v, but "+
"got channel %v",
expected[i], i, hop.ChannelID)
}
}
lastHop := rt.Hops[len(rt.Hops)-1]
require.NotNil(t, lastHop.MPP)
require.Equal(t, lastHop.MPP.PaymentAddr(), payAddr)
}
var payAddr [32]byte
_, err = rand.Read(payAddr[:])
require.NoError(t, err)
// Create hop list from the route node pubkeys.
hops := []route.Vertex{
ctx.aliases["b"], ctx.aliases["c"],
}
amt := lnwire.NewMSatFromSatoshis(100)
// Build the route for the given amount.
rt, err := ctx.router.BuildRoute(
&amt, hops, nil, 40, &payAddr,
)
if err != nil {
t.Fatal(err)
}
// Check that we get the expected route back. The total amount should be
// the amount to deliver to hop c (100 sats) plus the max fee for the
// connection b->c (6 sats).
checkHops(rt, []uint64{1, 7}, payAddr)
if rt.TotalAmount != 106000 {
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
}
// Build the route for the minimum amount.
rt, err = ctx.router.BuildRoute(
nil, hops, nil, 40, &payAddr,
)
if err != nil {
t.Fatal(err)
}
// Check that we get the expected route back. The minimum that we can
// send from b to c is 20 sats. Hop b charges 1200 msat for the
// forwarding. The channel between hop a and b can carry amounts in the
// range [5, 100], so 21200 msats is the minimum amount for this route.
checkHops(rt, []uint64{1, 7}, payAddr)
if rt.TotalAmount != 21200 {
t.Fatalf("unexpected total amount %v", rt.TotalAmount)
}
// Test a route that contains incompatible channel htlc constraints.
// There is no amount that can pass through both channel 5 and 4.
hops = []route.Vertex{
ctx.aliases["e"], ctx.aliases["c"],
}
_, err = ctx.router.BuildRoute(
nil, hops, nil, 40, nil,
)
errNoChannel, ok := err.(ErrNoChannel)
if !ok {
t.Fatalf("expected incompatible policies error, but got %v",
err)
}
if errNoChannel.position != 0 {
t.Fatalf("unexpected no channel error position")
}
if errNoChannel.fromNode != ctx.aliases["a"] {
t.Fatalf("unexpected no channel error node")
}
}
// TestGetPathEdges tests that the getPathEdges function returns the expected
// edges and amount when given a set of unifiers and does not panic.
func TestGetPathEdges(t *testing.T) {
t.Parallel()
const startingBlockHeight = 101
ctx := createTestCtxFromFile(t, startingBlockHeight, basicGraphFilePath)
testCases := []struct {
sourceNode route.Vertex
amt lnwire.MilliSatoshi
unifiers []*edgeUnifier
bandwidthHints *bandwidthManager
hops []route.Vertex
expectedEdges []*models.CachedEdgePolicy
expectedAmt lnwire.MilliSatoshi
expectedErr string
}{{
sourceNode: ctx.aliases["roasbeef"],
unifiers: []*edgeUnifier{
{
edges: []*unifiedEdge{},
localChan: true,
},
},
expectedErr: fmt.Sprintf("no matching outgoing channel "+
"available for node 0 (%v)", ctx.aliases["roasbeef"]),
}}
for _, tc := range testCases {
pathEdges, amt, err := getPathEdges(
tc.sourceNode, tc.amt, tc.unifiers, tc.bandwidthHints,
tc.hops,
)
if tc.expectedErr != "" {
require.Error(t, err)
require.ErrorContains(t, err, tc.expectedErr)
continue
}
require.NoError(t, err)
require.Equal(t, pathEdges, tc.expectedEdges)
require.Equal(t, amt, tc.expectedAmt)
}
}
// edgeCreationModifier is an enum-like type used to modify steps that are
// skipped when creating a channel in the test context.
type edgeCreationModifier uint8
const (
// edgeCreationNoFundingTx is used to skip adding the funding
// transaction of an edge to the chain.
edgeCreationNoFundingTx edgeCreationModifier = iota
// edgeCreationNoUTXO is used to skip adding the UTXO of a channel to
// the UTXO set.
edgeCreationNoUTXO
// edgeCreationBadScript is used to create the edge, but use the wrong
// scrip which should cause it to fail output validation.
edgeCreationBadScript
)
// newChannelEdgeInfo is a helper function used to create a new channel edge,
// possibly skipping adding it to parts of the chain/state as well.
func newChannelEdgeInfo(ctx *testCtx, fundingHeight uint32,
ecm edgeCreationModifier) (*models.ChannelEdgeInfo, error) {
node1, err := createTestNode()
if err != nil {
return nil, err
}
node2, err := createTestNode()
if err != nil {
return nil, err
}
fundingTx, _, chanID, err := createChannelEdge(
ctx, bitcoinKey1.SerializeCompressed(),
bitcoinKey2.SerializeCompressed(), 100, fundingHeight,
)
if err != nil {
return nil, fmt.Errorf("unable to create edge: %w", err)
}
edge := &models.ChannelEdgeInfo{
ChannelID: chanID.ToUint64(),
NodeKey1Bytes: node1.PubKeyBytes,
NodeKey2Bytes: node2.PubKeyBytes,
}
copy(edge.BitcoinKey1Bytes[:], bitcoinKey1.SerializeCompressed())
copy(edge.BitcoinKey2Bytes[:], bitcoinKey2.SerializeCompressed())
if ecm == edgeCreationNoFundingTx {
return edge, nil
}
fundingBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{fundingTx},
}
ctx.chain.addBlock(fundingBlock, chanID.BlockHeight, chanID.BlockHeight)
if ecm == edgeCreationNoUTXO {
ctx.chain.delUtxo(wire.OutPoint{
Hash: fundingTx.TxHash(),
})
}
if ecm == edgeCreationBadScript {
fundingTx.TxOut[0].PkScript[0] ^= 1
}
return edge, nil
}
func assertChanChainRejection(t *testing.T, ctx *testCtx,
edge *models.ChannelEdgeInfo, failCode errorCode) {
t.Helper()
err := ctx.router.AddEdge(edge)
if !IsError(err, failCode) {
t.Fatalf("validation should have failed: %v", err)
}
// This channel should now be present in the zombie channel index.
_, _, _, isZombie, err := ctx.graph.HasChannelEdge(
edge.ChannelID,
)
require.Nil(t, err)
require.True(t, isZombie, "edge should be marked as zombie")
}
// TestChannelOnChainRejectionZombie tests that if we fail validating a channel
// due to some sort of on-chain rejection (no funding transaction, or invalid
// UTXO), then we'll mark the channel as a zombie.
func TestChannelOnChainRejectionZombie(t *testing.T) {
t.Parallel()
ctx := createTestCtxSingleNode(t, 0)
// To start, we'll make an edge for the channel, but we won't add the
// funding transaction to the mock blockchain, which should cause the
// validation to fail below.
edge, err := newChannelEdgeInfo(ctx, 1, edgeCreationNoFundingTx)
require.Nil(t, err)
// We expect this to fail as the transaction isn't present in the
// chain (nor the block).
assertChanChainRejection(t, ctx, edge, ErrNoFundingTransaction)
// Next, we'll make another channel edge, but actually add it to the
// graph this time.
edge, err = newChannelEdgeInfo(ctx, 2, edgeCreationNoUTXO)
require.Nil(t, err)
// Instead now, we'll remove it from the set of UTXOs which should
// cause the spentness validation to fail.
assertChanChainRejection(t, ctx, edge, ErrChannelSpent)
// If we cause the funding transaction the chain to fail validation, we
// should see similar behavior.
edge, err = newChannelEdgeInfo(ctx, 3, edgeCreationBadScript)
require.Nil(t, err)
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(t, true, 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,
}
}
// TestBlockDifferenceFix tests if when the router is behind on blocks, the
// router catches up to the best block head.
func TestBlockDifferenceFix(t *testing.T) {
t.Parallel()
initialBlockHeight := uint32(0)
// Starting height here is set to 0, which is behind where we want to
// be.
ctx := createTestCtxSingleNode(t, initialBlockHeight)
// Add initial block to our mini blockchain.
block := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
ctx.chain.addBlock(block, initialBlockHeight, rand.Uint32())
// Let's generate a new block of height 5, 5 above where our node is at.
newBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
}
newBlockHeight := uint32(5)
blockDifference := newBlockHeight - initialBlockHeight
ctx.chainView.notifyBlockAck = make(chan struct{}, 1)
ctx.chain.addBlock(newBlock, newBlockHeight, rand.Uint32())
ctx.chain.setBestBlock(int32(newBlockHeight))
ctx.chainView.notifyBlock(block.BlockHash(), newBlockHeight,
[]*wire.MsgTx{}, t)
<-ctx.chainView.notifyBlockAck
// At this point, the chain notifier should have noticed that we're
// behind on blocks, and will send the n missing blocks that we
// need to the client's epochs channel. Let's replicate this
// functionality.
for i := 0; i < int(blockDifference); i++ {
currBlockHeight := int32(i + 1)
nonce := rand.Uint32()
newBlock := &wire.MsgBlock{
Transactions: []*wire.MsgTx{},
Header: wire.BlockHeader{Nonce: nonce},
}
ctx.chain.addBlock(newBlock, uint32(currBlockHeight), nonce)
currHash := newBlock.Header.BlockHash()
newEpoch := &chainntnfs.BlockEpoch{
Height: currBlockHeight,
Hash: &currHash,
}
ctx.notifier.EpochChan <- newEpoch
ctx.chainView.notifyBlock(currHash,
uint32(currBlockHeight), block.Transactions, t)
<-ctx.chainView.notifyBlockAck
}
err := wait.NoError(func() error {
// Then router height should be updated to the latest block.
if atomic.LoadUint32(&ctx.router.bestHeight) != newBlockHeight {
return fmt.Errorf("height should have been updated "+
"to %v, instead got %v", newBlockHeight,
ctx.router.bestHeight)
}
return nil
}, testTimeout)
require.NoError(t, err, "block height wasn't updated")
}
// TestSendToRouteSkipTempErrSuccess validates a successful payment send.
func TestSendToRouteSkipTempErrSuccess(t *testing.T) {
t.Parallel()
var (
payHash lntypes.Hash
payAmt = lnwire.MilliSatoshi(10000)
)
preimage := lntypes.Preimage{1}
testAttempt := makeSettledAttempt(t, int(payAmt), preimage)
node, err := createTestNode()
require.NoError(t, err)
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
require.NoError(t, err)
// Create mockers.
controlTower := &mockControlTower{}
payer := &mockPaymentAttemptDispatcher{}
missionControl := &mockMissionControl{}
// Create the router.
router := &ChannelRouter{cfg: &Config{
Control: controlTower,
Payer: payer,
MissionControl: missionControl,
Clock: clock.NewTestClock(time.Unix(1, 0)),
NextPaymentID: func() (uint64, error) {
return 0, nil
},
}}
// Register mockers with the expected method calls.
controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
controlTower.On("SettleAttempt",
payHash, mock.Anything, mock.Anything,
).Return(testAttempt, nil)
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(nil)
// Create a buffered chan and it will be returned by GetAttemptResult.
resultChan := make(chan *htlcswitch.PaymentResult, 1)
payer.On("GetAttemptResult",
mock.Anything, mock.Anything, mock.Anything,
).Return(resultChan, nil).Run(func(_ mock.Arguments) {
// Send a successful payment result.
resultChan <- &htlcswitch.PaymentResult{}
})
missionControl.On("ReportPaymentSuccess",
mock.Anything, rt,
).Return(nil)
// Mock the control tower to return the mocked payment.
payment := &mockMPPayment{}
controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()
// Mock the payment to return nil failure reason.
payment.On("TerminalInfo").Return(nil, nil)
// Expect a successful send to route.
attempt, err := router.SendToRouteSkipTempErr(payHash, rt)
require.NoError(t, err)
require.Equal(t, testAttempt, attempt)
// Assert the above methods are called as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestSendToRouteSkipTempErrNonMPP checks that an error is return when
// skipping temp error for non-MPP.
func TestSendToRouteSkipTempErrNonMPP(t *testing.T) {
t.Parallel()
var (
payHash lntypes.Hash
payAmt = lnwire.MilliSatoshi(10000)
)
node, err := createTestNode()
require.NoError(t, err)
// Create a simple 1-hop route without the MPP field.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
require.NoError(t, err)
// Create mockers.
controlTower := &mockControlTower{}
payer := &mockPaymentAttemptDispatcher{}
missionControl := &mockMissionControl{}
// Create the router.
router := &ChannelRouter{cfg: &Config{
Control: controlTower,
Payer: payer,
MissionControl: missionControl,
Clock: clock.NewTestClock(time.Unix(1, 0)),
NextPaymentID: func() (uint64, error) {
return 0, nil
},
}}
// Expect an error to be returned.
attempt, err := router.SendToRouteSkipTempErr(payHash, rt)
require.ErrorIs(t, ErrSkipTempErr, err)
require.Nil(t, attempt)
// Assert the above methods are not called.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
missionControl.AssertExpectations(t)
}
// TestSendToRouteSkipTempErrTempFailure validates a temporary failure won't
// cause the payment to be failed.
func TestSendToRouteSkipTempErrTempFailure(t *testing.T) {
t.Parallel()
var (
payHash lntypes.Hash
payAmt = lnwire.MilliSatoshi(10000)
)
testAttempt := makeFailedAttempt(t, int(payAmt))
node, err := createTestNode()
require.NoError(t, err)
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
require.NoError(t, err)
// Create mockers.
controlTower := &mockControlTower{}
payer := &mockPaymentAttemptDispatcher{}
missionControl := &mockMissionControl{}
// Create the router.
router := &ChannelRouter{cfg: &Config{
Control: controlTower,
Payer: payer,
MissionControl: missionControl,
Clock: clock.NewTestClock(time.Unix(1, 0)),
NextPaymentID: func() (uint64, error) {
return 0, nil
},
}}
// Create the error to be returned.
tempErr := htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{}, 1,
)
// Register mockers with the expected method calls.
controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
controlTower.On("FailAttempt",
payHash, mock.Anything, mock.Anything,
).Return(testAttempt, nil)
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(tempErr)
// Mock the control tower to return the mocked payment.
payment := &mockMPPayment{}
controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()
// Mock the mission control to return a nil reason from reporting the
// attempt failure.
missionControl.On("ReportPaymentFail",
mock.Anything, rt, mock.Anything, mock.Anything,
).Return(nil, nil)
// Mock the payment to return nil failure reason.
payment.On("TerminalInfo").Return(nil, nil)
// Expect a failed send to route.
attempt, err := router.SendToRouteSkipTempErr(payHash, rt)
require.Equal(t, tempErr, err)
require.Equal(t, testAttempt, attempt)
// Assert the above methods are called as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestSendToRouteSkipTempErrPermanentFailure validates a permanent failure
// will fail the payment.
func TestSendToRouteSkipTempErrPermanentFailure(t *testing.T) {
var (
payHash lntypes.Hash
payAmt = lnwire.MilliSatoshi(10000)
)
testAttempt := makeFailedAttempt(t, int(payAmt))
node, err := createTestNode()
require.NoError(t, err)
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
require.NoError(t, err)
// Create mockers.
controlTower := &mockControlTower{}
payer := &mockPaymentAttemptDispatcher{}
missionControl := &mockMissionControl{}
// Create the router.
router := &ChannelRouter{cfg: &Config{
Control: controlTower,
Payer: payer,
MissionControl: missionControl,
Clock: clock.NewTestClock(time.Unix(1, 0)),
NextPaymentID: func() (uint64, error) {
return 0, nil
},
}}
// Create the error to be returned.
permErr := htlcswitch.NewForwardingError(
&lnwire.FailIncorrectDetails{}, 1,
)
// Register mockers with the expected method calls.
controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
controlTower.On("FailAttempt",
payHash, mock.Anything, mock.Anything,
).Return(testAttempt, nil)
// Expect the payment to be failed.
controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)
// Mock an error to be returned from sending the htlc.
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(permErr)
failureReason := channeldb.FailureReasonPaymentDetails
missionControl.On("ReportPaymentFail",
mock.Anything, rt, mock.Anything, mock.Anything,
).Return(&failureReason, nil)
// Mock the control tower to return the mocked payment.
payment := &mockMPPayment{}
controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()
// Mock the payment to return a failure reason.
payment.On("TerminalInfo").Return(nil, &failureReason)
// Expect a failed send to route.
attempt, err := router.SendToRouteSkipTempErr(payHash, rt)
require.Equal(t, permErr, err)
require.Equal(t, testAttempt, attempt)
// Assert the above methods are called as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestSendToRouteTempFailure validates a temporary failure will cause the
// payment to be failed.
func TestSendToRouteTempFailure(t *testing.T) {
var (
payHash lntypes.Hash
payAmt = lnwire.MilliSatoshi(10000)
)
testAttempt := makeFailedAttempt(t, int(payAmt))
node, err := createTestNode()
require.NoError(t, err)
// Create a simple 1-hop route.
hops := []*route.Hop{
{
ChannelID: 1,
PubKeyBytes: node.PubKeyBytes,
AmtToForward: payAmt,
OutgoingTimeLock: 120,
MPP: record.NewMPP(payAmt, [32]byte{}),
},
}
rt, err := route.NewRouteFromHops(payAmt, 100, node.PubKeyBytes, hops)
require.NoError(t, err)
// Create mockers.
controlTower := &mockControlTower{}
payer := &mockPaymentAttemptDispatcher{}
missionControl := &mockMissionControl{}
// Create the router.
router := &ChannelRouter{cfg: &Config{
Control: controlTower,
Payer: payer,
MissionControl: missionControl,
Clock: clock.NewTestClock(time.Unix(1, 0)),
NextPaymentID: func() (uint64, error) {
return 0, nil
},
}}
// Create the error to be returned.
tempErr := htlcswitch.NewForwardingError(
&lnwire.FailTemporaryChannelFailure{}, 1,
)
// Register mockers with the expected method calls.
controlTower.On("InitPayment", payHash, mock.Anything).Return(nil)
controlTower.On("RegisterAttempt", payHash, mock.Anything).Return(nil)
controlTower.On("FailAttempt",
payHash, mock.Anything, mock.Anything,
).Return(testAttempt, nil)
// Expect the payment to be failed.
controlTower.On("FailPayment", payHash, mock.Anything).Return(nil)
payer.On("SendHTLC",
mock.Anything, mock.Anything, mock.Anything,
).Return(tempErr)
// Mock the control tower to return the mocked payment.
payment := &mockMPPayment{}
controlTower.On("FetchPayment", payHash).Return(payment, nil).Once()
// Mock the payment to return nil failure reason.
payment.On("TerminalInfo").Return(nil, nil)
// Return a nil reason to mock a temporary failure.
missionControl.On("ReportPaymentFail",
mock.Anything, rt, mock.Anything, mock.Anything,
).Return(nil, nil)
// Expect a failed send to route.
attempt, err := router.SendToRoute(payHash, rt)
require.Equal(t, tempErr, err)
require.Equal(t, testAttempt, attempt)
// Assert the above methods are called as expected.
controlTower.AssertExpectations(t)
payer.AssertExpectations(t)
missionControl.AssertExpectations(t)
payment.AssertExpectations(t)
}
// TestNewRouteRequest tests creation of route requests for blinded and
// unblinded routes.
func TestNewRouteRequest(t *testing.T) {
t.Parallel()
//nolint:lll
source, err := route.NewVertexFromStr("0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6")
require.NoError(t, err)
sourcePubkey, err := btcec.ParsePubKey(source[:])
require.NoError(t, err)
//nolint:lll
v1, err := route.NewVertexFromStr("026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318")
require.NoError(t, err)
pubkey1, err := btcec.ParsePubKey(v1[:])
require.NoError(t, err)
//nolint:lll
v2, err := route.NewVertexFromStr("03c19f0027ffbb0ae0e14a4d958788793f9d74e107462473ec0c3891e4feb12e99")
require.NoError(t, err)
pubkey2, err := btcec.ParsePubKey(v2[:])
require.NoError(t, err)
var (
unblindedCltv uint16 = 500
blindedCltv uint16 = 1000
)
blindedSelfIntro := &BlindedPayment{
CltvExpiryDelta: blindedCltv,
BlindedPath: &sphinx.BlindedPath{
IntroductionPoint: sourcePubkey,
BlindedHops: []*sphinx.BlindedHopInfo{{}},
},
}
blindedOtherIntro := &BlindedPayment{
CltvExpiryDelta: blindedCltv,
BlindedPath: &sphinx.BlindedPath{
IntroductionPoint: pubkey1,
BlindedHops: []*sphinx.BlindedHopInfo{
{},
},
},
}
blindedMultiHop := &BlindedPayment{
CltvExpiryDelta: blindedCltv,
BlindedPath: &sphinx.BlindedPath{
IntroductionPoint: pubkey1,
BlindedHops: []*sphinx.BlindedHopInfo{
{},
{
BlindedNodePub: pubkey2,
},
},
},
}
testCases := []struct {
name string
target *route.Vertex
routeHints RouteHints
blindedPayment *BlindedPayment
finalExpiry uint16
expectedTarget route.Vertex
expectedCltv uint16
err error
}{
{
name: "blinded and target",
target: &v1,
blindedPayment: blindedOtherIntro,
err: ErrTargetAndBlinded,
},
{
// For single-hop blinded we have a final cltv.
name: "blinded intro node only",
blindedPayment: blindedOtherIntro,
expectedTarget: v1,
expectedCltv: blindedCltv,
err: nil,
},
{
// For multi-hop blinded, we have no final cltv.
name: "blinded multi-hop",
blindedPayment: blindedMultiHop,
expectedTarget: v2,
expectedCltv: 0,
err: nil,
},
{
name: "unblinded",
target: &v2,
finalExpiry: unblindedCltv,
expectedTarget: v2,
expectedCltv: unblindedCltv,
err: nil,
},
{
name: "source node intro",
blindedPayment: blindedSelfIntro,
err: ErrSelfIntro,
},
{
name: "hints and blinded",
blindedPayment: blindedMultiHop,
routeHints: make(
map[route.Vertex][]AdditionalEdge,
),
err: ErrHintsAndBlinded,
},
{
name: "expiry and blinded",
blindedPayment: blindedMultiHop,
finalExpiry: unblindedCltv,
err: ErrExpiryAndBlinded,
},
{
name: "invalid blinded payment",
blindedPayment: &BlindedPayment{},
err: ErrNoBlindedPath,
},
}
for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
req, err := NewRouteRequest(
source, testCase.target, 1000, 0, nil, nil,
testCase.routeHints, testCase.blindedPayment,
testCase.finalExpiry,
)
require.ErrorIs(t, err, testCase.err)
// Skip request validation if we got a non-nil error.
if err != nil {
return
}
require.Equal(t, req.Target, testCase.expectedTarget)
require.Equal(
t, req.FinalExpiry, testCase.expectedCltv,
)
})
}
}