From f1cb54f943b9aa2dae5088bea1f451b125855333 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 16 May 2019 15:27:28 +0200 Subject: [PATCH] routing/router: define PaymentAttemptDispatcher interface The switch satisfies this interface, and makes it easy to mock the send method from the router. --- routing/mock_test.go | 31 ++++ routing/router.go | 35 +++-- routing/router_test.go | 317 ++++++++++++++++++++--------------------- server.go | 22 +-- 4 files changed, 214 insertions(+), 191 deletions(-) create mode 100644 routing/mock_test.go diff --git a/routing/mock_test.go b/routing/mock_test.go new file mode 100644 index 00000000..2552b2fd --- /dev/null +++ b/routing/mock_test.go @@ -0,0 +1,31 @@ +package routing + +import ( + "crypto/sha256" + + "github.com/lightningnetwork/lnd/htlcswitch" + "github.com/lightningnetwork/lnd/lnwire" +) + +type mockPaymentAttemptDispatcher struct { + onPayment func(firstHop lnwire.ShortChannelID) ([32]byte, error) +} + +var _ PaymentAttemptDispatcher = (*mockPaymentAttemptDispatcher)(nil) + +func (m *mockPaymentAttemptDispatcher) SendHTLC(firstHop lnwire.ShortChannelID, + _ *lnwire.UpdateAddHTLC, + _ htlcswitch.ErrorDecrypter) ([sha256.Size]byte, error) { + + if m.onPayment != nil { + return m.onPayment(firstHop) + } + + return [sha256.Size]byte{}, nil +} + +func (m *mockPaymentAttemptDispatcher) setPaymentResult( + f func(firstHop lnwire.ShortChannelID) ([32]byte, error)) { + + m.onPayment = f +} diff --git a/routing/router.go b/routing/router.go index d49533c6..3b1d8478 100644 --- a/routing/router.go +++ b/routing/router.go @@ -126,6 +126,18 @@ type ChannelGraphSource interface { e1, e2 *channeldb.ChannelEdgePolicy) error) error } +// PaymentAttemptDispatcher is used by the router to send payment attempts onto +// the network, and receive their results. +type PaymentAttemptDispatcher interface { + // SendHTLC is a function that directs a link-layer switch to + // forward a fully encoded payment to the first hop in the route + // denoted by its public key. A non-nil error is to be returned if the + // payment was unsuccessful. + SendHTLC(firstHop lnwire.ShortChannelID, + htlcAdd *lnwire.UpdateAddHTLC, + deobfuscator htlcswitch.ErrorDecrypter) ([sha256.Size]byte, error) +} + // FeeSchema is the set fee configuration for a Lightning Node on the network. // Using the coefficients described within the schema, the required fee to // forward outgoing payments can be derived. @@ -173,13 +185,10 @@ type Config struct { // we need in order to properly maintain the channel graph. ChainView chainview.FilteredChainView - // SendToSwitch is a function that directs a link-layer switch to - // forward a fully encoded payment to the first hop in the route - // denoted by its public key. A non-nil error is to be returned if the - // payment was unsuccessful. - SendToSwitch func(firstHop lnwire.ShortChannelID, - htlcAdd *lnwire.UpdateAddHTLC, - circuit *sphinx.Circuit) ([sha256.Size]byte, error) + // Payer is an instance of a PaymentAttemptDispatcher and is used by + // the router to send payment attempts onto the network, and receive + // their results. + Payer PaymentAttemptDispatcher // ChannelPruneExpiry is the duration used to determine if a channel // should be pruned or not. If the delta between now and when the @@ -1698,8 +1707,16 @@ func (r *ChannelRouter) sendToSwitch(route *route.Route, paymentHash [32]byte) ( firstHop := lnwire.NewShortChanIDFromInt( route.Hops[0].ChannelID, ) - return r.cfg.SendToSwitch( - firstHop, htlcAdd, circuit, + + // Using the created circuit, initialize the error decrypter so we can + // parse+decode any failures incurred by this payment within the + // switch. + errorDecryptor := &htlcswitch.SphinxErrorDecrypter{ + OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit), + } + + return r.cfg.Payer.SendHTLC( + firstHop, htlcAdd, errorDecryptor, ) } diff --git a/routing/router_test.go b/routing/router_test.go index f4367d29..b1e34383 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -15,7 +15,6 @@ import ( "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" - sphinx "github.com/lightningnetwork/lightning-onion" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lntypes" @@ -44,13 +43,10 @@ func (c *testCtx) RestartRouter() error { // 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, - SendToSwitch: func(_ lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { - return [32]byte{}, nil - }, + Graph: c.graph, + Chain: c.chain, + ChainView: c.chainView, + Payer: &mockPaymentAttemptDispatcher{}, ChannelPruneExpiry: time.Hour * 24, GraphPruneInterval: time.Hour * 2, }) @@ -85,14 +81,10 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr chain := newMockChain(startingHeight) chainView := newMockChainView(chain) router, err := New(Config{ - Graph: graphInstance.graph, - Chain: chain, - ChainView: chainView, - SendToSwitch: func(_ lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { - - return [32]byte{}, nil - }, + Graph: graphInstance.graph, + Chain: chain, + ChainView: chainView, + Payer: &mockPaymentAttemptDispatcher{}, ChannelPruneExpiry: time.Hour * 24, GraphPruneInterval: time.Hour * 2, QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { @@ -250,24 +242,24 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) { // router's configuration to ignore the path that has luo ji as the // first hop. This should force the router to instead take the // available two hop path (through satoshi). - ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - roasbeefLuoji := lnwire.NewShortChanIDFromInt(689530843) - if firstHop == roasbeefLuoji { - pub, err := sourceNode.PubKey() - if err != nil { - return preImage, err + roasbeefLuoji := lnwire.NewShortChanIDFromInt(689530843) + if firstHop == roasbeefLuoji { + pub, err := sourceNode.PubKey() + if err != nil { + return preImage, err + } + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: pub, + // TODO(roasbeef): temp node failure should be? + FailureMessage: &lnwire.FailTemporaryChannelFailure{}, + } } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: pub, - // TODO(roasbeef): temp node failure should be? - FailureMessage: &lnwire.FailTemporaryChannelFailure{}, - } - } - return preImage, nil - } + return preImage, nil + }) // Send off the payment request to the router, route through satoshi // should've been selected as a fall back and succeeded correctly. @@ -387,24 +379,24 @@ func TestChannelUpdateValidation(t *testing.T) { // 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.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - v := ctx.aliases["b"] - source, err := btcec.ParsePubKey( - v[:], btcec.S256(), - ) - if err != nil { - t.Fatal(err) - } + v := ctx.aliases["b"] + source, err := btcec.ParsePubKey( + v[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: source, - FailureMessage: &lnwire.FailFeeInsufficient{ - Update: errChanUpdate, - }, - } - } + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: source, + FailureMessage: &lnwire.FailFeeInsufficient{ + Update: errChanUpdate, + }, + } + }) // The payment parameter is mostly redundant in SendToRoute. Can be left // empty for this test. @@ -518,32 +510,32 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { // 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.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID) - if firstHop == roasbeefSongoku { - sourceKey, err := btcec.ParsePubKey( - sourceNode[:], btcec.S256(), - ) - if err != nil { - t.Fatal(err) + roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID) + if firstHop == roasbeefSongoku { + sourceKey, err := btcec.ParsePubKey( + sourceNode[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } + + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: sourceKey, + + // Within our error, we'll add a channel update + // which is meant to reflect he new fee + // schedule for the node/channel. + FailureMessage: &lnwire.FailFeeInsufficient{ + Update: errChanUpdate, + }, + } } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: sourceKey, - - // Within our error, we'll add a channel update - // which is meant to reflect he new fee - // schedule for the node/channel. - FailureMessage: &lnwire.FailFeeInsufficient{ - Update: errChanUpdate, - }, - } - } - - return preImage, nil - } + return preImage, nil + }) // Send off the payment request to the router, route through satoshi // should've been selected as a fall back and succeeded correctly. @@ -633,27 +625,27 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // 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.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - if firstHop == roasbeefSongoku { - sourceKey, err := btcec.ParsePubKey( - sourceNode[:], btcec.S256(), - ) - if err != nil { - t.Fatal(err) + if firstHop == roasbeefSongoku { + sourceKey, err := btcec.ParsePubKey( + sourceNode[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } + + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: sourceKey, + FailureMessage: &lnwire.FailExpiryTooSoon{ + Update: errChanUpdate, + }, + } } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: sourceKey, - FailureMessage: &lnwire.FailExpiryTooSoon{ - Update: errChanUpdate, - }, - } - } - - return preImage, nil - } + return preImage, nil + }) // assertExpectedPath is a helper function that asserts the returned // route properly routes around the failure we've introduced in the @@ -694,27 +686,27 @@ func TestSendPaymentErrorNonFinalTimeLockErrors(t *testing.T) { // 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.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - if firstHop == roasbeefSongoku { - sourceKey, err := btcec.ParsePubKey( - sourceNode[:], btcec.S256(), - ) - if err != nil { - t.Fatal(err) + if firstHop == roasbeefSongoku { + sourceKey, err := btcec.ParsePubKey( + sourceNode[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } + + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: sourceKey, + FailureMessage: &lnwire.FailIncorrectCltvExpiry{ + Update: errChanUpdate, + }, + } } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: sourceKey, - FailureMessage: &lnwire.FailIncorrectCltvExpiry{ - Update: errChanUpdate, - }, - } - } - - return preImage, nil - } + 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. @@ -771,40 +763,40 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // // TODO(roasbeef): filtering should be intelligent enough so just not // go through satoshi at all at this point. - ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - if firstHop == roasbeefLuoji { - // 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.ForwardingError{ - ErrorSource: sourcePub, - FailureMessage: &lnwire.FailTemporaryChannelFailure{}, - } - } - - // Next, we'll create an error from satoshi to indicate - // that the luoji node is not longer online, which should - // prune out the rest of the routes. - roasbeefSatoshi := lnwire.NewShortChanIDFromInt(2340213491) - if firstHop == roasbeefSatoshi { - vertex := ctx.aliases["satoshi"] - key, err := btcec.ParsePubKey( - vertex[:], btcec.S256(), - ) - if err != nil { - t.Fatal(err) + if firstHop == roasbeefLuoji { + // 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.ForwardingError{ + ErrorSource: sourcePub, + FailureMessage: &lnwire.FailTemporaryChannelFailure{}, + } } - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: key, - FailureMessage: &lnwire.FailUnknownNextPeer{}, - } - } + // Next, we'll create an error from satoshi to indicate + // that the luoji node is not longer online, which should + // prune out the rest of the routes. + roasbeefSatoshi := lnwire.NewShortChanIDFromInt(2340213491) + if firstHop == roasbeefSatoshi { + vertex := ctx.aliases["satoshi"] + key, err := btcec.ParsePubKey( + vertex[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } - return preImage, nil - } + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: key, + FailureMessage: &lnwire.FailUnknownNextPeer{}, + } + } + + return preImage, nil + }) ctx.router.missionControl.ResetHistory() @@ -826,18 +818,18 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // Next, we'll modify the SendToSwitch method to indicate that luo ji // wasn't originally online. This should also halt the send all // together as all paths contain luoji and he can't be reached. - ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - if firstHop == roasbeefLuoji { - return [32]byte{}, &htlcswitch.ForwardingError{ - ErrorSource: sourcePub, - FailureMessage: &lnwire.FailUnknownNextPeer{}, + if firstHop == roasbeefLuoji { + return [32]byte{}, &htlcswitch.ForwardingError{ + ErrorSource: sourcePub, + FailureMessage: &lnwire.FailUnknownNextPeer{}, + } } - } - return preImage, nil - } + return preImage, nil + }) // This shouldn't return an error, as we'll make a payment attempt via // the satoshi channel based on the assumption that there might be an @@ -869,20 +861,20 @@ func TestSendPaymentErrorPathPruning(t *testing.T) { // 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.SendToSwitch = func(firstHop lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult( + func(firstHop lnwire.ShortChannelID) ([32]byte, error) { - if firstHop == roasbeefLuoji { - // 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.ForwardingError{ - ErrorSource: sourcePub, - FailureMessage: &lnwire.FailTemporaryChannelFailure{}, + if firstHop == roasbeefLuoji { + // 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.ForwardingError{ + ErrorSource: sourcePub, + FailureMessage: &lnwire.FailTemporaryChannelFailure{}, + } } - } - return preImage, nil - } + return preImage, nil + }) paymentPreImage, rt, err = ctx.router.SendPayment(&payment) if err != nil { @@ -1525,13 +1517,10 @@ func TestWakeUpOnStaleBranch(t *testing.T) { // Create new router with same graph database. router, err := New(Config{ - Graph: ctx.graph, - Chain: ctx.chain, - ChainView: ctx.chainView, - SendToSwitch: func(_ lnwire.ShortChannelID, - _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { - return [32]byte{}, nil - }, + Graph: ctx.graph, + Chain: ctx.chain, + ChainView: ctx.chainView, + Payer: &mockPaymentAttemptDispatcher{}, ChannelPruneExpiry: time.Hour * 24, GraphPruneInterval: time.Hour * 2, }) diff --git a/server.go b/server.go index 72cbb1e3..02683c8f 100644 --- a/server.go +++ b/server.go @@ -610,24 +610,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, s.currentNodeAnn = nodeAnn s.chanRouter, err = routing.New(routing.Config{ - Graph: chanGraph, - Chain: cc.chainIO, - ChainView: cc.chainView, - SendToSwitch: func(firstHop lnwire.ShortChannelID, - htlcAdd *lnwire.UpdateAddHTLC, - circuit *sphinx.Circuit) ([32]byte, error) { - - // Using the created circuit, initialize the error - // decrypter so we can parse+decode any failures - // incurred by this payment within the switch. - errorDecryptor := &htlcswitch.SphinxErrorDecrypter{ - OnionErrorDecrypter: sphinx.NewOnionErrorDecrypter(circuit), - } - - return s.htlcSwitch.SendHTLC( - firstHop, htlcAdd, errorDecryptor, - ) - }, + Graph: chanGraph, + Chain: cc.chainIO, + ChainView: cc.chainView, + Payer: s.htlcSwitch, ChannelPruneExpiry: routing.DefaultChannelPruneExpiry, GraphPruneInterval: time.Duration(time.Hour), QueryBandwidth: func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {