diff --git a/routing/ann_validation.go b/routing/ann_validation.go index 184d7009c7e..952de89f2e4 100644 --- a/routing/ann_validation.go +++ b/routing/ann_validation.go @@ -129,7 +129,7 @@ func ValidateNodeAnn(a *lnwire.NodeAnnouncement) error { func ValidateChannelUpdateAnn(pubKey *btcec.PublicKey, capacity btcutil.Amount, a *lnwire.ChannelUpdate) error { - if err := validateOptionalFields(capacity, a); err != nil { + if err := ValidateOptionalFields(capacity, a); err != nil { return err } @@ -160,9 +160,9 @@ func VerifyChannelUpdateSignature(msg *lnwire.ChannelUpdate, return nil } -// validateOptionalFields validates a channel update's message flags and +// ValidateOptionalFields validates a channel update's message flags and // corresponding update fields. -func validateOptionalFields(capacity btcutil.Amount, +func ValidateOptionalFields(capacity btcutil.Amount, msg *lnwire.ChannelUpdate) error { if msg.MessageFlags.HasMaxHtlc() { diff --git a/routing/missioncontrol.go b/routing/missioncontrol.go index d3ecf8436c8..9ff0e5c3efd 100644 --- a/routing/missioncontrol.go +++ b/routing/missioncontrol.go @@ -1,6 +1,7 @@ package routing import ( + "bytes" "sync" "time" @@ -188,6 +189,11 @@ func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint, // Finally, create the channel edge from the hop hint // and add it to list of edges corresponding to the node // at the start of the channel. + v := NewVertex(hopHint.NodeID) + var flags lnwire.ChanUpdateChanFlags + if bytes.Compare(v[:], endNode.PubKeyBytes[:]) == 1 { + flags |= lnwire.ChanUpdateDirection + } edge := &channeldb.ChannelEdgePolicy{ Node: endNode, ChannelID: hopHint.ChannelID, @@ -198,9 +204,9 @@ func (m *missionControl) NewPaymentSession(routeHints [][]zpay32.HopHint, hopHint.FeeProportionalMillionths, ), TimeLockDelta: hopHint.CLTVExpiryDelta, + ChannelFlags: flags, } - v := NewVertex(hopHint.NodeID) edges[v] = append(edges[v], edge) } } diff --git a/routing/pathfind.go b/routing/pathfind.go index 30d2b0da94e..a10abffbb38 100644 --- a/routing/pathfind.go +++ b/routing/pathfind.go @@ -138,6 +138,18 @@ type Route struct { Hops []*Hop } +// containsChannel returns true if a channel is present in the target route, +// and false otherwise. The passed chanID should be the converted uint64 form +// of lnwire.ShortChannelID. +func (r *Route) containsChannel(chanID uint64) bool { + for _, hop := range r.Hops { + if hop.ChannelID == chanID { + return true + } + } + return false +} + // HopFee returns the fee charged by the route hop indicated by hopIndex. func (r *Route) HopFee(hopIndex int) lnwire.MilliSatoshi { var incomingAmt lnwire.MilliSatoshi diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go index d0e508f6629..c311f8f26ef 100644 --- a/routing/pathfind_test.go +++ b/routing/pathfind_test.go @@ -82,11 +82,14 @@ type testGraph struct { // testNode represents a node within the test graph above. We skip certain // information such as the node's IP address as that information isn't needed -// for our tests. +// for our tests. Private keys are optional. If set, they should be consistent +// with the public key. The private key is used to sign error messages +// sent from the node. type testNode struct { - Source bool `json:"source"` - PubKey string `json:"pubkey"` - Alias string `json:"alias"` + Source bool `json:"source"` + PubKey string `json:"pubkey"` + PrivKey string `json:"privkey"` + Alias string `json:"alias"` } // testChan represents the JSON version of a payment channel. This struct @@ -164,6 +167,7 @@ func parseTestGraph(path string) (*testGraphInstance, error) { } aliasMap := make(map[string]Vertex) + privKeyMap := make(map[string]*btcec.PrivateKey) var source *channeldb.LightningNode // First we insert all the nodes within the graph as vertexes. @@ -194,6 +198,27 @@ func parseTestGraph(path string) (*testGraphInstance, error) { // alias map for easy lookup. aliasMap[node.Alias] = dbNode.PubKeyBytes + // private keys are needed for signing error messages. If set + // check the consistency with the public key. + privBytes, err := hex.DecodeString(node.PrivKey) + if err != nil { + return nil, err + } + if len(privBytes) > 0 { + key, derivedPub := btcec.PrivKeyFromBytes(btcec.S256(), + privBytes) + + if !bytes.Equal(dbNode.PubKeyBytes[:], + derivedPub.SerializeCompressed()) { + + return nil, fmt.Errorf("%s public key and "+ + "private key are inconsistent", + node.Alias) + } + + privKeyMap[node.Alias] = key + } + // If the node is tagged as the source, then we create a // pointer to is so we can mark the source in the graph // properly. @@ -204,7 +229,8 @@ func parseTestGraph(path string) (*testGraphInstance, error) { // node can be the source in the graph. if source != nil { return nil, errors.New("JSON is invalid " + - "multiple nodes are tagged as the source") + "multiple nodes are tagged as the " + + "source") } source = dbNode @@ -291,9 +317,10 @@ func parseTestGraph(path string) (*testGraphInstance, error) { } return &testGraphInstance{ - graph: graph, - cleanUp: cleanUp, - aliasMap: aliasMap, + graph: graph, + cleanUp: cleanUp, + aliasMap: aliasMap, + privKeyMap: privKeyMap, }, nil } diff --git a/routing/payment_session.go b/routing/payment_session.go index 3fec06ee7e2..e495d0f8046 100644 --- a/routing/payment_session.go +++ b/routing/payment_session.go @@ -37,6 +37,51 @@ type paymentSession struct { pathFinder pathFinder } +// edgeLocatorOfEdgePolicy returns the edge locator corresponding to +// the channel edge policy +func edgeLocatorOfEdgePolicy(ep *channeldb.ChannelEdgePolicy) *EdgeLocator { + return &EdgeLocator{ + ChannelID: ep.ChannelID, + Direction: edgePolicyDirection(ep), + } +} + +// edgePolicyDirection returns the direction of the edge policy in the format +// used by edge locators (i.e. 1 if lnwire.ChanUpdateDirection is set) +func edgePolicyDirection(ep *channeldb.ChannelEdgePolicy) uint8 { + if ep.ChannelFlags&lnwire.ChanUpdateDirection == 0 { + return 0 + } + return 1 +} + +// updateEdgePolicy updates the channel edge policy parameters +func updateEdgePolicy(up *lnwire.ChannelUpdate, + ep *channeldb.ChannelEdgePolicy) { + + ep.FeeBaseMSat = lnwire.MilliSatoshi(up.BaseFee) + ep.FeeProportionalMillionths = lnwire.MilliSatoshi(up.FeeRate) + ep.TimeLockDelta = up.TimeLockDelta + ep.MinHTLC = lnwire.MilliSatoshi(up.HtlcMinimumMsat) +} + +// UpdateEdgePolicy updates edge policy of the routing hints kept in the +// payment session. +func (p *paymentSession) UpdateEdgePolicy(el *EdgeLocator, + update *lnwire.ChannelUpdate) { + + log.Debugf("Updating edge %v policy in Mission Control", el) + + // Run over hints and update policy if edge id and direction match. + for _, edges := range p.additionalEdges { + for _, edge := range edges { + if *el == *edgeLocatorOfEdgePolicy(edge) { + updateEdgePolicy(update, edge) + } + } + } +} + // ReportVertexFailure adds a vertex to the graph prune view after a client // reports a routing failure localized to the vertex. The time the vertex was // added is noted, as it'll be pruned from the shared view after a period of diff --git a/routing/router.go b/routing/router.go index 1025a4023ea..ae227480592 100644 --- a/routing/router.go +++ b/routing/router.go @@ -4,6 +4,7 @@ import ( "bytes" "crypto/sha256" "fmt" + "math" "runtime" "sort" "sync" @@ -1208,15 +1209,19 @@ func (r *ChannelRouter) processUpdate(msg interface{}) error { // Now that we know this isn't a stale update, we'll apply the // new edge policy to the proper directional edge within the // channel graph. - if err = r.cfg.Graph.UpdateEdgePolicy(msg); err != nil { - err := errors.Errorf("unable to add channel: %v", err) - log.Error(err) - return err - } + if exists { + err = r.cfg.Graph.UpdateEdgePolicy(msg) + if err != nil { + err := errors.Errorf("unable to update "+ + "channel policy: %v", err) + log.Error(err) + return err + } - invalidateCache = true - log.Tracef("New channel update applied: %v", - newLogClosure(func() string { return spew.Sdump(msg) })) + invalidateCache = true + log.Tracef("New channel update applied: %v", + newLogClosure(func() string { return spew.Sdump(msg) })) + } default: return errors.Errorf("wrong routing update message type") @@ -1805,7 +1810,9 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // update with id may not be available. failedEdge, err := getFailedEdge(route, errVertex) if err != nil { - return true + // If channel identification fails, ignore the error + // message and try again, until timeout. + return false } // processChannelUpdateAndRetry is a closure that @@ -1824,7 +1831,8 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, pubKey *btcec.PublicKey) { // Try to apply the channel update. - updateOk := r.applyChannelUpdate(update, pubKey) + updateOk := r.applyChannelUpdate(update, + pubKey, failedEdge) // If the update could not be applied, prune the // edge. There is no reason to continue trying @@ -1837,8 +1845,13 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, paySession.ReportEdgeFailure( failedEdge, ) + return } + // Update routing hints. + paySession.UpdateEdgePolicy(failedEdge, update) + + // Report edge policy failure. paySession.ReportEdgePolicyFailure( NewVertex(errSource), failedEdge, ) @@ -1886,7 +1899,8 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // that sent us this error, as it doesn't now what the // correct block height is. case *lnwire.FailExpiryTooSoon: - r.applyChannelUpdate(&onionErr.Update, errSource) + r.applyChannelUpdate(&onionErr.Update, + errSource, failedEdge) paySession.ReportVertexFailure(errVertex) return false @@ -1931,7 +1945,8 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // forward one is currently disabled, so we'll apply // the update and continue. case *lnwire.FailChannelDisabled: - r.applyChannelUpdate(&onionErr.Update, errSource) + r.applyChannelUpdate(&onionErr.Update, + errSource, failedEdge) paySession.ReportEdgeFailure(failedEdge) return false @@ -1939,7 +1954,8 @@ func (r *ChannelRouter) processSendError(paySession *paymentSession, // sufficient capacity, so we'll prune this edge for // now, and continue onwards with our path finding. case *lnwire.FailTemporaryChannelFailure: - r.applyChannelUpdate(onionErr.Update, errSource) + r.applyChannelUpdate(onionErr.Update, + errSource, failedEdge) paySession.ReportEdgeFailure(failedEdge) return false @@ -2053,27 +2069,42 @@ func getFailedEdge(route *Route, errSource Vertex) ( // applyChannelUpdate validates a channel update and if valid, applies it to the // database. It returns a bool indicating whether the updates was successful. func (r *ChannelRouter) applyChannelUpdate(msg *lnwire.ChannelUpdate, - pubKey *btcec.PublicKey) bool { + pubKey *btcec.PublicKey, e *EdgeLocator) bool { // If we get passed a nil channel update (as it's optional with some // onion errors), then we'll exit early with a success result. if msg == nil { return true } + // Verify update signature validates. + if err := ValidateChannelUpdateAnn(pubKey, math.MaxInt64, msg); err != nil { + log.Errorf("Failed to validate channel update: %v", err) + return false + } + + // If channel is in database, do further checks. Else, the + // update is for an additional private edge added by hints. ch, _, _, err := r.GetChannelByID(msg.ShortChannelID) if err != nil { log.Errorf("Unable to retrieve channel by id: %v", err) - return false + return true } - if err := ValidateChannelUpdateAnn(pubKey, ch.Capacity, msg); err != nil { + if err := ValidateOptionalFields(ch.Capacity, msg); err != nil { log.Errorf("Unable to validate channel update: %v", err) return false } + // Trust the direction of the edge locator, and not the one + // in the message. + if e.Direction == 1 { + msg.ChannelFlags |= lnwire.ChanUpdateDirection + } else { + msg.ChannelFlags ^= lnwire.ChanUpdateDirection + } err = r.UpdateEdge(&channeldb.ChannelEdgePolicy{ SigBytes: msg.Signature.ToSignatureBytes(), - ChannelID: msg.ShortChannelID.ToUint64(), + ChannelID: e.ChannelID, LastUpdate: time.Unix(int64(msg.Timestamp), 0), MessageFlags: msg.MessageFlags, ChannelFlags: msg.ChannelFlags, diff --git a/routing/router_test.go b/routing/router_test.go index df9ad9e6a8f..10f4a9f3a8c 100644 --- a/routing/router_test.go +++ b/routing/router_test.go @@ -5,6 +5,7 @@ import ( "fmt" "image/color" "math/rand" + "reflect" "strings" "testing" "time" @@ -33,6 +34,8 @@ type testCtx struct { aliases map[string]Vertex + privKeys map[string]*btcec.PrivateKey + chain *mockChain chainView *mockChainView @@ -112,6 +115,7 @@ func createTestCtxFromGraphInstance(startingHeight uint32, graphInstance *testGr router: router, graph: graphInstance.graph, aliases: graphInstance.aliasMap, + privKeys: graphInstance.privKeyMap, chain: chain, chainView: chainView, } @@ -164,6 +168,30 @@ func createTestCtxFromFile(startingHeight uint32, testGraph string) (*testCtx, f return createTestCtxFromGraphInstance(startingHeight, graphInstance) } +// Add valid signature to channel update simulated as error +// received from the network. +func signErrChanUpdate(key *btcec.PrivateKey, + errChanUpdate *lnwire.ChannelUpdate) error { + + chanUpdateMsg, err := errChanUpdate.DataToSign() + if err != nil { + return err + } + + digest := chainhash.DoubleHashB(chanUpdateMsg) + sig, err := key.Sign(digest) + if err != nil { + return err + } + + errChanUpdate.Signature, err = lnwire.NewSigFromSignature(sig) + if err != nil { + return err + } + + return nil +} + // TestFindRoutesFeeSorting asserts that routes found by the FindRoutes method // within the channel router are properly returned in a sorted order, with the // lowest fee route coming first. @@ -392,10 +420,10 @@ func TestChannelUpdateValidation(t *testing.T) { ctx, cleanUp, err := createTestCtxFromGraphInstance(startingBlockHeight, testGraph) - defer cleanUp() if err != nil { t.Fatalf("unable to create router: %v", err) } + defer cleanUp() // Assert that the initially configured fee is retrieved correctly. _, policy, _, err := ctx.router.GetChannelByID( @@ -538,7 +566,7 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { defer cleanUp() // Craft a LightningPayment struct that'll send a payment from roasbeef - // to luo ji for 100 satoshis. + // to sophon for 1000 satoshis. var payHash [32]byte amt := lnwire.NewMSatFromSatoshis(1000) payment := LightningPayment{ @@ -551,10 +579,10 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { 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. We'll obtain this as we'll need to to generate the + // 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. - chanID := uint64(12345) + chanID := uint64(3495345) _, _, edgeUpdateToFail, err := ctx.graph.FetchChannelEdgesByID(chanID) if err != nil { t.Fatalf("unable to fetch chan id: %v", err) @@ -572,6 +600,11 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { FeeRate: uint32(edgeUpdateToFail.FeeProportionalMillionths), } + err = signErrChanUpdate(ctx.privKeys["songoku"], &errChanUpdate) + if err != nil { + t.Fatalf("Failed to sign channel update error: %v ", err) + } + // The error will be returned by Son Goku. sourceNode := ctx.aliases["songoku"] @@ -581,7 +614,7 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { - roasbeefSongoku := lnwire.NewShortChanIDFromInt(chanID) + roasbeefSongoku := lnwire.NewShortChanIDFromInt(12345) if firstHop == roasbeefSongoku { sourceKey, err := btcec.ParsePubKey( sourceNode[:], btcec.S256(), @@ -605,7 +638,13 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { return preImage, nil } - // Send off the payment request to the router, route through satoshi + // processUpdate ensures that the funding transaction is not spent, + // before further processing an edge update. Toggel AssumeChannelValid + // to avoid dropping the channel update, as the utxo set query is + // not mocked in this test. + ctx.router.cfg.AssumeChannelValid = true + + // 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) if err != nil { @@ -627,13 +666,551 @@ func TestSendPaymentErrorRepeatedFeeInsufficient(t *testing.T) { // The route should have pham nuwen as the first hop. if route.Hops[0].PubKeyBytes != ctx.aliases["phamnuwen"] { - t.Fatalf("route should go through satoshi as first hop, "+ + t.Fatalf("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 successfuly +// route through the private channel in the second attempt. +func TestSendPaymentErrorFeeInsufficientPrivateEdge(t *testing.T) { + t.Parallel() + + const startingBlockHeight = 101 + ctx, cleanUp, err := createTestCtxFromFile(startingBlockHeight, basicGraphFilePath) + if err != nil { + t.Fatalf("unable to create router: %v", err) + } + defer cleanUp() + + // Craft a LightningPayment struct that'll send a payment from roasbeef + // to elst, through a private channel between son goku and elst + // for 1000 satoshis. + // This route has lower fees compared with the route through pham nuwen, + // as well as compared with the route through son goku -> sophon. This + // also holds when the private channel fee is updated to a higher value. + var payHash [32]byte + amt := lnwire.NewMSatFromSatoshis(1000) + privateChannelID := uint64(55555) + feeBaseMSat := uint32(15) + feeProportionalMillionths := uint32(10) + expiryDelta := uint16(32) + sgNode := ctx.aliases["songoku"] + sgNodeID, err := btcec.ParsePubKey(sgNode[:], btcec.S256()) + if err != nil { + t.Fatal(err) + } + hopHint := zpay32.HopHint{ + NodeID: sgNodeID, + ChannelID: privateChannelID, + FeeBaseMSat: feeBaseMSat, + FeeProportionalMillionths: feeProportionalMillionths, + CLTVExpiryDelta: expiryDelta, + } + routeHints := [][]zpay32.HopHint{{hopHint}} + payment := LightningPayment{ + Target: ctx.aliases["elst"], + Amount: amt, + FeeLimit: noFeeLimit, + PaymentHash: payHash, + RouteHints: routeHints, + } + + var preImage [32]byte + copy(preImage[:], bytes.Repeat([]byte{9}, 32)) + + // Prepare an error update for the private channel, with twice + // the original fee. + updatedFeeBaseMSat := feeBaseMSat * 2 + updatedFeeProportionalMillionths := feeProportionalMillionths + errChanUpdate := lnwire.ChannelUpdate{ + ShortChannelID: lnwire.NewShortChanIDFromInt(privateChannelID), + Timestamp: uint32(testTime.Add(time.Minute).Unix()), + BaseFee: updatedFeeBaseMSat, + FeeRate: updatedFeeProportionalMillionths, + TimeLockDelta: expiryDelta, + } + + err = signErrChanUpdate(ctx.privKeys["songoku"], &errChanUpdate) + if err != nil { + t.Fatalf("Failed to sign channel update error: %v ", err) + } + + // The error will be returned by son goku, before forwarding to the + // private channel, to simulate an increase in fee on the private + // channel that wasn't propagated back to the network. + sourceNode := ctx.aliases["songoku"] + + // We'll now modify the SendToSwitch method to return an error on + // the first attempt, and check that the correct route and fees + // where used the second attempt. + errorReturned := false + ctx.router.cfg.SendToSwitch = func(firstHop lnwire.ShortChannelID, + _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + + if !errorReturned { + errorReturned = true + 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 the new fee + // schedule for the node/channel. + FailureMessage: &lnwire.FailFeeInsufficient{ + Update: errChanUpdate, + }, + } + } + + return preImage, nil + } + + // processUpdate ensures that the funding transaction is not spent, + // before further processing an edge update. Toggel AssumeChannelValid + // to avoid dropping the channel update, as the utxo set query is + // not mocked in this test. + ctx.router.cfg.AssumeChannelValid = true + + // 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) + if err != nil { + t.Fatalf("unable to send payment: %v", err) + } + + if errorReturned == false { + t.Fatalf("failed to simulate error in the first payment " + + "attempt") + } + + // The route selected should have two hops. Make sure that + // the son goku -> sophon -> elst or the pham nuwen -> sophon + // -> elst are not selected instead. + if len(route.Hops) != 2 { + t.Fatalf("incorrect route length: expected %v got %v", 2, + len(route.Hops)) + } + + // The preimage should match up with the one created above. + if !bytes.Equal(paymentPreImage[:], preImage[:]) { + t.Fatalf("incorrect preimage used: expected %x got %x", + preImage[:], paymentPreImage[:]) + } + + // The route should have son goku as the first hop. + if route.Hops[0].PubKeyBytes != ctx.aliases["songoku"] { + t.Fatalf("route should go through son goku as first hop, "+ + "instead passes through: %v", + getAliasFromPubKey(route.Hops[0].PubKeyBytes, + ctx.aliases)) + } + + // The route should pass via the private channel. + if !route.containsChannel(privateChannelID) { + t.Fatalf("route did not pass through private channel " + + "between pham nuwen and elst") + } + + // The route should have the updated fee. + expectedFee := updatedFeeBaseMSat + + (updatedFeeProportionalMillionths*uint32(amt))/1000000 + if route.HopFee(0) != lnwire.MilliSatoshi(expectedFee) { + t.Fatalf("expected %v MSat fee to forward to the private "+ + "channel, instead got %v MSat", expectedFee, + route.HopFee(0)) + } +} + +// TestSendPaymentPrivateEdgeDirection tests that edge direction is +// taken into account properly when processing errors and pruning edges +// as a result. The setup below is used for the test: +// +// ┌──────────┐ +// ..........│ F │.......... +// . └──────────┘ . +// . . +// . . +// . 80 . 30 +// . . +// . . +// ┌──────────┐ 20 ┌──────────┐ +// │ D │...................│ E │ +// └──────────┘ └──────────┘ +// | | +// | | +// | 10 | 50 +// | | +// | | +// ┌──────────┐ ┌──────────┐ +// │ B │ │ C │ +// └──────────┘ └──────────┘ +// | | +// | | +// | 30 | 30 +// | | +// | ┌──────────┐ | +// └─────────│ A │─────────┘ +// └──────────┘ +// +// A-B, B-D, A-C, C-E are public, while D-E, D-F and E-F are private edges, +// learned through the following hints: +// h1: D->E->F +// h2: E->D->F +// +// The base fee is written on each edge. The porportional fee is set to 0. +// All edges are symmetric. +// +// The router is expected to come up with the following routes in order: +// r1: A->B->D->E->F total fees: 60 +// r2: A->C->E->F total fees: 80 +// r3: A->B->D->F total fees: 90 +// r4: A->C->E->D->F total fees: 150 +// +// r1 and r4 both pass through edge D-E, r1 transversing D->E and r4 E->D. +// +// The test errors on the D->E direction and prunes it as a result, and +// checks that the E->D direction is still a viable edge for r4, i.e. +// that direction is correctly taken into account when pruning an edge. +// +// The following edges are pruned for each route: +// r1: D->E is pruned +// r2: E->F is pruned +// r3: B->D is pruned +// leaving r4 as viable 4th route +// +// Since edge direction is determined by comparison of the public keys of +// the connected nodes, an identical test is performed where the E-D is +// transversed in the opposite direction, i.e. the fees in the maps are +// reversed, to lead to the following routes: +// r1: A->C->E->D->F total fees: 60 +// r2: A->B->D->F total fees: 80 +// r3: A->C->E->F total fees: 90 +// r4: A->B->D->E->F total fees: 150 +// +// with the following errors: +// r1: E->D is pruned +// r2: D->F is pruned +// r3: C->E is pruned +// leaving r4 as viable 4th route +// +// This test was added to test direction initialization in NewPaymentSession; +// without the fix payment would fail in the switched direction test. +func TestSendPaymentPrivateEdgeDirection(t *testing.T) { + t.Parallel() + + // Setup the public part of the node network. + // roasbeef is used instead of a, as createTestGraphFromChannels + // set roasbeef as source. + b2dChanID := uint64(200) + c2eChanID := uint64(201) + testChannels := []*testChannel{ + symmetricTestChannel("roasbeef", "b", 1000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: 30, + FeeRate: 0, + MinHTLC: 1, + }, 1), + symmetricTestChannel("roasbeef", "c", 1000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: 30, + FeeRate: 0, + MinHTLC: 1, + }, 2), + symmetricTestChannel("b", "d", 1000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: 10, + FeeRate: 0, + MinHTLC: 1, + }, b2dChanID), + symmetricTestChannel("c", "e", 1000, &testChannelPolicy{ + Expiry: 144, + FeeBaseMsat: 50, + FeeRate: 0, + MinHTLC: 1, + }, c2eChanID)} + + testGraph, err := createTestGraphFromChannels(testChannels) + if err != nil { + t.Fatalf("unable to create graph: %v", err) + } + defer testGraph.cleanUp() + + const startingBlockHeight = 101 + + ctx, cleanUp, err := createTestCtxFromGraphInstance(startingBlockHeight, + testGraph) + + if err != nil { + t.Fatalf("unable to create router: %v", err) + } + defer cleanUp() + + // Create private part of the network. + // f is a private node connected to both d and e. + keyBytes := []byte{ + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 80, + } + _, pubKey := btcec.PrivKeyFromBytes(btcec.S256(), keyBytes) + ctx.aliases["f"] = NewVertex(pubKey) + + // Prepare hints. + expiryDelta := uint16(32) + d2eChanID := uint64(100) + d2fChanID := uint64(101) + e2fChanID := uint64(102) + dNode := ctx.aliases["d"] + dNodeID, err := btcec.ParsePubKey(dNode[:], btcec.S256()) + if err != nil { + t.Fatal(err) + } + eNode := ctx.aliases["e"] + eNodeID, err := btcec.ParsePubKey(eNode[:], btcec.S256()) + if err != nil { + t.Fatal(err) + } + routeHints := [][]zpay32.HopHint{ + { + { + NodeID: dNodeID, + ChannelID: d2eChanID, + FeeBaseMSat: 20, + FeeProportionalMillionths: 0, + CLTVExpiryDelta: expiryDelta, + }, + { + NodeID: eNodeID, + ChannelID: e2fChanID, + FeeBaseMSat: 30, + FeeProportionalMillionths: 0, + CLTVExpiryDelta: expiryDelta, + }, + }, + { + { + NodeID: eNodeID, + ChannelID: d2eChanID, + FeeBaseMSat: 20, + FeeProportionalMillionths: 0, + CLTVExpiryDelta: expiryDelta, + }, + { + NodeID: dNodeID, + ChannelID: d2fChanID, + FeeBaseMSat: 80, + FeeProportionalMillionths: 0, + CLTVExpiryDelta: expiryDelta, + }, + }, + } + + // Prepare sequence of errors returned. + type errorEdge struct { + channelID uint64 + source string + } + var errorSeq = []errorEdge{ + { + source: "d", + channelID: d2eChanID, + }, + { + source: "e", + channelID: e2fChanID, + }, + { + source: "b", + channelID: b2dChanID, + }, + } + + amt := lnwire.NewMSatFromSatoshis(400) + var payHash [32]byte + payment := LightningPayment{ + Target: ctx.aliases["f"], + Amount: amt, + FeeLimit: noFeeLimit, + PaymentHash: payHash, + RouteHints: routeHints, + } + + // Temporary channel failure onion errors will be returned to prune channels. + update := lnwire.ChannelUpdate{} + chanFailure := lnwire.FailTemporaryChannelFailure{} + chanFailure.Update = &update + htlcError := htlcswitch.ForwardingError{} + htlcError.FailureMessage = &chanFailure + + // Mock SendToSwitch to return the sequence of errors. + ctx.router.cfg.SendToSwitch = func(chanID lnwire.ShortChannelID, + _ *lnwire.UpdateAddHTLC, _ *sphinx.Circuit) ([32]byte, error) { + + if len(errorSeq) == 0 { + return [32]byte{}, nil + } + + // Prepare the error + source := errorSeq[0].source + sourceNode := ctx.aliases[source] + sourceKey, err := btcec.ParsePubKey( + sourceNode[:], btcec.S256(), + ) + if err != nil { + t.Fatal(err) + } + channelID := errorSeq[0].channelID + htlcError.ErrorSource = sourceKey + update.ShortChannelID = lnwire.NewShortChanIDFromInt(channelID) + update.Timestamp = uint32(testTime.Add(time.Minute).Unix()) + + err = signErrChanUpdate(ctx.privKeys[source], &update) + if err != nil { + t.Fatalf("Failed to sign channel on behalf of %v error: %v ", source, err) + } + + // Pop the already returned error. + errorSeq = errorSeq[1:] + + return [32]byte{}, &htlcError + } + + // processUpdate ensures that the funding transaction is not spent, + // before further processing an edge update. Toggel AssumeChannelValid + // to avoid dropping the channel update, as the utxo set query is + // not mocked in this test. + ctx.router.cfg.AssumeChannelValid = true + + // Send payment + _, route, err := ctx.router.SendPayment(&payment) + if err != nil { + t.Fatalf("failed to send payment: %v %v", err, payment) + } + + // Verify all errors in planned sequence were returned. + if len(errorSeq) > 0 { + t.Fatalf("not all errors returned: %v errors left in sequence", + len(errorSeq)) + } + + // Check route is: A->C->E->D->F. + var expectedSeq = []string{"c", "e", "d", "f"} + var actualSeq []string + for i := 0; i < len(route.Hops); i++ { + actualSeq = append(actualSeq, + getAliasFromPubKey(route.Hops[i].PubKeyBytes, + ctx.aliases), + ) + } + if !reflect.DeepEqual(expectedSeq, actualSeq) { + t.Fatalf("incorrect route: expected %s got %s", + strings.Join(expectedSeq, "->"), + strings.Join(actualSeq, "->")) + } + + // The route should have the updated fee. + if route.TotalFees != lnwire.MilliSatoshi(150) { + t.Fatalf("expected %v MSat total fee, "+ + "instead got %v MSat", 150, route.TotalFees) + } + + // Now switch directions and test again. + ctx.router.missionControl.ResetHistory() + + // Switch fees on public channels. + _, e1, e2, err := ctx.graph.FetchChannelEdgesByID(b2dChanID) + if err != nil { + t.Fatalf("unable to fetch edge: %v", err) + } + e1.FeeBaseMSat = 50 + if err := ctx.graph.UpdateEdgePolicy(e1); err != nil { + t.Fatalf("unable to update edge: %v", err) + } + e2.FeeBaseMSat = 50 + if err := ctx.graph.UpdateEdgePolicy(e2); err != nil { + t.Fatalf("unable to update edge: %v", err) + } + + _, e1, e2, err = ctx.graph.FetchChannelEdgesByID(c2eChanID) + if err != nil { + t.Fatalf("unable to fetch edge: %v", err) + } + e1.FeeBaseMSat = 10 + if err := ctx.graph.UpdateEdgePolicy(e1); err != nil { + t.Fatalf("unable to update edge: %v", err) + } + e2.FeeBaseMSat = 10 + if err := ctx.graph.UpdateEdgePolicy(e2); err != nil { + t.Fatalf("unable to update edge: %v", err) + } + + // Switch route hints. + routeHints[0][1].FeeBaseMSat, routeHints[1][1].FeeBaseMSat = + routeHints[1][1].FeeBaseMSat, routeHints[0][1].FeeBaseMSat + + // Switch sequence of errors. + errorSeq = []errorEdge{ + { + source: "e", + channelID: d2eChanID, + }, + { + source: "d", + channelID: d2fChanID, + }, + { + source: "c", + channelID: c2eChanID, + }, + } + + // Send payment + _, route, err = ctx.router.SendPayment(&payment) + if err != nil { + t.Fatalf("failed to send payment (direction switched): "+ + "%v %v", err, payment) + } + + // Verify all errors in planned sequence were returned. + if len(errorSeq) > 0 { + t.Fatalf("not all errors returned: %v errors left in sequence"+ + " (direction switched)", len(errorSeq)) + } + + // Check route is: A->B->D->E->F. + expectedSeq = []string{"b", "d", "e", "f"} + actualSeq = []string{} + for i := 0; i < len(route.Hops); i++ { + actualSeq = append(actualSeq, + getAliasFromPubKey(route.Hops[i].PubKeyBytes, + ctx.aliases), + ) + } + if !reflect.DeepEqual(expectedSeq, actualSeq) { + t.Fatalf("incorrect route (direction switched): expected %s "+ + "got %s", strings.Join(expectedSeq, "->"), + strings.Join(actualSeq, "->")) + } + + // The route should have the updated fee. + if route.TotalFees != lnwire.MilliSatoshi(150) { + t.Fatalf("expected %v MSat total fee (direction switched), "+ + "instead got %v MSat", 150, route.TotalFees) + } +} + // 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 witin a mission control session. This diff --git a/routing/testdata/basic_graph.json b/routing/testdata/basic_graph.json index c04430b6a89..7e4e3636ede 100644 --- a/routing/testdata/basic_graph.json +++ b/routing/testdata/basic_graph.json @@ -39,7 +39,8 @@ }, { "source": false, - "pubkey": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add", + "pubkey": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318", + "privkey": "82b266f659bd83a976bac11b2cc442baec5508e84e61085d7ec2b0fc52156c87", "alias": "songoku" }, { @@ -154,7 +155,7 @@ "capacity": 120000 }, { - "node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add", + "node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318", "node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6", "channel_id": 12345, "channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0", @@ -168,7 +169,7 @@ "capacity": 100000 }, { - "node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add", + "node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318", "node_2": "0367cec75158a4129177bfb8b269cb586efe93d751b43800d456485e81c2620ca6", "channel_id": 12345, "channel_point": "89dc56859c6a082d15ba1a7f6cb6be3fea62e1746e2cb8497b1189155c21a233:0", @@ -182,7 +183,7 @@ "capacity": 100000 }, { - "node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add", + "node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318", "node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb", "channel_id": 3495345, "channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0", @@ -196,7 +197,7 @@ "capacity": 110000 }, { - "node_1": "032b480de5d002f1a8fd1fe1bbf0a0f1b07760f65f052e66d56f15d71097c01add", + "node_1": "026c43a8ac1cd8519985766e90748e1e06871dab0ff6b8af27e8c1a61640481318", "node_2": "036264734b40c9e91d3d990a8cdfbbe23b5b0b7ad3cd0e080a25dcd05d39eeb7eb", "channel_id": 3495345, "channel_point": "9f155756b33a0a6827713965babbd561b55f9520444ac5db0cf7cb2eb0deb5bc:0",