diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go index 8c8829420..ba9825c90 100644 --- a/routing/result_interpretation.go +++ b/routing/result_interpretation.go @@ -108,6 +108,16 @@ func (i *interpretedResult) processFail( return } + // If the payment was to a blinded route and we received an error from + // after the introduction point, handle this error separately - there + // has been a protocol violation from the introduction node. This + // penalty applies regardless of the error code that is returned. + introIdx, isBlinded := introductionPointIndex(rt) + if isBlinded && introIdx < *errSourceIdx { + i.processPaymentOutcomeBadIntro(rt, introIdx, *errSourceIdx) + return + } + switch *errSourceIdx { // We are the source of the failure. @@ -129,6 +139,33 @@ func (i *interpretedResult) processFail( } } +// processPaymentOutcomeBadIntro handles the case where we have made payment +// to a blinded route, but received an error from a node after the introduction +// node. This indicates that the introduction node is not obeying the route +// blinding specification, as we expect all errors from the introduction node +// to be source from it. +func (i *interpretedResult) processPaymentOutcomeBadIntro(route *route.Route, + introIdx, errSourceIdx int) { + + // We fail the introduction node for not obeying the specification. + i.failNode(route, introIdx) + + // Other preceding channels in the route forwarded correctly. Note + // that we do not assign success to the incoming link to the + // introduction node because it has not handled the error correctly. + if introIdx > 1 { + i.successPairRange(route, 0, introIdx-2) + } + + // If the source of the failure was from the final node, we also set + // a final failure reason because the recipient can't process the + // payment (independent of the introduction failing to convert the + // error, we can't complete the payment if the last hop fails). + if errSourceIdx == len(route.Hops) { + i.finalFailureReason = &reasonError + } +} + // processPaymentOutcomeSelf handles failures sent by ourselves. func (i *interpretedResult) processPaymentOutcomeSelf( rt *route.Route, failure lnwire.FailureMessage) { @@ -401,6 +438,20 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate( } } +// introductionPointIndex returns the index of an introduction point in a +// route, using the same indexing in the route that we use for errorSourceIdx +// (i.e., that we consider our own node to be at index zero). A boolean is +// returned to indicate whether the route contains a blinded portion at all. +func introductionPointIndex(route *route.Route) (int, bool) { + for i, hop := range route.Hops { + if hop.BlindingPoint != nil { + return i + 1, true + } + } + + return 0, false +} + // processPaymentOutcomeUnknown processes a payment outcome for which no failure // message or source is available. func (i *interpretedResult) processPaymentOutcomeUnknown(route *route.Route) { diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go index b9fcc68d1..3fbf14989 100644 --- a/routing/result_interpretation_test.go +++ b/routing/result_interpretation_test.go @@ -4,6 +4,7 @@ import ( "reflect" "testing" + "github.com/btcsuite/btcd/btcec/v2" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" @@ -14,6 +15,10 @@ var ( {1, 0}, {1, 1}, {1, 2}, {1, 3}, {1, 4}, } + // blindingPoint provides a non-nil blinding point (value is never + // used). + blindingPoint = &btcec.PublicKey{} + routeOneHop = route.Route{ SourcePubKey: hops[0], TotalAmount: 100, @@ -51,6 +56,40 @@ var ( {PubKeyBytes: hops[4], AmtToForward: 90}, }, } + + // blindedMultiHop is a blinded path where there are cleartext hops + // before the introduction node, and an intermediate blinded hop before + // the recipient after it. + blindedMultiHop = route.Route{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*route.Hop{ + {PubKeyBytes: hops[1], AmtToForward: 99}, + { + PubKeyBytes: hops[2], + AmtToForward: 95, + BlindingPoint: blindingPoint, + }, + {PubKeyBytes: hops[3], AmtToForward: 88}, + {PubKeyBytes: hops[4], AmtToForward: 77}, + }, + } + + // blindedMultiToIntroduction is a blinded path which goes directly + // to the introduction node, with multiple blinded hops after it. + blindedMultiToIntroduction = route.Route{ + SourcePubKey: hops[0], + TotalAmount: 100, + Hops: []*route.Hop{ + { + PubKeyBytes: hops[1], + AmtToForward: 90, + BlindingPoint: blindingPoint, + }, + {PubKeyBytes: hops[2], AmtToForward: 75}, + {PubKeyBytes: hops[3], AmtToForward: 58}, + }, + } ) func getTestPair(from, to int) DirectedNodePair { @@ -366,6 +405,52 @@ var resultTestCases = []resultTestCase{ policyFailure: getPolicyFailure(1, 2), }, }, + // Test the case where a node after the introduction node returns a + // error. In this case the introduction node is penalized because it + // has not followed the specification properly. + { + name: "error after introduction", + route: &blindedMultiToIntroduction, + failureSrcIdx: 2, + // Note that the failure code doesn't matter in this case - + // all we're worried about is errors originating after the + // introduction node. + failure: &lnwire.FailExpiryTooSoon{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): failPairResult(0), + getTestPair(1, 0): failPairResult(0), + getTestPair(1, 2): failPairResult(0), + getTestPair(2, 1): failPairResult(0), + }, + // Note: introduction node is failed even though the + // error source is after it. + nodeFailure: &hops[1], + }, + }, + // Test the case where we get a blinding failure from a blinded final + // hop when we expected the introduction node to convert. + { + name: "final failure expected intro", + route: &blindedMultiHop, + failureSrcIdx: 4, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + getTestPair(1, 2): failPairResult(0), + getTestPair(2, 1): failPairResult(0), + getTestPair(2, 3): failPairResult(0), + getTestPair(3, 2): failPairResult(0), + }, + // Note that the introduction node is penalized, not + // the final hop. + nodeFailure: &hops[2], + finalFailureReason: &reasonError, + }, + }, } // TestResultInterpretation executes a list of test cases that test the result