routing/router: define PaymentAttemptDispatcher interface

The switch satisfies this interface, and makes it easy to mock the send
method from the router.
This commit is contained in:
Johan T. Halseth 2019-05-16 15:27:28 +02:00
parent 27ae22fa6c
commit f1cb54f943
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
4 changed files with 214 additions and 191 deletions

31
routing/mock_test.go Normal file

@ -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
}

@ -126,6 +126,18 @@ type ChannelGraphSource interface {
e1, e2 *channeldb.ChannelEdgePolicy) error) error 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. // FeeSchema is the set fee configuration for a Lightning Node on the network.
// Using the coefficients described within the schema, the required fee to // Using the coefficients described within the schema, the required fee to
// forward outgoing payments can be derived. // forward outgoing payments can be derived.
@ -173,13 +185,10 @@ type Config struct {
// we need in order to properly maintain the channel graph. // we need in order to properly maintain the channel graph.
ChainView chainview.FilteredChainView ChainView chainview.FilteredChainView
// SendToSwitch is a function that directs a link-layer switch to // Payer is an instance of a PaymentAttemptDispatcher and is used by
// forward a fully encoded payment to the first hop in the route // the router to send payment attempts onto the network, and receive
// denoted by its public key. A non-nil error is to be returned if the // their results.
// payment was unsuccessful. Payer PaymentAttemptDispatcher
SendToSwitch func(firstHop lnwire.ShortChannelID,
htlcAdd *lnwire.UpdateAddHTLC,
circuit *sphinx.Circuit) ([sha256.Size]byte, error)
// ChannelPruneExpiry is the duration used to determine if a channel // ChannelPruneExpiry is the duration used to determine if a channel
// should be pruned or not. If the delta between now and when the // 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( firstHop := lnwire.NewShortChanIDFromInt(
route.Hops[0].ChannelID, 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,
) )
} }

