From b82478a7e7a7abcdac93bab26bdc15ba67e6220e Mon Sep 17 00:00:00 2001 From: Carla Kirk-Cohen Date: Mon, 6 Nov 2023 11:32:30 -0500 Subject: [PATCH] routing: add result interpretation for intermediate invalid blinding This commit adds handling for route blinding errors that are reported by the introduction node in a multi-hop blinded route. As the introduction node is always responsible for handling blinded errors, it is not penalized - only the final hop is penalized to discourage the blinded route without filling up mission control with ephemeral results. If this error code is reported by a node that is not an introduction node, we penalize the node because it is returning an error code that it should not be using. --- routing/result_interpretation.go | 64 ++++++++++++++ routing/result_interpretation_test.go | 123 ++++++++++++++++++++++++++ 2 files changed, 187 insertions(+) diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go index ba9825c90..1534ba6de 100644 --- a/routing/result_interpretation.go +++ b/routing/result_interpretation.go @@ -431,6 +431,70 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate( case *lnwire.FailExpiryTooSoon: reportAll() + // We only expect to get FailInvalidBlinding from an introduction node + // in a blinded route. The introduction node in a blinded route is + // always responsible for reporting errors for the blinded portion of + // the route (to protect the privacy of the members of the route), so + // we need to be careful not to unfairly "shoot the messenger". + // + // The introduction node has no incentive to falsely report errors to + // sabotage the blinded route because: + // 1. Its ability to route this payment is strictly tied to the + // blinded route. + // 2. The pubkeys in the blinded route are ephemeral, so doing so + // will have no impact on the nodes beyond the individual payment. + // + // Here we handle a few cases where we could unexpectedly receive this + // error: + // 1. Outside of a blinded route: erring node is not spec compliant. + // 2. Before the introduction point: erring node is not spec compliant. + // + // Note that we expect the case where this error is sent from a node + // after the introduction node to be handled elsewhere as this is part + // of a more general class of errors where the introduction node has + // failed to convert errors for the blinded route. + case *lnwire.FailInvalidBlinding: + introIdx, isBlinded := introductionPointIndex(route) + + // Deal with cases where a node has incorrectly returned a + // blinding error: + // 1. A node before the introduction point returned it. + // 2. A node in a non-blinded route returned it. + if errorSourceIdx < introIdx || !isBlinded { + reportNode() + return + } + + // Otherwise, the error was at the introduction node. All + // nodes up until the introduction node forwarded correctly, + // so we award them as successful. + if introIdx >= 1 { + i.successPairRange(route, 0, introIdx-1) + } + + // If the hop after the introduction node that sent us an + // error is the final recipient, then we finally fail the + // payment because the receiver has generated a blinded route + // that they're unable to use. We have this special case so + // that we don't penalize the introduction node, and there is + // no point in retrying the payment while LND only supports + // one blinded route per payment. + // + // Note that if LND is extended to support multiple blinded + // routes, this will terminate the payment without re-trying + // the other routes. + if introIdx == len(route.Hops)-1 { + i.finalFailureReason = &reasonError + } else { + // If there are other hops between the recipient and + // introduction node, then we just penalize the last + // hop in the blinded route to minimize the storage of + // results for ephemeral keys. + i.failPairBalance( + route, len(route.Hops)-1, + ) + } + // In all other cases, we penalize the reporting node. These are all // failures that should not happen. default: diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go index 3fbf14989..92ac255ff 100644 --- a/routing/result_interpretation_test.go +++ b/routing/result_interpretation_test.go @@ -75,6 +75,22 @@ var ( }, } + // blindedSingleHop is a blinded path with a single blinded hop after + // the introduction node. + blindedSingleHop = 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}, + }, + } + // blindedMultiToIntroduction is a blinded path which goes directly // to the introduction node, with multiple blinded hops after it. blindedMultiToIntroduction = route.Route{ @@ -451,6 +467,113 @@ var resultTestCases = []resultTestCase{ finalFailureReason: &reasonError, }, }, + // Test a multi-hop blinded route where the failure occurs at the + // introduction point. + { + name: "blinded multi-hop introduction", + route: &blindedMultiHop, + failureSrcIdx: 2, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + getTestPair(1, 2): successPairResult(99), + getTestPair(3, 4): failPairResult(88), + }, + }, + }, + // Test a multi-hop blinded route where the failure occurs at the + // introduction point, which is a direct peer. + { + name: "blinded multi-hop introduction peer", + route: &blindedMultiToIntroduction, + failureSrcIdx: 1, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + getTestPair(2, 3): failPairResult(75), + }, + }, + }, + // Test a single-hop blinded route where the recipient is directly + // connected to the introduction node. + { + name: "blinded single hop introduction failure", + route: &blindedSingleHop, + failureSrcIdx: 2, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + getTestPair(1, 2): successPairResult(99), + }, + finalFailureReason: &reasonError, + }, + }, + // Test the case where a node before the introduction node returns a + // blinding error and is penalized for returning the wrong error. + { + name: "error before introduction", + route: &blindedMultiHop, + failureSrcIdx: 1, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + // Failures from failing hops[1]. + getTestPair(0, 1): failPairResult(0), + getTestPair(1, 0): failPairResult(0), + getTestPair(1, 2): failPairResult(0), + getTestPair(2, 1): failPairResult(0), + }, + nodeFailure: &hops[1], + }, + }, + // Test the case where an intermediate node that is not in a blinded + // route returns an invalid blinding error and there was one + // successful hop before the incorrect error. + { + name: "intermediate unexpected blinding", + route: &routeThreeHop, + failureSrcIdx: 2, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + // Failures from failing hops[2]. + getTestPair(1, 2): failPairResult(0), + getTestPair(2, 1): failPairResult(0), + getTestPair(2, 3): failPairResult(0), + getTestPair(3, 2): failPairResult(0), + }, + nodeFailure: &hops[2], + }, + }, + // Test the case where an intermediate node that is not in a blinded + // route returns an invalid blinding error and there were no successful + // hops before the erring incoming link (the erring node if our peer). + { + name: "peer unexpected blinding", + route: &routeThreeHop, + failureSrcIdx: 1, + failure: &lnwire.FailInvalidBlinding{}, + + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + // Failures from failing hops[1]. + getTestPair(0, 1): failPairResult(0), + getTestPair(1, 0): failPairResult(0), + getTestPair(1, 2): failPairResult(0), + getTestPair(2, 1): failPairResult(0), + }, + nodeFailure: &hops[1], + }, + }, } // TestResultInterpretation executes a list of test cases that test the result