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.
This commit is contained in:
2 files changed
+187
No files matched your search
@@ -431,6 +431,70 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(
|
|||||||
case *lnwire.FailExpiryTooSoon:
|
case *lnwire.FailExpiryTooSoon:
|
||||||
reportAll()
|
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
|
// In all other cases, we penalize the reporting node. These are all
|
||||||
// failures that should not happen.
|
// failures that should not happen.
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -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
|
// blindedMultiToIntroduction is a blinded path which goes directly
|
||||||
// to the introduction node, with multiple blinded hops after it.
|
// to the introduction node, with multiple blinded hops after it.
|
||||||
blindedMultiToIntroduction = route.Route{
|
blindedMultiToIntroduction = route.Route{
|
||||||
@@ -451,6 +467,113 @@ var resultTestCases = []resultTestCase{
|
|||||||
finalFailureReason: &reasonError,
|
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
|
// TestResultInterpretation executes a list of test cases that test the result
|
||||||
|
|||||||
Reference in new issue
Block a user