diff --git a/routing/mock_test.go b/routing/mock_test.go index 30d5dedc..cf828baf 100644 --- a/routing/mock_test.go +++ b/routing/mock_test.go @@ -302,20 +302,38 @@ func (m *mockControlTower) RegisterAttempt(phash lntypes.Hash, m.Lock() defer m.Unlock() - // Cannot register attempts for successful or failed payments. - if _, ok := m.successful[phash]; ok { - return channeldb.ErrPaymentAlreadySucceeded - } - - if _, ok := m.failed[phash]; ok { - return channeldb.ErrPaymentAlreadyFailed - } - + // Lookup payment. p, ok := m.payments[phash] if !ok { return channeldb.ErrPaymentNotInitiated } + var inFlight bool + for _, a := range p.attempts { + if a.Settle != nil { + continue + } + + if a.Failure != nil { + continue + } + + inFlight = true + } + + // Cannot register attempts for successful or failed payments. + _, settled := m.successful[phash] + _, failed := m.failed[phash] + + if settled && !inFlight { + return channeldb.ErrPaymentAlreadySucceeded + } + + if failed && !inFlight { + return channeldb.ErrPaymentAlreadyFailed + } + + // Add attempt to payment. p.attempts = append(p.attempts, channeldb.HTLCAttempt{ HTLCAttemptInfo: *a, }) diff --git a/routing/payment_lifecycle_test.go b/routing/payment_lifecycle_test.go index 036dc8ff..e8aa8c51 100644 --- a/routing/payment_lifecycle_test.go +++ b/routing/payment_lifecycle_test.go @@ -615,6 +615,41 @@ func TestRouterPaymentStateMachine(t *testing.T) { }, paymentErr: channeldb.FailureReasonPaymentDetails, }, + { + // A MP payment scenario when our path finding returns + // after we've just received a terminal failure, and + // demonstrates a bug where the payment will return with + // and "unexpected" ErrPaymentAlreadyFailed rather than + // failing with a permanent error. This results in the + // payment getting stuck. + name: "MP path found after failure", + + steps: []string{ + routerInitPayment, + + // shard 0 + routeRelease, + routerRegisterAttempt, + sendToSwitchSuccess, + + // The first shard fail with a terminal error. + getPaymentResultTerminalFailure, + routerFailAttempt, + routerFailPayment, + + // shard 1 fails because we've had a terminal + // failure. + routeRelease, + routerRegisterAttempt, + + // Payment fails. + paymentError, + }, + routes: []*route.Route{ + shard, shard, + }, + paymentErr: channeldb.ErrPaymentAlreadyFailed, + }, } for _, test := range tests {