Merge pull request #2800 from Roasbeef/simplify-timeout-resolver
contractcourt: simplify htlcTimeoutResolver
This commit is contained in:
commit
6d3b142f2a
@ -511,8 +511,10 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
|
||||
// The force close request should trigger broadcast of the commitment
|
||||
// transaction.
|
||||
assertStateTransitions(t, arbLog.newStates,
|
||||
StateBroadcastCommit, StateCommitmentBroadcasted)
|
||||
assertStateTransitions(
|
||||
t, arbLog.newStates, StateBroadcastCommit,
|
||||
StateCommitmentBroadcasted,
|
||||
)
|
||||
select {
|
||||
case <-respChan:
|
||||
case <-time.After(5 * time.Second):
|
||||
@ -529,7 +531,17 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
}
|
||||
|
||||
// Now notify about the local force close getting confirmed.
|
||||
closeTx := &wire.MsgTx{}
|
||||
closeTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: wire.OutPoint{},
|
||||
Witness: [][]byte{
|
||||
{0x1},
|
||||
{0x2},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
htlcOp := wire.OutPoint{
|
||||
Hash: closeTx.TxHash(),
|
||||
@ -569,8 +581,10 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
&channeldb.ChannelCloseSummary{},
|
||||
}
|
||||
|
||||
assertStateTransitions(t, arbLog.newStates, StateContractClosed,
|
||||
StateWaitingFullResolution)
|
||||
assertStateTransitions(
|
||||
t, arbLog.newStates, StateContractClosed,
|
||||
StateWaitingFullResolution,
|
||||
)
|
||||
|
||||
// htlcOutgoingContestResolver is now active and waiting for the HTLC to
|
||||
// expire. It should not yet have passed it on for incubation.
|
||||
@ -592,12 +606,13 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
t.Fatalf("no response received")
|
||||
}
|
||||
|
||||
// Notify resolver that output of the commitment has been spent.
|
||||
notifier.confChan <- &chainntnfs.TxConfirmation{}
|
||||
// Notify resolver that the HTLC output of the commitment has been
|
||||
// spent.
|
||||
notifier.spendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx}
|
||||
|
||||
// As this is our own commitment transaction, the HTLC will go through
|
||||
// to the second level. Channel arbitrator should still not be marked as
|
||||
// resolved.
|
||||
// to the second level. Channel arbitrator should still not be marked
|
||||
// as resolved.
|
||||
select {
|
||||
case <-resolved:
|
||||
t.Fatalf("channel resolved prematurely")
|
||||
@ -605,7 +620,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
}
|
||||
|
||||
// Notify resolver that the second level transaction is spent.
|
||||
notifier.spendChan <- &chainntnfs.SpendDetail{}
|
||||
notifier.spendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx}
|
||||
|
||||
// At this point channel should be marked as resolved.
|
||||
assertStateTransitions(t, arbLog.newStates, StateFullyResolved)
|
||||
|
@ -1 +0,0 @@
|
||||
package contractcourt
|
@ -4,13 +4,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
// htlcOutgoingContestResolver is a ContractResolver that's able to resolve an
|
||||
@ -44,108 +38,22 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// claimCleanUp is a helper function that's called once the HTLC output
|
||||
// is spent by the remote party. It'll extract the preimage, add it to
|
||||
// the global cache, and finally send the appropriate clean up message.
|
||||
claimCleanUp := func(commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
|
||||
// Depending on if this is our commitment or not, then we'll be
|
||||
// looking for a different witness pattern.
|
||||
spenderIndex := commitSpend.SpenderInputIndex
|
||||
spendingInput := commitSpend.SpendingTx.TxIn[spenderIndex]
|
||||
|
||||
log.Infof("%T(%v): extracting preimage! remote party spent "+
|
||||
"HTLC with tx=%v", h, h.htlcResolution.ClaimOutpoint,
|
||||
spew.Sdump(commitSpend.SpendingTx))
|
||||
|
||||
// If this is the remote party's commitment, then we'll be
|
||||
// looking for them to spend using the second-level success
|
||||
// transaction.
|
||||
var preimageBytes []byte
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
// The witness stack when the remote party sweeps the
|
||||
// output to them looks like:
|
||||
//
|
||||
// * <sender sig> <recvr sig> <preimage> <witness script>
|
||||
preimageBytes = spendingInput.Witness[3]
|
||||
} else {
|
||||
// Otherwise, they'll be spending directly from our
|
||||
// commitment output. In which case the witness stack
|
||||
// looks like:
|
||||
//
|
||||
// * <sig> <preimage> <witness script>
|
||||
preimageBytes = spendingInput.Witness[1]
|
||||
}
|
||||
|
||||
preimage, err := lntypes.MakePreimage(preimageBytes)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("%T(%v): extracting preimage=%v from on-chain "+
|
||||
"spend!", h, h.htlcResolution.ClaimOutpoint,
|
||||
preimage)
|
||||
|
||||
// With the preimage obtained, we can now add it to the global
|
||||
// cache.
|
||||
if err := h.PreimageDB.AddPreimages(preimage); err != nil {
|
||||
log.Errorf("%T(%v): unable to add witness to cache",
|
||||
h, h.htlcResolution.ClaimOutpoint)
|
||||
}
|
||||
|
||||
var pre [32]byte
|
||||
copy(pre[:], preimage[:])
|
||||
|
||||
// Finally, we'll send the clean up message, mark ourselves as
|
||||
// resolved, then exit.
|
||||
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
||||
SourceChan: h.ShortChanID,
|
||||
HtlcIndex: h.htlcIndex,
|
||||
PreImage: &pre,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.resolved = true
|
||||
return nil, h.Checkpoint(h)
|
||||
}
|
||||
|
||||
// Otherwise, we'll watch for two external signals to decide if we'll
|
||||
// morph into another resolver, or fully resolve the contract.
|
||||
|
||||
//
|
||||
// The output we'll be watching for is the *direct* spend from the HTLC
|
||||
// output. If this isn't our commitment transaction, it'll be right on
|
||||
// the resolution. Otherwise, we fetch this pointer from the input of
|
||||
// the time out transaction.
|
||||
var (
|
||||
outPointToWatch wire.OutPoint
|
||||
scriptToWatch []byte
|
||||
err error
|
||||
)
|
||||
|
||||
// TODO(joostjager): output already set properly in
|
||||
// lnwallet.newOutgoingHtlcResolution? And script too?
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
outPointToWatch = h.htlcResolution.ClaimOutpoint
|
||||
scriptToWatch = h.htlcResolution.SweepSignDesc.Output.PkScript
|
||||
} else {
|
||||
// If this is the remote party's commitment, then we'll need to
|
||||
// grab watch the output that our timeout transaction points
|
||||
// to. We can directly grab the outpoint, then also extract the
|
||||
// witness script (the last element of the witness stack) to
|
||||
// re-construct the pkScipt we need to watch.
|
||||
outPointToWatch = h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
||||
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
|
||||
scriptToWatch, err = input.WitnessScriptHash(
|
||||
witness[len(witness)-1],
|
||||
)
|
||||
outPointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// First, we'll register for a spend notification for this output. If
|
||||
// the remote party sweeps with the pre-image, we'll be notified.
|
||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
||||
&outPointToWatch, scriptToWatch, h.broadcastHeight,
|
||||
outPointToWatch, scriptToWatch, h.broadcastHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -161,7 +69,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
||||
}
|
||||
|
||||
// TODO(roasbeef): Checkpoint?
|
||||
return claimCleanUp(commitSpend)
|
||||
return h.claimCleanUp(commitSpend)
|
||||
|
||||
// If it hasn't, then we'll watch for both the expiration, and the
|
||||
// sweeping out this output.
|
||||
@ -190,7 +98,6 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
||||
//
|
||||
// Source:
|
||||
// https://github.com/btcsuite/btcd/blob/991d32e72fe84d5fbf9c47cd604d793a0cd3a072/blockchain/validate.go#L154
|
||||
|
||||
if uint32(currentHeight) >= h.htlcResolution.Expiry-1 {
|
||||
log.Infof("%T(%v): HTLC has expired (height=%v, expiry=%v), "+
|
||||
"transforming into timeout resolver", h,
|
||||
@ -242,7 +149,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
||||
// party is by revealing the preimage. So we'll perform
|
||||
// our duties to clean up the contract once it has been
|
||||
// claimed.
|
||||
return claimCleanUp(commitSpend)
|
||||
return h.claimCleanUp(commitSpend)
|
||||
|
||||
case <-h.Quit:
|
||||
return nil, fmt.Errorf("resolver cancelled")
|
||||
|
@ -6,6 +6,10 @@ import (
|
||||
"io"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
)
|
||||
@ -67,6 +71,154 @@ func (h *htlcTimeoutResolver) ResolverKey() []byte {
|
||||
return key[:]
|
||||
}
|
||||
|
||||
const (
|
||||
// expectedRemoteWitnessSuccessSize is the expected size of the witness
|
||||
// on the remote commitment transaction for an outgoing HTLC that is
|
||||
// swept on-chain by them with pre-image.
|
||||
expectedRemoteWitnessSuccessSize = 5
|
||||
|
||||
// remotePreimageIndex index within the witness on the remote
|
||||
// commitment transaction that will hold they pre-image if they go to
|
||||
// sweep it on chain.
|
||||
remotePreimageIndex = 3
|
||||
|
||||
// localPreimageIndex is the index within the witness on the local
|
||||
// commitment transaction for an outgoing HTLC that will hold the
|
||||
// pre-image if the remote party sweeps it.
|
||||
localPreimageIndex = 1
|
||||
)
|
||||
|
||||
// claimCleanUp is a helper method that's called once the HTLC output is spent
|
||||
// by the remote party. It'll extract the preimage, add it to the global cache,
|
||||
// and finally send the appropriate clean up message.
|
||||
func (h *htlcTimeoutResolver) claimCleanUp(
|
||||
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
|
||||
|
||||
// Depending on if this is our commitment or not, then we'll be looking
|
||||
// for a different witness pattern.
|
||||
spenderIndex := commitSpend.SpenderInputIndex
|
||||
spendingInput := commitSpend.SpendingTx.TxIn[spenderIndex]
|
||||
|
||||
log.Infof("%T(%v): extracting preimage! remote party spent "+
|
||||
"HTLC with tx=%v", h, h.htlcResolution.ClaimOutpoint,
|
||||
spew.Sdump(commitSpend.SpendingTx))
|
||||
|
||||
// If this is the remote party's commitment, then we'll be looking for
|
||||
// them to spend using the second-level success transaction.
|
||||
var preimageBytes []byte
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
// The witness stack when the remote party sweeps the output to
|
||||
// them looks like:
|
||||
//
|
||||
// * <0> <sender sig> <recvr sig> <preimage> <witness script>
|
||||
preimageBytes = spendingInput.Witness[remotePreimageIndex]
|
||||
} else {
|
||||
// Otherwise, they'll be spending directly from our commitment
|
||||
// output. In which case the witness stack looks like:
|
||||
//
|
||||
// * <sig> <preimage> <witness script>
|
||||
preimageBytes = spendingInput.Witness[localPreimageIndex]
|
||||
}
|
||||
|
||||
preimage, err := lntypes.MakePreimage(preimageBytes)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to create pre-image from "+
|
||||
"witness: %v", err)
|
||||
}
|
||||
|
||||
log.Infof("%T(%v): extracting preimage=%v from on-chain "+
|
||||
"spend!", h, h.htlcResolution.ClaimOutpoint, preimage)
|
||||
|
||||
// With the preimage obtained, we can now add it to the global cache.
|
||||
if err := h.PreimageDB.AddPreimages(preimage); err != nil {
|
||||
log.Errorf("%T(%v): unable to add witness to cache",
|
||||
h, h.htlcResolution.ClaimOutpoint)
|
||||
}
|
||||
|
||||
var pre [32]byte
|
||||
copy(pre[:], preimage[:])
|
||||
|
||||
// Finally, we'll send the clean up message, mark ourselves as
|
||||
// resolved, then exit.
|
||||
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
||||
SourceChan: h.ShortChanID,
|
||||
HtlcIndex: h.htlcIndex,
|
||||
PreImage: &pre,
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.resolved = true
|
||||
return nil, h.Checkpoint(h)
|
||||
}
|
||||
|
||||
// chainDetailsToWatch returns the output and script which we use to watch for
|
||||
// spends from the direct HTLC output on the commitment transaction.
|
||||
//
|
||||
// TODO(joostjager): output already set properly in
|
||||
// lnwallet.newOutgoingHtlcResolution? And script too?
|
||||
func (h *htlcTimeoutResolver) chainDetailsToWatch() (*wire.OutPoint, []byte, error) {
|
||||
// If there's no timeout transaction, then the claim output is the
|
||||
// output directly on the commitment transaction, so we'll just use
|
||||
// that.
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
outPointToWatch := h.htlcResolution.ClaimOutpoint
|
||||
scriptToWatch := h.htlcResolution.SweepSignDesc.Output.PkScript
|
||||
|
||||
return &outPointToWatch, scriptToWatch, nil
|
||||
}
|
||||
|
||||
// If this is the remote party's commitment, then we'll need to grab
|
||||
// watch the output that our timeout transaction points to. We can
|
||||
// directly grab the outpoint, then also extract the witness script
|
||||
// (the last element of the witness stack) to re-construct the pkScript
|
||||
// we need to watch.
|
||||
outPointToWatch := h.htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint
|
||||
witness := h.htlcResolution.SignedTimeoutTx.TxIn[0].Witness
|
||||
scriptToWatch, err := input.WitnessScriptHash(witness[len(witness)-1])
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
|
||||
return &outPointToWatch, scriptToWatch, nil
|
||||
}
|
||||
|
||||
// isSuccessSpend returns true if the passed spend on the specified commitment
|
||||
// is a success spend that reveals the pre-image or not.
|
||||
func isSuccessSpend(spend *chainntnfs.SpendDetail, localCommit bool) bool {
|
||||
// Based on the spending input index and transaction, obtain the
|
||||
// witness that tells us what type of spend this is.
|
||||
spenderIndex := spend.SpenderInputIndex
|
||||
spendingInput := spend.SpendingTx.TxIn[spenderIndex]
|
||||
spendingWitness := spendingInput.Witness
|
||||
|
||||
// If this is the remote commitment then the only possible spends for
|
||||
// outgoing HTLCs are:
|
||||
//
|
||||
// RECVR: <0> <sender sig> <recvr sig> <preimage> (2nd level success spend)
|
||||
// REVOK: <sig> <key>
|
||||
// SENDR: <sig> 0
|
||||
//
|
||||
// In this case, if 5 witness elements are present (factoring the
|
||||
// witness script), and the 3rd element is the size of the pre-image,
|
||||
// then this is a remote spend. If not, then we swept it ourselves, or
|
||||
// revoked their output.
|
||||
if !localCommit {
|
||||
return len(spendingWitness) == expectedRemoteWitnessSuccessSize &&
|
||||
len(spendingWitness[remotePreimageIndex]) == lntypes.HashSize
|
||||
}
|
||||
|
||||
// Otherwise, for our commitment, the only possible spends for an
|
||||
// outgoing HTLC are:
|
||||
//
|
||||
// SENDR: <0> <sendr sig> <recvr sig> <0> (2nd level timeout)
|
||||
// RECVR: <recvr sig> <preimage>
|
||||
// REVOK: <revoke sig> <revoke key>
|
||||
//
|
||||
// So the only success case has the pre-image as the 2nd (index 1)
|
||||
// element in the witness.
|
||||
return len(spendingWitness[localPreimageIndex]) == lntypes.HashSize
|
||||
}
|
||||
|
||||
// Resolve kicks off full resolution of an outgoing HTLC output. If it's our
|
||||
// commitment, it isn't resolved until we see the second level HTLC txn
|
||||
// confirmed. If it's the remote party's commitment, we don't resolve until we
|
||||
@ -129,40 +281,33 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// With the output sent to the nursery, we'll now wait until the output
|
||||
// has been fully resolved before sending the clean up message.
|
||||
//
|
||||
// TODO(roasbeef): need to be able to cancel nursery?
|
||||
// * if they pull on-chain while we're waiting
|
||||
|
||||
// If we don't have a second layer transaction, then this is a remote
|
||||
// party's commitment, so we'll watch for a direct spend.
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
// We'll block until: the HTLC output has been spent, and the
|
||||
// transaction spending that output is sufficiently confirmed.
|
||||
log.Infof("%T(%v): waiting for nursery to spend CLTV-locked "+
|
||||
"output", h, h.htlcResolution.ClaimOutpoint)
|
||||
if err := waitForOutputResolution(); err != nil {
|
||||
// Now that we've handed off the HTLC to the nursery, we'll watch for a
|
||||
// spend of the output, and make our next move off of that. Depending
|
||||
// on if this is our commitment, or the remote party's commitment,
|
||||
// we'll be watching a different outpoint and script.
|
||||
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// Otherwise, this is our commitment, so we'll watch for the
|
||||
// second-level transaction to be sufficiently confirmed.
|
||||
secondLevelTXID := h.htlcResolution.SignedTimeoutTx.TxHash()
|
||||
sweepScript := h.htlcResolution.SignedTimeoutTx.TxOut[0].PkScript
|
||||
confNtfn, err := h.Notifier.RegisterConfirmationsNtfn(
|
||||
&secondLevelTXID, sweepScript, 1, h.broadcastHeight,
|
||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
||||
outpointToWatch, scriptToWatch, h.broadcastHeight,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("%T(%v): waiting second-level tx (txid=%v) to be "+
|
||||
log.Infof("%T(%v): waiting for HTLC output %v to be spent"+
|
||||
"fully confirmed", h, h.htlcResolution.ClaimOutpoint,
|
||||
secondLevelTXID)
|
||||
outpointToWatch)
|
||||
|
||||
// We'll block here until either we exit, or the HTLC output on the
|
||||
// commitment transaction has been spent.
|
||||
var (
|
||||
spend *chainntnfs.SpendDetail
|
||||
ok bool
|
||||
)
|
||||
select {
|
||||
case _, ok := <-confNtfn.Confirmed:
|
||||
case spend, ok = <-spendNtfn.Spend:
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("quitting")
|
||||
}
|
||||
@ -170,19 +315,25 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||
case <-h.Quit:
|
||||
return nil, fmt.Errorf("quitting")
|
||||
}
|
||||
}
|
||||
|
||||
// TODO(roasbeef): need to watch for remote party sweeping with pre-image?
|
||||
// * have another waiting on spend above, will check the type, if it's
|
||||
// pre-image, then we'll cancel, and send a clean up back with
|
||||
// pre-image, also add to preimage cache
|
||||
// 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.
|
||||
if isSuccessSpend(spend, h.htlcResolution.SignedTimeoutTx != nil) {
|
||||
log.Infof("%T(%v): HTLC has been swept with pre-image by "+
|
||||
"remote party during timeout flow! Adding pre-image to "+
|
||||
"witness cache", h.htlcResolution.ClaimOutpoint)
|
||||
|
||||
return h.claimCleanUp(spend)
|
||||
}
|
||||
|
||||
log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+
|
||||
"confirmed", h, h.htlcResolution.ClaimOutpoint)
|
||||
|
||||
// At this point, the second-level transaction is sufficiently
|
||||
// confirmed, or a transaction directly spending the output is.
|
||||
// Therefore, we can now send back our clean up message.
|
||||
// Therefore, we can now send back our clean up message, failing the
|
||||
// HTLC on the incoming link.
|
||||
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
||||
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
||||
SourceChan: h.ShortChanID,
|
||||
@ -193,7 +344,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||
}
|
||||
|
||||
// Finally, if this was an output on our commitment transaction, we'll
|
||||
// for the second-level HTLC output to be spent, and for that
|
||||
// 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 "+
|
||||
|
364
contractcourt/htlc_timeout_resolver_test.go
Normal file
364
contractcourt/htlc_timeout_resolver_test.go
Normal file
@ -0,0 +1,364 @@
|
||||
package contractcourt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
)
|
||||
|
||||
type mockSigner struct {
|
||||
}
|
||||
|
||||
func (m *mockSigner) SignOutputRaw(tx *wire.MsgTx,
|
||||
signDesc *input.SignDescriptor) ([]byte, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *mockSigner) ComputeInputScript(tx *wire.MsgTx,
|
||||
signDesc *input.SignDescriptor) (*input.Script, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
type mockWitnessBeacon struct {
|
||||
preImageUpdates chan lntypes.Preimage
|
||||
|
||||
newPreimages chan []lntypes.Preimage
|
||||
}
|
||||
|
||||
func (m *mockWitnessBeacon) SubscribeUpdates() *WitnessSubscription {
|
||||
return &WitnessSubscription{
|
||||
WitnessUpdates: m.preImageUpdates,
|
||||
CancelSubscription: func() {},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockWitnessBeacon) LookupPreimage(payhash lntypes.Hash) (lntypes.Preimage, bool) {
|
||||
return lntypes.Preimage{}, false
|
||||
}
|
||||
|
||||
func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error {
|
||||
m.newPreimages <- preimages
|
||||
return nil
|
||||
}
|
||||
|
||||
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
|
||||
// variations of possible local+remote spends.
|
||||
func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||
|
||||
var (
|
||||
htlcOutpoint wire.OutPoint
|
||||
fakePreimage lntypes.Preimage
|
||||
)
|
||||
fakeSignDesc := &input.SignDescriptor{
|
||||
Output: &wire.TxOut{},
|
||||
}
|
||||
|
||||
copy(fakePreimage[:], fakePreimageBytes)
|
||||
|
||||
signer := &mockSigner{}
|
||||
sweepTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
Witness: [][]byte{{0x01}},
|
||||
},
|
||||
},
|
||||
}
|
||||
fakeTimeout := int32(5)
|
||||
|
||||
templateTx := &wire.MsgTx{
|
||||
TxIn: []*wire.TxIn{
|
||||
{
|
||||
PreviousOutPoint: htlcOutpoint,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
// name is a human readable description of the test case.
|
||||
name string
|
||||
|
||||
// remoteCommit denotes if the commitment broadcast was the
|
||||
// remote commitment or not.
|
||||
remoteCommit bool
|
||||
|
||||
// timeout denotes if the HTLC should be let timeout, or if the
|
||||
// "remote" party should sweep it on-chain. This also affects
|
||||
// what type of resolution message we expect.
|
||||
timeout bool
|
||||
|
||||
// txToBroadcast is a function closure that should generate the
|
||||
// transaction that should spend the HTLC output. Test authors
|
||||
// can use this to customize the witness used when spending to
|
||||
// trigger various redemption cases.
|
||||
txToBroadcast func() (*wire.MsgTx, error)
|
||||
}{
|
||||
// Remote commitment is broadcast, we time out the HTLC on
|
||||
// chain, and should expect a fail HTLC resolution.
|
||||
{
|
||||
name: "timeout remote tx",
|
||||
remoteCommit: true,
|
||||
timeout: true,
|
||||
txToBroadcast: func() (*wire.MsgTx, error) {
|
||||
witness, err := input.ReceiverHtlcSpendTimeout(
|
||||
signer, fakeSignDesc, sweepTx,
|
||||
fakeTimeout,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
},
|
||||
|
||||
// Our local commitment is broadcast, we timeout the HTLC and
|
||||
// still expect an HTLC fail resolution.
|
||||
{
|
||||
name: "timeout local tx",
|
||||
remoteCommit: false,
|
||||
timeout: true,
|
||||
txToBroadcast: func() (*wire.MsgTx, error) {
|
||||
witness, err := input.SenderHtlcSpendTimeout(
|
||||
nil, signer, fakeSignDesc, sweepTx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
},
|
||||
|
||||
// The remote commitment is broadcast, they sweep with the
|
||||
// pre-image, we should get a settle HTLC resolution.
|
||||
{
|
||||
name: "success remote tx",
|
||||
remoteCommit: true,
|
||||
timeout: false,
|
||||
txToBroadcast: func() (*wire.MsgTx, error) {
|
||||
witness, err := input.ReceiverHtlcSpendRedeem(
|
||||
nil, fakePreimageBytes, signer,
|
||||
fakeSignDesc, sweepTx,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
},
|
||||
|
||||
// The local commitment is broadcast, they sweep it with a
|
||||
// timeout from the output, and we should still get the HTLC
|
||||
// settle resolution back.
|
||||
{
|
||||
name: "success local tx",
|
||||
remoteCommit: false,
|
||||
timeout: false,
|
||||
txToBroadcast: func() (*wire.MsgTx, error) {
|
||||
witness, err := input.SenderHtlcSpendRedeem(
|
||||
signer, fakeSignDesc, sweepTx,
|
||||
fakePreimageBytes,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
notifier := &mockNotifier{
|
||||
epochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
spendChan: make(chan *chainntnfs.SpendDetail),
|
||||
confChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
witnessBeacon := &mockWitnessBeacon{
|
||||
preImageUpdates: make(chan lntypes.Preimage, 1),
|
||||
newPreimages: make(chan []lntypes.Preimage),
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Logf("Running test case: %v", testCase.name)
|
||||
|
||||
checkPointChan := make(chan struct{}, 1)
|
||||
incubateChan := make(chan struct{}, 1)
|
||||
resolutionChan := make(chan ResolutionMsg, 1)
|
||||
|
||||
chainCfg := ChannelArbitratorConfig{
|
||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||
Notifier: notifier,
|
||||
PreimageDB: witnessBeacon,
|
||||
IncubateOutputs: func(wire.OutPoint,
|
||||
*lnwallet.CommitOutputResolution,
|
||||
*lnwallet.OutgoingHtlcResolution,
|
||||
*lnwallet.IncomingHtlcResolution,
|
||||
uint32) error {
|
||||
|
||||
incubateChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
||||
if len(msgs) != 1 {
|
||||
return fmt.Errorf("expected 1 "+
|
||||
"resolution msg, instead got %v",
|
||||
len(msgs))
|
||||
}
|
||||
|
||||
resolutionChan <- msgs[0]
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
resolver := &htlcTimeoutResolver{
|
||||
ResolverKit: ResolverKit{
|
||||
ChannelArbitratorConfig: chainCfg,
|
||||
Checkpoint: func(_ ContractResolver) error {
|
||||
checkPointChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
resolver.htlcResolution.SweepSignDesc = *fakeSignDesc
|
||||
|
||||
// If the test case needs the remote commitment to be
|
||||
// broadcast, then we'll set the timeout commit to a fake
|
||||
// transaction to force the code path.
|
||||
if !testCase.remoteCommit {
|
||||
resolver.htlcResolution.SignedTimeoutTx = sweepTx
|
||||
}
|
||||
|
||||
// With all the setup above complete, we can initiate the
|
||||
// resolution process, and the bulk of our test.
|
||||
var wg sync.WaitGroup
|
||||
resolveErr := make(chan error, 1)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, err := resolver.Resolve()
|
||||
if err != nil {
|
||||
resolveErr <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// At the output isn't yet in the nursery, we expect that we
|
||||
// should receive an incubation request.
|
||||
select {
|
||||
case <-incubateChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to receive incubation request")
|
||||
}
|
||||
|
||||
// Next, the resolver should request a spend notification for
|
||||
// the direct HTLC output. We'll use the txToBroadcast closure
|
||||
// for the test case to generate the transaction that we'll
|
||||
// send to the resolver.
|
||||
spendingTx, err := testCase.txToBroadcast()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate tx: %v", err)
|
||||
}
|
||||
select {
|
||||
case notifier.spendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
|
||||
if !testCase.timeout {
|
||||
// If the resolver should settle now, then we'll
|
||||
// extract the pre-image to be extracted and the
|
||||
// resolution message sent.
|
||||
select {
|
||||
case newPreimage := <-witnessBeacon.newPreimages:
|
||||
if newPreimage[0] != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, newPreimage)
|
||||
}
|
||||
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("pre-image not added")
|
||||
}
|
||||
|
||||
// Finally, we should get a resolution message with the
|
||||
// pre-image set within the message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
// Once again, the pre-images should match up.
|
||||
if *resolutionMsg.PreImage != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, resolutionMsg.PreImage)
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, the HTLC should now timeout. First, we
|
||||
// should get a resolution message with a populated
|
||||
// failure message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
if resolutionMsg.Failure == nil {
|
||||
t.Fatalf("expected failure resolution msg")
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
|
||||
// We should also get another request for the spend
|
||||
// notification of the second-level transaction to
|
||||
// indicate that it's been swept by the nursery, but
|
||||
// only if this is a local commitment transaction.
|
||||
if !testCase.remoteCommit {
|
||||
select {
|
||||
case notifier.spendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, before the resolver exits, it should checkpoint
|
||||
// its final state.
|
||||
select {
|
||||
case <-checkPointChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("check point not received")
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Finally, the resolver should be marked as resolved.
|
||||
if !resolver.resolved {
|
||||
t.Fatalf("resolver should be marked as resolved")
|
||||
}
|
||||
}
|
||||
}
|
@ -894,7 +894,7 @@ func (u *utxoNursery) sweepCribOutput(classHeight uint32, baby *babyOutput) erro
|
||||
// We'll now broadcast the HTLC transaction, then wait for it to be
|
||||
// confirmed before transitioning it to kindergarten.
|
||||
err := u.cfg.PublishTransaction(baby.timeoutTx)
|
||||
if err != nil {
|
||||
if err != nil && err != lnwallet.ErrDoubleSpend {
|
||||
utxnLog.Errorf("Unable to broadcast baby tx: "+
|
||||
"%v, %v", err, spew.Sdump(baby.timeoutTx))
|
||||
return err
|
||||
|
Loading…
Reference in New Issue
Block a user