mirror of
https://github.com/lightningnetwork/lnd.git
synced 2025-09-01 10:11:11 +02:00
Merge pull request #2800 from Roasbeef/simplify-timeout-resolver
contractcourt: simplify htlcTimeoutResolver
This commit is contained in:
@@ -511,8 +511,10 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
|
|
||||||
// The force close request should trigger broadcast of the commitment
|
// The force close request should trigger broadcast of the commitment
|
||||||
// transaction.
|
// transaction.
|
||||||
assertStateTransitions(t, arbLog.newStates,
|
assertStateTransitions(
|
||||||
StateBroadcastCommit, StateCommitmentBroadcasted)
|
t, arbLog.newStates, StateBroadcastCommit,
|
||||||
|
StateCommitmentBroadcasted,
|
||||||
|
)
|
||||||
select {
|
select {
|
||||||
case <-respChan:
|
case <-respChan:
|
||||||
case <-time.After(5 * time.Second):
|
case <-time.After(5 * time.Second):
|
||||||
@@ -529,7 +531,17 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Now notify about the local force close getting confirmed.
|
// 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{
|
htlcOp := wire.OutPoint{
|
||||||
Hash: closeTx.TxHash(),
|
Hash: closeTx.TxHash(),
|
||||||
@@ -569,8 +581,10 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
&channeldb.ChannelCloseSummary{},
|
&channeldb.ChannelCloseSummary{},
|
||||||
}
|
}
|
||||||
|
|
||||||
assertStateTransitions(t, arbLog.newStates, StateContractClosed,
|
assertStateTransitions(
|
||||||
StateWaitingFullResolution)
|
t, arbLog.newStates, StateContractClosed,
|
||||||
|
StateWaitingFullResolution,
|
||||||
|
)
|
||||||
|
|
||||||
// htlcOutgoingContestResolver is now active and waiting for the HTLC to
|
// htlcOutgoingContestResolver is now active and waiting for the HTLC to
|
||||||
// expire. It should not yet have passed it on for incubation.
|
// 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")
|
t.Fatalf("no response received")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Notify resolver that output of the commitment has been spent.
|
// Notify resolver that the HTLC output of the commitment has been
|
||||||
notifier.confChan <- &chainntnfs.TxConfirmation{}
|
// spent.
|
||||||
|
notifier.spendChan <- &chainntnfs.SpendDetail{SpendingTx: closeTx}
|
||||||
|
|
||||||
// As this is our own commitment transaction, the HTLC will go through
|
// As this is our own commitment transaction, the HTLC will go through
|
||||||
// to the second level. Channel arbitrator should still not be marked as
|
// to the second level. Channel arbitrator should still not be marked
|
||||||
// resolved.
|
// as resolved.
|
||||||
select {
|
select {
|
||||||
case <-resolved:
|
case <-resolved:
|
||||||
t.Fatalf("channel resolved prematurely")
|
t.Fatalf("channel resolved prematurely")
|
||||||
@@ -605,7 +620,7 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Notify resolver that the second level transaction is spent.
|
// 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.
|
// At this point channel should be marked as resolved.
|
||||||
assertStateTransitions(t, arbLog.newStates, StateFullyResolved)
|
assertStateTransitions(t, arbLog.newStates, StateFullyResolved)
|
||||||
|
@@ -1 +0,0 @@
|
|||||||
package contractcourt
|
|
@@ -4,13 +4,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
|
||||||
"github.com/btcsuite/btcutil"
|
"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
|
// htlcOutgoingContestResolver is a ContractResolver that's able to resolve an
|
||||||
@@ -44,108 +38,22 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
return nil, nil
|
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
|
// Otherwise, we'll watch for two external signals to decide if we'll
|
||||||
// morph into another resolver, or fully resolve the contract.
|
// morph into another resolver, or fully resolve the contract.
|
||||||
|
//
|
||||||
// The output we'll be watching for is the *direct* spend from the HTLC
|
// 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
|
// 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 resolution. Otherwise, we fetch this pointer from the input of
|
||||||
// the time out transaction.
|
// the time out transaction.
|
||||||
var (
|
outPointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||||
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],
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// First, we'll register for a spend notification for this output. If
|
// First, we'll register for a spend notification for this output. If
|
||||||
// the remote party sweeps with the pre-image, we'll be notified.
|
// the remote party sweeps with the pre-image, we'll be notified.
|
||||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
||||||
&outPointToWatch, scriptToWatch, h.broadcastHeight,
|
outPointToWatch, scriptToWatch, h.broadcastHeight,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -161,7 +69,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): Checkpoint?
|
// TODO(roasbeef): Checkpoint?
|
||||||
return claimCleanUp(commitSpend)
|
return h.claimCleanUp(commitSpend)
|
||||||
|
|
||||||
// If it hasn't, then we'll watch for both the expiration, and the
|
// If it hasn't, then we'll watch for both the expiration, and the
|
||||||
// sweeping out this output.
|
// sweeping out this output.
|
||||||
@@ -190,7 +98,6 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
//
|
//
|
||||||
// Source:
|
// Source:
|
||||||
// https://github.com/btcsuite/btcd/blob/991d32e72fe84d5fbf9c47cd604d793a0cd3a072/blockchain/validate.go#L154
|
// https://github.com/btcsuite/btcd/blob/991d32e72fe84d5fbf9c47cd604d793a0cd3a072/blockchain/validate.go#L154
|
||||||
|
|
||||||
if uint32(currentHeight) >= h.htlcResolution.Expiry-1 {
|
if uint32(currentHeight) >= h.htlcResolution.Expiry-1 {
|
||||||
log.Infof("%T(%v): HTLC has expired (height=%v, expiry=%v), "+
|
log.Infof("%T(%v): HTLC has expired (height=%v, expiry=%v), "+
|
||||||
"transforming into timeout resolver", h,
|
"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
|
// party is by revealing the preimage. So we'll perform
|
||||||
// our duties to clean up the contract once it has been
|
// our duties to clean up the contract once it has been
|
||||||
// claimed.
|
// claimed.
|
||||||
return claimCleanUp(commitSpend)
|
return h.claimCleanUp(commitSpend)
|
||||||
|
|
||||||
case <-h.Quit:
|
case <-h.Quit:
|
||||||
return nil, fmt.Errorf("resolver cancelled")
|
return nil, fmt.Errorf("resolver cancelled")
|
||||||
|
@@ -6,6 +6,10 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"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/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
)
|
)
|
||||||
@@ -67,6 +71,154 @@ func (h *htlcTimeoutResolver) ResolverKey() []byte {
|
|||||||
return key[:]
|
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
|
// 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
|
// 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
|
// 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
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the output sent to the nursery, we'll now wait until the output
|
// Now that we've handed off the HTLC to the nursery, we'll watch for a
|
||||||
// has been fully resolved before sending the clean up message.
|
// 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,
|
||||||
// TODO(roasbeef): need to be able to cancel nursery?
|
// we'll be watching a different outpoint and script.
|
||||||
// * if they pull on-chain while we're waiting
|
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||||
|
if err != nil {
|
||||||
// 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 {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
} else {
|
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
||||||
// Otherwise, this is our commitment, so we'll watch for the
|
outpointToWatch, scriptToWatch, h.broadcastHeight,
|
||||||
// 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,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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,
|
"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 {
|
select {
|
||||||
case _, ok := <-confNtfn.Confirmed:
|
case spend, ok = <-spendNtfn.Spend:
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, fmt.Errorf("quitting")
|
return nil, fmt.Errorf("quitting")
|
||||||
}
|
}
|
||||||
@@ -170,19 +315,25 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|||||||
case <-h.Quit:
|
case <-h.Quit:
|
||||||
return nil, fmt.Errorf("quitting")
|
return nil, fmt.Errorf("quitting")
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// TODO(roasbeef): need to watch for remote party sweeping with pre-image?
|
// If the spend reveals the pre-image, then we'll enter the clean up
|
||||||
// * have another waiting on spend above, will check the type, if it's
|
// workflow to pass the pre-image back to the incoming link, add it to
|
||||||
// pre-image, then we'll cancel, and send a clean up back with
|
// the witness cache, and exit.
|
||||||
// pre-image, also add to preimage cache
|
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 "+
|
log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+
|
||||||
"confirmed", h, h.htlcResolution.ClaimOutpoint)
|
"confirmed", h, h.htlcResolution.ClaimOutpoint)
|
||||||
|
|
||||||
// At this point, the second-level transaction is sufficiently
|
// At this point, the second-level transaction is sufficiently
|
||||||
// confirmed, or a transaction directly spending the output is.
|
// 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{}
|
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
||||||
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
||||||
SourceChan: h.ShortChanID,
|
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
|
// 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.
|
// transaction itself to confirm.
|
||||||
if h.htlcResolution.SignedTimeoutTx != nil {
|
if h.htlcResolution.SignedTimeoutTx != nil {
|
||||||
log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+
|
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
|
// We'll now broadcast the HTLC transaction, then wait for it to be
|
||||||
// confirmed before transitioning it to kindergarten.
|
// confirmed before transitioning it to kindergarten.
|
||||||
err := u.cfg.PublishTransaction(baby.timeoutTx)
|
err := u.cfg.PublishTransaction(baby.timeoutTx)
|
||||||
if err != nil {
|
if err != nil && err != lnwallet.ErrDoubleSpend {
|
||||||
utxnLog.Errorf("Unable to broadcast baby tx: "+
|
utxnLog.Errorf("Unable to broadcast baby tx: "+
|
||||||
"%v, %v", err, spew.Sdump(baby.timeoutTx))
|
"%v, %v", err, spew.Sdump(baby.timeoutTx))
|
||||||
return err
|
return err
|
||||||
|
Reference in New Issue
Block a user