Skip to content

Commit 39b8699

Browse files
committed
lnwallet/channeldb: restore fee updates after restart
Fix a bug where UpdateFee entries persisted under remoteUnsignedLocalUpdatesKey were silently skipped during restoreStateLogs, leaving the update log empty after a restart. This caused a retransmitted commit_sig (covering the new fee rate) to fail verification after channel reestablishment, incorrectly triggering a force close. Also refactor AdvanceCommitChainTail to avoid an early return when unsignedAckedUpdates is nil, so that the full validation path always runs. Adds TestChanSyncUpdateFeeRestartAfterRevoke (lnwallet) and TestChannelLinkFailDuringRestart (htlcswitch) to reproduce the scenario at both the channel and link layers.
1 parent 2aec8f3 commit 39b8699

3 files changed

Lines changed: 257 additions & 13 deletions

File tree

channeldb/channel.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3423,18 +3423,14 @@ func (c *OpenChannel) AdvanceCommitChainTail(fwdPkg *FwdPkg,
34233423
// Persist the unsigned acked updates that are not included
34243424
// in their new commitment.
34253425
updateBytes := chanBucket.Get(unsignedAckedUpdatesKey)
3426-
if updateBytes == nil {
3427-
// This shouldn't normally happen as we always store
3428-
// the number of updates, but could still be
3429-
// encountered by nodes that are upgrading.
3430-
newRemoteCommit = &newCommit.Commitment
3431-
return nil
3432-
}
34333426

3434-
r := bytes.NewReader(updateBytes)
3435-
unsignedUpdates, err := deserializeLogUpdates(r)
3436-
if err != nil {
3437-
return err
3427+
var unsignedUpdates []LogUpdate
3428+
if updateBytes != nil {
3429+
r := bytes.NewReader(updateBytes)
3430+
unsignedUpdates, err = deserializeLogUpdates(r)
3431+
if err != nil {
3432+
return err
3433+
}
34383434
}
34393435

34403436
var validUpdates []LogUpdate

htlcswitch/link_test.go

Lines changed: 119 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -445,6 +445,122 @@ func TestChannelLinkSigThenRev(t *testing.T) {
445445
ctx.receiveRevAndAckAliceToBob()
446446
}
447447

448+
// TestChannelLinkFailDuringRestart tests that the link fails (force-closing
449+
// the channel) if the commit dance is not completed after sharing the
450+
// update_fee and one of the peers restarts before completion.
451+
//
452+
// Specifically, this tests the following scenario:
453+
//
454+
// Alice Bob
455+
// ----update_fee----->
456+
// -----commit_sig---->
457+
// <--revoke_and_ack----
458+
// ==== restart node ====
459+
// <-----commit_sig----
460+
// ==== force close ====
461+
func TestChannelLinkFailDuringRestart(t *testing.T) {
462+
const chanAmt = btcutil.SatoshiPerBitcoin * 5
463+
464+
harness, err := newSingleLinkTestHarness(t, chanAmt, 0)
465+
require.NoError(t, err)
466+
467+
err = harness.start()
468+
require.NoError(t, err)
469+
defer harness.aliceLink.Stop()
470+
471+
alice := newPersistentLinkHarness(
472+
t, harness.aliceSwitch, harness.aliceLink,
473+
harness.aliceBatchTicker, harness.aliceRestore,
474+
)
475+
alice.coreLink.markReestablished()
476+
477+
// ----update_fee----->
478+
// -----commit_sig---->
479+
err = alice.coreLink.updateChannelFee(t.Context(), 85727)
480+
require.NoError(t, err)
481+
482+
// Bob receive update_fee.
483+
select {
484+
case msg := <-alice.msgs:
485+
updateFee := msg.(*lnwire.UpdateFee)
486+
harness.bobChannel.ReceiveUpdateFee(
487+
chainfee.SatPerKWeight(updateFee.FeePerKw),
488+
)
489+
case <-time.After(5 * time.Second):
490+
t.Fatalf("did not receive update_fee message")
491+
}
492+
493+
// Bob receive commit_sig, respond with revoke_and_ack.
494+
select {
495+
case msg := <-alice.msgs:
496+
commitSig := msg.(*lnwire.CommitSig)
497+
498+
err = harness.bobChannel.ReceiveNewCommitment(&lnwallet.CommitSigs{
499+
CommitSig: commitSig.CommitSig,
500+
HtlcSigs: commitSig.HtlcSigs,
501+
PartialSig: commitSig.PartialSig,
502+
})
503+
require.NoError(t, err)
504+
505+
// <--revoke_and_ack----
506+
nextRevocation, _, _, err := harness.bobChannel.RevokeCurrentCommitment()
507+
require.NoError(t, err)
508+
harness.aliceLink.HandleChannelUpdate(nextRevocation)
509+
510+
// Bob will generate a commit sig, but this message will not be
511+
// received by Alice, and before that, one of the sides restarts
512+
harness.bobChannel.SignNextCommitment(t.Context())
513+
514+
case <-time.After(5 * time.Second):
515+
t.Fatalf("did not receive commit_sig message")
516+
}
517+
518+
// ==== restart node ====
519+
// Restart Alice so she sends and accepts ChannelReestablish.
520+
alice.restart(true, true)
521+
522+
// Restart Bob by re-instantiating NewLightningChannel.
523+
bobSigner := harness.bobChannel.Signer
524+
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
525+
bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
526+
bobChannel, err := lnwallet.NewLightningChannel(
527+
bobSigner, harness.bobChannel.State(), bobPool,
528+
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
529+
lnwallet.WithAuxSigner(signerMock),
530+
)
531+
require.NoError(t, err)
532+
err = bobPool.Start()
533+
require.NoError(t, err)
534+
535+
// <--channel_reestablish--
536+
bobReest, err := bobChannel.State().ChanSyncMsg()
537+
require.NoError(t, err)
538+
alice.coreLink.HandleChannelUpdate(bobReest)
539+
540+
// --channel_reestablish-->
541+
// Bob processes Alice's reestablish and re-sends commit_sig.
542+
// Alice then force-closes upon receiving the commit_sig, which should
543+
// not happen, indicating a bug.
544+
select {
545+
case msg := <-alice.msgs:
546+
reestMsg, ok := msg.(*lnwire.ChannelReestablish)
547+
require.True(t, ok)
548+
549+
msgsToReSend, _, _, err := bobChannel.ProcessChanSyncMsg(
550+
t.Context(), reestMsg,
551+
)
552+
require.NoError(t, err)
553+
554+
// <-----commit_sig----
555+
// ==== force close ====
556+
alice.coreLink.HandleChannelUpdate(msgsToReSend[0])
557+
time.Sleep(5 * time.Second)
558+
559+
case <-time.After(5 * time.Second):
560+
t.Fatalf("did not receive channel_reestablish message")
561+
}
562+
}
563+
448564
// TestChannelLinkSingleHopPayment in this test we checks the interaction
449565
// between Alice and Bob within scope of one channel.
450566
func TestChannelLinkSingleHopPayment(t *testing.T) {
@@ -4905,9 +5021,10 @@ func (h *persistentLinkHarness) restartLink(
49055021
},
49065022
FetchLastChannelUpdate: mockGetChanUpdateMessage,
49075023
PreimageCache: pCache,
4908-
OnChannelFailure: func(lnwire.ChannelID,
4909-
lnwire.ShortChannelID, LinkFailureError) {
5024+
OnChannelFailure: func(_ lnwire.ChannelID,
5025+
_ lnwire.ShortChannelID, linkErr LinkFailureError) {
49105026

5027+
require.NotEqual(t, linkErr.FailureAction, LinkFailureForceClose)
49115028
},
49125029
UpdateContractSignals: func(*contractcourt.ContractSignals) error {
49135030
return nil

lnwallet/channel_test.go

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2895,6 +2895,137 @@ func TestUpdateFeeReceiverCommits(t *testing.T) {
28952895
require.NoError(t, err, "bob unable to process alice's revocation")
28962896
}
28972897

2898+
// TestChanSyncUpdateFeeRestartAfterRevoke tests a scenario where Alice (channel
2899+
// initiator) sends update_fee and commit_sig, Bob responds with revoke_and_ack
2900+
// and commit_sig, but Alice receives only the revoke_and_ack and then restarts
2901+
// before Bob's commit_sig arrives.
2902+
//
2903+
// After restarting, channel reestablishment causes Bob to retransmit his
2904+
// commit_sig. Alice should be able to validate it, but currently fails because
2905+
// the fee update is not properly restored in her update log from disk.
2906+
//
2907+
// Root cause: during ReceiveRevocation, the fee update is persisted under
2908+
// remoteUnsignedLocalUpdatesKey. However, restoreStateLogs only handles
2909+
// HTLC-type updates (UpdateFulfillHTLC, UpdateFailHTLC, etc.) in that
2910+
// restoration path and silently skips UpdateFee via "default: continue". So
2911+
// after restart Alice's update log is empty, she computes the expected
2912+
// commitment with the old fee rate, Bob's signature (covering the new fee
2913+
// rate) fails verification, and LND incorrectly triggers a force close.
2914+
func TestChanSyncUpdateFeeRestartAfterRevoke(t *testing.T) {
2915+
t.Parallel()
2916+
2917+
aliceChannel, bobChannel, err := CreateTestChannels(
2918+
t, channeldb.SingleFunderTweaklessBit,
2919+
)
2920+
require.NoError(t, err, "unable to create test channels")
2921+
2922+
// Alice is the channel initiator and sends an update_fee to Bob.
2923+
newFee := chainfee.SatPerKWeight(12500)
2924+
err = aliceChannel.UpdateFee(newFee)
2925+
require.NoError(t, err, "alice unable to update fee")
2926+
err = bobChannel.ReceiveUpdateFee(newFee)
2927+
require.NoError(t, err, "bob unable to receive fee update")
2928+
2929+
// Alice signs the next commitment, which includes the fee update.
2930+
aliceNewCommit, err := aliceChannel.SignNextCommitment(ctxb)
2931+
require.NoError(t, err, "alice unable to sign commitment")
2932+
2933+
// Bob receives and processes Alice's commitment.
2934+
err = bobChannel.ReceiveNewCommitment(aliceNewCommit.CommitSigs)
2935+
require.NoError(t, err, "bob unable to process alice's new commitment")
2936+
2937+
// Bob revokes his old commitment (producing a revoke_and_ack)...
2938+
bobRevocation, _, _, err := bobChannel.RevokeCurrentCommitment()
2939+
require.NoError(t, err, "bob unable to revoke commitment")
2940+
2941+
// ...and signs a new commitment for Alice.
2942+
_, err = bobChannel.SignNextCommitment(ctxb)
2943+
require.NoError(t, err, "bob unable to sign alice's commitment")
2944+
2945+
// Alice receives Bob's revoke_and_ack. This advances Bob's remote tail
2946+
// in Alice's view. During ReceiveRevocation, the still-unacknowledged
2947+
// fee update is written to disk under remoteUnsignedLocalUpdatesKey.
2948+
//
2949+
// The connection then drops before Alice receives Bob's commit_sig.
2950+
_, _, err = aliceChannel.ReceiveRevocation(bobRevocation)
2951+
require.NoError(t, err, "alice unable to process bob's revocation")
2952+
2953+
// Alice restarts. Due to the bug, the fee update stored under
2954+
// remoteUnsignedLocalUpdatesKey is not restored into Alice's update
2955+
// log (UpdateFee is skipped by the default: continue in restoreStateLogs).
2956+
aliceChannel, err = restartChannel(aliceChannel)
2957+
require.NoError(t, err, "unable to restart alice channel")
2958+
2959+
// Both sides produce their ChannelReestablish messages.
2960+
aliceSyncMsg, err := aliceChannel.channelState.ChanSyncMsg()
2961+
require.NoError(t, err, "alice unable to produce chan sync msg")
2962+
bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
2963+
require.NoError(t, err, "bob unable to produce chan sync msg")
2964+
2965+
// Alice processes Bob's ChannelReestablish. Bob's NextLocalCommitHeight
2966+
// is 2, and Alice's remoteTipHeight is 1, so 2 == 1+1: Alice concludes
2967+
// the remote side is in sync and has nothing to retransmit.
2968+
aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(
2969+
ctxb, bobSyncMsg,
2970+
)
2971+
require.NoError(t, err, "alice unable to process bob's chan sync msg")
2972+
require.Empty(t, aliceMsgsToSend,
2973+
"alice should have no messages to retransmit after reestablish",
2974+
)
2975+
2976+
// Bob processes Alice's ChannelReestablish. Alice's NextLocalCommitHeight
2977+
// is 1, which equals Bob's remoteTipHeight (1), so Bob detects he owes
2978+
// Alice a commitment signature and prepares to retransmit it.
2979+
bobMsgsToSend, _, _, err := bobChannel.ProcessChanSyncMsg(
2980+
ctxb, aliceSyncMsg,
2981+
)
2982+
require.NoError(t, err, "bob unable to process alice's chan sync msg")
2983+
require.Len(t, bobMsgsToSend, 1,
2984+
"bob should retransmit exactly one message (his commit_sig)",
2985+
)
2986+
2987+
retransmittedSig, ok := bobMsgsToSend[0].(*lnwire.CommitSig)
2988+
require.True(t, ok, "expected bob to retransmit a CommitSig")
2989+
2990+
// Alice receives Bob's retransmitted commit_sig. This should succeed:
2991+
// Alice must have the fee update in her update log to compute the
2992+
// correct commitment and verify Bob's signature.
2993+
//
2994+
// Currently this FAILS because the fee update is not restored after
2995+
// restart, so Alice uses the old fee rate and Bob's signature (covering
2996+
// the new fee rate) does not verify. In the link layer this causes
2997+
// LinkFailureForceClose — an incorrect force close of a healthy channel.
2998+
err = aliceChannel.ReceiveNewCommitment(&CommitSigs{
2999+
CommitSig: retransmittedSig.CommitSig,
3000+
HtlcSigs: retransmittedSig.HtlcSigs,
3001+
})
3002+
require.NoError(t, err,
3003+
"alice should process bob's retransmitted commit_sig without "+
3004+
"error; fee update not restored after restart causes "+
3005+
"spurious force close",
3006+
)
3007+
3008+
// Complete the commit dance and verify the fee is locked in on both sides.
3009+
aliceRevocation, _, _, err := aliceChannel.RevokeCurrentCommitment()
3010+
require.NoError(t, err, "alice unable to revoke commitment")
3011+
3012+
_, _, err = bobChannel.ReceiveRevocation(aliceRevocation)
3013+
require.NoError(t, err, "bob unable to process alice's revocation")
3014+
3015+
require.Equal(t, newFee,
3016+
chainfee.SatPerKWeight(
3017+
aliceChannel.channelState.LocalCommitment.FeePerKw,
3018+
),
3019+
"alice's fee rate was not properly locked in",
3020+
)
3021+
require.Equal(t, newFee,
3022+
chainfee.SatPerKWeight(
3023+
bobChannel.channelState.LocalCommitment.FeePerKw,
3024+
),
3025+
"bob's fee rate was not properly locked in",
3026+
)
3027+
}
3028+
28983029
// TestUpdateFeeReceiverSendsUpdate tests that receiving a fee update as channel
28993030
// initiator fails, and that trying to initiate fee update as non-initiation
29003031
// fails.

0 commit comments

Comments
 (0)