@@ -22,6 +22,7 @@ import (
2222 "github.com/lightningnetwork/lnd/lnutils"
2323 "github.com/lightningnetwork/lnd/lnwallet"
2424 "github.com/lightningnetwork/lnd/lnwire"
25+ "github.com/lightningnetwork/lnd/multimutex"
2526 paymentsdb "github.com/lightningnetwork/lnd/payments/db"
2627 "github.com/lightningnetwork/lnd/record"
2728 "github.com/lightningnetwork/lnd/routing/route"
@@ -334,6 +335,13 @@ type ChannelRouter struct {
334335
335336 quit chan struct {}
336337 wg sync.WaitGroup
338+
339+ // paymentLifecycleMtx ensures only one payment lifecycle for a given
340+ // payment hash can be active at a time. Failed payments are retryable,
341+ // so the lock must be held until resumePayment fully exits rather than
342+ // only until the terminal status update is published.
343+ paymentLifecycleMtx * multimutex.Mutex [lntypes.Hash ]
344+ paymentLifecycleMtxOnce sync.Once
337345}
338346
339347// New creates a new instance of the ChannelRouter with the specified
@@ -343,8 +351,9 @@ type ChannelRouter struct {
343351// to fully sync to the latest state of the UTXO set.
344352func New (cfg Config ) (* ChannelRouter , error ) {
345353 return & ChannelRouter {
346- cfg : & cfg ,
347- quit : make (chan struct {}),
354+ cfg : & cfg ,
355+ quit : make (chan struct {}),
356+ paymentLifecycleMtx : multimutex .NewMutex [lntypes.Hash ](),
348357 }, nil
349358}
350359
@@ -903,7 +912,9 @@ func (l *LightningPayment) Identifier() [32]byte {
903912func (r * ChannelRouter ) SendPayment (ctx context.Context ,
904913 payment * LightningPayment ) ([32 ]byte , * route.Route , error ) {
905914
906- paySession , shardTracker , err := r .PreparePayment (payment )
915+ paySession , shardTracker , releaseLifecycle , err := r .PreparePayment (
916+ payment ,
917+ )
907918 if err != nil {
908919 return [32 ]byte {}, nil , err
909920 }
@@ -914,14 +925,15 @@ func (r *ChannelRouter) SendPayment(ctx context.Context,
914925 return r .sendPayment (
915926 ctx , payment .FeeLimit , payment .Identifier (),
916927 payment .PayAttemptTimeout , paySession , shardTracker ,
917- payment .FirstHopCustomRecords ,
928+ payment .FirstHopCustomRecords , releaseLifecycle ,
918929 )
919930}
920931
921932// SendPaymentAsync is the non-blocking version of SendPayment. The payment
922933// result needs to be retrieved via the control tower.
923934func (r * ChannelRouter ) SendPaymentAsync (ctx context.Context ,
924- payment * LightningPayment , ps PaymentSession , st shards.ShardTracker ) {
935+ payment * LightningPayment , ps PaymentSession , st shards.ShardTracker ,
936+ releaseLifecycle func ()) {
925937
926938 // Since this is the first time this payment is being made, we pass nil
927939 // for the existing attempt.
@@ -935,7 +947,7 @@ func (r *ChannelRouter) SendPaymentAsync(ctx context.Context,
935947 _ , _ , err := r .sendPayment (
936948 ctx , payment .FeeLimit , payment .Identifier (),
937949 payment .PayAttemptTimeout , ps , st ,
938- payment .FirstHopCustomRecords ,
950+ payment .FirstHopCustomRecords , releaseLifecycle ,
939951 )
940952 if err != nil {
941953 log .Errorf ("Payment %x failed: %v" ,
@@ -966,24 +978,43 @@ func spewPayment(payment *LightningPayment) lnutils.LogClosure {
966978 })
967979}
968980
981+ // lockPaymentLifecycle locks the payment lifecycle mutex for the given payment
982+ // hash and returns a release function.
983+ func (r * ChannelRouter ) lockPaymentLifecycle (paymentHash lntypes.Hash ) func () {
984+ r .paymentLifecycleMtxOnce .Do (func () {
985+ if r .paymentLifecycleMtx == nil {
986+ r .paymentLifecycleMtx = multimutex .
987+ NewMutex [lntypes.Hash ]()
988+ }
989+ })
990+
991+ r .paymentLifecycleMtx .Lock (paymentHash )
992+
993+ return func () {
994+ r .paymentLifecycleMtx .Unlock (paymentHash )
995+ }
996+ }
997+
969998// PreparePayment creates the payment session and registers the payment with the
970- // control tower.
999+ // control tower. The returned release function must be called after the payment
1000+ // lifecycle has fully exited.
9711001func (r * ChannelRouter ) PreparePayment (payment * LightningPayment ) (
972- PaymentSession , shards.ShardTracker , error ) {
1002+ PaymentSession , shards.ShardTracker , func (), error ) {
9731003
9741004 ctx := context .TODO ()
9751005
9761006 // Assemble any custom data we want to send to the first hop only.
9771007 var firstHopData fn.Option [tlv.Blob ]
9781008 if len (payment .FirstHopCustomRecords ) > 0 {
9791009 if err := payment .FirstHopCustomRecords .Validate (); err != nil {
980- return nil , nil , fmt .Errorf ("invalid first hop custom " +
981- "records: %w" , err )
1010+ return nil , nil , nil , fmt .Errorf (
1011+ "invalid first hop custom records: %w" , err ,
1012+ )
9821013 }
9831014
9841015 firstHopBlob , err := payment .FirstHopCustomRecords .Serialize ()
9851016 if err != nil {
986- return nil , nil , fmt .Errorf ("unable to serialize " +
1017+ return nil , nil , nil , fmt .Errorf ("unable to serialize " +
9871018 "first hop custom records: %w" , err )
9881019 }
9891020
@@ -997,7 +1028,7 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) (
9971028 payment , firstHopData , r .cfg .TrafficShaper ,
9981029 )
9991030 if err != nil {
1000- return nil , nil , err
1031+ return nil , nil , nil , err
10011032 }
10021033
10031034 // Record this payment hash with the ControlTower, ensuring it is not
@@ -1032,12 +1063,16 @@ func (r *ChannelRouter) PreparePayment(payment *LightningPayment) (
10321063 )
10331064 }
10341065
1066+ releaseLifecycle := r .lockPaymentLifecycle (payment .Identifier ())
1067+
10351068 err = r .cfg .Control .InitPayment (ctx , payment .Identifier (), info )
10361069 if err != nil {
1037- return nil , nil , err
1070+ releaseLifecycle ()
1071+
1072+ return nil , nil , nil , err
10381073 }
10391074
1040- return paySession , shardTracker , nil
1075+ return paySession , shardTracker , releaseLifecycle , nil
10411076}
10421077
10431078// SendToRoute sends a payment using the provided route and fails the payment
@@ -1264,8 +1299,12 @@ func (r *ChannelRouter) sendPayment(ctx context.Context,
12641299 feeLimit lnwire.MilliSatoshi , identifier lntypes.Hash ,
12651300 paymentAttemptTimeout time.Duration , paySession PaymentSession ,
12661301 shardTracker shards.ShardTracker ,
1267- firstHopCustomRecords lnwire.CustomRecords ) ([32 ]byte , * route.Route ,
1268- error ) {
1302+ firstHopCustomRecords lnwire.CustomRecords ,
1303+ releaseLifecycle func ()) ([32 ]byte , * route.Route , error ) {
1304+
1305+ if releaseLifecycle != nil {
1306+ defer releaseLifecycle ()
1307+ }
12691308
12701309 // If the user provides a timeout, we will additionally wrap the context
12711310 // in a deadline.
@@ -1511,9 +1550,11 @@ func (r *ChannelRouter) resumePayments() error {
15111550 // attempt has finished anyway. We also set a zero fee limit,
15121551 // as no more routes should be tried.
15131552 noTimeout := time .Duration (0 )
1553+ releaseLifecycle := r .lockPaymentLifecycle (payHash )
15141554 _ , _ , err := r .sendPayment (
15151555 context .Background (), 0 , payHash , noTimeout , paySession ,
15161556 shardTracker , payment .Info .FirstHopCustomRecords ,
1557+ releaseLifecycle ,
15171558 )
15181559 if err != nil {
15191560 log .Errorf ("Resuming payment %v failed: %v" , payHash ,
0 commit comments