d9ec158412
This commit overhauls the interpretation of failed payments. It changes the interpretation rules so that we always apply the strongest possible set of penalties, without making assumptions that would hurt good nodes. Main changes are: - Apply different rule sets for intermediate and final nodes. Both types of nodes have different sets of failures that we expect. Penalize nodes that send unexpected failure messages. - Distinguish between direct payments and multi-hop payments. For direct payments, we can infer more about the performance of our peer because we trust ourselves. - In many cases it is impossible for the sender to determine which of the two nodes in a pair is responsible for the failure. In this situation, we now penalize bidirectionally. This does not hurt the good node of the pair, because only its connection to a bad node is penalized. - Previously we always penalized the outgoing connection of the reporting node. This is incorrect for policy related failures. For policy related failures, it could also be that the reporting node received a wrongly crafted htlc from its predecessor. By penalizing the incoming channel, we surely hit the responsible node. - FailExpiryTooSoon is a failure that could have been caused by any node up to the reporting node by delaying forwarding of the htlc. We don't know which node is responsible, therefore we now penalize all node pairs in the route.
147 lines
3.3 KiB
Go
147 lines
3.3 KiB
Go
package routing
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
|
|
"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},
|
|
}
|
|
|
|
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},
|
|
},
|
|
}
|
|
|
|
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},
|
|
},
|
|
}
|
|
)
|
|
|
|
func getTestPair(from, to int) DirectedNodePair {
|
|
return NewDirectedNodePair(hops[from], hops[to])
|
|
}
|
|
|
|
type resultTestCase struct {
|
|
name string
|
|
route *route.Route
|
|
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]lnwire.MilliSatoshi{
|
|
getTestPair(1, 2): 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]lnwire.MilliSatoshi{
|
|
getTestPair(0, 1): 0,
|
|
getTestPair(1, 0): 0,
|
|
getTestPair(1, 2): 0,
|
|
getTestPair(2, 1): 0,
|
|
getTestPair(2, 3): 0,
|
|
getTestPair(3, 2): 0,
|
|
},
|
|
},
|
|
},
|
|
|
|
// 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],
|
|
},
|
|
},
|
|
|
|
// 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],
|
|
},
|
|
},
|
|
}
|
|
|
|
// TestResultInterpretation executes a list of test cases that test the result
|
|
// interpretation logic.
|
|
func TestResultInterpretation(t *testing.T) {
|
|
emptyResults := make(map[DirectedNodePair]lnwire.MilliSatoshi)
|
|
|
|
for _, testCase := range resultTestCases {
|
|
t.Run(testCase.name, func(t *testing.T) {
|
|
i := interpretResult(
|
|
testCase.route, &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.Fatal("unexpected result")
|
|
}
|
|
})
|
|
}
|
|
}
|