diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index ac4ba1e8..70da04fc 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -5012,3 +5012,98 @@ func TestMinHTLC(t *testing.T) { t.Fatalf("expected ErrBelowMinHTLC, instead received: %v", err) } } + +// TestNewBreachRetributionSkipsDustHtlcs ensures that in the case of a +// contract breach, all dust HTLCs are ignored and not reflected in the +// produced BreachRetribution struct. We ignore these HTLCs as they aren't +// actually manifested on the commitment transaction, as a result we can't +// actually revoked them. +func TestNewBreachRetributionSkipsDustHtlcs(t *testing.T) { + t.Parallel() + + // We'll kick off the test by creating our channels which both are + // loaded with 5 BTC each. + aliceChannel, bobChannel, cleanUp, err := createTestChannels(1) + if err != nil { + t.Fatalf("unable to create test channels: %v", err) + } + defer cleanUp() + + var fakeOnionBlob [lnwire.OnionPacketSize]byte + copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize)) + + // We'll modify the dust settings on both channels to be a predictable + // value for the prurpose of the test. + dustValue := btcutil.Amount(200) + aliceChannel.channelState.LocalChanCfg.DustLimit = dustValue + aliceChannel.channelState.RemoteChanCfg.DustLimit = dustValue + bobChannel.channelState.LocalChanCfg.DustLimit = dustValue + bobChannel.channelState.RemoteChanCfg.DustLimit = dustValue + + // We'll now create a series of dust HTLC's, and send then from Alice + // to Bob, finally locking both of them in. + var bobPreimage [32]byte + copy(bobPreimage[:], bytes.Repeat([]byte{0xbb}, 32)) + for i := 0; i < 3; i++ { + rHash := sha256.Sum256(bobPreimage[:]) + h := &lnwire.UpdateAddHTLC{ + PaymentHash: rHash, + Amount: lnwire.NewMSatFromSatoshis(dustValue), + Expiry: uint32(10), + OnionBlob: fakeOnionBlob, + } + + htlcIndex, err := aliceChannel.AddHTLC(h, nil) + if err != nil { + t.Fatalf("unable to add bob's htlc: %v", err) + } + + h.ID = htlcIndex + if _, err := bobChannel.ReceiveHTLC(h); err != nil { + t.Fatalf("unable to recv bob's htlc: %v", err) + } + } + + // With the HTLC's applied to both update logs, we'll initiate a state + // transition from Alice. + if err := forceStateTransition(bobChannel, aliceChannel); err != nil { + t.Fatalf("unable to complete bob's state transition: %v", err) + } + + // At this point, we'll capture the current state number, as well as + // the current commitment. + revokedCommit := bobChannel.channelState.LocalCommitment.CommitTx + revokedStateNum := aliceChannel.channelState.LocalCommitment.CommitHeight + + // We'll now have Bob settle those HTLC's to Alice and then advance + // forward to a new state. + for i := 0; i < 3; i++ { + err := bobChannel.SettleHTLC(bobPreimage, uint64(i), nil, nil, nil) + if err != nil { + t.Fatalf("unable to settle htlc: %v", err) + } + err = aliceChannel.ReceiveHTLCSettle(bobPreimage, uint64(i)) + if err != nil { + t.Fatalf("unable to settle htlc: %v", err) + } + } + if err := forceStateTransition(bobChannel, aliceChannel); err != nil { + t.Fatalf("unable to complete bob's state transition: %v", err) + } + + // At this point, we'll now simulate a contract breach by Bob using the + // NewBreachRetribution method. + breachRet, err := NewBreachRetribution( + aliceChannel.channelState, revokedStateNum, revokedCommit, 100, + ) + if err != nil { + t.Fatalf("unable to create breach retribution: %v", err) + } + + // The retribution shouldn't have any HTLCs set as they were all below + // dust for both parties. + if len(breachRet.HtlcRetributions) != 0 { + t.Fatalf("zero HTLC retributions should have been created, "+ + "instead %v were", len(breachRet.HtlcRetributions)) + } +}