@ -15,7 +15,6 @@ import (
"github.com/btcsuite/btcutil" "github.com/btcsuite/btcutil"
"github.com/davecgh/go-spew/spew" "github.com/davecgh/go-spew/spew"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/htlcswitch"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
@ -44,13 +43,10 @@ func (c *testCtx) RestartRouter() error {
// With the chainView reset, we'll now re-create the router itself, and // With the chainView reset, we'll now re-create the router itself, and
// start it. // start it.
router, err := New(Config{ router, err := New(Config{
Graph: c.graph, Graph: c.graph,
Chain: c.chain, Chain: c.chain,
ChainView: c.chainView, ChainView: c.chainView,
SendToSwitch: func(_ lnwire.ShortChannelID, Payer: &mockPaymentAttemptDispatcher{},
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) {
return [32]byte{}, nil
},
ChannelPruneExpiry: time.Hour * 24, ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2, GraphPruneInterval: time.Hour * 2,
}) })
@ -85,14 +81,10 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr
chain := newMockChain(startingHeight) chain := newMockChain(startingHeight)
chainView := newMockChainView(chain) chainView := newMockChainView(chain)
router, err := New(Config{ router, err := New(Config{
Graph: graphInstance.graph, Graph: graphInstance.graph,
Chain: chain, Chain: chain,
ChainView: chainView, ChainView: chainView,
SendToSwitch: func(_ lnwire.ShortChannelID, Payer: &mockPaymentAttemptDispatcher{},
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) {
return [32]byte{}, nil
},
ChannelPruneExpiry: time.Hour * 24, ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2, GraphPruneInterval: time.Hour * 2,
QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { QueryBandwidth: func(e *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {
@ -250,24 +242,24 @@ func TestSendPaymentRouteFailureFallback(t *testing.T) {
// router's configuration to ignore the path that has luo ji as the // router's configuration to ignore the path that has luo ji as the
// first hop. This should force the router to instead take the // first hop. This should force the router to instead take the
// available two hop path (through satoshi). // available two hop path (through satoshi).
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
roasbeefLuoji := lnwire.NewShortChanIDFromInt(689530843) roasbeefLuoji := lnwire.NewShortChanIDFromInt(689530843)
if firstHop == roasbeefLuoji { if firstHop == roasbeefLuoji {
pub, err := sourceNode.PubKey() pub, err := sourceNode.PubKey()
if err != nil { if err != nil {
return preImage, err 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 // Send off the payment request to the router, route through satoshi
// should've been selected as a fall back and succeeded correctly. // 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 // We'll modify the SendToSwitch method so that it simulates a failed
// payment with an error originating from the first hop of the route. // payment with an error originating from the first hop of the route.
// The unsigned channel update is attached to the failure message. // The unsigned channel update is attached to the failure message.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
v := ctx.aliases["b"] v := ctx.aliases["b"]
source, err := btcec.ParsePubKey( source, err := btcec.ParsePubKey(
v[:], btcec.S256(), v[:], btcec.S256(),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
return [32]byte{}, &htlcswitch.ForwardingError{ return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: source, ErrorSource: source,
FailureMessage: &lnwire.FailFeeInsufficient{ FailureMessage: &lnwire.FailFeeInsufficient{
Update: errChanUpdate, Update: errChanUpdate,
}, },
} }
} })
// The payment parameter is mostly redundant in SendToRoute. Can be left // The payment parameter is mostly redundant in SendToRoute. Can be left
// empty for this test. // 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 // We'll now modify the SendToSwitch method to return an error for the
// outgoing channel to Son goku. This will be a fee related error, so // outgoing channel to Son goku. This will be a fee related error, so
// it should only cause the edge to be pruned after the second attempt. // it should only cause the edge to be pruned after the second attempt.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID) roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID)
if firstHop == roasbeefSongoku { if firstHop == roasbeefSongoku {
sourceKey, err := btcec.ParsePubKey( sourceKey, err := btcec.ParsePubKey(
sourceNode[:], btcec.S256(), sourceNode[:], btcec.S256(),
) )
if err != nil { if err != nil {
t.Fatal(err) 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{ return preImage, nil
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
}
// Send off the payment request to the router, route through satoshi // Send off the payment request to the router, route through satoshi
// should've been selected as a fall back and succeeded correctly. // 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 // outgoing channel to son goku. Since this is a time lock related
// error, we should fail the payment flow all together, as Goku is the // error, we should fail the payment flow all together, as Goku is the
// only channel to Sophon. // only channel to Sophon.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku { if firstHop == roasbeefSongoku {
sourceKey, err := btcec.ParsePubKey( sourceKey, err := btcec.ParsePubKey(
sourceNode[:], btcec.S256(), sourceNode[:], btcec.S256(),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
}
return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: sourceKey,
FailureMessage: &lnwire.FailExpiryTooSoon{
Update: errChanUpdate,
},
}
} }
return [32]byte{}, &htlcswitch.ForwardingError{ return preImage, nil
ErrorSource: sourceKey, })
FailureMessage: &lnwire.FailExpiryTooSoon{
Update: errChanUpdate,
},
}
}
return preImage, nil
}
// assertExpectedPath is a helper function that asserts the returned // assertExpectedPath is a helper function that asserts the returned
// route properly routes around the failure we've introduced in the // 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 // We'll now modify the error return an IncorrectCltvExpiry error
// instead, this should result in the same behavior of roasbeef routing // instead, this should result in the same behavior of roasbeef routing
// around the faulty Son Goku node. // around the faulty Son Goku node.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefSongoku { if firstHop == roasbeefSongoku {
sourceKey, err := btcec.ParsePubKey( sourceKey, err := btcec.ParsePubKey(
sourceNode[:], btcec.S256(), sourceNode[:], btcec.S256(),
) )
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
}
return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: sourceKey,
FailureMessage: &lnwire.FailIncorrectCltvExpiry{
Update: errChanUpdate,
},
}
} }
return [32]byte{}, &htlcswitch.ForwardingError{ return preImage, nil
ErrorSource: sourceKey, })
FailureMessage: &lnwire.FailIncorrectCltvExpiry{
Update: errChanUpdate,
},
}
}
return preImage, nil
}
// Once again, Roasbeef should route around Goku since they disagree // Once again, Roasbeef should route around Goku since they disagree
// w.r.t to the block height, and instead go through Pham Nuwen. // 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 // TODO(roasbeef): filtering should be intelligent enough so just not
// go through satoshi at all at this point. // go through satoshi at all at this point.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefLuoji { if firstHop == roasbeefLuoji {
// We'll first simulate an error from the first // We'll first simulate an error from the first
// outgoing link to simulate the channel from luo ji to // outgoing link to simulate the channel from luo ji to
// roasbeef not having enough capacity. // roasbeef not having enough capacity.
return [32]byte{}, &htlcswitch.ForwardingError{ return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: sourcePub, ErrorSource: sourcePub,
FailureMessage: &lnwire.FailTemporaryChannelFailure{}, 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)
} }
return [32]byte{}, &htlcswitch.ForwardingError{ // Next, we'll create an error from satoshi to indicate
ErrorSource: key, // that the luoji node is not longer online, which should
FailureMessage: &lnwire.FailUnknownNextPeer{}, // 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() 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 // Next, we'll modify the SendToSwitch method to indicate that luo ji
// wasn't originally online. This should also halt the send all // wasn't originally online. This should also halt the send all
// together as all paths contain luoji and he can't be reached. // together as all paths contain luoji and he can't be reached.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefLuoji { if firstHop == roasbeefLuoji {
return [32]byte{}, &htlcswitch.ForwardingError{ return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: sourcePub, ErrorSource: sourcePub,
FailureMessage: &lnwire.FailUnknownNextPeer{}, FailureMessage: &lnwire.FailUnknownNextPeer{},
}
} }
}
return preImage, nil return preImage, nil
} })
// This shouldn't return an error, as we'll make a payment attempt via // 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 // 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 // Finally, we'll modify the SendToSwitch function to indicate that the
// roasbeef -> luoji channel has insufficient capacity. This should // roasbeef -> luoji channel has insufficient capacity. This should
// again cause us to instead go via the satoshi route. // again cause us to instead go via the satoshi route.
ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, ctx.router.cfg.Payer.(*mockPaymentAttemptDispatcher).setPaymentResult(
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { func(firstHop lnwire.ShortChannelID) ([32]byte, error) {
if firstHop == roasbeefLuoji { if firstHop == roasbeefLuoji {
// We'll first simulate an error from the first // We'll first simulate an error from the first
// outgoing link to simulate the channel from luo ji to // outgoing link to simulate the channel from luo ji to
// roasbeef not having enough capacity. // roasbeef not having enough capacity.
return [32]byte{}, &htlcswitch.ForwardingError{ return [32]byte{}, &htlcswitch.ForwardingError{
ErrorSource: sourcePub, ErrorSource: sourcePub,
FailureMessage: &lnwire.FailTemporaryChannelFailure{}, FailureMessage: &lnwire.FailTemporaryChannelFailure{},
}
} }
} return preImage, nil
return preImage, nil })
}
paymentPreImage, rt, err = ctx.router.SendPayment(&payment) paymentPreImage, rt, err = ctx.router.SendPayment(&payment)
if err != nil { if err != nil {
@ -1525,13 +1517,10 @@ func TestWakeUpOnStaleBranch(t *testing.T) {
// Create new router with same graph database. // Create new router with same graph database.
router, err := New(Config{ router, err := New(Config{
Graph: ctx.graph, Graph: ctx.graph,
Chain: ctx.chain, Chain: ctx.chain,
ChainView: ctx.chainView, ChainView: ctx.chainView,
SendToSwitch: func(_ lnwire.ShortChannelID, Payer: &mockPaymentAttemptDispatcher{},
_ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) {
return [32]byte{}, nil
},
ChannelPruneExpiry: time.Hour * 24, ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2, GraphPruneInterval: time.Hour * 2,
}) })

@ -610,24 +610,10 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl,
s.currentNodeAnn = nodeAnn s.currentNodeAnn = nodeAnn
s.chanRouter, err = routing.New(routing.Config{ s.chanRouter, err = routing.New(routing.Config{
Graph: chanGraph, Graph: chanGraph,
Chain: cc.chainIO, Chain: cc.chainIO,
ChainView: cc.chainView, ChainView: cc.chainView,
SendToSwitch: func(firstHop lnwire.ShortChannelID, Payer: s.htlcSwitch,
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,
)
},
ChannelPruneExpiry: routing.DefaultChannelPruneExpiry, ChannelPruneExpiry: routing.DefaultChannelPruneExpiry,
GraphPruneInterval: time.Duration(time.Hour), GraphPruneInterval: time.Duration(time.Hour),
QueryBandwidth: func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi { QueryBandwidth: func(edge *channeldb.ChannelEdgeInfo) lnwire.MilliSatoshi {