From 028dec80cb43e0f613582feb43486aa2184836a4 Mon Sep 17 00:00:00 2001 From: eugene Date: Wed, 9 Feb 2022 12:02:13 -0500 Subject: [PATCH] htlcswitch: only pipeline settle if add is locked-in The counter-party shouldn't be doing this anyways as they would be giving away a preimage for free. Them doing this would bork their own channel due to open circuits not getting trimmed on startup. Removing this faulty behavior also makes it easier to reason about the circuit logic. --- htlcswitch/link.go | 22 +++++ htlcswitch/link_test.go | 179 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 198 insertions(+), 3 deletions(-) diff --git a/htlcswitch/link.go b/htlcswitch/link.go index 38e1bbd76..55df2d6cc 100644 --- a/htlcswitch/link.go +++ b/htlcswitch/link.go @@ -1676,6 +1676,28 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) { case *lnwire.UpdateFulfillHTLC: pre := msg.PaymentPreimage idx := msg.ID + + // Before we pipeline the settle, we'll check the set of active + // htlc's to see if the related UpdateAddHTLC has been fully + // locked-in. + var lockedin bool + htlcs := l.channel.ActiveHtlcs() + for _, add := range htlcs { + // The HTLC will be outgoing and match idx. + if !add.Incoming && add.HtlcIndex == idx { + lockedin = true + break + } + } + + if !lockedin { + l.fail( + LinkFailureError{code: ErrInvalidUpdate}, + "unable to handle upstream settle", + ) + return + } + if err := l.channel.ReceiveHTLCSettle(pre, idx); err != nil { l.fail( LinkFailureError{ diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go index 8def75aad..6170d3622 100644 --- a/htlcswitch/link_test.go +++ b/htlcswitch/link_test.go @@ -5474,8 +5474,8 @@ func TestChannelLinkFail(t *testing.T) { false, }, { - // Test that we force close the channel if we receive - // an invalid Settle message. + // Test that we don't force close the channel if we + // receive an invalid Settle message. func(c *channelLink) { }, func(t *testing.T, c *channelLink, _ *lnwallet.LightningChannel) { @@ -5487,7 +5487,7 @@ func TestChannelLinkFail(t *testing.T) { } c.HandleChannelUpdate(htlcSettle) }, - true, + false, false, }, { @@ -6658,6 +6658,179 @@ func TestShutdownIfChannelClean(t *testing.T) { } } +// TestPipelineSettle tests that a link should only pipeline a settle if the +// related add is fully locked-in meaning it is on both sides' commitment txns. +func TestPipelineSettle(t *testing.T) { + t.Parallel() + + const chanAmt = btcutil.SatoshiPerBitcoin * 5 + const chanReserve = btcutil.SatoshiPerBitcoin * 1 + aliceLink, bobChannel, _, start, cleanUp, restore, err := + newSingleLinkTestHarness(chanAmt, chanReserve) + require.NoError(t, err) + defer cleanUp() + + alice := newPersistentLinkHarness( + t, aliceLink, nil, restore, + ) + + linkErrors := make(chan LinkFailureError, 1) + + // Modify OnChannelFailure so we are notified when the link is failed. + alice.coreLink.cfg.OnChannelFailure = func(_ lnwire.ChannelID, + _ lnwire.ShortChannelID, linkErr LinkFailureError) { + + linkErrors <- linkErr + } + + // Modify ForwardPackets so we are notified if a settle packet is + // erroneously forwarded. If the forwardChan is closed before the last + // step, then the test will fail. + forwardChan := make(chan struct{}) + fwdPkts := func(c chan struct{}, hp ...*htlcPacket) error { + close(forwardChan) + return nil + } + alice.coreLink.cfg.ForwardPackets = fwdPkts + + // Put Alice in ExitSettle mode, so we can simulate a multi-hop route + // without actually doing so. This allows us to test the locked-in add + // logic without having the add being removed by Alice sending a + // settle. + alice.coreLink.cfg.HodlMask = hodl.Mask(hodl.ExitSettle) + + err = start() + require.NoError(t, err) + + ctx := linkTestContext{ + t: t, + aliceLink: alice.link, + bobChannel: bobChannel, + aliceMsgs: alice.msgs, + } + + // First lock in an HTLC from Bob to Alice. + htlc1, invoice1 := generateHtlcAndInvoice(t, 0) + preimage1 := invoice1.Terms.PaymentPreimage + + // Add the invoice to Alice's registry so she expects it. + aliceReg := alice.coreLink.cfg.Registry.(*mockInvoiceRegistry) + err = aliceReg.AddInvoice(*invoice1, htlc1.PaymentHash) + require.NoError(t, err) + + // <---add----- + ctx.sendHtlcBobToAlice(htlc1) + // <---sig----- + ctx.sendCommitSigBobToAlice(1) + // ----rev----> + ctx.receiveRevAndAckAliceToBob() + // ----sig----> + ctx.receiveCommitSigAliceToBob(1) + // <---rev----- + ctx.sendRevAndAckBobToAlice() + + // Bob will send the preimage for the HTLC he just sent. This will test + // the check that the HTLC is locked-in. The channel should not be + // force closed if everything is working correctly. + settle1 := &lnwire.UpdateFulfillHTLC{ + ID: 0, + PaymentPreimage: *preimage1, + } + ctx.aliceLink.HandleChannelUpdate(settle1) + + // ForceClose should be false. + select { + case linkErr := <-linkErrors: + require.False(t, linkErr.ForceClose) + case <-forwardChan: + t.Fatal("packet was erroneously forwarded") + } + + // Restart Alice's link with the hodl.ExitSettle and hodl.Commit flags. + alice.restart(false, false, hodl.ExitSettle, hodl.Commit) + ctx.aliceLink = alice.link + ctx.aliceMsgs = alice.msgs + + alice.coreLink.cfg.OnChannelFailure = func(_ lnwire.ChannelID, + _ lnwire.ShortChannelID, linkErr LinkFailureError) { + + linkErrors <- linkErr + } + alice.coreLink.cfg.ForwardPackets = fwdPkts + + // Alice will now send an HTLC to Bob, but won't sign a commitment for + // it. This HTLC will have the same payment hash as the one above. + htlc2 := htlc1 + + // ----add---> + ctx.sendHtlcAliceToBob(0, htlc2) + ctx.receiveHtlcAliceToBob() + + // Now Bob will send a settle backwards before the HTLC is locked in + // and the link should be failed again. + settle2 := &lnwire.UpdateFulfillHTLC{ + ID: 0, + PaymentPreimage: *preimage1, + } + ctx.aliceLink.HandleChannelUpdate(settle2) + + // ForceClose should be false. + select { + case linkErr := <-linkErrors: + require.False(t, linkErr.ForceClose) + case <-forwardChan: + t.Fatal("packet was erroneously forwarded") + } + + // Restart Alice's link without the hodl.Commit flag. + alice.restart(false, false, hodl.ExitSettle) + ctx.aliceLink = alice.link + ctx.aliceMsgs = alice.msgs + + alice.coreLink.cfg.OnChannelFailure = func(_ lnwire.ChannelID, + _ lnwire.ShortChannelID, linkErr LinkFailureError) { + + linkErrors <- linkErr + } + alice.coreLink.cfg.ForwardPackets = fwdPkts + + // Alice's mailbox should give the link the HTLC to send again. + select { + case msg := <-ctx.aliceMsgs: + _, ok := msg.(*lnwire.UpdateAddHTLC) + require.True(t, ok) + case <-time.After(5 * time.Second): + t.Fatal("did not receive htlc from alice") + } + + // Trigger the BatchTicker. + select { + case alice.batchTicker <- time.Now(): + case <-time.After(5 * time.Second): + t.Fatalf("could not force commit sig") + } + + // ----sig---> + ctx.receiveCommitSigAliceToBob(2) + // <---rev---- + ctx.sendRevAndAckBobToAlice() + // <---sig---- + ctx.sendCommitSigBobToAlice(2) + // ----rev---> + ctx.receiveRevAndAckAliceToBob() + + // Bob should now be able to send the settle to Alice without making + // the link fail. + ctx.aliceLink.HandleChannelUpdate(settle2) + + select { + case <-linkErrors: + t.Fatal("should not have received a link error") + case <-forwardChan: + // success + } +} + // assertFailureCode asserts that an error is of type ClearTextError and that // the failure code is as expected. func assertFailureCode(t *testing.T, err error, code lnwire.FailCode) {