Skip to content

Commit 4cf18ee

Browse files
authored
Merge pull request lightningnetwork#9703 from yyforyongyu/fix-attempt-hash
Patch htlc attempt hash for legacy payments
2 parents ff6f98a + 79ab2b2 commit 4cf18ee

File tree

3 files changed

+100
-1
lines changed

3 files changed

+100
-1
lines changed

docs/release-notes/release-notes-0.19.0.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ keysend payment validation is stricter.
111111
process of the node won't be interrupted if a non-fatal error is returned from
112112
the subsystems.
113113

114+
* [Fixed](https://github.com/lightningnetwork/lnd/pull/9703) a possible panic
115+
when reloading legacy inflight payments which don't have the MPP feature.
116+
114117
# New Features
115118

116119
* Add support for [archiving channel backup](https://github.com/lightningnetwork/lnd/pull/9232)

routing/payment_lifecycle.go

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
"github.com/lightningnetwork/lnd/graph/db/models"
1515
"github.com/lightningnetwork/lnd/htlcswitch"
1616
"github.com/lightningnetwork/lnd/lntypes"
17+
"github.com/lightningnetwork/lnd/lnutils"
1718
"github.com/lightningnetwork/lnd/lnwire"
1819
"github.com/lightningnetwork/lnd/routing/route"
1920
"github.com/lightningnetwork/lnd/routing/shards"
@@ -500,7 +501,8 @@ func (p *paymentLifecycle) collectResultAsync(attempt *channeldb.HTLCAttempt) {
500501
func (p *paymentLifecycle) collectResult(
501502
attempt *channeldb.HTLCAttempt) (*htlcswitch.PaymentResult, error) {
502503

503-
log.Tracef("Collecting result for attempt %v", spew.Sdump(attempt))
504+
log.Tracef("Collecting result for attempt %v",
505+
lnutils.SpewLogClosure(attempt))
504506

505507
result := &htlcswitch.PaymentResult{}
506508

@@ -1060,6 +1062,38 @@ func marshallError(sendError error, time time.Time) *channeldb.HTLCFailInfo {
10601062
return response
10611063
}
10621064

1065+
// patchLegacyPaymentHash will make a copy of the passed attempt and sets its
1066+
// Hash field to be the payment hash if it's nil.
1067+
//
1068+
// NOTE: For legacy payments, which were created before the AMP feature was
1069+
// enabled, the `Hash` field in their HTLC attempts is nil. In that case, we use
1070+
// the payment hash as the `attempt.Hash` as they are identical.
1071+
func (p *paymentLifecycle) patchLegacyPaymentHash(
1072+
a channeldb.HTLCAttempt) channeldb.HTLCAttempt {
1073+
1074+
// Exit early if this is not a legacy attempt.
1075+
if a.Hash != nil {
1076+
return a
1077+
}
1078+
1079+
// Log a warning if the user is still using legacy payments, which has
1080+
// weaker support.
1081+
log.Warnf("Found legacy htlc attempt %v in payment %v", a.AttemptID,
1082+
p.identifier)
1083+
1084+
// Set the attempt's hash to be the payment hash, which is the payment's
1085+
// `PaymentHash`` in the `PaymentCreationInfo`. For legacy payments
1086+
// before AMP feature, the `Hash` field was not set so we use the
1087+
// payment hash instead.
1088+
//
1089+
// NOTE: During the router's startup, we have a similar logic in
1090+
// `resumePayments`, in which we will use the payment hash instead if
1091+
// the attempt's hash is nil.
1092+
a.Hash = &p.identifier
1093+
1094+
return a
1095+
}
1096+
10631097
// reloadInflightAttempts is called when the payment lifecycle is resumed after
10641098
// a restart. It reloads all inflight attempts from the control tower and
10651099
// collects the results of the attempts that have been sent before.
@@ -1075,6 +1109,10 @@ func (p *paymentLifecycle) reloadInflightAttempts() (DBMPPayment, error) {
10751109
log.Infof("Resuming HTLC attempt %v for payment %v",
10761110
a.AttemptID, p.identifier)
10771111

1112+
// Potentially attach the payment hash to the `Hash` field if
1113+
// it's a legacy payment.
1114+
a = p.patchLegacyPaymentHash(a)
1115+
10781116
p.resultCollector(&a)
10791117
}
10801118

routing/payment_lifecycle_test.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1809,3 +1809,61 @@ func TestHandleAttemptResultSuccess(t *testing.T) {
18091809
require.NoError(t, err, "expected no error")
18101810
require.Equal(t, attempt, attemptResult.attempt)
18111811
}
1812+
1813+
// TestReloadInflightAttemptsLegacy checks that when handling a legacy HTLC
1814+
// attempt, `collectResult` behaves as expected.
1815+
func TestReloadInflightAttemptsLegacy(t *testing.T) {
1816+
t.Parallel()
1817+
1818+
// Create a test paymentLifecycle with the initial two calls mocked.
1819+
p, m := newTestPaymentLifecycle(t)
1820+
1821+
// Mount the resultCollector to check the full call path.
1822+
p.resultCollector = p.collectResultAsync
1823+
1824+
// Create testing params.
1825+
paymentAmt := 10_000
1826+
preimage := lntypes.Preimage{1}
1827+
attempt := makeSettledAttempt(t, paymentAmt, preimage)
1828+
1829+
// Make the attempt.Hash to be nil to mock a legacy payment.
1830+
attempt.Hash = nil
1831+
1832+
// Create a mock result returned from the switch.
1833+
result := &htlcswitch.PaymentResult{
1834+
Preimage: preimage,
1835+
}
1836+
1837+
// 1. calls `FetchPayment` and return the payment.
1838+
m.control.On("FetchPayment", p.identifier).Return(m.payment, nil).Once()
1839+
1840+
// 2. calls `InFlightHTLCs` and return the attempt.
1841+
attempts := []channeldb.HTLCAttempt{*attempt}
1842+
m.payment.On("InFlightHTLCs").Return(attempts).Once()
1843+
1844+
// 3. Mock the htlcswitch to return a the result chan.
1845+
resultChan := make(chan *htlcswitch.PaymentResult, 1)
1846+
m.payer.On("GetAttemptResult",
1847+
attempt.AttemptID, p.identifier, mock.Anything,
1848+
).Return(resultChan, nil).Once().Run(func(args mock.Arguments) {
1849+
// Send the preimage to the result chan.
1850+
resultChan <- result
1851+
})
1852+
1853+
// Now call the method under test.
1854+
payment, err := p.reloadInflightAttempts()
1855+
require.NoError(t, err)
1856+
require.Equal(t, m.payment, payment)
1857+
1858+
var r *switchResult
1859+
1860+
// Assert the result is returned within testTimeout.
1861+
waitErr := wait.NoError(func() error {
1862+
r = <-p.resultCollected
1863+
return nil
1864+
}, testTimeout)
1865+
require.NoError(t, waitErr, "timeout waiting for result")
1866+
1867+
// Assert the result is received as expected.
1868+
require.Equal(t, result, r.result)
1869+
}

0 commit comments

Comments
 (0)