mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-20 02:27:21 +01:00
b82478a7e7
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.
606 lines
17 KiB
Go
606 lines
17 KiB
Go
package routing
|
|
|
|
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"
|
|
)
|
|
|
|
var (
|
|
hops = []route.Vertex{
|
|
{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,
|
|
Hops: []*route.Hop{
|
|
{PubKeyBytes: hops[1], AmtToForward: 99},
|
|
},
|
|
}
|
|
|
|
routeTwoHop = route.Route{
|
|
SourcePubKey: hops[0],
|
|
TotalAmount: 100,
|
|
Hops: []*route.Hop{
|
|
{PubKeyBytes: hops[1], AmtToForward: 99},
|
|
{PubKeyBytes: hops[2], AmtToForward: 97},
|
|
},
|
|
}
|
|
|
|
routeThreeHop = route.Route{
|
|
SourcePubKey: hops[0],
|
|
TotalAmount: 100,
|
|
Hops: []*route.Hop{
|
|
{PubKeyBytes: hops[1], AmtToForward: 99},
|
|
{PubKeyBytes: hops[2], AmtToForward: 97},
|
|
{PubKeyBytes: hops[3], AmtToForward: 94},
|
|
},
|
|
}
|
|
|
|
routeFourHop = route.Route{
|
|
SourcePubKey: hops[0],
|
|
TotalAmount: 100,
|
|
Hops: []*route.Hop{
|
|
{PubKeyBytes: hops[1], AmtToForward: 99},
|
|
{PubKeyBytes: hops[2], AmtToForward: 97},
|
|
{PubKeyBytes: hops[3], AmtToForward: 94},
|
|
{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},
|
|
},
|
|
}
|
|
|
|
// 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{
|
|
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 {
|
|
return NewDirectedNodePair(hops[from], hops[to])
|
|
}
|
|
|
|
func getPolicyFailure(from, to int) *DirectedNodePair {
|
|
pair := getTestPair(from, to)
|
|
return &pair
|
|
}
|
|
|
|
type resultTestCase struct {
|
|
name string
|
|
route *route.Route
|
|
success bool
|
|
failureSrcIdx int
|
|
failure lnwire.FailureMessage
|
|
|
|
expectedResult *interpretedResult
|
|
}
|
|
|
|
var resultTestCases = []resultTestCase{
|
|
// Tests that a temporary channel failure result is properly
|
|
// interpreted.
|
|
{
|
|
name: "fail",
|
|
route: &routeTwoHop,
|
|
failureSrcIdx: 1,
|
|
failure: lnwire.NewTemporaryChannelFailure(nil),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
getTestPair(1, 2): failPairResult(99),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests that a expiry too soon failure result is properly interpreted.
|
|
{
|
|
name: "fail expiry too soon",
|
|
route: &routeFourHop,
|
|
failureSrcIdx: 3,
|
|
failure: lnwire.NewExpiryTooSoon(lnwire.ChannelUpdate{}),
|
|
|
|
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),
|
|
getTestPair(2, 3): failPairResult(0),
|
|
getTestPair(3, 2): failPairResult(0),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests an incorrect payment details result. This should be a final
|
|
// failure, but mark all pairs along the route as successful.
|
|
{
|
|
name: "fail incorrect details",
|
|
route: &routeTwoHop,
|
|
failureSrcIdx: 2,
|
|
failure: lnwire.NewFailIncorrectDetails(97, 0),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
getTestPair(1, 2): successPairResult(99),
|
|
},
|
|
finalFailureReason: &reasonIncorrectDetails,
|
|
},
|
|
},
|
|
|
|
// Tests a successful direct payment.
|
|
{
|
|
name: "success direct",
|
|
route: &routeOneHop,
|
|
success: true,
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests a successful two hop payment.
|
|
{
|
|
name: "success",
|
|
route: &routeTwoHop,
|
|
success: true,
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
getTestPair(1, 2): successPairResult(99),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests a malformed htlc from a direct peer.
|
|
{
|
|
name: "fail malformed htlc from direct peer",
|
|
route: &routeTwoHop,
|
|
failureSrcIdx: 0,
|
|
failure: lnwire.NewInvalidOnionKey(nil),
|
|
|
|
expectedResult: &interpretedResult{
|
|
nodeFailure: &hops[1],
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(1, 0): failPairResult(0),
|
|
getTestPair(1, 2): failPairResult(0),
|
|
getTestPair(0, 1): failPairResult(0),
|
|
getTestPair(2, 1): failPairResult(0),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests a malformed htlc from a direct peer that is also the final
|
|
// destination.
|
|
{
|
|
name: "fail malformed htlc from direct final peer",
|
|
route: &routeOneHop,
|
|
failureSrcIdx: 0,
|
|
failure: lnwire.NewInvalidOnionKey(nil),
|
|
|
|
expectedResult: &interpretedResult{
|
|
finalFailureReason: &reasonError,
|
|
nodeFailure: &hops[1],
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(1, 0): failPairResult(0),
|
|
getTestPair(0, 1): failPairResult(0),
|
|
},
|
|
},
|
|
},
|
|
|
|
// Tests that a fee insufficient failure to an intermediate hop with
|
|
// index 2 results in the first hop marked as success, and then a
|
|
// bidirectional failure for the incoming channel. It should also result
|
|
// in a policy failure for the outgoing hop.
|
|
{
|
|
name: "fail fee insufficient intermediate",
|
|
route: &routeFourHop,
|
|
failureSrcIdx: 2,
|
|
failure: lnwire.NewFeeInsufficient(0, lnwire.ChannelUpdate{}),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): {
|
|
success: true,
|
|
amt: 100,
|
|
},
|
|
getTestPair(1, 2): {},
|
|
getTestPair(2, 1): {},
|
|
},
|
|
policyFailure: getPolicyFailure(2, 3),
|
|
},
|
|
},
|
|
|
|
// Tests an invalid onion payload from a final hop. The final hop should
|
|
// be failed while the proceeding hops are reproed as successes. The
|
|
// failure is terminal since the receiver can't process our onion.
|
|
{
|
|
name: "fail invalid onion payload final hop four",
|
|
route: &routeFourHop,
|
|
failureSrcIdx: 4,
|
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): {
|
|
success: true,
|
|
amt: 100,
|
|
},
|
|
getTestPair(1, 2): {
|
|
success: true,
|
|
amt: 99,
|
|
},
|
|
getTestPair(2, 3): {
|
|
success: true,
|
|
amt: 97,
|
|
},
|
|
getTestPair(4, 3): {},
|
|
getTestPair(3, 4): {},
|
|
},
|
|
finalFailureReason: &reasonError,
|
|
nodeFailure: &hops[4],
|
|
},
|
|
},
|
|
|
|
// Tests an invalid onion payload from a final hop on a three hop route.
|
|
{
|
|
name: "fail invalid onion payload final hop three",
|
|
route: &routeThreeHop,
|
|
failureSrcIdx: 3,
|
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): {
|
|
success: true,
|
|
amt: 100,
|
|
},
|
|
getTestPair(1, 2): {
|
|
success: true,
|
|
amt: 99,
|
|
},
|
|
getTestPair(3, 2): {},
|
|
getTestPair(2, 3): {},
|
|
},
|
|
finalFailureReason: &reasonError,
|
|
nodeFailure: &hops[3],
|
|
},
|
|
},
|
|
|
|
// Tests an invalid onion payload from an intermediate hop. Only the
|
|
// reporting node should be failed. The failure is non-terminal since we
|
|
// can still try other paths.
|
|
{
|
|
name: "fail invalid onion payload intermediate",
|
|
route: &routeFourHop,
|
|
failureSrcIdx: 3,
|
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): {
|
|
success: true,
|
|
amt: 100,
|
|
},
|
|
getTestPair(1, 2): {
|
|
success: true,
|
|
amt: 99,
|
|
},
|
|
getTestPair(3, 2): {},
|
|
getTestPair(3, 4): {},
|
|
getTestPair(2, 3): {},
|
|
getTestPair(4, 3): {},
|
|
},
|
|
nodeFailure: &hops[3],
|
|
},
|
|
},
|
|
|
|
// Tests an invalid onion payload in a direct peer that is also the
|
|
// final hop. The final node should be failed and the error is terminal
|
|
// since the remote node can't process our onion.
|
|
{
|
|
name: "fail invalid onion payload direct",
|
|
route: &routeOneHop,
|
|
failureSrcIdx: 1,
|
|
failure: lnwire.NewInvalidOnionPayload(0, 0),
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(1, 0): {},
|
|
getTestPair(0, 1): {},
|
|
},
|
|
finalFailureReason: &reasonError,
|
|
nodeFailure: &hops[1],
|
|
},
|
|
},
|
|
|
|
// Tests a single hop mpp timeout. Test that final node is not
|
|
// penalized. This is a temporary measure while we decide how to
|
|
// penalize mpp timeouts.
|
|
{
|
|
name: "one hop mpp timeout",
|
|
route: &routeOneHop,
|
|
failureSrcIdx: 1,
|
|
failure: &lnwire.FailMPPTimeout{},
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
},
|
|
nodeFailure: nil,
|
|
},
|
|
},
|
|
|
|
// Tests a two hop mpp timeout. Test that final node is not penalized
|
|
// and the intermediate hop is attributed the success. This is a
|
|
// temporary measure while we decide how to penalize mpp timeouts.
|
|
{
|
|
name: "two hop mpp timeout",
|
|
route: &routeTwoHop,
|
|
failureSrcIdx: 2,
|
|
failure: &lnwire.FailMPPTimeout{},
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(0, 1): successPairResult(100),
|
|
getTestPair(1, 2): successPairResult(99),
|
|
},
|
|
nodeFailure: nil,
|
|
},
|
|
},
|
|
|
|
// Test a channel disabled failure from the final hop in two hops. Only the
|
|
// disabled channel should be penalized for any amount.
|
|
{
|
|
name: "two hop channel disabled",
|
|
route: &routeTwoHop,
|
|
failureSrcIdx: 1,
|
|
failure: &lnwire.FailChannelDisabled{},
|
|
|
|
expectedResult: &interpretedResult{
|
|
pairResults: map[DirectedNodePair]pairResult{
|
|
getTestPair(1, 2): failPairResult(0),
|
|
getTestPair(2, 1): failPairResult(0),
|
|
getTestPair(0, 1): successPairResult(100),
|
|
},
|
|
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,
|
|
},
|
|
},
|
|
// 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
|
|
// interpretation logic.
|
|
func TestResultInterpretation(t *testing.T) {
|
|
emptyResults := make(map[DirectedNodePair]pairResult)
|
|
|
|
for _, testCase := range resultTestCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
i := interpretResult(
|
|
testCase.route, testCase.success,
|
|
&testCase.failureSrcIdx, testCase.failure,
|
|
)
|
|
|
|
expected := testCase.expectedResult
|
|
|
|
// Replace nil pairResults with empty map to satisfy
|
|
// DeepEqual.
|
|
if expected.pairResults == nil {
|
|
expected.pairResults = emptyResults
|
|
}
|
|
|
|
if !reflect.DeepEqual(i, expected) {
|
|
t.Fatalf("unexpected result\nwant: %v\ngot: %v",
|
|
spew.Sdump(expected), spew.Sdump(i))
|
|
}
|
|
})
|
|
}
|
|
}
|