diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 09088839..366e4fd5 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -261,8 +261,6 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - spendTxID := commitSpend.SpenderTxHash - // If the spend reveals the pre-image, then we'll enter the clean up // workflow to pass the pre-image back to the incoming link, add it to // the witness cache, and exit. @@ -290,54 +288,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - var reports []*channeldb.ResolverReport - - // Finally, if this was an output on our commitment transaction, we'll - // wait for the second-level HTLC output to be spent, and for that - // transaction itself to confirm. - if h.htlcResolution.SignedTimeoutTx != nil { - log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ - "output", h, h.htlcResolution.ClaimOutpoint) - sweep, err := waitForSpend( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - // Update the spend txid to the hash of the sweep transaction. - spendTxID = sweep.SpenderTxHash - - // Once our timeout tx has confirmed, we add a resolution for - // our timeoutTx tx first stage transaction. - timeoutTx := h.htlcResolution.SignedTimeoutTx - spendHash := timeoutTx.TxHash() - - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, - Amount: h.htlc.Amt.ToSatoshis(), - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &spendHash, - }) - } - - // With the clean up message sent, we'll now mark the contract - // resolved, record the timeout and the sweep txid on disk, and wait. - h.resolved = true - - amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: h.htlcResolution.ClaimOutpoint, - Amount: amt, - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeTimeout, - SpendTxID: spendTxID, - }) - - return nil, h.Checkpoint(h, reports...) + return h.sweepSecondLevelTransaction(commitSpend) } // spendHtlcOutput handles the initial spend of an HTLC output via the timeout @@ -394,6 +345,71 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) return spend, err } +// sweepSecondLevelTransaction sweeps the output of the confirmed second-level +// timeout transaction into our wallet. The given SpendDetail should be the +// confirmed timeout tx spending the HTLC output on the commitment tx. +func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( + commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { + + var ( + // spendTxID will be the ultimate spend of the claimOutpoint. + // We set it to the commit spend for now, as this is the + // ultimate spend in case this is a remote commitment. If we go + // through the second-level transaction, we'll update this + // accordingly. + spendTxID = commitSpend.SpenderTxHash + + reports []*channeldb.ResolverReport + ) + + // Finally, if this was an output on our commitment transaction, we'll + // wait for the second-level HTLC output to be spent, and for that + // transaction itself to confirm. + if h.htlcResolution.SignedTimeoutTx != nil { + log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ + "output", h, h.htlcResolution.ClaimOutpoint) + sweep, err := waitForSpend( + &h.htlcResolution.ClaimOutpoint, + h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + // Update the spend txid to the hash of the sweep transaction. + spendTxID = sweep.SpenderTxHash + + // Once our sweep of the timeout tx has confirmed, we add a + // resolution for our timeoutTx tx first stage transaction. + timeoutTx := commitSpend.SpendingTx + spendHash := timeoutTx.TxHash() + + reports = append(reports, &channeldb.ResolverReport{ + OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, + Amount: h.htlc.Amt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &spendHash, + }) + } + + // With the clean up message sent, we'll now mark the contract + // resolved, record the timeout and the sweep txid on disk, and wait. + h.resolved = true + + amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) + reports = append(reports, &channeldb.ResolverReport{ + OutPoint: h.htlcResolution.ClaimOutpoint, + Amount: amt, + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: spendTxID, + }) + + return nil, h.Checkpoint(h, reports...) +} + // Stop signals the resolver to cancel any current resolution processes, and // suspend. // diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 6e41ad79..ffcee1e2 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -3,6 +3,7 @@ package contractcourt import ( "bytes" "fmt" + "reflect" "sync" "testing" "time" @@ -17,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" ) type mockWitnessBeacon struct { @@ -127,6 +129,16 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -148,6 +160,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness // Set the outpoint to be on our commitment, since @@ -174,6 +197,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -196,6 +230,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -282,16 +327,19 @@ func TestHtlcTimeoutResolver(t *testing.T) { // broadcast, then we'll set the timeout commit to a fake // transaction to force the code path. if !testCase.remoteCommit { - resolver.htlcResolution.SignedTimeoutTx = sweepTx + timeoutTx, err := testCase.txToBroadcast() + require.NoError(t, err) + + resolver.htlcResolution.SignedTimeoutTx = timeoutTx if testCase.timeout { - success := sweepTx.TxHash() + timeoutTxID := timeoutTx.TxHash() reports = append(reports, &channeldb.ResolverReport{ - OutPoint: sweepTx.TxIn[0].PreviousOutPoint, + OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, Amount: testHtlcAmt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &success, + SpendTxID: &timeoutTxID, }) } }