From d35709b38ea21239f64f63ce254150438921a78a Mon Sep 17 00:00:00 2001 From: Olaoluwa Osuntokun Date: Tue, 16 Jan 2018 18:45:06 -0800 Subject: [PATCH] lnwallet: extend TestForceClose to close with both incoming+outgoing HTLCs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit In this commit we add some additional scenarios to the TestForceClose test. With this expanded test case, we now ensure the the party that force closes is able to properly sweep both incoming and outgoing HTLC’s fully with the information contained the HtlcResolution struct. --- lnwallet/channel_test.go | 178 ++++++++++++++++++++++++++++++--------- 1 file changed, 138 insertions(+), 40 deletions(-) diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go index d668c2c2..122d3dcf 100644 --- a/lnwallet/channel_test.go +++ b/lnwallet/channel_test.go @@ -874,12 +874,12 @@ func TestCooperativeChannelClosure(t *testing.T) { // TestForceClose checks that the resulting ForceCloseSummary is correct when a // peer is ForceClosing the channel. Will check outputs both above and below -// the dust limit. +// the dust limit. Additionally, we'll ensure that the node which executed the +// force close generates HTLC resolutions that are capable of sweeping both +// incoming and outgoing HTLC's. func TestForceClose(t *testing.T) { t.Parallel() - // TODO(roasbeef): modify to add some HTLC's before closing? - // 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. @@ -895,85 +895,108 @@ func TestForceClose(t *testing.T) { // will still be present within the broadcast commitment transaction. // We'll ensure that the HTLC amount is above Alice's dust limit. htlcAmount := lnwire.NewMSatFromSatoshis(20000) - htlc, _ := createHTLC(0, htlcAmount) - if _, err := aliceChannel.AddHTLC(htlc); err != nil { + htlcAlice, _ := createHTLC(0, htlcAmount) + if _, err := aliceChannel.AddHTLC(htlcAlice); err != nil { t.Fatalf("alice unable to add htlc: %v", err) } - if _, err := bobChannel.ReceiveHTLC(htlc); err != nil { + if _, err := bobChannel.ReceiveHTLC(htlcAlice); err != nil { t.Fatalf("bob unable to recv add htlc: %v", err) } + + // We'll also a distinct HTLC from Bob -> Alice. This way, Alice will + // have both an incoming and outgoing HTLC on her commitment + // transaction. + htlcBob, preimageBob := createHTLC(0, htlcAmount) + if _, err := bobChannel.AddHTLC(htlcBob); err != nil { + t.Fatalf("alice unable to add htlc: %v", err) + } + if _, err := aliceChannel.ReceiveHTLC(htlcBob); err != nil { + t.Fatalf("bob unable to recv add htlc: %v", err) + } + + // Next, we'll perform two state transitions to ensure that both HTLC's + // get fully locked-in. if err := forceStateTransition(aliceChannel, bobChannel); err != nil { t.Fatalf("Can't update the channel state: %v", err) } + if err := forceStateTransition(bobChannel, aliceChannel); err != nil { + t.Fatalf("Can't update the channel state: %v", err) + } - // Now with the HTLC in tact, we'll perform a force close on Alice's - // part. + // Before we force close Alice's channel, we'll add the pre-image of + // Bob's HTLC to her preimage cache. + aliceChannel.pCache.AddPreimage(preimageBob[:]) + + // With the cache populated, we'll now attempt the force close + // initiated by Alice. closeSummary, err := aliceChannel.ForceClose() if err != nil { t.Fatalf("unable to force close channel: %v", err) } - // Alice's force close summary should have a single HTLC resolution. - if len(closeSummary.HtlcResolutions) != 1 { - t.Fatalf("alice htlc resolutions not populated: expected %v "+ + // Alice should detect that she can sweep the outgoing HTLC after a + // timeout, but also that she's able to sweep in incoming HTLC Bob sent + // her. + if len(closeSummary.HtlcResolutions.OutgoingHTLCs) != 1 { + t.Fatalf("alice out htlc resolutions not populated: expected %v "+ "htlcs, got %v htlcs", - 1, len(closeSummary.HtlcResolutions)) + 1, len(closeSummary.HtlcResolutions.OutgoingHTLCs)) + } + if len(closeSummary.HtlcResolutions.IncomingHTLCs) != 1 { + t.Fatalf("alice in htlc resolutions not populated: expected %v "+ + "htlcs, got %v htlcs", + 1, len(closeSummary.HtlcResolutions.IncomingHTLCs)) } // The SelfOutputSignDesc should be non-nil since the output to-self is // non-dust. - if closeSummary.SelfOutputSignDesc == nil { + aliceCommitResolution := closeSummary.CommitResolution + if aliceCommitResolution == nil { t.Fatalf("alice fails to include to-self output in " + "ForceCloseSummary") } // The rest of the close summary should have been populated properly. aliceDelayPoint := aliceChannel.channelState.LocalChanCfg.DelayBasePoint - if !closeSummary.SelfOutputSignDesc.PubKey.IsEqual(aliceDelayPoint) { + if !aliceCommitResolution.SelfOutputSignDesc.PubKey.IsEqual( + aliceDelayPoint, + ) { t.Fatalf("alice incorrect pubkey in SelfOutputSignDesc") } // Factoring in the fee rate, Alice's amount should properly reflect - // that we've added an additional HTLC to the commitment transaction. - totalCommitWeight := CommitWeight + HtlcWeight + // that we've added two additional HTLC to the commitment transaction. + totalCommitWeight := CommitWeight + (HtlcWeight * 2) feePerKw := aliceChannel.channelState.LocalCommitment.FeePerKw commitFee := btcutil.Amount((int64(feePerKw) * totalCommitWeight) / 1000) expectedAmount := (aliceChannel.Capacity / 2) - htlcAmount.ToSatoshis() - commitFee - if closeSummary.SelfOutputSignDesc.Output.Value != int64(expectedAmount) { + if aliceCommitResolution.SelfOutputSignDesc.Output.Value != int64(expectedAmount) { t.Fatalf("alice incorrect output value in SelfOutputSignDesc, "+ "expected %v, got %v", int64(expectedAmount), - closeSummary.SelfOutputSignDesc.Output.Value) + aliceCommitResolution.SelfOutputSignDesc.Output.Value) } // Alice's listed CSV delay should also match the delay that was // pre-committed to at channel opening. - if closeSummary.SelfOutputMaturity != + if aliceCommitResolution.MaturityDelay != uint32(aliceChannel.localChanCfg.CsvDelay) { t.Fatalf("alice: incorrect local CSV delay in ForceCloseSummary, "+ "expected %v, got %v", aliceChannel.channelState.LocalChanCfg.CsvDelay, - closeSummary.SelfOutputMaturity) + aliceCommitResolution.MaturityDelay) } // Next, we'll ensure that the second level HTLC transaction it itself // spendable, and also that the delivery output (with delay) itself has // a valid sign descriptor. - var senderHtlcPkScript []byte - for _, txOut := range closeSummary.CloseTx.TxOut { - if txOut.Value == int64(htlcAmount.ToSatoshis()) { - senderHtlcPkScript = txOut.PkScript - break - } - } - - if senderHtlcPkScript == nil { - t.Fatalf("unable to find htlc script") - } + htlcResolution := closeSummary.HtlcResolutions.OutgoingHTLCs[0] + outHtlcIndex := htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint.Index + senderHtlcPkScript := closeSummary.CloseTx.TxOut[outHtlcIndex].PkScript // First, verify that the second level transaction can properly spend - // the multi-sig clause within the - htlcResolution := closeSummary.HtlcResolutions[0] + // the multi-sig clause within the output on the commitment transaction + // that produces this HTLC. timeoutTx := htlcResolution.SignedTimeoutTx vm, err := txscript.NewEngine(senderHtlcPkScript, timeoutTx, 0, txscript.StandardVerifyFlags, nil, @@ -1029,33 +1052,86 @@ func TestForceClose(t *testing.T) { t.Fatalf("alice: incorrect close transaction txid") } + // We'll now perform similar set of checks to ensure that Alice is able + // to sweep the output that Bob sent to her on-chain with knowledge of + // the preimage. + inHtlcResolution := closeSummary.HtlcResolutions.IncomingHTLCs[0] + inHtlcIndex := inHtlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint.Index + receiverHtlcScript := closeSummary.CloseTx.TxOut[inHtlcIndex].PkScript + + // With the original pkscript located, we'll now verify that the second + // level transaction can spend from the multi-sig out. + successTx := inHtlcResolution.SignedSuccessTx + vm, err = txscript.NewEngine(receiverHtlcScript, + successTx, 0, txscript.StandardVerifyFlags, nil, + nil, int64(htlcAmount.ToSatoshis())) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("htlc success spend is invalid: %v", err) + } + + // Finally, we'll construct a transaction to spend the produced + // second-level output with the attached SignDescriptor. + sweepTx = wire.NewMsgTx(2) + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: inHtlcResolution.ClaimOutpoint, + }) + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: receiverHtlcScript, + Value: inHtlcResolution.SweepSignDesc.Output.Value, + }) + inHtlcResolution.SweepSignDesc.InputIndex = 0 + sweepTx.TxIn[0].Witness, err = htlcSpendSuccess(aliceChannel.signer, + &inHtlcResolution.SweepSignDesc, sweepTx, + uint32(aliceChannel.channelState.LocalChanCfg.CsvDelay)) + if err != nil { + t.Fatalf("unable to gen witness for timeout output: %v", err) + } + + // The spend we create above spending the second level HTLC output + // should validate without any issues. + vm, err = txscript.NewEngine( + inHtlcResolution.SweepSignDesc.Output.PkScript, + sweepTx, 0, txscript.StandardVerifyFlags, nil, + nil, inHtlcResolution.SweepSignDesc.Output.Value, + ) + if err != nil { + t.Fatalf("unable to create engine: %v", err) + } + if err := vm.Execute(); err != nil { + t.Fatalf("htlc timeout spend is invalid: %v", err) + } + // Check the same for Bob's ForceCloseSummary. closeSummary, err = bobChannel.ForceClose() if err != nil { t.Fatalf("unable to force close channel: %v", err) } - if closeSummary.SelfOutputSignDesc == nil { + bobCommitResolution := closeSummary.CommitResolution + if bobCommitResolution == nil { t.Fatalf("bob fails to include to-self output in ForceCloseSummary") } bobDelayPoint := bobChannel.channelState.LocalChanCfg.DelayBasePoint - if !closeSummary.SelfOutputSignDesc.PubKey.IsEqual(bobDelayPoint) { + if !bobCommitResolution.SelfOutputSignDesc.PubKey.IsEqual(bobDelayPoint) { t.Fatalf("bob incorrect pubkey in SelfOutputSignDesc") } - if closeSummary.SelfOutputSignDesc.Output.Value != - int64(bobAmount.ToSatoshis()) { + if bobCommitResolution.SelfOutputSignDesc.Output.Value != + int64(bobAmount.ToSatoshis()-htlcAmount.ToSatoshis()) { t.Fatalf("bob incorrect output value in SelfOutputSignDesc, "+ "expected %v, got %v", bobAmount.ToSatoshis(), - int64(closeSummary.SelfOutputSignDesc.Output.Value)) + int64(bobCommitResolution.SelfOutputSignDesc.Output.Value)) } - if closeSummary.SelfOutputMaturity != + if bobCommitResolution.MaturityDelay != uint32(bobChannel.channelState.LocalChanCfg.CsvDelay) { t.Fatalf("bob: incorrect local CSV delay in ForceCloseSummary, "+ "expected %v, got %v", bobChannel.channelState.LocalChanCfg.CsvDelay, - closeSummary.SelfOutputMaturity) + bobCommitResolution.MaturityDelay) } closeTxHash = closeSummary.CloseTx.TxHash() @@ -1063,6 +1139,28 @@ func TestForceClose(t *testing.T) { if !bytes.Equal(closeTxHash[:], commitTxHash[:]) { t.Fatalf("bob: incorrect close transaction txid") } + + // As we didn't add the preimage of Alice's HTLC to bob's preimage + // cache, he should only detect that he can sweep only his outgoing + // HTLC upon force close. + if len(closeSummary.HtlcResolutions.OutgoingHTLCs) != 1 { + t.Fatalf("alice out htlc resolutions not populated: expected %v "+ + "htlcs, got %v htlcs", + 1, len(closeSummary.HtlcResolutions.OutgoingHTLCs)) + } + + // Bob should recognize that the incoming HTLC is there, but the + // preimage should be empty as he doesn't have the knowledge required + // to sweep it. + if len(closeSummary.HtlcResolutions.IncomingHTLCs) != 1 { + t.Fatalf("bob in htlc resolutions not populated: expected %v "+ + "htlcs, got %v htlcs", + 1, len(closeSummary.HtlcResolutions.IncomingHTLCs)) + } + var zeroHash [32]byte + if closeSummary.HtlcResolutions.IncomingHTLCs[0].Preimage != zeroHash { + t.Fatalf("bob shouldn't know preimage but does") + } } // TestForceCloseDustOutput tests that if either side force closes with an