What changed, and why it matters
This commit only adds a new test case to an existing unit test. It does not change any production code. The new test verifies that when a special 'noop' (no-operation) HTLC is retransmitted after a restart, it matches the original HTLC exactly, so that both channel partners agree on the commitment signature and avoid accidentally force-closing the channel. Because no real code behavior is changed, this commit by itself does not introduce or fix a live security vulnerability.
No immediate action required. Treat as normal test-coverage improvement. If the underlying production behavior for noop HTLC retransmission is a concern, review the related production code paths separately; this commit does not alter them.
Security signals we found
Test-only change; no production logic modified
Relates to commitment-signature resynchronization after restart
Targets tapscript-root channel type with noop HTLCs
Mentions avoiding force-closures due to commit sig mismatches
Evidence from the diff
The change extends lnwallet/channel_test.go’s testChanSyncOweCommitment to accept a noop boolean and, when true, attaches a custom TLV record (NoOpHtlcTLVType) to the test HTLCs. It adds a new sub-test ‘tapscript root with noop’ and adjusts balance assertions for the noop case. The production retransmission logic is not modified; the commit is purely test coverage aimed at preventing commit-signature mismatches for tapscript-root channels carrying noop HTLCs.
Changed components
lnwallet/channel_test.gotestChanSyncOweCommitment unit testInspect captured patch +55 / −27
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index c37d059..d2800dd 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -3232,7 +3232,9 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) {
// he receives Alice's CommitSig message, then Alice concludes that she needs
// to re-send the CommitDiff. After the diff has been sent, both nodes should
// resynchronize and be able to complete the dangling commit.
-func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
+func testChanSyncOweCommitment(t *testing.T,
+ chanType channeldb.ChannelType, noop bool) {
+
// Create a test channel which will be used for the duration of this
// unittest. The channel will be funded evenly with Alice having 5 BTC,
// and Bob having 5 BTC.
@@ -3242,6 +3244,17 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
var fakeOnionBlob [lnwire.OnionPacketSize]byte
copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
+ // Let's create the noop add TLV record. This will only be
+ // effective for channels that have a tapscript root.
+ noopRecord := tlv.NewPrimitiveRecord[NoOpHtlcTLVType, bool](true)
+ records, err := tlv.RecordsToMap([]tlv.Record{noopRecord.Record()})
+ require.NoError(t, err)
+
+ // If the noop flag is not set for this test, nullify the records.
+ if !noop {
+ records = nil
+ }
+
// We'll start off the scenario with Bob sending 3 HTLC's to Alice in a
// single state update.
htlcAmt := lnwire.NewMSatFromSatoshis(20000)
@@ -3251,10 +3264,11 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
for i := 0; i < 3; i++ {
rHash := sha256.Sum256(bobPreimage[:])
h := &lnwire.UpdateAddHTLC{
- PaymentHash: rHash,
- Amount: htlcAmt,
- Expiry: uint32(10),
- OnionBlob: fakeOnionBlob,
+ PaymentHash: rHash,
+ Amount: htlcAmt,
+ Expiry: uint32(10),
+ OnionBlob: fakeOnionBlob,
+ CustomRecords: records,
}
htlcIndex, err := bobChannel.AddHTLC(h, nil)
@@ -3290,15 +3304,17 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
t.Fatalf("unable to settle htlc: %v", err)
}
}
+
var alicePreimage [32]byte
copy(alicePreimage[:], bytes.Repeat([]byte{0xaa}, 32))
rHash := sha256.Sum256(alicePreimage[:])
aliceHtlc := &lnwire.UpdateAddHTLC{
- ChanID: chanID,
- PaymentHash: rHash,
- Amount: htlcAmt,
- Expiry: uint32(10),
- OnionBlob: fakeOnionBlob,
+ ChanID: chanID,
+ PaymentHash: rHash,
+ Amount: htlcAmt,
+ Expiry: uint32(10),
+ OnionBlob: fakeOnionBlob,
+ CustomRecords: records,
}
aliceHtlcIndex, err := aliceChannel.AddHTLC(aliceHtlc, nil)
require.NoError(t, err, "unable to add alice's htlc")
@@ -3519,22 +3535,25 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
// At this point, the final balances of both parties should properly
// reflect the amount of HTLC's sent.
- bobMsatSent := numBobHtlcs * htlcAmt
- if aliceChannel.channelState.TotalMSatSent != htlcAmt {
- t.Fatalf("wrong value for msat sent: expected %v, got %v",
- htlcAmt, aliceChannel.channelState.TotalMSatSent)
- }
- if aliceChannel.channelState.TotalMSatReceived != bobMsatSent {
- t.Fatalf("wrong value for msat recv: expected %v, got %v",
- bobMsatSent, aliceChannel.channelState.TotalMSatReceived)
- }
- if bobChannel.channelState.TotalMSatSent != bobMsatSent {
- t.Fatalf("wrong value for msat sent: expected %v, got %v",
- bobMsatSent, bobChannel.channelState.TotalMSatSent)
- }
- if bobChannel.channelState.TotalMSatReceived != htlcAmt {
- t.Fatalf("wrong value for msat recv: expected %v, got %v",
- htlcAmt, bobChannel.channelState.TotalMSatReceived)
+ if noop {
+ // If this test-case includes noop HTLCs, then we don't expect
+ // any balance changes.
+ require.Zero(t, aliceChannel.channelState.TotalMSatSent)
+ require.Zero(t, aliceChannel.channelState.TotalMSatReceived)
+ require.Zero(t, bobChannel.channelState.TotalMSatSent)
+ require.Zero(t, bobChannel.channelState.TotalMSatReceived)
+ } else {
+ // Otherwise, calculate the expected changes and assert them.
+ bobMsatSent := numBobHtlcs * htlcAmt
+
+ aliceChan := aliceChannel.channelState
+ bobChan := bobChannel.channelState
+
+ require.Equal(t, aliceChan.TotalMSatSent, htlcAmt)
+ require.Equal(t, aliceChan.TotalMSatReceived, bobMsatSent)
+
+ require.Equal(t, bobChan.TotalMSatSent, bobMsatSent)
+ require.Equal(t, bobChan.TotalMSatReceived, htlcAmt)
}
}
@@ -3548,6 +3567,7 @@ func TestChanSyncOweCommitment(t *testing.T) {
testCases := []struct {
name string
chanType channeldb.ChannelType
+ noop bool
}{
{
name: "tweakless",
@@ -3571,10 +3591,18 @@ func TestChanSyncOweCommitment(t *testing.T) {
channeldb.SimpleTaprootFeatureBit |
channeldb.TapscriptRootBit,
},
+ {
+ name: "tapscript root with noop",
+ chanType: channeldb.SingleFunderTweaklessBit |
+ channeldb.AnchorOutputsBit |
+ channeldb.SimpleTaprootFeatureBit |
+ channeldb.TapscriptRootBit,
+ noop: true,
+ },
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
- testChanSyncOweCommitment(t, tc.chanType)
+ testChanSyncOweCommitment(t, tc.chanType, tc.noop)
})
}
}
Why this scored 12/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.