Skip to content

Commit 952f500

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 Alice's update log empty after a restart. This caused Bob's 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 to reproduce the scenario.
1 parent 2aec8f3 commit 952f500

2 files changed

Lines changed: 138 additions & 11 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

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)