diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go index 4118286e6..b998968d0 100644 --- a/routing/result_interpretation.go +++ b/routing/result_interpretation.go @@ -93,8 +93,27 @@ func interpretResult(rt *route.Route, success bool, failureSrcIdx *int, // processSuccess processes a successful payment attempt. func (i *interpretedResult) processSuccess(route *route.Route) { - // For successes, all nodes must have acted in the right way. Therefore - // we mark all of them with a success result. + // For successes, all nodes must have acted in the right way. + // Therefore we mark all of them with a success result. However we need + // to handle the blinded route part separately because for intermediate + // blinded nodes the amount field is set to zero so we use the receiver + // amount. + introIdx, isBlinded := introductionPointIndex(route) + if isBlinded { + // Report success for all the pairs until the introduction + // point. + i.successPairRange(route, 0, introIdx-1) + + // Handle the blinded route part. + // + // NOTE: The introIdx index here does describe the node after + // the introduction point. + i.markBlindedRouteSuccess(route, introIdx) + + return + } + + // Mark nodes as successful in the non-blinded case of the payment. i.successPairRange(route, 0, len(route.Hops)-1) } @@ -505,13 +524,22 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate( 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, - ) + // We penalize the final hop of the blinded route which + // is sufficient to not reuse this route again and is + // also more memory efficient because the other hops + // of the blinded path are ephemeral and will only be + // used in conjunction with the final hop. Moreover we + // don't want to punish the introduction node because + // the blinded failure does not necessarily mean that + // the introduction node was at fault. + // + // TODO(ziggie): Make sure we only keep mc data for + // blinded paths, in both the success and failure case, + // in memory during the time of the payment and remove + // it afterwards. Blinded paths and their blinded hop + // keys are always changing per blinded route so there + // is no point in persisting this data. + i.failBlindedRoute(route) } // In all other cases, we penalize the reporting node. These are all @@ -624,6 +652,43 @@ func (i *interpretedResult) successPairRange( } } +// failBlindedRoute marks a blinded route as failed for the specific amount to +// send by only punishing the last pair. +func (i *interpretedResult) failBlindedRoute(rt *route.Route) { + // We fail the last pair of the route, in order to fail the complete + // blinded route. This is because the combination of ephemeral pubkeys + // is unique to the route. We fail the last pair in order to not punish + // the introduction node, since we don't want to disincentivize them + // from providing that service. + pair, _ := getPair(rt, len(rt.Hops)-1) + + // Since all the hops along a blinded path don't have any amount set, we + // extract the minimal amount to punish from the value that is tried to + // be sent to the receiver. + amt := rt.Hops[len(rt.Hops)-1].AmtToForward + + i.pairResults[pair] = failPairResult(amt) +} + +// markBlindedRouteSuccess marks the hops of the blinded route AFTER the +// introduction node as successful. +// +// NOTE: The introIdx must be the index of the first hop of the blinded route +// AFTER the introduction node. +func (i *interpretedResult) markBlindedRouteSuccess(rt *route.Route, + introIdx int) { + + // For blinded hops we do not have the forwarding amount so we take the + // minimal amount which went through the route by looking at the last + // hop. + successAmt := rt.Hops[len(rt.Hops)-1].AmtToForward + for idx := introIdx; idx < len(rt.Hops); idx++ { + pair, _ := getPair(rt, idx) + + i.pairResults[pair] = successPairResult(successAmt) + } +} + // getPair returns a node pair from the route and the amount passed between that // pair. func getPair(rt *route.Route, channelIdx int) (DirectedNodePair, diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go index bf7d6d3ed..0b8a2e262 100644 --- a/routing/result_interpretation_test.go +++ b/routing/result_interpretation_test.go @@ -64,14 +64,27 @@ var ( SourcePubKey: hops[0], TotalAmount: 100, Hops: []*route.Hop{ - {PubKeyBytes: hops[1], AmtToForward: 99}, { - PubKeyBytes: hops[2], - AmtToForward: 95, - BlindingPoint: blindingPoint, + PubKeyBytes: hops[1], + AmtToForward: 99, + }, + { + PubKeyBytes: hops[2], + // Intermediate blinded hops don't have an + // amount set. + AmtToForward: 0, + BlindingPoint: genTestPubKey(), + }, + { + PubKeyBytes: hops[3], + // Intermediate blinded hops don't have an + // amount set. + AmtToForward: 0, + }, + { + PubKeyBytes: hops[4], + AmtToForward: 77, }, - {PubKeyBytes: hops[3], AmtToForward: 88}, - {PubKeyBytes: hops[4], AmtToForward: 77}, }, } @@ -81,13 +94,21 @@ var ( SourcePubKey: hops[0], TotalAmount: 100, Hops: []*route.Hop{ - {PubKeyBytes: hops[1], AmtToForward: 99}, { - PubKeyBytes: hops[2], - AmtToForward: 95, - BlindingPoint: blindingPoint, + PubKeyBytes: hops[1], + AmtToForward: 99, + }, + { + PubKeyBytes: hops[2], + // Intermediate blinded hops don't have an + // amount set. + AmtToForward: 0, + BlindingPoint: genTestPubKey(), + }, + { + PubKeyBytes: hops[3], + AmtToForward: 88, }, - {PubKeyBytes: hops[3], AmtToForward: 88}, }, } @@ -98,12 +119,22 @@ var ( TotalAmount: 100, Hops: []*route.Hop{ { - PubKeyBytes: hops[1], - AmtToForward: 90, - BlindingPoint: blindingPoint, + PubKeyBytes: hops[1], + // Intermediate blinded hops don't have an + // amount set. + AmtToForward: 0, + BlindingPoint: genTestPubKey(), + }, + { + PubKeyBytes: hops[2], + // Intermediate blinded hops don't have an + // amount set. + AmtToForward: 0, + }, + { + PubKeyBytes: hops[3], + AmtToForward: 58, }, - {PubKeyBytes: hops[2], AmtToForward: 75}, - {PubKeyBytes: hops[3], AmtToForward: 58}, }, } @@ -113,7 +144,10 @@ var ( SourcePubKey: hops[0], TotalAmount: 100, Hops: []*route.Hop{ - {PubKeyBytes: hops[1], AmtToForward: 95}, + { + PubKeyBytes: hops[1], + AmtToForward: 95, + }, { PubKeyBytes: hops[2], AmtToForward: 90, @@ -123,6 +157,12 @@ var ( } ) +func genTestPubKey() *btcec.PublicKey { + key, _ := btcec.NewPrivateKey() + + return key.PubKey() +} + func getTestPair(from, to int) DirectedNodePair { return NewDirectedNodePair(hops[from], hops[to]) } @@ -494,7 +534,12 @@ var resultTestCases = []resultTestCase{ pairResults: map[DirectedNodePair]pairResult{ getTestPair(0, 1): successPairResult(100), getTestPair(1, 2): successPairResult(99), - getTestPair(3, 4): failPairResult(88), + + // The amount for the last hop is always the + // receiver amount because the amount to forward + // is always set to 0 for intermediate blinded + // hops. + getTestPair(3, 4): failPairResult(77), }, }, }, @@ -509,7 +554,12 @@ var resultTestCases = []resultTestCase{ expectedResult: &interpretedResult{ pairResults: map[DirectedNodePair]pairResult{ getTestPair(0, 1): successPairResult(100), - getTestPair(2, 3): failPairResult(75), + + // The amount for the last hop is always the + // receiver amount because the amount to forward + // is always set to 0 for intermediate blinded + // hops. + getTestPair(2, 3): failPairResult(58), }, }, }, @@ -624,6 +674,25 @@ var resultTestCases = []resultTestCase{ finalFailureReason: &reasonError, }, }, + // Test a multi-hop blinded route and that in a success case the amounts + // for the blinded route part are correctly set to the receiver amount. + { + name: "blinded multi-hop success", + route: &blindedMultiToIntroduction, + success: true, + expectedResult: &interpretedResult{ + pairResults: map[DirectedNodePair]pairResult{ + getTestPair(0, 1): successPairResult(100), + + // For the route blinded part of the route the + // success amount is determined by the receiver + // amount because the intermediate blinded hops + // set the forwarded amount to 0. + getTestPair(1, 2): successPairResult(58), + getTestPair(2, 3): successPairResult(58), + }, + }, + }, } // TestResultInterpretation executes a list of test cases that test the result