Merge pull request #4779 from halseth/anchor-htlc-aggregation
[anchors] HTLC second level aggregation in the sweeper
This commit is contained in:
commit
4af24158c4
@ -6,7 +6,9 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||||
@ -275,6 +277,13 @@ var (
|
|||||||
// the full set of resolutions for a channel.
|
// the full set of resolutions for a channel.
|
||||||
resolutionsKey = []byte("resolutions")
|
resolutionsKey = []byte("resolutions")
|
||||||
|
|
||||||
|
// resolutionsSignDetailsKey is the key under the logScope where we
|
||||||
|
// will store input.SignDetails for each HTLC resolution. If this is
|
||||||
|
// not found under the logScope, it means it was written before
|
||||||
|
// SignDetails was introduced, and should be set nil for each HTLC
|
||||||
|
// resolution.
|
||||||
|
resolutionsSignDetailsKey = []byte("resolutions-sign-details")
|
||||||
|
|
||||||
// anchorResolutionKey is the key under the logScope that we'll use to
|
// anchorResolutionKey is the key under the logScope that we'll use to
|
||||||
// store the anchor resolution, if any.
|
// store the anchor resolution, if any.
|
||||||
anchorResolutionKey = []byte("anchor-resolution")
|
anchorResolutionKey = []byte("anchor-resolution")
|
||||||
@ -656,6 +665,10 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As we write the HTLC resolutions, we'll serialize the sign
|
||||||
|
// details for each, to store under a new key.
|
||||||
|
var signDetailsBuf bytes.Buffer
|
||||||
|
|
||||||
// With the output for the commitment transaction written, we
|
// With the output for the commitment transaction written, we
|
||||||
// can now write out the resolutions for the incoming and
|
// can now write out the resolutions for the incoming and
|
||||||
// outgoing HTLC's.
|
// outgoing HTLC's.
|
||||||
@ -668,6 +681,11 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
numOutgoing := uint32(len(c.HtlcResolutions.OutgoingHTLCs))
|
numOutgoing := uint32(len(c.HtlcResolutions.OutgoingHTLCs))
|
||||||
if err := binary.Write(&b, endian, numOutgoing); err != nil {
|
if err := binary.Write(&b, endian, numOutgoing); err != nil {
|
||||||
@ -678,13 +696,28 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Put the resolutions under the resolutionsKey.
|
||||||
err = scopeBucket.Put(resolutionsKey, b.Bytes())
|
err = scopeBucket.Put(resolutionsKey, b.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We'll put the serialized sign details under its own key to
|
||||||
|
// stay backwards compatible.
|
||||||
|
err = scopeBucket.Put(
|
||||||
|
resolutionsSignDetailsKey, signDetailsBuf.Bytes(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// Write out the anchor resolution if present.
|
// Write out the anchor resolution if present.
|
||||||
if c.AnchorResolution != nil {
|
if c.AnchorResolution != nil {
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
@ -779,6 +812,33 @@ func (b *boltArbitratorLog) FetchContractResolutions() (*ContractResolutions, er
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Now we attempt to get the sign details for our HTLC
|
||||||
|
// resolutions. If not present the channel is of a type that
|
||||||
|
// doesn't need them. If present there will be SignDetails
|
||||||
|
// encoded for each HTLC resolution.
|
||||||
|
signDetailsBytes := scopeBucket.Get(resolutionsSignDetailsKey)
|
||||||
|
if signDetailsBytes != nil {
|
||||||
|
r := bytes.NewReader(signDetailsBytes)
|
||||||
|
|
||||||
|
// They will be encoded in the same order as the
|
||||||
|
// resolutions: firs incoming HTLCs, then outgoing.
|
||||||
|
for i := uint32(0); i < numIncoming; i++ {
|
||||||
|
htlc := &c.HtlcResolutions.IncomingHTLCs[i]
|
||||||
|
htlc.SignDetails, err = decodeSignDetails(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint32(0); i < numOutgoing; i++ {
|
||||||
|
htlc := &c.HtlcResolutions.OutgoingHTLCs[i]
|
||||||
|
htlc.SignDetails, err = decodeSignDetails(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
anchorResBytes := scopeBucket.Get(anchorResolutionKey)
|
anchorResBytes := scopeBucket.Get(anchorResolutionKey)
|
||||||
if anchorResBytes != nil {
|
if anchorResBytes != nil {
|
||||||
c.AnchorResolution = &lnwallet.AnchorResolution{}
|
c.AnchorResolution = &lnwallet.AnchorResolution{}
|
||||||
@ -941,6 +1001,11 @@ func (b *boltArbitratorLog) WipeHistory() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = scopeBucket.Delete(resolutionsSignDetailsKey)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// We'll delete any chain actions that are still stored by
|
// We'll delete any chain actions that are still stored by
|
||||||
// removing the enclosing bucket.
|
// removing the enclosing bucket.
|
||||||
err = scopeBucket.DeleteNestedBucket(actionsBucketKey)
|
err = scopeBucket.DeleteNestedBucket(actionsBucketKey)
|
||||||
@ -980,6 +1045,79 @@ func (b *boltArbitratorLog) checkpointContract(c ContractResolver,
|
|||||||
}, func() {})
|
}, func() {})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// encodeSignDetails encodes the gived SignDetails struct to the writer.
|
||||||
|
// SignDetails is allowed to be nil, in which we will encode that it is not
|
||||||
|
// present.
|
||||||
|
func encodeSignDetails(w io.Writer, s *input.SignDetails) error {
|
||||||
|
// If we don't have sign details, write false and return.
|
||||||
|
if s == nil {
|
||||||
|
return binary.Write(w, endian, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise write true, and the contents of the SignDetails.
|
||||||
|
if err := binary.Write(w, endian, true); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err := input.WriteSignDescriptor(w, &s.SignDesc)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = binary.Write(w, endian, uint32(s.SigHashType))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write the DER-encoded signature.
|
||||||
|
b := s.PeerSig.Serialize()
|
||||||
|
if err := wire.WriteVarBytes(w, 0, b); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// decodeSignDetails extracts a single SignDetails from the reader. It is
|
||||||
|
// allowed to return nil in case the SignDetails were empty.
|
||||||
|
func decodeSignDetails(r io.Reader) (*input.SignDetails, error) {
|
||||||
|
var present bool
|
||||||
|
if err := binary.Read(r, endian, &present); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simply return nil if the next SignDetails was not present.
|
||||||
|
if !present {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise decode the elements of the SignDetails.
|
||||||
|
s := input.SignDetails{}
|
||||||
|
err := input.ReadSignDescriptor(r, &s.SignDesc)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sigHash uint32
|
||||||
|
err = binary.Read(r, endian, &sigHash)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.SigHashType = txscript.SigHashType(sigHash)
|
||||||
|
|
||||||
|
// Read DER-encoded signature.
|
||||||
|
rawSig, err := wire.ReadVarBytes(r, 0, 200, "signature")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sig, err := btcec.ParseDERSignature(rawSig, btcec.S256())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
s.PeerSig = sig
|
||||||
|
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
func encodeIncomingResolution(w io.Writer, i *lnwallet.IncomingHtlcResolution) error {
|
func encodeIncomingResolution(w io.Writer, i *lnwallet.IncomingHtlcResolution) error {
|
||||||
if _, err := w.Write(i.Preimage[:]); err != nil {
|
if _, err := w.Write(i.Preimage[:]); err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -102,6 +102,58 @@ var (
|
|||||||
},
|
},
|
||||||
HashType: txscript.SigHashAll,
|
HashType: txscript.SigHashAll,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testTx = &wire.MsgTx{
|
||||||
|
Version: 2,
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: testChanPoint2,
|
||||||
|
SignatureScript: []byte{0x12, 0x34},
|
||||||
|
Witness: [][]byte{
|
||||||
|
{
|
||||||
|
0x00, 0x14, 0xee, 0x91, 0x41,
|
||||||
|
0x7e, 0x85, 0x6c, 0xde, 0x10,
|
||||||
|
0xa2, 0x91, 0x1e, 0xdc, 0xbd,
|
||||||
|
0xbd, 0x69, 0xe2, 0xef, 0xb5,
|
||||||
|
0x71, 0x48,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Sequence: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 5000000000,
|
||||||
|
PkScript: []byte{
|
||||||
|
0xc6, 0xa5, 0x9d, 0xc2, 0x26, 0xc2,
|
||||||
|
0x86, 0x24, 0xe1, 0x81, 0x75, 0xe8,
|
||||||
|
0x51, 0xc9, 0x6b, 0x97, 0x3d, 0x81,
|
||||||
|
0xb0, 0x1c, 0xc3, 0x1f, 0x04, 0x78,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
LockTime: 123,
|
||||||
|
}
|
||||||
|
|
||||||
|
// A valid, DER-encoded signature (taken from btcec unit tests).
|
||||||
|
testSigBytes = []byte{
|
||||||
|
0x30, 0x44, 0x02, 0x20, 0x4e, 0x45, 0xe1, 0x69,
|
||||||
|
0x32, 0xb8, 0xaf, 0x51, 0x49, 0x61, 0xa1, 0xd3,
|
||||||
|
0xa1, 0xa2, 0x5f, 0xdf, 0x3f, 0x4f, 0x77, 0x32,
|
||||||
|
0xe9, 0xd6, 0x24, 0xc6, 0xc6, 0x15, 0x48, 0xab,
|
||||||
|
0x5f, 0xb8, 0xcd, 0x41, 0x02, 0x20, 0x18, 0x15,
|
||||||
|
0x22, 0xec, 0x8e, 0xca, 0x07, 0xde, 0x48, 0x60,
|
||||||
|
0xa4, 0xac, 0xdd, 0x12, 0x90, 0x9d, 0x83, 0x1c,
|
||||||
|
0xc5, 0x6c, 0xbb, 0xac, 0x46, 0x22, 0x08, 0x22,
|
||||||
|
0x21, 0xa8, 0x76, 0x8d, 0x1d, 0x09,
|
||||||
|
}
|
||||||
|
testSig, _ = btcec.ParseDERSignature(testSigBytes, btcec.S256())
|
||||||
|
|
||||||
|
testSignDetails = &input.SignDetails{
|
||||||
|
SignDesc: testSignDesc,
|
||||||
|
SigHashType: txscript.SigHashSingle,
|
||||||
|
PeerSig: testSig,
|
||||||
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
func makeTestDB() (kvdb.Backend, func(), error) {
|
func makeTestDB() (kvdb.Backend, func(), error) {
|
||||||
@ -219,13 +271,13 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver,
|
|||||||
case *htlcOutgoingContestResolver:
|
case *htlcOutgoingContestResolver:
|
||||||
diskRes := diskResolver.(*htlcOutgoingContestResolver)
|
diskRes := diskResolver.(*htlcOutgoingContestResolver)
|
||||||
assertTimeoutResEqual(
|
assertTimeoutResEqual(
|
||||||
&ogRes.htlcTimeoutResolver, &diskRes.htlcTimeoutResolver,
|
ogRes.htlcTimeoutResolver, diskRes.htlcTimeoutResolver,
|
||||||
)
|
)
|
||||||
|
|
||||||
case *htlcIncomingContestResolver:
|
case *htlcIncomingContestResolver:
|
||||||
diskRes := diskResolver.(*htlcIncomingContestResolver)
|
diskRes := diskResolver.(*htlcIncomingContestResolver)
|
||||||
assertSuccessResEqual(
|
assertSuccessResEqual(
|
||||||
&ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver,
|
ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver,
|
||||||
)
|
)
|
||||||
|
|
||||||
if ogRes.htlcExpiry != diskRes.htlcExpiry {
|
if ogRes.htlcExpiry != diskRes.htlcExpiry {
|
||||||
@ -323,13 +375,13 @@ func TestContractInsertionRetrieval(t *testing.T) {
|
|||||||
contestTimeout := timeoutResolver
|
contestTimeout := timeoutResolver
|
||||||
contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint()
|
contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint()
|
||||||
resolvers = append(resolvers, &htlcOutgoingContestResolver{
|
resolvers = append(resolvers, &htlcOutgoingContestResolver{
|
||||||
htlcTimeoutResolver: contestTimeout,
|
htlcTimeoutResolver: &contestTimeout,
|
||||||
})
|
})
|
||||||
contestSuccess := successResolver
|
contestSuccess := successResolver
|
||||||
contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint()
|
contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint()
|
||||||
resolvers = append(resolvers, &htlcIncomingContestResolver{
|
resolvers = append(resolvers, &htlcIncomingContestResolver{
|
||||||
htlcExpiry: 100,
|
htlcExpiry: 100,
|
||||||
htlcSuccessResolver: contestSuccess,
|
htlcSuccessResolver: &contestSuccess,
|
||||||
})
|
})
|
||||||
|
|
||||||
// For quick lookup during the test, we'll create this map which allow
|
// For quick lookup during the test, we'll create this map which allow
|
||||||
@ -469,7 +521,7 @@ func TestContractSwapping(t *testing.T) {
|
|||||||
|
|
||||||
// We'll create two resolvers, a regular timeout resolver, and the
|
// We'll create two resolvers, a regular timeout resolver, and the
|
||||||
// contest resolver that eventually turns into the timeout resolver.
|
// contest resolver that eventually turns into the timeout resolver.
|
||||||
timeoutResolver := htlcTimeoutResolver{
|
timeoutResolver := &htlcTimeoutResolver{
|
||||||
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
||||||
Expiry: 99,
|
Expiry: 99,
|
||||||
SignedTimeoutTx: nil,
|
SignedTimeoutTx: nil,
|
||||||
@ -497,7 +549,7 @@ func TestContractSwapping(t *testing.T) {
|
|||||||
|
|
||||||
// With the resolver inserted, we'll now attempt to atomically swap it
|
// With the resolver inserted, we'll now attempt to atomically swap it
|
||||||
// for its underlying timeout resolver.
|
// for its underlying timeout resolver.
|
||||||
err = testLog.SwapContract(contestResolver, &timeoutResolver)
|
err = testLog.SwapContract(contestResolver, timeoutResolver)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to swap contracts: %v", err)
|
t.Fatalf("unable to swap contracts: %v", err)
|
||||||
}
|
}
|
||||||
@ -514,7 +566,7 @@ func TestContractSwapping(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// That single contract should be the underlying timeout resolver.
|
// That single contract should be the underlying timeout resolver.
|
||||||
assertResolversEqual(t, &timeoutResolver, dbContracts[0])
|
assertResolversEqual(t, timeoutResolver, dbContracts[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestContractResolutionsStorage tests that we're able to properly store and
|
// TestContractResolutionsStorage tests that we're able to properly store and
|
||||||
@ -550,8 +602,38 @@ func TestContractResolutionsStorage(t *testing.T) {
|
|||||||
ClaimOutpoint: randOutPoint(),
|
ClaimOutpoint: randOutPoint(),
|
||||||
SweepSignDesc: testSignDesc,
|
SweepSignDesc: testSignDesc,
|
||||||
},
|
},
|
||||||
|
|
||||||
|
// We add a resolution with SignDetails.
|
||||||
|
{
|
||||||
|
Preimage: testPreimage,
|
||||||
|
SignedSuccessTx: testTx,
|
||||||
|
SignDetails: testSignDetails,
|
||||||
|
CsvDelay: 900,
|
||||||
|
ClaimOutpoint: randOutPoint(),
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
},
|
||||||
|
|
||||||
|
// We add a resolution with a signed tx, but no
|
||||||
|
// SignDetails.
|
||||||
|
{
|
||||||
|
Preimage: testPreimage,
|
||||||
|
SignedSuccessTx: testTx,
|
||||||
|
CsvDelay: 900,
|
||||||
|
ClaimOutpoint: randOutPoint(),
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
||||||
|
// We add a resolution with a signed tx, but no
|
||||||
|
// SignDetails.
|
||||||
|
{
|
||||||
|
Expiry: 103,
|
||||||
|
SignedTimeoutTx: testTx,
|
||||||
|
CsvDelay: 923923,
|
||||||
|
ClaimOutpoint: randOutPoint(),
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
},
|
||||||
|
// Resolution without signed tx.
|
||||||
{
|
{
|
||||||
Expiry: 103,
|
Expiry: 103,
|
||||||
SignedTimeoutTx: nil,
|
SignedTimeoutTx: nil,
|
||||||
@ -559,6 +641,15 @@ func TestContractResolutionsStorage(t *testing.T) {
|
|||||||
ClaimOutpoint: randOutPoint(),
|
ClaimOutpoint: randOutPoint(),
|
||||||
SweepSignDesc: testSignDesc,
|
SweepSignDesc: testSignDesc,
|
||||||
},
|
},
|
||||||
|
// Resolution with SignDetails.
|
||||||
|
{
|
||||||
|
Expiry: 103,
|
||||||
|
SignedTimeoutTx: testTx,
|
||||||
|
SignDetails: testSignDetails,
|
||||||
|
CsvDelay: 923923,
|
||||||
|
ClaimOutpoint: randOutPoint(),
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
AnchorResolution: &lnwallet.AnchorResolution{
|
AnchorResolution: &lnwallet.AnchorResolution{
|
||||||
@ -585,8 +676,15 @@ func TestContractResolutionsStorage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !reflect.DeepEqual(&res, diskRes) {
|
if !reflect.DeepEqual(&res, diskRes) {
|
||||||
t.Fatalf("resolution mismatch: expected %#v\n, got %#v",
|
for _, h := range res.HtlcResolutions.IncomingHTLCs {
|
||||||
&res, diskRes)
|
h.SweepSignDesc.KeyDesc.PubKey.Curve = nil
|
||||||
|
}
|
||||||
|
for _, h := range diskRes.HtlcResolutions.IncomingHTLCs {
|
||||||
|
h.SweepSignDesc.KeyDesc.PubKey.Curve = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Fatalf("resolution mismatch: expected %v\n, got %v",
|
||||||
|
spew.Sdump(&res), spew.Sdump(diskRes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll now delete the state, then attempt to retrieve the set of
|
// We'll now delete the state, then attempt to retrieve the set of
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
@ -79,10 +80,12 @@ func (c *commitSweepResolver) ResolverKey() []byte {
|
|||||||
|
|
||||||
// waitForHeight registers for block notifications and waits for the provided
|
// waitForHeight registers for block notifications and waits for the provided
|
||||||
// block height to be reached.
|
// block height to be reached.
|
||||||
func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
|
func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier,
|
||||||
|
quit <-chan struct{}) error {
|
||||||
|
|
||||||
// Register for block epochs. After registration, the current height
|
// Register for block epochs. After registration, the current height
|
||||||
// will be sent on the channel immediately.
|
// will be sent on the channel immediately.
|
||||||
blockEpochs, err := c.Notifier.RegisterBlockEpochNtfn(nil)
|
blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -99,12 +102,38 @@ func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
case <-c.quit:
|
case <-quit:
|
||||||
return errResolverShuttingDown
|
return errResolverShuttingDown
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// waitForSpend waits for the given outpoint to be spent, and returns the
|
||||||
|
// details of the spending tx.
|
||||||
|
func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32,
|
||||||
|
notifier chainntnfs.ChainNotifier, quit <-chan struct{}) (
|
||||||
|
*chainntnfs.SpendDetail, error) {
|
||||||
|
|
||||||
|
spendNtfn, err := notifier.RegisterSpendNtfn(
|
||||||
|
op, pkScript, heightHint,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case spendDetail, ok := <-spendNtfn.Spend:
|
||||||
|
if !ok {
|
||||||
|
return nil, errResolverShuttingDown
|
||||||
|
}
|
||||||
|
|
||||||
|
return spendDetail, nil
|
||||||
|
|
||||||
|
case <-quit:
|
||||||
|
return nil, errResolverShuttingDown
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// getCommitTxConfHeight waits for confirmation of the commitment tx and returns
|
// getCommitTxConfHeight waits for confirmation of the commitment tx and returns
|
||||||
// the confirmation height.
|
// the confirmation height.
|
||||||
func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
|
func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) {
|
||||||
@ -169,7 +198,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) {
|
|||||||
|
|
||||||
// We only need to wait for the block before the block that
|
// We only need to wait for the block before the block that
|
||||||
// unlocks the spend path.
|
// unlocks the spend path.
|
||||||
err := c.waitForHeight(unlockHeight - 1)
|
err := waitForHeight(unlockHeight-1, c.Notifier, c.quit)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
@ -103,17 +103,19 @@ func (i *commitSweepResolverTestContext) waitForResult() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockSweeper struct {
|
type mockSweeper struct {
|
||||||
sweptInputs chan input.Input
|
sweptInputs chan input.Input
|
||||||
updatedInputs chan wire.OutPoint
|
updatedInputs chan wire.OutPoint
|
||||||
sweepTx *wire.MsgTx
|
sweepTx *wire.MsgTx
|
||||||
sweepErr error
|
sweepErr error
|
||||||
|
createSweepTxChan chan *wire.MsgTx
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockSweeper() *mockSweeper {
|
func newMockSweeper() *mockSweeper {
|
||||||
return &mockSweeper{
|
return &mockSweeper{
|
||||||
sweptInputs: make(chan input.Input),
|
sweptInputs: make(chan input.Input),
|
||||||
updatedInputs: make(chan wire.OutPoint),
|
updatedInputs: make(chan wire.OutPoint),
|
||||||
sweepTx: &wire.MsgTx{},
|
sweepTx: &wire.MsgTx{},
|
||||||
|
createSweepTxChan: make(chan *wire.MsgTx),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -133,7 +135,9 @@ func (s *mockSweeper) SweepInput(input input.Input, params sweep.Params) (
|
|||||||
func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
|
func (s *mockSweeper) CreateSweepTx(inputs []input.Input, feePref sweep.FeePreference,
|
||||||
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
currentBlockHeight uint32) (*wire.MsgTx, error) {
|
||||||
|
|
||||||
return nil, nil
|
// We will wait for the test to supply the sweep tx to return.
|
||||||
|
sweepTx := <-s.createSweepTxChan
|
||||||
|
return sweepTx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *mockSweeper) RelayFeePerKW() chainfee.SatPerKWeight {
|
func (s *mockSweeper) RelayFeePerKW() chainfee.SatPerKWeight {
|
||||||
|
@ -20,6 +20,10 @@ const (
|
|||||||
// sweepConfTarget is the default number of blocks that we'll use as a
|
// sweepConfTarget is the default number of blocks that we'll use as a
|
||||||
// confirmation target when sweeping.
|
// confirmation target when sweeping.
|
||||||
sweepConfTarget = 6
|
sweepConfTarget = 6
|
||||||
|
|
||||||
|
// secondLevelConfTarget is the confirmation target we'll use when
|
||||||
|
// adding fees to our second-level HTLC transactions.
|
||||||
|
secondLevelConfTarget = 6
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContractResolver is an interface which packages a state machine which is
|
// ContractResolver is an interface which packages a state machine which is
|
||||||
|
@ -31,7 +31,7 @@ type htlcIncomingContestResolver struct {
|
|||||||
|
|
||||||
// htlcSuccessResolver is the inner resolver that may be utilized if we
|
// htlcSuccessResolver is the inner resolver that may be utilized if we
|
||||||
// learn of the preimage.
|
// learn of the preimage.
|
||||||
htlcSuccessResolver
|
*htlcSuccessResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// newIncomingContestResolver instantiates a new incoming htlc contest resolver.
|
// newIncomingContestResolver instantiates a new incoming htlc contest resolver.
|
||||||
@ -45,7 +45,7 @@ func newIncomingContestResolver(
|
|||||||
|
|
||||||
return &htlcIncomingContestResolver{
|
return &htlcIncomingContestResolver{
|
||||||
htlcExpiry: htlc.RefundTimeout,
|
htlcExpiry: htlc.RefundTimeout,
|
||||||
htlcSuccessResolver: *success,
|
htlcSuccessResolver: success,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -189,7 +189,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &h.htlcSuccessResolver, nil
|
return h.htlcSuccessResolver, nil
|
||||||
|
|
||||||
// If the htlc was failed, mark the htlc as
|
// If the htlc was failed, mark the htlc as
|
||||||
// resolved.
|
// resolved.
|
||||||
@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
return &h.htlcSuccessResolver, nil
|
return h.htlcSuccessResolver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
witnessUpdates = preimageSubscription.WitnessUpdates
|
witnessUpdates = preimageSubscription.WitnessUpdates
|
||||||
@ -315,7 +315,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
// We've learned of the preimage and this information
|
// We've learned of the preimage and this information
|
||||||
// has been added to our inner resolver. We return it so
|
// has been added to our inner resolver. We return it so
|
||||||
// it can continue contract resolution.
|
// it can continue contract resolution.
|
||||||
return &h.htlcSuccessResolver, nil
|
return h.htlcSuccessResolver, nil
|
||||||
|
|
||||||
case hodlItem := <-hodlChan:
|
case hodlItem := <-hodlChan:
|
||||||
htlcResolution := hodlItem.(invoices.HtlcResolution)
|
htlcResolution := hodlItem.(invoices.HtlcResolution)
|
||||||
@ -420,7 +420,7 @@ func newIncomingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
h.htlcSuccessResolver = *successResolver
|
h.htlcSuccessResolver = successResolver
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
@ -342,7 +342,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
resolver := &htlcIncomingContestResolver{
|
resolver := &htlcIncomingContestResolver{
|
||||||
htlcSuccessResolver: htlcSuccessResolver{
|
htlcSuccessResolver: &htlcSuccessResolver{
|
||||||
contractResolverKit: *newContractResolverKit(cfg),
|
contractResolverKit: *newContractResolverKit(cfg),
|
||||||
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
||||||
htlc: channeldb.HTLC{
|
htlc: channeldb.HTLC{
|
||||||
|
@ -17,7 +17,7 @@ import (
|
|||||||
type htlcOutgoingContestResolver struct {
|
type htlcOutgoingContestResolver struct {
|
||||||
// htlcTimeoutResolver is the inner solver that this resolver may turn
|
// htlcTimeoutResolver is the inner solver that this resolver may turn
|
||||||
// into. This only happens if the HTLC expires on-chain.
|
// into. This only happens if the HTLC expires on-chain.
|
||||||
htlcTimeoutResolver
|
*htlcTimeoutResolver
|
||||||
}
|
}
|
||||||
|
|
||||||
// newOutgoingContestResolver instantiates a new outgoing contested htlc
|
// newOutgoingContestResolver instantiates a new outgoing contested htlc
|
||||||
@ -31,7 +31,7 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
|
|||||||
)
|
)
|
||||||
|
|
||||||
return &htlcOutgoingContestResolver{
|
return &htlcOutgoingContestResolver{
|
||||||
htlcTimeoutResolver: *timeout,
|
htlcTimeoutResolver: timeout,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -131,7 +131,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
|||||||
"into timeout resolver", h,
|
"into timeout resolver", h,
|
||||||
h.htlcResolution.ClaimOutpoint,
|
h.htlcResolution.ClaimOutpoint,
|
||||||
newHeight, h.htlcResolution.Expiry)
|
newHeight, h.htlcResolution.Expiry)
|
||||||
return &h.htlcTimeoutResolver, nil
|
return h.htlcTimeoutResolver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// The output has been spent! This means the preimage has been
|
// The output has been spent! This means the preimage has been
|
||||||
@ -209,7 +209,7 @@ func newOutgoingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
h.htlcTimeoutResolver = *timeoutResolver
|
h.htlcTimeoutResolver = timeoutResolver
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -177,7 +177,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resolver := &htlcOutgoingContestResolver{
|
resolver := &htlcOutgoingContestResolver{
|
||||||
htlcTimeoutResolver: htlcTimeoutResolver{
|
htlcTimeoutResolver: &htlcTimeoutResolver{
|
||||||
contractResolverKit: *newContractResolverKit(cfg),
|
contractResolverKit: *newContractResolverKit(cfg),
|
||||||
htlcResolution: outgoingRes,
|
htlcResolution: outgoingRes,
|
||||||
htlc: channeldb.HTLC{
|
htlc: channeldb.HTLC{
|
||||||
|
@ -3,11 +3,13 @@ package contractcourt
|
|||||||
import (
|
import (
|
||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/labels"
|
"github.com/lightningnetwork/lnd/labels"
|
||||||
@ -29,7 +31,12 @@ type htlcSuccessResolver struct {
|
|||||||
htlcResolution lnwallet.IncomingHtlcResolution
|
htlcResolution lnwallet.IncomingHtlcResolution
|
||||||
|
|
||||||
// outputIncubating returns true if we've sent the output to the output
|
// outputIncubating returns true if we've sent the output to the output
|
||||||
// incubator (utxo nursery).
|
// incubator (utxo nursery). In case the htlcResolution has non-nil
|
||||||
|
// SignDetails, it means we will let the Sweeper handle broadcasting
|
||||||
|
// the secondd-level transaction, and sweeping its output. In this case
|
||||||
|
// we let this field indicate whether we need to broadcast the
|
||||||
|
// second-level tx (false) or if it has confirmed and we must sweep the
|
||||||
|
// second-level output (true).
|
||||||
outputIncubating bool
|
outputIncubating bool
|
||||||
|
|
||||||
// resolved reflects if the contract has been fully resolved or not.
|
// resolved reflects if the contract has been fully resolved or not.
|
||||||
@ -50,6 +57,15 @@ type htlcSuccessResolver struct {
|
|||||||
// htlc contains information on the htlc that we are resolving on-chain.
|
// htlc contains information on the htlc that we are resolving on-chain.
|
||||||
htlc channeldb.HTLC
|
htlc channeldb.HTLC
|
||||||
|
|
||||||
|
// currentReport stores the current state of the resolver for reporting
|
||||||
|
// over the rpc interface. This should only be reported in case we have
|
||||||
|
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
|
||||||
|
// will produce reports.
|
||||||
|
currentReport ContractReport
|
||||||
|
|
||||||
|
// reportLock prevents concurrent access to the resolver report.
|
||||||
|
reportLock sync.Mutex
|
||||||
|
|
||||||
contractResolverKit
|
contractResolverKit
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -58,12 +74,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution,
|
|||||||
broadcastHeight uint32, htlc channeldb.HTLC,
|
broadcastHeight uint32, htlc channeldb.HTLC,
|
||||||
resCfg ResolverConfig) *htlcSuccessResolver {
|
resCfg ResolverConfig) *htlcSuccessResolver {
|
||||||
|
|
||||||
return &htlcSuccessResolver{
|
h := &htlcSuccessResolver{
|
||||||
contractResolverKit: *newContractResolverKit(resCfg),
|
contractResolverKit: *newContractResolverKit(resCfg),
|
||||||
htlcResolution: res,
|
htlcResolution: res,
|
||||||
broadcastHeight: broadcastHeight,
|
broadcastHeight: broadcastHeight,
|
||||||
htlc: htlc,
|
htlc: htlc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.initReport()
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolverKey returns an identifier which should be globally unique for this
|
// ResolverKey returns an identifier which should be globally unique for this
|
||||||
@ -105,104 +125,58 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
|||||||
// If we don't have a success transaction, then this means that this is
|
// If we don't have a success transaction, then this means that this is
|
||||||
// an output on the remote party's commitment transaction.
|
// an output on the remote party's commitment transaction.
|
||||||
if h.htlcResolution.SignedSuccessTx == nil {
|
if h.htlcResolution.SignedSuccessTx == nil {
|
||||||
// If we don't already have the sweep transaction constructed,
|
return h.resolveRemoteCommitOutput()
|
||||||
// we'll do so and broadcast it.
|
|
||||||
if h.sweepTx == nil {
|
|
||||||
log.Infof("%T(%x): crafting sweep tx for "+
|
|
||||||
"incoming+remote htlc confirmed", h,
|
|
||||||
h.htlc.RHash[:])
|
|
||||||
|
|
||||||
// Before we can craft out sweeping transaction, we
|
|
||||||
// need to create an input which contains all the items
|
|
||||||
// required to add this input to a sweeping transaction,
|
|
||||||
// and generate a witness.
|
|
||||||
inp := input.MakeHtlcSucceedInput(
|
|
||||||
&h.htlcResolution.ClaimOutpoint,
|
|
||||||
&h.htlcResolution.SweepSignDesc,
|
|
||||||
h.htlcResolution.Preimage[:],
|
|
||||||
h.broadcastHeight,
|
|
||||||
h.htlcResolution.CsvDelay,
|
|
||||||
)
|
|
||||||
|
|
||||||
// With the input created, we can now generate the full
|
|
||||||
// sweep transaction, that we'll use to move these
|
|
||||||
// coins back into the backing wallet.
|
|
||||||
//
|
|
||||||
// TODO: Set tx lock time to current block height
|
|
||||||
// instead of zero. Will be taken care of once sweeper
|
|
||||||
// implementation is complete.
|
|
||||||
//
|
|
||||||
// TODO: Use time-based sweeper and result chan.
|
|
||||||
var err error
|
|
||||||
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
|
||||||
[]input.Input{&inp},
|
|
||||||
sweep.FeePreference{
|
|
||||||
ConfTarget: sweepConfTarget,
|
|
||||||
}, 0,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%T(%x): crafted sweep tx=%v", h,
|
|
||||||
h.htlc.RHash[:], spew.Sdump(h.sweepTx))
|
|
||||||
|
|
||||||
// With the sweep transaction signed, we'll now
|
|
||||||
// Checkpoint our state.
|
|
||||||
if err := h.Checkpoint(h); err != nil {
|
|
||||||
log.Errorf("unable to Checkpoint: %v", err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Regardless of whether an existing transaction was found or newly
|
|
||||||
// constructed, we'll broadcast the sweep transaction to the
|
|
||||||
// network.
|
|
||||||
label := labels.MakeLabel(
|
|
||||||
labels.LabelTypeChannelClose, &h.ShortChanID,
|
|
||||||
)
|
|
||||||
err := h.PublishTx(h.sweepTx, label)
|
|
||||||
if err != nil {
|
|
||||||
log.Infof("%T(%x): unable to publish tx: %v",
|
|
||||||
h, h.htlc.RHash[:], err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// With the sweep transaction broadcast, we'll wait for its
|
|
||||||
// confirmation.
|
|
||||||
sweepTXID := h.sweepTx.TxHash()
|
|
||||||
sweepScript := h.sweepTx.TxOut[0].PkScript
|
|
||||||
confNtfn, err := h.Notifier.RegisterConfirmationsNtfn(
|
|
||||||
&sweepTXID, sweepScript, 1, h.broadcastHeight,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+
|
|
||||||
"confirmed", h, h.htlc.RHash[:], sweepTXID)
|
|
||||||
|
|
||||||
select {
|
|
||||||
case _, ok := <-confNtfn.Confirmed:
|
|
||||||
if !ok {
|
|
||||||
return nil, errResolverShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-h.quit:
|
|
||||||
return nil, errResolverShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
// Once the transaction has received a sufficient number of
|
|
||||||
// confirmations, we'll mark ourselves as fully resolved and exit.
|
|
||||||
h.resolved = true
|
|
||||||
|
|
||||||
// Checkpoint the resolver, and write the outcome to disk.
|
|
||||||
return nil, h.checkpointClaim(
|
|
||||||
&sweepTXID,
|
|
||||||
channeldb.ResolverOutcomeClaimed,
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Otherwise this an output on our own commitment, and we must start by
|
||||||
|
// broadcasting the second-level success transaction.
|
||||||
|
secondLevelOutpoint, err := h.broadcastSuccessTx()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// To wrap this up, we'll wait until the second-level transaction has
|
||||||
|
// been spent, then fully resolve the contract.
|
||||||
|
log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+
|
||||||
|
"after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay)
|
||||||
|
|
||||||
|
spend, err := waitForSpend(
|
||||||
|
secondLevelOutpoint,
|
||||||
|
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||||
|
h.broadcastHeight, h.Notifier, h.quit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.reportLock.Lock()
|
||||||
|
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
|
||||||
|
h.currentReport.LimboBalance = 0
|
||||||
|
h.reportLock.Unlock()
|
||||||
|
|
||||||
|
h.resolved = true
|
||||||
|
return nil, h.checkpointClaim(
|
||||||
|
spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// broadcastSuccessTx handles an HTLC output on our local commitment by
|
||||||
|
// broadcasting the second-level success transaction. It returns the ultimate
|
||||||
|
// outpoint of the second-level tx, that we must wait to be spent for the
|
||||||
|
// resolver to be fully resolved.
|
||||||
|
func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) {
|
||||||
|
// If we have non-nil SignDetails, this means that have a 2nd level
|
||||||
|
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
|
||||||
|
// (the case for anchor type channels). In this case we can re-sign it
|
||||||
|
// and attach fees at will. We let the sweeper handle this job.
|
||||||
|
// We use the checkpointed outputIncubating field to determine if we
|
||||||
|
// already swept the HTLC output into the second level transaction.
|
||||||
|
if h.htlcResolution.SignDetails != nil {
|
||||||
|
return h.broadcastReSignedSuccessTx()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise we'll publish the second-level transaction directly and
|
||||||
|
// offer the resolution to the nursery to handle.
|
||||||
log.Infof("%T(%x): broadcasting second-layer transition tx: %v",
|
log.Infof("%T(%x): broadcasting second-layer transition tx: %v",
|
||||||
h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx))
|
h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx))
|
||||||
|
|
||||||
@ -241,35 +215,236 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// To wrap this up, we'll wait until the second-level transaction has
|
return &h.htlcResolution.ClaimOutpoint, nil
|
||||||
// been spent, then fully resolve the contract.
|
}
|
||||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
|
||||||
&h.htlcResolution.ClaimOutpoint,
|
// broadcastReSignedSuccessTx handles the case where we have non-nil
|
||||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
// SignDetails, and offers the second level transaction to the Sweeper, that
|
||||||
h.broadcastHeight,
|
// will re-sign it and attach fees at will.
|
||||||
|
func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() (
|
||||||
|
*wire.OutPoint, error) {
|
||||||
|
|
||||||
|
// Keep track of the tx spending the HTLC output on the commitment, as
|
||||||
|
// this will be the confirmed second-level tx we'll ultimately sweep.
|
||||||
|
var commitSpend *chainntnfs.SpendDetail
|
||||||
|
|
||||||
|
// We will have to let the sweeper re-sign the success tx and wait for
|
||||||
|
// it to confirm, if we haven't already.
|
||||||
|
if !h.outputIncubating {
|
||||||
|
log.Infof("%T(%x): offering second-layer transition tx to "+
|
||||||
|
"sweeper: %v", h, h.htlc.RHash[:],
|
||||||
|
spew.Sdump(h.htlcResolution.SignedSuccessTx))
|
||||||
|
|
||||||
|
secondLevelInput := input.MakeHtlcSecondLevelSuccessAnchorInput(
|
||||||
|
h.htlcResolution.SignedSuccessTx,
|
||||||
|
h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
|
||||||
|
h.broadcastHeight,
|
||||||
|
)
|
||||||
|
_, err := h.Sweeper.SweepInput(
|
||||||
|
&secondLevelInput,
|
||||||
|
sweep.Params{
|
||||||
|
Fee: sweep.FeePreference{
|
||||||
|
ConfTarget: secondLevelConfTarget,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%T(%x): waiting for second-level HTLC success "+
|
||||||
|
"transaction to confirm", h, h.htlc.RHash[:])
|
||||||
|
|
||||||
|
// Wait for the second level transaction to confirm.
|
||||||
|
commitSpend, err = waitForSpend(
|
||||||
|
&h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint,
|
||||||
|
h.htlcResolution.SignDetails.SignDesc.Output.PkScript,
|
||||||
|
h.broadcastHeight, h.Notifier, h.quit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the second-level transaction has confirmed, we
|
||||||
|
// checkpoint the state so we'll go to the next stage in case
|
||||||
|
// of restarts.
|
||||||
|
h.outputIncubating = true
|
||||||
|
if err := h.Checkpoint(h); err != nil {
|
||||||
|
log.Errorf("unable to Checkpoint: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%T(%x): second-level HTLC success transaction "+
|
||||||
|
"confirmed!", h, h.htlc.RHash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we ended up here after a restart, we must again get the
|
||||||
|
// spend notification.
|
||||||
|
if commitSpend == nil {
|
||||||
|
var err error
|
||||||
|
commitSpend, err = waitForSpend(
|
||||||
|
&h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint,
|
||||||
|
h.htlcResolution.SignDetails.SignDesc.Output.PkScript,
|
||||||
|
h.broadcastHeight, h.Notifier, h.quit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The HTLC success tx has a CSV lock that we must wait for.
|
||||||
|
waitHeight := uint32(commitSpend.SpendingHeight) +
|
||||||
|
h.htlcResolution.CsvDelay - 1
|
||||||
|
|
||||||
|
// Now that the sweeper has broadcasted the second-level transaction,
|
||||||
|
// it has confirmed, and we have checkpointed our state, we'll sweep
|
||||||
|
// the second level output. We report the resolver has moved the next
|
||||||
|
// stage.
|
||||||
|
h.reportLock.Lock()
|
||||||
|
h.currentReport.Stage = 2
|
||||||
|
h.currentReport.MaturityHeight = waitHeight
|
||||||
|
h.reportLock.Unlock()
|
||||||
|
|
||||||
|
log.Infof("%T(%x): waiting for CSV lock to expire at height %v",
|
||||||
|
h, h.htlc.RHash[:], waitHeight)
|
||||||
|
|
||||||
|
err := waitForHeight(waitHeight, h.Notifier, h.quit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll use this input index to determine the second-level output
|
||||||
|
// index on the transaction, as the signatures requires the indexes to
|
||||||
|
// be the same. We don't look for the second-level output script
|
||||||
|
// directly, as there might be more than one HTLC output to the same
|
||||||
|
// pkScript.
|
||||||
|
op := &wire.OutPoint{
|
||||||
|
Hash: *commitSpend.SpenderTxHash,
|
||||||
|
Index: commitSpend.SpenderInputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, let the sweeper sweep the second-level output.
|
||||||
|
log.Infof("%T(%x): CSV lock expired, offering second-layer "+
|
||||||
|
"output to sweeper: %v", h, h.htlc.RHash[:], op)
|
||||||
|
|
||||||
|
inp := input.NewCsvInput(
|
||||||
|
op, input.HtlcAcceptedSuccessSecondLevel,
|
||||||
|
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
|
||||||
|
h.htlcResolution.CsvDelay,
|
||||||
|
)
|
||||||
|
_, err = h.Sweeper.SweepInput(
|
||||||
|
inp,
|
||||||
|
sweep.Params{
|
||||||
|
Fee: sweep.FeePreference{
|
||||||
|
ConfTarget: sweepConfTarget,
|
||||||
|
},
|
||||||
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+
|
// Will return this outpoint, when this is spent the resolver is fully
|
||||||
"after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay)
|
// resolved.
|
||||||
|
return op, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote
|
||||||
|
// commitment with the preimage. In this case we can sweep the output directly,
|
||||||
|
// and don't have to broadcast a second-level transaction.
|
||||||
|
func (h *htlcSuccessResolver) resolveRemoteCommitOutput() (
|
||||||
|
ContractResolver, error) {
|
||||||
|
|
||||||
|
// If we don't already have the sweep transaction constructed, we'll do
|
||||||
|
// so and broadcast it.
|
||||||
|
if h.sweepTx == nil {
|
||||||
|
log.Infof("%T(%x): crafting sweep tx for incoming+remote "+
|
||||||
|
"htlc confirmed", h, h.htlc.RHash[:])
|
||||||
|
|
||||||
|
// Before we can craft out sweeping transaction, we need to
|
||||||
|
// create an input which contains all the items required to add
|
||||||
|
// this input to a sweeping transaction, and generate a
|
||||||
|
// witness.
|
||||||
|
inp := input.MakeHtlcSucceedInput(
|
||||||
|
&h.htlcResolution.ClaimOutpoint,
|
||||||
|
&h.htlcResolution.SweepSignDesc,
|
||||||
|
h.htlcResolution.Preimage[:],
|
||||||
|
h.broadcastHeight,
|
||||||
|
h.htlcResolution.CsvDelay,
|
||||||
|
)
|
||||||
|
|
||||||
|
// With the input created, we can now generate the full sweep
|
||||||
|
// transaction, that we'll use to move these coins back into
|
||||||
|
// the backing wallet.
|
||||||
|
//
|
||||||
|
// TODO: Set tx lock time to current block height instead of
|
||||||
|
// zero. Will be taken care of once sweeper implementation is
|
||||||
|
// complete.
|
||||||
|
//
|
||||||
|
// TODO: Use time-based sweeper and result chan.
|
||||||
|
var err error
|
||||||
|
h.sweepTx, err = h.Sweeper.CreateSweepTx(
|
||||||
|
[]input.Input{&inp},
|
||||||
|
sweep.FeePreference{
|
||||||
|
ConfTarget: sweepConfTarget,
|
||||||
|
}, 0,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%T(%x): crafted sweep tx=%v", h,
|
||||||
|
h.htlc.RHash[:], spew.Sdump(h.sweepTx))
|
||||||
|
|
||||||
|
// TODO(halseth): should checkpoint sweep tx to DB? Since after
|
||||||
|
// a restart we might create a different tx, that will conflict
|
||||||
|
// with the published one.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Regardless of whether an existing transaction was found or newly
|
||||||
|
// constructed, we'll broadcast the sweep transaction to the network.
|
||||||
|
label := labels.MakeLabel(
|
||||||
|
labels.LabelTypeChannelClose, &h.ShortChanID,
|
||||||
|
)
|
||||||
|
err := h.PublishTx(h.sweepTx, label)
|
||||||
|
if err != nil {
|
||||||
|
log.Infof("%T(%x): unable to publish tx: %v",
|
||||||
|
h, h.htlc.RHash[:], err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the sweep transaction broadcast, we'll wait for its
|
||||||
|
// confirmation.
|
||||||
|
sweepTXID := h.sweepTx.TxHash()
|
||||||
|
sweepScript := h.sweepTx.TxOut[0].PkScript
|
||||||
|
confNtfn, err := h.Notifier.RegisterConfirmationsNtfn(
|
||||||
|
&sweepTXID, sweepScript, 1, h.broadcastHeight,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+
|
||||||
|
"confirmed", h, h.htlc.RHash[:], sweepTXID)
|
||||||
|
|
||||||
var spendTxid *chainhash.Hash
|
|
||||||
select {
|
select {
|
||||||
case spend, ok := <-spendNtfn.Spend:
|
case _, ok := <-confNtfn.Confirmed:
|
||||||
if !ok {
|
if !ok {
|
||||||
return nil, errResolverShuttingDown
|
return nil, errResolverShuttingDown
|
||||||
}
|
}
|
||||||
spendTxid = spend.SpenderTxHash
|
|
||||||
|
|
||||||
case <-h.quit:
|
case <-h.quit:
|
||||||
return nil, errResolverShuttingDown
|
return nil, errResolverShuttingDown
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Once the transaction has received a sufficient number of
|
||||||
|
// confirmations, we'll mark ourselves as fully resolved and exit.
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
|
|
||||||
|
// Checkpoint the resolver, and write the outcome to disk.
|
||||||
return nil, h.checkpointClaim(
|
return nil, h.checkpointClaim(
|
||||||
spendTxid, channeldb.ResolverOutcomeClaimed,
|
&sweepTXID,
|
||||||
|
channeldb.ResolverOutcomeClaimed,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -330,6 +505,40 @@ func (h *htlcSuccessResolver) IsResolved() bool {
|
|||||||
return h.resolved
|
return h.resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report returns a report on the resolution state of the contract.
|
||||||
|
func (h *htlcSuccessResolver) report() *ContractReport {
|
||||||
|
// If the sign details are nil, the report will be created by handled
|
||||||
|
// by the nursery.
|
||||||
|
if h.htlcResolution.SignDetails == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.reportLock.Lock()
|
||||||
|
defer h.reportLock.Unlock()
|
||||||
|
copy := h.currentReport
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *htlcSuccessResolver) initReport() {
|
||||||
|
// We create the initial report. This will only be reported for
|
||||||
|
// resolvers not handled by the nursery.
|
||||||
|
finalAmt := h.htlc.Amt.ToSatoshis()
|
||||||
|
if h.htlcResolution.SignedSuccessTx != nil {
|
||||||
|
finalAmt = btcutil.Amount(
|
||||||
|
h.htlcResolution.SignedSuccessTx.TxOut[0].Value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.currentReport = ContractReport{
|
||||||
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
|
Type: ReportOutputIncomingHtlc,
|
||||||
|
Amount: finalAmt,
|
||||||
|
MaturityHeight: h.htlcResolution.CsvDelay,
|
||||||
|
LimboBalance: finalAmt,
|
||||||
|
Stage: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Encode writes an encoded version of the ContractResolver into the passed
|
// Encode writes an encoded version of the ContractResolver into the passed
|
||||||
// Writer.
|
// Writer.
|
||||||
//
|
//
|
||||||
@ -355,6 +564,12 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We encode the sign details last for backwards compatibility.
|
||||||
|
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -388,6 +603,18 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign details is a new field that was added to the htlc resolution,
|
||||||
|
// so it is serialized last for backwards compatibility. We try to read
|
||||||
|
// it, but don't error out if there are not bytes left.
|
||||||
|
signDetails, err := decodeSignDetails(r)
|
||||||
|
if err == nil {
|
||||||
|
h.htlcResolution.SignDetails = signDetails
|
||||||
|
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.initReport()
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,13 +1,19 @@
|
|||||||
package contractcourt
|
package contractcourt
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"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/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||||
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -15,33 +21,59 @@ import (
|
|||||||
|
|
||||||
var testHtlcAmt = lnwire.MilliSatoshi(200000)
|
var testHtlcAmt = lnwire.MilliSatoshi(200000)
|
||||||
|
|
||||||
type htlcSuccessResolverTestContext struct {
|
type htlcResolverTestContext struct {
|
||||||
resolver *htlcSuccessResolver
|
resolver ContractResolver
|
||||||
|
|
||||||
|
checkpoint func(_ ContractResolver,
|
||||||
|
_ ...*channeldb.ResolverReport) error
|
||||||
|
|
||||||
notifier *mock.ChainNotifier
|
notifier *mock.ChainNotifier
|
||||||
resolverResultChan chan resolveResult
|
resolverResultChan chan resolveResult
|
||||||
t *testing.T
|
resolutionChan chan ResolutionMsg
|
||||||
|
|
||||||
|
t *testing.T
|
||||||
}
|
}
|
||||||
|
|
||||||
func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestContext {
|
func newHtlcResolverTestContext(t *testing.T,
|
||||||
|
newResolver func(htlc channeldb.HTLC,
|
||||||
|
cfg ResolverConfig) ContractResolver) *htlcResolverTestContext {
|
||||||
|
|
||||||
notifier := &mock.ChainNotifier{
|
notifier := &mock.ChainNotifier{
|
||||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
|
||||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||||
}
|
}
|
||||||
|
|
||||||
checkPointChan := make(chan struct{}, 1)
|
testCtx := &htlcResolverTestContext{
|
||||||
|
checkpoint: nil,
|
||||||
testCtx := &htlcSuccessResolverTestContext{
|
notifier: notifier,
|
||||||
notifier: notifier,
|
resolutionChan: make(chan ResolutionMsg, 1),
|
||||||
t: t,
|
t: t,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
witnessBeacon := newMockWitnessBeacon()
|
||||||
chainCfg := ChannelArbitratorConfig{
|
chainCfg := ChannelArbitratorConfig{
|
||||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||||
Notifier: notifier,
|
Notifier: notifier,
|
||||||
|
PreimageDB: witnessBeacon,
|
||||||
PublishTx: func(_ *wire.MsgTx, _ string) error {
|
PublishTx: func(_ *wire.MsgTx, _ string) error {
|
||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
|
Sweeper: newMockSweeper(),
|
||||||
|
IncubateOutputs: func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution,
|
||||||
|
*lnwallet.IncomingHtlcResolution, uint32) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
||||||
|
if len(msgs) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 "+
|
||||||
|
"resolution msg, instead got %v",
|
||||||
|
len(msgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
testCtx.resolutionChan <- msgs[0]
|
||||||
|
return nil
|
||||||
|
},
|
||||||
},
|
},
|
||||||
PutResolverReport: func(_ kvdb.RwTx,
|
PutResolverReport: func(_ kvdb.RwTx,
|
||||||
report *channeldb.ResolverReport) error {
|
report *channeldb.ResolverReport) error {
|
||||||
@ -49,31 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon
|
|||||||
return nil
|
return nil
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
// Since we want to replace this checkpoint method later in the test,
|
||||||
|
// we wrap the call to it in a closure. The linter will complain about
|
||||||
|
// this so set nolint directive.
|
||||||
|
checkpointFunc := func(c ContractResolver, // nolint
|
||||||
|
r ...*channeldb.ResolverReport) error {
|
||||||
|
return testCtx.checkpoint(c, r...)
|
||||||
|
}
|
||||||
|
|
||||||
cfg := ResolverConfig{
|
cfg := ResolverConfig{
|
||||||
ChannelArbitratorConfig: chainCfg,
|
ChannelArbitratorConfig: chainCfg,
|
||||||
Checkpoint: func(_ ContractResolver,
|
Checkpoint: checkpointFunc,
|
||||||
_ ...*channeldb.ResolverReport) error {
|
|
||||||
|
|
||||||
checkPointChan <- struct{}{}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
testCtx.resolver = &htlcSuccessResolver{
|
htlc := channeldb.HTLC{
|
||||||
contractResolverKit: *newContractResolverKit(cfg),
|
RHash: testResHash,
|
||||||
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
OnionBlob: testOnionBlob,
|
||||||
htlc: channeldb.HTLC{
|
Amt: testHtlcAmt,
|
||||||
RHash: testResHash,
|
|
||||||
OnionBlob: testOnionBlob,
|
|
||||||
Amt: testHtlcAmt,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
testCtx.resolver = newResolver(htlc, cfg)
|
||||||
|
|
||||||
return testCtx
|
return testCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *htlcSuccessResolverTestContext) resolve() {
|
func (i *htlcResolverTestContext) resolve() {
|
||||||
// Start resolver.
|
// Start resolver.
|
||||||
i.resolverResultChan = make(chan resolveResult, 1)
|
i.resolverResultChan = make(chan resolveResult, 1)
|
||||||
go func() {
|
go func() {
|
||||||
@ -85,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() {
|
|||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *htlcSuccessResolverTestContext) waitForResult() {
|
func (i *htlcResolverTestContext) waitForResult() {
|
||||||
i.t.Helper()
|
i.t.Helper()
|
||||||
|
|
||||||
result := <-i.resolverResultChan
|
result := <-i.resolverResultChan
|
||||||
@ -98,8 +130,9 @@ func (i *htlcSuccessResolverTestContext) waitForResult() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSingleStageSuccess tests successful sweep of a single stage htlc claim.
|
// TestHtlcSuccessSingleStage tests successful sweep of a single stage htlc
|
||||||
func TestSingleStageSuccess(t *testing.T) {
|
// claim.
|
||||||
|
func TestHtlcSuccessSingleStage(t *testing.T) {
|
||||||
htlcOutpoint := wire.OutPoint{Index: 3}
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
sweepTx := &wire.MsgTx{
|
sweepTx := &wire.MsgTx{
|
||||||
@ -114,15 +147,6 @@ func TestSingleStageSuccess(t *testing.T) {
|
|||||||
ClaimOutpoint: htlcOutpoint,
|
ClaimOutpoint: htlcOutpoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We send a confirmation for our sweep tx to indicate that our sweep
|
|
||||||
// succeeded.
|
|
||||||
resolve := func(ctx *htlcSuccessResolverTestContext) {
|
|
||||||
ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
|
|
||||||
Tx: ctx.resolver.sweepTx,
|
|
||||||
BlockHeight: testInitialBlockHeight - 1,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sweepTxid := sweepTx.TxHash()
|
sweepTxid := sweepTx.TxHash()
|
||||||
claim := &channeldb.ResolverReport{
|
claim := &channeldb.ResolverReport{
|
||||||
OutPoint: htlcOutpoint,
|
OutPoint: htlcOutpoint,
|
||||||
@ -131,14 +155,46 @@ func TestSingleStageSuccess(t *testing.T) {
|
|||||||
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||||
SpendTxID: &sweepTxid,
|
SpendTxID: &sweepTxid,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// We send a confirmation for our sweep tx to indicate
|
||||||
|
// that our sweep succeeded.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
// The resolver will create and publish a sweep
|
||||||
|
// tx.
|
||||||
|
resolver := ctx.resolver.(*htlcSuccessResolver)
|
||||||
|
resolver.Sweeper.(*mockSweeper).
|
||||||
|
createSweepTxChan <- sweepTx
|
||||||
|
|
||||||
|
// Confirm the sweep, which should resolve it.
|
||||||
|
ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{
|
||||||
|
Tx: sweepTx,
|
||||||
|
BlockHeight: testInitialBlockHeight - 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// report.
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
claim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
testHtlcSuccess(
|
testHtlcSuccess(
|
||||||
t, singleStageResolution, resolve, sweepTx, claim,
|
t, singleStageResolution, checkpoints,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestSecondStageResolution tests successful sweep of a second stage htlc
|
// TestSecondStageResolution tests successful sweep of a second stage htlc
|
||||||
// claim.
|
// claim, going through the Nursery.
|
||||||
func TestSecondStageResolution(t *testing.T) {
|
func TestHtlcSuccessSecondStageResolution(t *testing.T) {
|
||||||
commitOutpoint := wire.OutPoint{Index: 2}
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
htlcOutpoint := wire.OutPoint{Index: 3}
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
@ -158,20 +214,17 @@ func TestSecondStageResolution(t *testing.T) {
|
|||||||
PreviousOutPoint: commitOutpoint,
|
PreviousOutPoint: commitOutpoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
TxOut: []*wire.TxOut{},
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 111,
|
||||||
|
PkScript: []byte{0xaa, 0xaa},
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
ClaimOutpoint: htlcOutpoint,
|
ClaimOutpoint: htlcOutpoint,
|
||||||
SweepSignDesc: testSignDesc,
|
SweepSignDesc: testSignDesc,
|
||||||
}
|
}
|
||||||
|
|
||||||
// We send a spend notification for our output to resolve our htlc.
|
|
||||||
resolve := func(ctx *htlcSuccessResolverTestContext) {
|
|
||||||
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
|
||||||
SpendingTx: sweepTx,
|
|
||||||
SpenderTxHash: &sweepHash,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
successTx := twoStageResolution.SignedSuccessTx.TxHash()
|
successTx := twoStageResolution.SignedSuccessTx.TxHash()
|
||||||
firstStage := &channeldb.ResolverReport{
|
firstStage := &channeldb.ResolverReport{
|
||||||
OutPoint: commitOutpoint,
|
OutPoint: commitOutpoint,
|
||||||
@ -189,54 +242,373 @@ func TestSecondStageResolution(t *testing.T) {
|
|||||||
SpendTxID: &sweepHash,
|
SpendTxID: &sweepHash,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// The resolver will send the output to the Nursery.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// It will then wait for the Nursery to spend the
|
||||||
|
// output. We send a spend notification for our output
|
||||||
|
// to resolve our htlc.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: sweepTx,
|
||||||
|
SpenderTxHash: &sweepHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
secondStage,
|
||||||
|
firstStage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
testHtlcSuccess(
|
testHtlcSuccess(
|
||||||
t, twoStageResolution, resolve, sweepTx, secondStage, firstStage,
|
t, twoStageResolution, checkpoints,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// testHtlcSuccess tests resolution of a success resolver. It takes a resolve
|
// TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with
|
||||||
// function which triggers resolution and the sweeptxid that will resolve it.
|
// non-nil SignDetails will offer the second-level transaction to the sweeper
|
||||||
|
// for re-signing.
|
||||||
|
func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
successTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 123,
|
||||||
|
PkScript: []byte{0xff, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
reSignedSuccessTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{0xaa, 0xbb},
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
successTx.TxIn[0],
|
||||||
|
{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{0xaa, 0xbb},
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 111,
|
||||||
|
PkScript: []byte{0xaa, 0xaa},
|
||||||
|
},
|
||||||
|
successTx.TxOut[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reSignedHash := successTx.TxHash()
|
||||||
|
|
||||||
|
sweepTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
|
||||||
|
{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: reSignedHash,
|
||||||
|
Index: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
sweepHash := sweepTx.TxHash()
|
||||||
|
|
||||||
|
// twoStageResolution is a resolution for htlc on our own commitment
|
||||||
|
// which is spent from the signed success tx.
|
||||||
|
twoStageResolution := lnwallet.IncomingHtlcResolution{
|
||||||
|
Preimage: [32]byte{},
|
||||||
|
CsvDelay: 4,
|
||||||
|
SignedSuccessTx: successTx,
|
||||||
|
SignDetails: &input.SignDetails{
|
||||||
|
SignDesc: testSignDesc,
|
||||||
|
PeerSig: testSig,
|
||||||
|
},
|
||||||
|
ClaimOutpoint: htlcOutpoint,
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStage := &channeldb.ResolverReport{
|
||||||
|
OutPoint: commitOutpoint,
|
||||||
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
|
ResolverType: channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||||
|
SpendTxID: &reSignedHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
secondStage := &channeldb.ResolverReport{
|
||||||
|
OutPoint: htlcOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeIncomingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||||
|
SpendTxID: &sweepHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// The HTLC output on the commitment should be offered
|
||||||
|
// to the sweeper. We'll notify that it gets spent.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
|
||||||
|
resolver := ctx.resolver.(*htlcSuccessResolver)
|
||||||
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
|
||||||
|
op := inp.OutPoint()
|
||||||
|
if *op != commitOutpoint {
|
||||||
|
return fmt.Errorf("outpoint %v swept, "+
|
||||||
|
"expected %v", op,
|
||||||
|
commitOutpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: reSignedSuccessTx,
|
||||||
|
SpenderTxHash: &reSignedHash,
|
||||||
|
SpenderInputIndex: 1,
|
||||||
|
SpendingHeight: 10,
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
|
||||||
|
},
|
||||||
|
// incubating=true is used to signal that the
|
||||||
|
// second-level transaction was confirmed.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// The resolver will wait for the second-level's CSV
|
||||||
|
// lock to expire.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
resumed bool) error {
|
||||||
|
|
||||||
|
// If we are resuming from a checkpoint, we
|
||||||
|
// expect the resolver to re-subscribe to a
|
||||||
|
// spend, hence we must resend it.
|
||||||
|
if resumed {
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: reSignedSuccessTx,
|
||||||
|
SpenderTxHash: &reSignedHash,
|
||||||
|
SpenderInputIndex: 1,
|
||||||
|
SpendingHeight: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{
|
||||||
|
Height: 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect it to sweep the second-level
|
||||||
|
// transaction we notfied about above.
|
||||||
|
resolver := ctx.resolver.(*htlcSuccessResolver)
|
||||||
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
|
||||||
|
op := inp.OutPoint()
|
||||||
|
exp := wire.OutPoint{
|
||||||
|
Hash: reSignedHash,
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
if *op != exp {
|
||||||
|
return fmt.Errorf("swept outpoint %v, expected %v",
|
||||||
|
op, exp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify about the spend, which should resolve
|
||||||
|
// the resolver.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: sweepTx,
|
||||||
|
SpenderTxHash: &sweepHash,
|
||||||
|
SpendingHeight: 14,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
secondStage,
|
||||||
|
firstStage,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcSuccess(t, twoStageResolution, checkpoints)
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkpoint holds expected data we expect the resolver to checkpoint itself
|
||||||
|
// to the DB next.
|
||||||
|
type checkpoint struct {
|
||||||
|
// preCheckpoint is a method that will be called before we reach the
|
||||||
|
// checkpoint, to carry out any needed operations to drive the resolver
|
||||||
|
// in this stage.
|
||||||
|
preCheckpoint func(*htlcResolverTestContext, bool) error
|
||||||
|
|
||||||
|
// data we expect the resolver to be checkpointed with next.
|
||||||
|
incubating bool
|
||||||
|
resolved bool
|
||||||
|
reports []*channeldb.ResolverReport
|
||||||
|
}
|
||||||
|
|
||||||
|
// testHtlcSuccess tests resolution of a success resolver. It takes a a list of
|
||||||
|
// checkpoints that it expects the resolver to go through. And will run the
|
||||||
|
// resolver all the way through these checkpoints, and also attempt to resume
|
||||||
|
// the resolver from every checkpoint.
|
||||||
func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
|
func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution,
|
||||||
resolve func(*htlcSuccessResolverTestContext),
|
checkpoints []checkpoint) {
|
||||||
sweepTx *wire.MsgTx, reports ...*channeldb.ResolverReport) {
|
|
||||||
|
|
||||||
defer timeout(t)()
|
defer timeout(t)()
|
||||||
|
|
||||||
ctx := newHtlcSuccessResolverTextContext(t)
|
// We first run the resolver from start to finish, ensuring it gets
|
||||||
|
// checkpointed at every expected stage. We store the checkpointed data
|
||||||
|
// for the next portion of the test.
|
||||||
|
ctx := newHtlcResolverTestContext(t,
|
||||||
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
|
||||||
|
return &htlcSuccessResolver{
|
||||||
|
contractResolverKit: *newContractResolverKit(cfg),
|
||||||
|
htlc: htlc,
|
||||||
|
htlcResolution: resolution,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// Replace our checkpoint with one which will push reports into a
|
checkpointedState := runFromCheckpoint(t, ctx, checkpoints)
|
||||||
// channel for us to consume. We replace this function on the resolver
|
|
||||||
// itself because it is created by the test context.
|
// Now, from every checkpoint created, we re-create the resolver, and
|
||||||
reportChan := make(chan *channeldb.ResolverReport)
|
// run the test from that checkpoint.
|
||||||
ctx.resolver.Checkpoint = func(_ ContractResolver,
|
for i := range checkpointedState {
|
||||||
|
cp := bytes.NewReader(checkpointedState[i])
|
||||||
|
ctx := newHtlcResolverTestContext(t,
|
||||||
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
|
||||||
|
resolver, err := newSuccessResolverFromReader(cp, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.Supplement(htlc)
|
||||||
|
resolver.htlcResolution = resolution
|
||||||
|
return resolver
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run from the given checkpoint, ensuring we'll hit the rest.
|
||||||
|
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// runFromCheckpoint executes the Resolve method on the success resolver, and
|
||||||
|
// asserts that it checkpoints itself according to the expected checkpoints.
|
||||||
|
func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext,
|
||||||
|
expectedCheckpoints []checkpoint) [][]byte {
|
||||||
|
|
||||||
|
defer timeout(t)()
|
||||||
|
|
||||||
|
var checkpointedState [][]byte
|
||||||
|
|
||||||
|
// Replace our checkpoint method with one which we'll use to assert the
|
||||||
|
// checkpointed state and reports are equal to what we expect.
|
||||||
|
nextCheckpoint := 0
|
||||||
|
checkpointChan := make(chan struct{})
|
||||||
|
ctx.checkpoint = func(resolver ContractResolver,
|
||||||
reports ...*channeldb.ResolverReport) error {
|
reports ...*channeldb.ResolverReport) error {
|
||||||
|
|
||||||
// Send all of our reports into the channel.
|
if nextCheckpoint >= len(expectedCheckpoints) {
|
||||||
for _, report := range reports {
|
t.Fatal("did not expect more checkpoints")
|
||||||
reportChan <- report
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resolved, incubating bool
|
||||||
|
if h, ok := resolver.(*htlcSuccessResolver); ok {
|
||||||
|
resolved = h.resolved
|
||||||
|
incubating = h.outputIncubating
|
||||||
|
}
|
||||||
|
if h, ok := resolver.(*htlcTimeoutResolver); ok {
|
||||||
|
resolved = h.resolved
|
||||||
|
incubating = h.outputIncubating
|
||||||
|
}
|
||||||
|
|
||||||
|
cp := expectedCheckpoints[nextCheckpoint]
|
||||||
|
|
||||||
|
if resolved != cp.resolved {
|
||||||
|
t.Fatalf("expected checkpoint to be resolve=%v, had %v",
|
||||||
|
cp.resolved, resolved)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !reflect.DeepEqual(incubating, cp.incubating) {
|
||||||
|
t.Fatalf("expected checkpoint to be have "+
|
||||||
|
"incubating=%v, had %v", cp.incubating,
|
||||||
|
incubating)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check we go the expected reports.
|
||||||
|
if len(reports) != len(cp.reports) {
|
||||||
|
t.Fatalf("unexpected number of reports. Expected %v "+
|
||||||
|
"got %v", len(cp.reports), len(reports))
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, report := range reports {
|
||||||
|
if !reflect.DeepEqual(report, cp.reports[i]) {
|
||||||
|
t.Fatalf("expected: %v, got: %v",
|
||||||
|
spew.Sdump(cp.reports[i]),
|
||||||
|
spew.Sdump(report))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally encode the resolver, and store it for later use.
|
||||||
|
b := bytes.Buffer{}
|
||||||
|
if err := resolver.Encode(&b); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpointedState = append(checkpointedState, b.Bytes())
|
||||||
|
nextCheckpoint++
|
||||||
|
checkpointChan <- struct{}{}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.resolver.htlcResolution = resolution
|
|
||||||
|
|
||||||
// We set the sweepTx to be non-nil and mark the output as already
|
|
||||||
// incubating so that we do not need to set test values for crafting
|
|
||||||
// our own sweep transaction.
|
|
||||||
ctx.resolver.sweepTx = sweepTx
|
|
||||||
ctx.resolver.outputIncubating = true
|
|
||||||
|
|
||||||
// Start the htlc success resolver.
|
// Start the htlc success resolver.
|
||||||
ctx.resolve()
|
ctx.resolve()
|
||||||
|
|
||||||
// Trigger and event that will resolve our test context.
|
// Go through our list of expected checkpoints, so we can run the
|
||||||
resolve(ctx)
|
// preCheckpoint logic if needed.
|
||||||
|
resumed := true
|
||||||
|
for i, cp := range expectedCheckpoints {
|
||||||
|
if cp.preCheckpoint != nil {
|
||||||
|
if err := cp.preCheckpoint(ctx, resumed); err != nil {
|
||||||
|
t.Fatalf("failure at stage %d: %v", i, err)
|
||||||
|
}
|
||||||
|
|
||||||
for _, report := range reports {
|
}
|
||||||
assertResolverReport(t, reportChan, report)
|
resumed = false
|
||||||
|
|
||||||
|
// Wait for the resolver to have checkpointed its state.
|
||||||
|
<-checkpointChan
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait for the resolver to fully complete.
|
// Wait for the resolver to fully complete.
|
||||||
ctx.waitForResult()
|
ctx.waitForResult()
|
||||||
|
|
||||||
|
if nextCheckpoint < len(expectedCheckpoints) {
|
||||||
|
t.Fatalf("not all checkpoints hit")
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkpointedState
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
"sync"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"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"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
)
|
)
|
||||||
|
|
||||||
// htlcTimeoutResolver is a ContractResolver that's capable of resolving an
|
// htlcTimeoutResolver is a ContractResolver that's capable of resolving an
|
||||||
@ -46,6 +47,15 @@ type htlcTimeoutResolver struct {
|
|||||||
// htlc contains information on the htlc that we are resolving on-chain.
|
// htlc contains information on the htlc that we are resolving on-chain.
|
||||||
htlc channeldb.HTLC
|
htlc channeldb.HTLC
|
||||||
|
|
||||||
|
// currentReport stores the current state of the resolver for reporting
|
||||||
|
// over the rpc interface. This should only be reported in case we have
|
||||||
|
// a non-nil SignDetails on the htlcResolution, otherwise the nursery
|
||||||
|
// will produce reports.
|
||||||
|
currentReport ContractReport
|
||||||
|
|
||||||
|
// reportLock prevents concurrent access to the resolver report.
|
||||||
|
reportLock sync.Mutex
|
||||||
|
|
||||||
contractResolverKit
|
contractResolverKit
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -54,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
|
|||||||
broadcastHeight uint32, htlc channeldb.HTLC,
|
broadcastHeight uint32, htlc channeldb.HTLC,
|
||||||
resCfg ResolverConfig) *htlcTimeoutResolver {
|
resCfg ResolverConfig) *htlcTimeoutResolver {
|
||||||
|
|
||||||
return &htlcTimeoutResolver{
|
h := &htlcTimeoutResolver{
|
||||||
contractResolverKit: *newContractResolverKit(resCfg),
|
contractResolverKit: *newContractResolverKit(resCfg),
|
||||||
htlcResolution: res,
|
htlcResolution: res,
|
||||||
broadcastHeight: broadcastHeight,
|
broadcastHeight: broadcastHeight,
|
||||||
htlc: htlc,
|
htlc: htlc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.initReport()
|
||||||
|
|
||||||
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolverKey returns an identifier which should be globally unique for this
|
// ResolverKey returns an identifier which should be globally unique for this
|
||||||
@ -254,9 +268,83 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we haven't already sent the output to the utxo nursery, then
|
// Start by spending the HTLC output, either by broadcasting the
|
||||||
// we'll do so now.
|
// second-level timeout transaction, or directly if this is the remote
|
||||||
if !h.outputIncubating {
|
// commitment.
|
||||||
|
commitSpend, err := h.spendHtlcOutput()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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(commitSpend, 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(commitSpend)
|
||||||
|
}
|
||||||
|
|
||||||
|
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, failing the
|
||||||
|
// HTLC on the incoming link.
|
||||||
|
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
||||||
|
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
||||||
|
SourceChan: h.ShortChanID,
|
||||||
|
HtlcIndex: h.htlc.HtlcIndex,
|
||||||
|
Failure: failureMsg,
|
||||||
|
}); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Depending on whether this was a local or remote commit, we must
|
||||||
|
// handle the spending transaction accordingly.
|
||||||
|
return h.handleCommitSpend(commitSpend)
|
||||||
|
}
|
||||||
|
|
||||||
|
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
|
||||||
|
// clause. If this is our local commitment, the second-level timeout TX will be
|
||||||
|
// used to spend the output into the next stage. If this is the remote
|
||||||
|
// commitment, the output will be swept directly without the timeout
|
||||||
|
// transaction.
|
||||||
|
func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) {
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// If we have non-nil SignDetails, this means that have a 2nd level
|
||||||
|
// HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY
|
||||||
|
// (the case for anchor type channels). In this case we can re-sign it
|
||||||
|
// and attach fees at will. We let the sweeper handle this job.
|
||||||
|
case h.htlcResolution.SignDetails != nil && !h.outputIncubating:
|
||||||
|
log.Infof("%T(%x): offering second-layer timeout tx to "+
|
||||||
|
"sweeper: %v", h, h.htlc.RHash[:],
|
||||||
|
spew.Sdump(h.htlcResolution.SignedTimeoutTx))
|
||||||
|
|
||||||
|
inp := input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
||||||
|
h.htlcResolution.SignedTimeoutTx,
|
||||||
|
h.htlcResolution.SignDetails,
|
||||||
|
h.broadcastHeight,
|
||||||
|
)
|
||||||
|
_, err := h.Sweeper.SweepInput(
|
||||||
|
&inp,
|
||||||
|
sweep.Params{
|
||||||
|
Fee: sweep.FeePreference{
|
||||||
|
ConfTarget: secondLevelConfTarget,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have no SignDetails, and we haven't already sent the output to
|
||||||
|
// the utxo nursery, then we'll do so now.
|
||||||
|
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
||||||
log.Tracef("%T(%v): incubating htlc output", h,
|
log.Tracef("%T(%v): incubating htlc output", h,
|
||||||
h.htlcResolution.ClaimOutpoint)
|
h.htlcResolution.ClaimOutpoint)
|
||||||
|
|
||||||
@ -276,51 +364,14 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var spendTxID *chainhash.Hash
|
// Now that we've handed off the HTLC to the nursery or sweeper, we'll
|
||||||
|
// watch for a spend of the output, and make our next move off of that.
|
||||||
// waitForOutputResolution waits for the HTLC output to be fully
|
// Depending on if this is our commitment, or the remote party's
|
||||||
// resolved. The output is considered fully resolved once it has been
|
// commitment, we'll be watching a different outpoint and script.
|
||||||
// spent, and the spending transaction has been fully confirmed.
|
|
||||||
waitForOutputResolution := func() error {
|
|
||||||
// We first need to register to see when the HTLC output itself
|
|
||||||
// has been spent by a confirmed transaction.
|
|
||||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
|
||||||
&h.htlcResolution.ClaimOutpoint,
|
|
||||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
|
||||||
h.broadcastHeight,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
select {
|
|
||||||
case spendDetail, ok := <-spendNtfn.Spend:
|
|
||||||
if !ok {
|
|
||||||
return errResolverShuttingDown
|
|
||||||
}
|
|
||||||
spendTxID = spendDetail.SpenderTxHash
|
|
||||||
|
|
||||||
case <-h.quit:
|
|
||||||
return errResolverShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
return 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()
|
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
|
||||||
outpointToWatch, scriptToWatch, h.broadcastHeight,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("%T(%v): waiting for HTLC output %v to be spent"+
|
log.Infof("%T(%v): waiting for HTLC output %v to be spent"+
|
||||||
"fully confirmed", h, h.htlcResolution.ClaimOutpoint,
|
"fully confirmed", h, h.htlcResolution.ClaimOutpoint,
|
||||||
@ -328,81 +379,158 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
|||||||
|
|
||||||
// We'll block here until either we exit, or the HTLC output on the
|
// We'll block here until either we exit, or the HTLC output on the
|
||||||
// commitment transaction has been spent.
|
// commitment transaction has been spent.
|
||||||
var (
|
spend, err := waitForSpend(
|
||||||
spend *chainntnfs.SpendDetail
|
outpointToWatch, scriptToWatch, h.broadcastHeight,
|
||||||
ok bool
|
h.Notifier, h.quit,
|
||||||
)
|
)
|
||||||
select {
|
if err != nil {
|
||||||
case spend, ok = <-spendNtfn.Spend:
|
|
||||||
if !ok {
|
|
||||||
return nil, errResolverShuttingDown
|
|
||||||
}
|
|
||||||
spendTxID = spend.SpenderTxHash
|
|
||||||
|
|
||||||
case <-h.quit:
|
|
||||||
return nil, errResolverShuttingDown
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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, failing the
|
|
||||||
// HTLC on the incoming link.
|
|
||||||
failureMsg := &lnwire.FailPermanentChannelFailure{}
|
|
||||||
if err := h.DeliverResolutionMsg(ResolutionMsg{
|
|
||||||
SourceChan: h.ShortChanID,
|
|
||||||
HtlcIndex: h.htlc.HtlcIndex,
|
|
||||||
Failure: failureMsg,
|
|
||||||
}); err != nil {
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
var reports []*channeldb.ResolverReport
|
// If this was the second level transaction published by the sweeper,
|
||||||
|
// we can checkpoint the resolver now that it's confirmed.
|
||||||
|
if h.htlcResolution.SignDetails != nil && !h.outputIncubating {
|
||||||
|
h.outputIncubating = true
|
||||||
|
if err := h.Checkpoint(h); err != nil {
|
||||||
|
log.Errorf("unable to Checkpoint: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return spend, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// handleCommitSpend handles the spend of the HTLC output on the commitment
|
||||||
|
// transaction. If this was our local commitment, the spend will be he
|
||||||
|
// confirmed second-level timeout transaction, and we'll sweep that into our
|
||||||
|
// wallet. If the was a remote commitment, the resolver will resolve
|
||||||
|
// immetiately.
|
||||||
|
func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||||
|
commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) {
|
||||||
|
|
||||||
|
var (
|
||||||
|
// claimOutpoint will be the outpoint of the second level
|
||||||
|
// transaction, or on the remote commitment directly. It will
|
||||||
|
// start out as set in the resolution, but we'll update it if
|
||||||
|
// the second-level goes through the sweeper and changes its
|
||||||
|
// txid.
|
||||||
|
claimOutpoint = h.htlcResolution.ClaimOutpoint
|
||||||
|
|
||||||
|
// 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
|
||||||
|
)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// If the sweeper is handling the second level transaction, wait for
|
||||||
|
// the CSV lock to expire, before sweeping the output on the
|
||||||
|
// second-level.
|
||||||
|
case h.htlcResolution.SignDetails != nil:
|
||||||
|
waitHeight := uint32(commitSpend.SpendingHeight) +
|
||||||
|
h.htlcResolution.CsvDelay - 1
|
||||||
|
|
||||||
|
h.reportLock.Lock()
|
||||||
|
h.currentReport.Stage = 2
|
||||||
|
h.currentReport.MaturityHeight = waitHeight
|
||||||
|
h.reportLock.Unlock()
|
||||||
|
|
||||||
|
log.Infof("%T(%x): waiting for CSV lock to expire at height %v",
|
||||||
|
h, h.htlc.RHash[:], waitHeight)
|
||||||
|
|
||||||
|
err := waitForHeight(waitHeight, h.Notifier, h.quit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll use this input index to determine the second-level
|
||||||
|
// output index on the transaction, as the signatures requires
|
||||||
|
// the indexes to be the same. We don't look for the
|
||||||
|
// second-level output script directly, as there might be more
|
||||||
|
// than one HTLC output to the same pkScript.
|
||||||
|
op := &wire.OutPoint{
|
||||||
|
Hash: *commitSpend.SpenderTxHash,
|
||||||
|
Index: commitSpend.SpenderInputIndex,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Let the sweeper sweep the second-level output now that the
|
||||||
|
// CSV delay has passed.
|
||||||
|
log.Infof("%T(%x): CSV lock expired, offering second-layer "+
|
||||||
|
"output to sweeper: %v", h, h.htlc.RHash[:], op)
|
||||||
|
|
||||||
|
inp := input.NewCsvInput(
|
||||||
|
op, input.HtlcOfferedTimeoutSecondLevel,
|
||||||
|
&h.htlcResolution.SweepSignDesc,
|
||||||
|
h.broadcastHeight,
|
||||||
|
h.htlcResolution.CsvDelay,
|
||||||
|
)
|
||||||
|
_, err = h.Sweeper.SweepInput(
|
||||||
|
inp,
|
||||||
|
sweep.Params{
|
||||||
|
Fee: sweep.FeePreference{
|
||||||
|
ConfTarget: sweepConfTarget,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update the claim outpoint to point to the second-level
|
||||||
|
// transaction created by the sweeper.
|
||||||
|
claimOutpoint = *op
|
||||||
|
fallthrough
|
||||||
|
|
||||||
// Finally, if this was an output on our commitment transaction, we'll
|
// 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
|
// 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 {
|
case h.htlcResolution.SignedTimeoutTx != nil:
|
||||||
log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+
|
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
||||||
"output", h, h.htlcResolution.ClaimOutpoint)
|
"delayed output", h, claimOutpoint)
|
||||||
if err := waitForOutputResolution(); err != nil {
|
sweep, err := waitForSpend(
|
||||||
|
&claimOutpoint,
|
||||||
|
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||||
|
h.broadcastHeight, h.Notifier, h.quit,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Once our timeout tx has confirmed, we add a resolution for
|
// Update the spend txid to the hash of the sweep transaction.
|
||||||
// our timeoutTx tx first stage transaction.
|
spendTxID = sweep.SpenderTxHash
|
||||||
timeoutTx := h.htlcResolution.SignedTimeoutTx
|
|
||||||
spendHash := timeoutTx.TxHash()
|
// Once our sweep of the timeout tx has confirmed, we add a
|
||||||
|
// resolution for our timeoutTx tx first stage transaction.
|
||||||
|
timeoutTx := commitSpend.SpendingTx
|
||||||
|
index := commitSpend.SpenderInputIndex
|
||||||
|
spendHash := commitSpend.SpenderTxHash
|
||||||
|
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
reports = append(reports, &channeldb.ResolverReport{
|
||||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
OutPoint: timeoutTx.TxIn[index].PreviousOutPoint,
|
||||||
Amount: h.htlc.Amt.ToSatoshis(),
|
Amount: h.htlc.Amt.ToSatoshis(),
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||||
SpendTxID: &spendHash,
|
SpendTxID: spendHash,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// With the clean up message sent, we'll now mark the contract
|
// With the clean up message sent, we'll now mark the contract
|
||||||
// resolved, record the timeout and the sweep txid on disk, and wait.
|
// resolved, update the recovered balance, record the timeout and the
|
||||||
|
// sweep txid on disk, and wait.
|
||||||
h.resolved = true
|
h.resolved = true
|
||||||
|
h.reportLock.Lock()
|
||||||
|
h.currentReport.RecoveredBalance = h.currentReport.LimboBalance
|
||||||
|
h.currentReport.LimboBalance = 0
|
||||||
|
h.reportLock.Unlock()
|
||||||
|
|
||||||
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
|
amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value)
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
reports = append(reports, &channeldb.ResolverReport{
|
||||||
OutPoint: h.htlcResolution.ClaimOutpoint,
|
OutPoint: claimOutpoint,
|
||||||
Amount: amt,
|
Amount: amt,
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
||||||
@ -428,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
|
|||||||
return h.resolved
|
return h.resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// report returns a report on the resolution state of the contract.
|
||||||
|
func (h *htlcTimeoutResolver) report() *ContractReport {
|
||||||
|
// If the sign details are nil, the report will be created by handled
|
||||||
|
// by the nursery.
|
||||||
|
if h.htlcResolution.SignDetails == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
h.reportLock.Lock()
|
||||||
|
defer h.reportLock.Unlock()
|
||||||
|
copy := h.currentReport
|
||||||
|
return ©
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *htlcTimeoutResolver) initReport() {
|
||||||
|
// We create the initial report. This will only be reported for
|
||||||
|
// resolvers not handled by the nursery.
|
||||||
|
finalAmt := h.htlc.Amt.ToSatoshis()
|
||||||
|
if h.htlcResolution.SignedTimeoutTx != nil {
|
||||||
|
finalAmt = btcutil.Amount(
|
||||||
|
h.htlcResolution.SignedTimeoutTx.TxOut[0].Value,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
h.currentReport = ContractReport{
|
||||||
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
|
Type: ReportOutputOutgoingHtlc,
|
||||||
|
Amount: finalAmt,
|
||||||
|
MaturityHeight: h.htlcResolution.Expiry,
|
||||||
|
LimboBalance: finalAmt,
|
||||||
|
Stage: 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Encode writes an encoded version of the ContractResolver into the passed
|
// Encode writes an encoded version of the ContractResolver into the passed
|
||||||
// Writer.
|
// Writer.
|
||||||
//
|
//
|
||||||
@ -455,6 +617,12 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We encode the sign details last for backwards compatibility.
|
||||||
|
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -490,6 +658,18 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sign details is a new field that was added to the htlc resolution,
|
||||||
|
// so it is serialized last for backwards compatibility. We try to read
|
||||||
|
// it, but don't error out if there are not bytes left.
|
||||||
|
signDetails, err := decodeSignDetails(r)
|
||||||
|
if err == nil {
|
||||||
|
h.htlcResolution.SignDetails = signDetails
|
||||||
|
} else if err != io.EOF && err != io.ErrUnexpectedEOF {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.initReport()
|
||||||
|
|
||||||
return h, nil
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3,10 +3,12 @@ package contractcourt
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
@ -17,6 +19,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockWitnessBeacon struct {
|
type mockWitnessBeacon struct {
|
||||||
@ -127,6 +130,16 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
return nil, err
|
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
|
templateTx.TxIn[0].Witness = witness
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
},
|
},
|
||||||
@ -148,6 +161,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
return nil, err
|
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
|
templateTx.TxIn[0].Witness = witness
|
||||||
|
|
||||||
// Set the outpoint to be on our commitment, since
|
// Set the outpoint to be on our commitment, since
|
||||||
@ -174,6 +198,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
return nil, err
|
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
|
templateTx.TxIn[0].Witness = witness
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
},
|
},
|
||||||
@ -196,6 +231,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
return nil, err
|
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
|
templateTx.TxIn[0].Witness = witness
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
},
|
},
|
||||||
@ -282,16 +328,19 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
// broadcast, then we'll set the timeout commit to a fake
|
// broadcast, then we'll set the timeout commit to a fake
|
||||||
// transaction to force the code path.
|
// transaction to force the code path.
|
||||||
if !testCase.remoteCommit {
|
if !testCase.remoteCommit {
|
||||||
resolver.htlcResolution.SignedTimeoutTx = sweepTx
|
timeoutTx, err := testCase.txToBroadcast()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
||||||
|
|
||||||
if testCase.timeout {
|
if testCase.timeout {
|
||||||
success := sweepTx.TxHash()
|
timeoutTxID := timeoutTx.TxHash()
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
reports = append(reports, &channeldb.ResolverReport{
|
||||||
OutPoint: sweepTx.TxIn[0].PreviousOutPoint,
|
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
||||||
Amount: testHtlcAmt.ToSatoshis(),
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||||
SpendTxID: &success,
|
SpendTxID: &timeoutTxID,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -433,3 +482,834 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NOTE: the following tests essentially checks many of the same scenarios as
|
||||||
|
// the test above, but they expand on it by checking resuming from checkpoints
|
||||||
|
// at every stage.
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSingleStage tests a remote commitment confirming, and the
|
||||||
|
// local node sweeping the HTLC output directly after timeout.
|
||||||
|
func TestHtlcTimeoutSingleStage(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
sweepTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{{}},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
// singleStageResolution is a resolution for a htlc on the remote
|
||||||
|
// party's commitment.
|
||||||
|
singleStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: commitOutpoint,
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepTxid := sweepTx.TxHash()
|
||||||
|
claim := &channeldb.ResolverReport{
|
||||||
|
OutPoint: commitOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
||||||
|
SpendTxID: &sweepTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// Output should be handed off to the nursery.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a confirmation the sweep tx from published
|
||||||
|
// by the nursery.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
// The nursery will create and publish a sweep
|
||||||
|
// tx.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: sweepTx,
|
||||||
|
SpenderTxHash: &sweepTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver should deliver a failure
|
||||||
|
// resolition message (indicating we
|
||||||
|
// successfully timed out the HTLC).
|
||||||
|
select {
|
||||||
|
case resolutionMsg := <-ctx.resolutionChan:
|
||||||
|
if resolutionMsg.Failure == nil {
|
||||||
|
t.Fatalf("expected failure resolution msg")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("resolution not sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// report.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
claim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, singleStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSecondStage tests a local commitment being confirmed, and the
|
||||||
|
// local node claiming the HTLC output using the second-level timeout tx.
|
||||||
|
func TestHtlcTimeoutSecondStage(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
sweepTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{{}},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
sweepHash := sweepTx.TxHash()
|
||||||
|
|
||||||
|
timeoutTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 111,
|
||||||
|
PkScript: []byte{0xaa, 0xaa},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
signer := &mock.DummySigner{}
|
||||||
|
witness, err := input.SenderHtlcSpendTimeout(
|
||||||
|
&mock.DummySignature{}, txscript.SigHashAll,
|
||||||
|
signer, &testSignDesc, timeoutTx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
timeoutTx.TxIn[0].Witness = witness
|
||||||
|
|
||||||
|
timeoutTxid := timeoutTx.TxHash()
|
||||||
|
|
||||||
|
// twoStageResolution is a resolution for a htlc on the local
|
||||||
|
// party's commitment.
|
||||||
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: htlcOutpoint,
|
||||||
|
SignedTimeoutTx: timeoutTx,
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStage := &channeldb.ResolverReport{
|
||||||
|
OutPoint: commitOutpoint,
|
||||||
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||||
|
SpendTxID: &timeoutTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
secondState := &channeldb.ResolverReport{
|
||||||
|
OutPoint: htlcOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
||||||
|
SpendTxID: &sweepHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// Output should be handed off to the nursery.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a confirmation for our sweep tx to indicate
|
||||||
|
// that our sweep succeeded.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
// The nursery will publish the timeout tx.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: timeoutTx,
|
||||||
|
SpenderTxHash: &timeoutTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver should deliver a failure
|
||||||
|
// resolution message (indicating we
|
||||||
|
// successfully timed out the HTLC).
|
||||||
|
select {
|
||||||
|
case resolutionMsg := <-ctx.resolutionChan:
|
||||||
|
if resolutionMsg.Failure == nil {
|
||||||
|
t.Fatalf("expected failure resolution msg")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
t.Fatalf("resolution not sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deliver spend of timeout tx.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: sweepTx,
|
||||||
|
SpenderTxHash: &sweepHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// reports.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
firstStage, secondState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, twoStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSingleStageRemoteSpend tests that when a local commitment
|
||||||
|
// confirms, and the remote spends the HTLC output directly, we detect this and
|
||||||
|
// extract the preimage.
|
||||||
|
func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
spendTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{{}},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||||
|
var fakePreimage lntypes.Preimage
|
||||||
|
copy(fakePreimage[:], fakePreimageBytes)
|
||||||
|
|
||||||
|
signer := &mock.DummySigner{}
|
||||||
|
witness, err := input.SenderHtlcSpendRedeem(
|
||||||
|
signer, &testSignDesc, spendTx,
|
||||||
|
fakePreimageBytes,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
spendTx.TxIn[0].Witness = witness
|
||||||
|
|
||||||
|
spendTxHash := spendTx.TxHash()
|
||||||
|
|
||||||
|
timeoutTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 123,
|
||||||
|
PkScript: []byte{0xff, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
timeoutWitness, err := input.SenderHtlcSpendTimeout(
|
||||||
|
&mock.DummySignature{}, txscript.SigHashAll,
|
||||||
|
signer, &testSignDesc, timeoutTx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
timeoutTx.TxIn[0].Witness = timeoutWitness
|
||||||
|
|
||||||
|
// twoStageResolution is a resolution for a htlc on the local
|
||||||
|
// party's commitment.
|
||||||
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: htlcOutpoint,
|
||||||
|
SignedTimeoutTx: timeoutTx,
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
claim := &channeldb.ResolverReport{
|
||||||
|
OutPoint: htlcOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||||
|
SpendTxID: &spendTxHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// Output should be handed off to the nursery.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a spend notification for a remote spend with
|
||||||
|
// the preimage.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
|
||||||
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
|
||||||
|
|
||||||
|
// The remote spends the output direcly with
|
||||||
|
// the preimage.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should extract the preimage.
|
||||||
|
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 := <-ctx.resolutionChan:
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the success tx has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// report.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
claim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, twoStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
|
||||||
|
// confirms, and the remote spends the output using the success tx, we
|
||||||
|
// properly detect this and extract the preimage.
|
||||||
|
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
|
||||||
|
remoteSuccessTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||||
|
var fakePreimage lntypes.Preimage
|
||||||
|
copy(fakePreimage[:], fakePreimageBytes)
|
||||||
|
|
||||||
|
signer := &mock.DummySigner{}
|
||||||
|
witness, err := input.ReceiverHtlcSpendRedeem(
|
||||||
|
&mock.DummySignature{}, txscript.SigHashAll,
|
||||||
|
fakePreimageBytes, signer,
|
||||||
|
&testSignDesc, remoteSuccessTx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
remoteSuccessTx.TxIn[0].Witness = witness
|
||||||
|
successTxid := remoteSuccessTx.TxHash()
|
||||||
|
|
||||||
|
// singleStageResolution allwoing the local node to sweep HTLC output
|
||||||
|
// directly from the remote commitment after timeout.
|
||||||
|
singleStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: commitOutpoint,
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
claim := &channeldb.ResolverReport{
|
||||||
|
OutPoint: commitOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||||
|
SpendTxID: &successTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// Output should be handed off to the nursery.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a confirmation for the remote's second layer
|
||||||
|
// success transcation.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: remoteSuccessTx,
|
||||||
|
SpenderTxHash: &successTxid,
|
||||||
|
}
|
||||||
|
|
||||||
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
|
||||||
|
|
||||||
|
// We expect the preimage to be extracted,
|
||||||
|
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 := <-ctx.resolutionChan:
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// report.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
claim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, singleStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSecondStageSweeper tests that for anchor channels, when a
|
||||||
|
// local commitment confirms, the timeout tx is handed to the sweeper to claim
|
||||||
|
// the HTLC output.
|
||||||
|
func TestHtlcTimeoutSecondStageSweeper(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
sweepTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{{}},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
sweepHash := sweepTx.TxHash()
|
||||||
|
|
||||||
|
timeoutTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 123,
|
||||||
|
PkScript: []byte{0xff, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set the timeout witness since the script is used when subscribing
|
||||||
|
// to spends.
|
||||||
|
signer := &mock.DummySigner{}
|
||||||
|
timeoutWitness, err := input.SenderHtlcSpendTimeout(
|
||||||
|
&mock.DummySignature{}, txscript.SigHashAll,
|
||||||
|
signer, &testSignDesc, timeoutTx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
timeoutTx.TxIn[0].Witness = timeoutWitness
|
||||||
|
|
||||||
|
reSignedTimeoutTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{0xaa, 0xbb},
|
||||||
|
Index: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
timeoutTx.TxIn[0],
|
||||||
|
{
|
||||||
|
PreviousOutPoint: wire.OutPoint{
|
||||||
|
Hash: chainhash.Hash{0xaa, 0xbb},
|
||||||
|
Index: 2,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 111,
|
||||||
|
PkScript: []byte{0xaa, 0xaa},
|
||||||
|
},
|
||||||
|
timeoutTx.TxOut[0],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
reSignedHash := reSignedTimeoutTx.TxHash()
|
||||||
|
reSignedOutPoint := wire.OutPoint{
|
||||||
|
Hash: reSignedHash,
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
// twoStageResolution is a resolution for a htlc on the local
|
||||||
|
// party's commitment, where the timout tx can be re-signed.
|
||||||
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: htlcOutpoint,
|
||||||
|
SignedTimeoutTx: timeoutTx,
|
||||||
|
SignDetails: &input.SignDetails{
|
||||||
|
SignDesc: testSignDesc,
|
||||||
|
PeerSig: testSig,
|
||||||
|
},
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
firstStage := &channeldb.ResolverReport{
|
||||||
|
OutPoint: commitOutpoint,
|
||||||
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||||
|
SpendTxID: &reSignedHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
secondState := &channeldb.ResolverReport{
|
||||||
|
OutPoint: reSignedOutPoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
||||||
|
SpendTxID: &sweepHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// The output should be given to the sweeper.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
|
||||||
|
resolver := ctx.resolver.(*htlcTimeoutResolver)
|
||||||
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
|
||||||
|
op := inp.OutPoint()
|
||||||
|
if *op != commitOutpoint {
|
||||||
|
return fmt.Errorf("outpoint %v swept, "+
|
||||||
|
"expected %v", op,
|
||||||
|
commitOutpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulat the sweeper spending using the
|
||||||
|
// re-signed timeout tx.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: reSignedTimeoutTx,
|
||||||
|
SpenderInputIndex: 1,
|
||||||
|
SpenderTxHash: &reSignedHash,
|
||||||
|
SpendingHeight: 10,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
// incubating=true is used to signal that the
|
||||||
|
// second-level transaction was confirmed.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a confirmation for our sweep tx to indicate
|
||||||
|
// that our sweep succeeded.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
resumed bool) error {
|
||||||
|
|
||||||
|
// If we are resuming from a checkpoing, we
|
||||||
|
// expect the resolver to re-subscribe to a
|
||||||
|
// spend, hence we must resend it.
|
||||||
|
if resumed {
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: reSignedTimeoutTx,
|
||||||
|
SpenderInputIndex: 1,
|
||||||
|
SpenderTxHash: &reSignedHash,
|
||||||
|
SpendingHeight: 10,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The resolver should deliver a failure
|
||||||
|
// resolution message (indicating we
|
||||||
|
// successfully timed out the HTLC).
|
||||||
|
select {
|
||||||
|
case resolutionMsg := <-ctx.resolutionChan:
|
||||||
|
if resolutionMsg.Failure == nil {
|
||||||
|
t.Fatalf("expected failure resolution msg")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 1):
|
||||||
|
t.Fatalf("resolution not sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mimic CSV lock expiring.
|
||||||
|
ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{
|
||||||
|
Height: 13,
|
||||||
|
}
|
||||||
|
|
||||||
|
// The timout tx output should now be given to
|
||||||
|
// the sweeper.
|
||||||
|
resolver := ctx.resolver.(*htlcTimeoutResolver)
|
||||||
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
|
||||||
|
op := inp.OutPoint()
|
||||||
|
exp := wire.OutPoint{
|
||||||
|
Hash: reSignedHash,
|
||||||
|
Index: 1,
|
||||||
|
}
|
||||||
|
if *op != exp {
|
||||||
|
return fmt.Errorf("wrong outpoint swept")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notify about the spend, which should resolve
|
||||||
|
// the resolver.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: sweepTx,
|
||||||
|
SpenderTxHash: &sweepHash,
|
||||||
|
SpendingHeight: 14,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// reports.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
firstStage,
|
||||||
|
secondState,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, twoStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutSecondStageSweeperRemoteSpend tests that if a local timeout
|
||||||
|
// tx is offered to the sweeper, but the output is swept by the remote node, we
|
||||||
|
// properly detect this and extract the preimage.
|
||||||
|
func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
|
||||||
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||||
|
|
||||||
|
timeoutTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{
|
||||||
|
{
|
||||||
|
PreviousOutPoint: commitOutpoint,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
TxOut: []*wire.TxOut{
|
||||||
|
{
|
||||||
|
Value: 123,
|
||||||
|
PkScript: []byte{0xff, 0xff},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// We set the timeout witness since the script is used when subscribing
|
||||||
|
// to spends.
|
||||||
|
signer := &mock.DummySigner{}
|
||||||
|
timeoutWitness, err := input.SenderHtlcSpendTimeout(
|
||||||
|
&mock.DummySignature{}, txscript.SigHashAll,
|
||||||
|
signer, &testSignDesc, timeoutTx,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
timeoutTx.TxIn[0].Witness = timeoutWitness
|
||||||
|
|
||||||
|
spendTx := &wire.MsgTx{
|
||||||
|
TxIn: []*wire.TxIn{{}},
|
||||||
|
TxOut: []*wire.TxOut{{}},
|
||||||
|
}
|
||||||
|
|
||||||
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||||
|
var fakePreimage lntypes.Preimage
|
||||||
|
copy(fakePreimage[:], fakePreimageBytes)
|
||||||
|
|
||||||
|
witness, err := input.SenderHtlcSpendRedeem(
|
||||||
|
signer, &testSignDesc, spendTx,
|
||||||
|
fakePreimageBytes,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
spendTx.TxIn[0].Witness = witness
|
||||||
|
|
||||||
|
spendTxHash := spendTx.TxHash()
|
||||||
|
|
||||||
|
// twoStageResolution is a resolution for a htlc on the local
|
||||||
|
// party's commitment, where the timout tx can be re-signed.
|
||||||
|
twoStageResolution := lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: htlcOutpoint,
|
||||||
|
SignedTimeoutTx: timeoutTx,
|
||||||
|
SignDetails: &input.SignDetails{
|
||||||
|
SignDesc: testSignDesc,
|
||||||
|
PeerSig: testSig,
|
||||||
|
},
|
||||||
|
SweepSignDesc: testSignDesc,
|
||||||
|
}
|
||||||
|
|
||||||
|
claim := &channeldb.ResolverReport{
|
||||||
|
OutPoint: htlcOutpoint,
|
||||||
|
Amount: btcutil.Amount(testSignDesc.Output.Value),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||||
|
SpendTxID: &spendTxHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
checkpoints := []checkpoint{
|
||||||
|
{
|
||||||
|
// The output should be given to the sweeper.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
_ bool) error {
|
||||||
|
|
||||||
|
resolver := ctx.resolver.(*htlcTimeoutResolver)
|
||||||
|
inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs
|
||||||
|
op := inp.OutPoint()
|
||||||
|
if *op != commitOutpoint {
|
||||||
|
return fmt.Errorf("outpoint %v swept, "+
|
||||||
|
"expected %v", op,
|
||||||
|
commitOutpoint)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Emulate the remote sweeping the output with the preimage.
|
||||||
|
// re-signed timeout tx.
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
// incubating=true is used to signal that the
|
||||||
|
// second-level transaction was confirmed.
|
||||||
|
incubating: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// We send a confirmation for our sweep tx to indicate
|
||||||
|
// that our sweep succeeded.
|
||||||
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
|
resumed bool) error {
|
||||||
|
|
||||||
|
// If we are resuming from a checkpoing, we
|
||||||
|
// expect the resolver to re-subscribe to a
|
||||||
|
// spend, hence we must resend it.
|
||||||
|
if resumed {
|
||||||
|
fmt.Println("resumed")
|
||||||
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: spendTx,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon)
|
||||||
|
|
||||||
|
// We should extract the preimage.
|
||||||
|
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 := <-ctx.resolutionChan:
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
|
||||||
|
// After the sweep has confirmed, we expect the
|
||||||
|
// checkpoint to be resolved, and with the above
|
||||||
|
// reports.
|
||||||
|
incubating: true,
|
||||||
|
resolved: true,
|
||||||
|
reports: []*channeldb.ResolverReport{
|
||||||
|
claim,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
testHtlcTimeout(
|
||||||
|
t, twoStageResolution, checkpoints,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
||||||
|
checkpoints []checkpoint) {
|
||||||
|
|
||||||
|
defer timeout(t)()
|
||||||
|
|
||||||
|
// We first run the resolver from start to finish, ensuring it gets
|
||||||
|
// checkpointed at every expected stage. We store the checkpointed data
|
||||||
|
// for the next portion of the test.
|
||||||
|
ctx := newHtlcResolverTestContext(t,
|
||||||
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
|
||||||
|
return &htlcTimeoutResolver{
|
||||||
|
contractResolverKit: *newContractResolverKit(cfg),
|
||||||
|
htlc: htlc,
|
||||||
|
htlcResolution: resolution,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
checkpointedState := runFromCheckpoint(t, ctx, checkpoints)
|
||||||
|
|
||||||
|
// Now, from every checkpoint created, we re-create the resolver, and
|
||||||
|
// run the test from that checkpoint.
|
||||||
|
for i := range checkpointedState {
|
||||||
|
cp := bytes.NewReader(checkpointedState[i])
|
||||||
|
ctx := newHtlcResolverTestContext(t,
|
||||||
|
func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver {
|
||||||
|
resolver, err := newTimeoutResolverFromReader(cp, cfg)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resolver.Supplement(htlc)
|
||||||
|
resolver.htlcResolution = resolution
|
||||||
|
return resolver
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
// Run from the given checkpoint, ensuring we'll hit the rest.
|
||||||
|
_ = runFromCheckpoint(t, ctx, checkpoints[i+1:])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
140
input/input.go
140
input/input.go
@ -4,6 +4,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/btcsuite/btcutil"
|
"github.com/btcsuite/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Input represents an abstract UTXO which is to be spent using a sweeping
|
// Input represents an abstract UTXO which is to be spent using a sweeping
|
||||||
@ -67,6 +68,22 @@ type TxInfo struct {
|
|||||||
Weight int64
|
Weight int64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SignDetails is a struct containing information needed to resign certain
|
||||||
|
// inputs. It is used to re-sign 2nd level HTLC transactions that uses the
|
||||||
|
// SINGLE|ANYONECANPAY sighash type, as we have a signature provided by our
|
||||||
|
// peer, but we can aggregate multiple of these 2nd level transactions into a
|
||||||
|
// new transaction, that needs to be signed by us.
|
||||||
|
type SignDetails struct {
|
||||||
|
// SignDesc is the sign descriptor needed for us to sign the input.
|
||||||
|
SignDesc SignDescriptor
|
||||||
|
|
||||||
|
// PeerSig is the peer's signature for this input.
|
||||||
|
PeerSig Signature
|
||||||
|
|
||||||
|
// SigHashType is the sighash signed by the peer.
|
||||||
|
SigHashType txscript.SigHashType
|
||||||
|
}
|
||||||
|
|
||||||
type inputKit struct {
|
type inputKit struct {
|
||||||
outpoint wire.OutPoint
|
outpoint wire.OutPoint
|
||||||
witnessType WitnessType
|
witnessType WitnessType
|
||||||
@ -241,7 +258,130 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HtlcsSecondLevelAnchorInput is an input type used to spend HTLC outputs
|
||||||
|
// using a re-signed second level transaction, either via the timeout or success
|
||||||
|
// paths.
|
||||||
|
type HtlcSecondLevelAnchorInput struct {
|
||||||
|
inputKit
|
||||||
|
|
||||||
|
// SignedTx is the original second level transaction signed by the
|
||||||
|
// channel peer.
|
||||||
|
SignedTx *wire.MsgTx
|
||||||
|
|
||||||
|
// createWitness creates a witness allowing the passed transaction to
|
||||||
|
// spend the input.
|
||||||
|
createWitness func(signer Signer, txn *wire.MsgTx,
|
||||||
|
hashCache *txscript.TxSigHashes, txinIdx int) (wire.TxWitness, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredTxOut returns the tx out needed to be present on the sweep tx for
|
||||||
|
// the spend of the input to be valid.
|
||||||
|
func (i *HtlcSecondLevelAnchorInput) RequiredTxOut() *wire.TxOut {
|
||||||
|
return i.SignedTx.TxOut[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequiredLockTime returns the locktime needed for the sweep tx for the spend
|
||||||
|
// of the input to be valid. For a second level HTLC timeout this will be the
|
||||||
|
// CLTV expiry, for HTLC success it will be zero.
|
||||||
|
func (i *HtlcSecondLevelAnchorInput) RequiredLockTime() (uint32, bool) {
|
||||||
|
return i.SignedTx.LockTime, true
|
||||||
|
}
|
||||||
|
|
||||||
|
// CraftInputScript returns a valid set of input scripts allowing this output
|
||||||
|
// to be spent. The returns input scripts should target the input at location
|
||||||
|
// txIndex within the passed transaction. The input scripts generated by this
|
||||||
|
// method support spending p2wkh, p2wsh, and also nested p2sh outputs.
|
||||||
|
func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer,
|
||||||
|
txn *wire.MsgTx, hashCache *txscript.TxSigHashes,
|
||||||
|
txinIdx int) (*Script, error) {
|
||||||
|
|
||||||
|
witness, err := i.createWitness(signer, txn, hashCache, txinIdx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &Script{
|
||||||
|
Witness: witness,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper
|
||||||
|
// to spend the HTLC output on our commit using the second level timeout
|
||||||
|
// transaction.
|
||||||
|
func MakeHtlcSecondLevelTimeoutAnchorInput(signedTx *wire.MsgTx,
|
||||||
|
signDetails *SignDetails, heightHint uint32) HtlcSecondLevelAnchorInput {
|
||||||
|
|
||||||
|
// Spend an HTLC output on our local commitment tx using the
|
||||||
|
// 2nd timeout transaction.
|
||||||
|
createWitness := func(signer Signer, txn *wire.MsgTx,
|
||||||
|
hashCache *txscript.TxSigHashes,
|
||||||
|
txinIdx int) (wire.TxWitness, error) {
|
||||||
|
|
||||||
|
desc := signDetails.SignDesc
|
||||||
|
desc.SigHashes = txscript.NewTxSigHashes(txn)
|
||||||
|
desc.InputIndex = txinIdx
|
||||||
|
|
||||||
|
return SenderHtlcSpendTimeout(
|
||||||
|
signDetails.PeerSig, signDetails.SigHashType, signer,
|
||||||
|
&desc, txn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HtlcSecondLevelAnchorInput{
|
||||||
|
inputKit: inputKit{
|
||||||
|
outpoint: signedTx.TxIn[0].PreviousOutPoint,
|
||||||
|
witnessType: HtlcOfferedTimeoutSecondLevelInputConfirmed,
|
||||||
|
signDesc: signDetails.SignDesc,
|
||||||
|
heightHint: heightHint,
|
||||||
|
|
||||||
|
// CSV delay is always 1 for these inputs.
|
||||||
|
blockToMaturity: 1,
|
||||||
|
},
|
||||||
|
SignedTx: signedTx,
|
||||||
|
createWitness: createWitness,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper
|
||||||
|
// to spend the HTLC output on our commit using the second level success
|
||||||
|
// transaction.
|
||||||
|
func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx,
|
||||||
|
signDetails *SignDetails, preimage lntypes.Preimage,
|
||||||
|
heightHint uint32) HtlcSecondLevelAnchorInput {
|
||||||
|
|
||||||
|
// Spend an HTLC output on our local commitment tx using the 2nd
|
||||||
|
// success transaction.
|
||||||
|
createWitness := func(signer Signer, txn *wire.MsgTx,
|
||||||
|
hashCache *txscript.TxSigHashes,
|
||||||
|
txinIdx int) (wire.TxWitness, error) {
|
||||||
|
|
||||||
|
desc := signDetails.SignDesc
|
||||||
|
desc.SigHashes = hashCache
|
||||||
|
desc.InputIndex = txinIdx
|
||||||
|
|
||||||
|
return ReceiverHtlcSpendRedeem(
|
||||||
|
signDetails.PeerSig, signDetails.SigHashType,
|
||||||
|
preimage[:], signer, &desc, txn,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return HtlcSecondLevelAnchorInput{
|
||||||
|
inputKit: inputKit{
|
||||||
|
outpoint: signedTx.TxIn[0].PreviousOutPoint,
|
||||||
|
witnessType: HtlcAcceptedSuccessSecondLevelInputConfirmed,
|
||||||
|
signDesc: signDetails.SignDesc,
|
||||||
|
heightHint: heightHint,
|
||||||
|
|
||||||
|
// CSV delay is always 1 for these inputs.
|
||||||
|
blockToMaturity: 1,
|
||||||
|
},
|
||||||
|
SignedTx: signedTx,
|
||||||
|
createWitness: createWitness,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Compile-time constraints to ensure each input struct implement the Input
|
// Compile-time constraints to ensure each input struct implement the Input
|
||||||
// interface.
|
// interface.
|
||||||
var _ Input = (*BaseInput)(nil)
|
var _ Input = (*BaseInput)(nil)
|
||||||
var _ Input = (*HtlcSucceedInput)(nil)
|
var _ Input = (*HtlcSucceedInput)(nil)
|
||||||
|
var _ Input = (*HtlcSecondLevelAnchorInput)(nil)
|
||||||
|
@ -328,6 +328,12 @@ const (
|
|||||||
AcceptedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 8*1 + 20 + 4*1 +
|
AcceptedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 8*1 + 20 + 4*1 +
|
||||||
33 + 5*1 + 4 + 8*1
|
33 + 5*1 + 4 + 8*1
|
||||||
|
|
||||||
|
// AcceptedHtlcScriptSizeConfirmed 143 bytes
|
||||||
|
//
|
||||||
|
// TODO(halseth): the non-confirmed version currently includes the
|
||||||
|
// overhead.
|
||||||
|
AcceptedHtlcScriptSizeConfirmed = AcceptedHtlcScriptSize // + HtlcConfirmedScriptOverhead
|
||||||
|
|
||||||
// AcceptedHtlcTimeoutWitnessSize 219
|
// AcceptedHtlcTimeoutWitnessSize 219
|
||||||
// - number_of_witness_elements: 1 byte
|
// - number_of_witness_elements: 1 byte
|
||||||
// - sender_sig_length: 1 byte
|
// - sender_sig_length: 1 byte
|
||||||
@ -361,6 +367,12 @@ const (
|
|||||||
AcceptedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
|
AcceptedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
|
||||||
AcceptedHtlcScriptSize
|
AcceptedHtlcScriptSize
|
||||||
|
|
||||||
|
// AcceptedHtlcSuccessWitnessSizeConfirmed 327 bytes
|
||||||
|
//
|
||||||
|
// Input to second level success tx, spending 1 CSV delayed HTLC output.
|
||||||
|
AcceptedHtlcSuccessWitnessSizeConfirmed = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
|
||||||
|
AcceptedHtlcScriptSizeConfirmed
|
||||||
|
|
||||||
// OfferedHtlcScriptSize 136 bytes
|
// OfferedHtlcScriptSize 136 bytes
|
||||||
// - OP_DUP: 1 byte
|
// - OP_DUP: 1 byte
|
||||||
// - OP_HASH160: 1 byte
|
// - OP_HASH160: 1 byte
|
||||||
@ -398,6 +410,12 @@ const (
|
|||||||
// - OP_ENDIF: 1 byte
|
// - OP_ENDIF: 1 byte
|
||||||
OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 7*1
|
OfferedHtlcScriptSize = 3*1 + 20 + 5*1 + 33 + 10*1 + 33 + 5*1 + 20 + 7*1
|
||||||
|
|
||||||
|
// OfferedHtlcScriptSizeConfirmed 136 bytes
|
||||||
|
//
|
||||||
|
// TODO(halseth): the non-confirmed version currently includes the
|
||||||
|
// overhead.
|
||||||
|
OfferedHtlcScriptSizeConfirmed = OfferedHtlcScriptSize // + HtlcConfirmedScriptOverhead
|
||||||
|
|
||||||
// OfferedHtlcSuccessWitnessSize 245 bytes
|
// OfferedHtlcSuccessWitnessSize 245 bytes
|
||||||
// - number_of_witness_elements: 1 byte
|
// - number_of_witness_elements: 1 byte
|
||||||
// - receiver_sig_length: 1 byte
|
// - receiver_sig_length: 1 byte
|
||||||
@ -420,6 +438,12 @@ const (
|
|||||||
// - witness_script (offered_htlc_script)
|
// - witness_script (offered_htlc_script)
|
||||||
OfferedHtlcTimeoutWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize
|
OfferedHtlcTimeoutWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 + OfferedHtlcScriptSize
|
||||||
|
|
||||||
|
// OfferedHtlcTimeoutWitnessSizeConfirmed 288 bytes
|
||||||
|
//
|
||||||
|
// Input to second level timeout tx, spending 1 CSV delayed HTLC output.
|
||||||
|
OfferedHtlcTimeoutWitnessSizeConfirmed = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 1 +
|
||||||
|
OfferedHtlcScriptSizeConfirmed
|
||||||
|
|
||||||
// OfferedHtlcPenaltyWitnessSize 246 bytes
|
// OfferedHtlcPenaltyWitnessSize 246 bytes
|
||||||
// - number_of_witness_elements: 1 byte
|
// - number_of_witness_elements: 1 byte
|
||||||
// - revocation_sig_length: 1 byte
|
// - revocation_sig_length: 1 byte
|
||||||
|
@ -80,6 +80,14 @@ const (
|
|||||||
// result, we can only spend this after a CSV delay.
|
// result, we can only spend this after a CSV delay.
|
||||||
HtlcOfferedTimeoutSecondLevel StandardWitnessType = 5
|
HtlcOfferedTimeoutSecondLevel StandardWitnessType = 5
|
||||||
|
|
||||||
|
// HtlcOfferedTimeoutSecondLevelInputConfirmed is a witness that allows
|
||||||
|
// us to sweep an HTLC output that we extended to a party, but was
|
||||||
|
// never fulfilled. This _is_ the HTLC output directly on our
|
||||||
|
// commitment transaction, and the input to the second-level HTLC
|
||||||
|
// tiemout transaction. It can only be spent after CLTV expiry, and
|
||||||
|
// commitment confirmation.
|
||||||
|
HtlcOfferedTimeoutSecondLevelInputConfirmed StandardWitnessType = 15
|
||||||
|
|
||||||
// HtlcAcceptedSuccessSecondLevel is a witness that allows us to sweep
|
// HtlcAcceptedSuccessSecondLevel is a witness that allows us to sweep
|
||||||
// an HTLC output that was offered to us, and for which we have a
|
// an HTLC output that was offered to us, and for which we have a
|
||||||
// payment preimage. This HTLC output isn't directly on our commitment
|
// payment preimage. This HTLC output isn't directly on our commitment
|
||||||
@ -87,6 +95,14 @@ const (
|
|||||||
// transaction. As a result, we can only spend this after a CSV delay.
|
// transaction. As a result, we can only spend this after a CSV delay.
|
||||||
HtlcAcceptedSuccessSecondLevel StandardWitnessType = 6
|
HtlcAcceptedSuccessSecondLevel StandardWitnessType = 6
|
||||||
|
|
||||||
|
// HtlcAcceptedSuccessSecondLevelInputConfirmed is a witness that
|
||||||
|
// allows us to sweep an HTLC output that was offered to us, and for
|
||||||
|
// which we have a payment preimage. This _is_ the HTLC output directly
|
||||||
|
// on our commitment transaction, and the input to the second-level
|
||||||
|
// HTLC success transaction. It can only be spent after the commitment
|
||||||
|
// has confirmed.
|
||||||
|
HtlcAcceptedSuccessSecondLevelInputConfirmed StandardWitnessType = 16
|
||||||
|
|
||||||
// HtlcOfferedRemoteTimeout is a witness that allows us to sweep an
|
// HtlcOfferedRemoteTimeout is a witness that allows us to sweep an
|
||||||
// HTLC that we offered to the remote party which lies in the
|
// HTLC that we offered to the remote party which lies in the
|
||||||
// commitment transaction of the remote party. We can spend this output
|
// commitment transaction of the remote party. We can spend this output
|
||||||
@ -163,9 +179,15 @@ func (wt StandardWitnessType) String() string {
|
|||||||
case HtlcOfferedTimeoutSecondLevel:
|
case HtlcOfferedTimeoutSecondLevel:
|
||||||
return "HtlcOfferedTimeoutSecondLevel"
|
return "HtlcOfferedTimeoutSecondLevel"
|
||||||
|
|
||||||
|
case HtlcOfferedTimeoutSecondLevelInputConfirmed:
|
||||||
|
return "HtlcOfferedTimeoutSecondLevelInputConfirmed"
|
||||||
|
|
||||||
case HtlcAcceptedSuccessSecondLevel:
|
case HtlcAcceptedSuccessSecondLevel:
|
||||||
return "HtlcAcceptedSuccessSecondLevel"
|
return "HtlcAcceptedSuccessSecondLevel"
|
||||||
|
|
||||||
|
case HtlcAcceptedSuccessSecondLevelInputConfirmed:
|
||||||
|
return "HtlcAcceptedSuccessSecondLevelInputConfirmed"
|
||||||
|
|
||||||
case HtlcOfferedRemoteTimeout:
|
case HtlcOfferedRemoteTimeout:
|
||||||
return "HtlcOfferedRemoteTimeout"
|
return "HtlcOfferedRemoteTimeout"
|
||||||
|
|
||||||
@ -375,12 +397,20 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) {
|
|||||||
case HtlcOfferedTimeoutSecondLevel:
|
case HtlcOfferedTimeoutSecondLevel:
|
||||||
return ToLocalTimeoutWitnessSize, false, nil
|
return ToLocalTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
|
// Input to the outgoing HTLC second layer timeout transaction.
|
||||||
|
case HtlcOfferedTimeoutSecondLevelInputConfirmed:
|
||||||
|
return OfferedHtlcTimeoutWitnessSizeConfirmed, false, nil
|
||||||
|
|
||||||
// Incoming second layer HTLC's that have confirmed within the
|
// Incoming second layer HTLC's that have confirmed within the
|
||||||
// chain, and the output they produced is now mature enough to
|
// chain, and the output they produced is now mature enough to
|
||||||
// sweep.
|
// sweep.
|
||||||
case HtlcAcceptedSuccessSecondLevel:
|
case HtlcAcceptedSuccessSecondLevel:
|
||||||
return ToLocalTimeoutWitnessSize, false, nil
|
return ToLocalTimeoutWitnessSize, false, nil
|
||||||
|
|
||||||
|
// Input to the incoming second-layer HTLC success transaction.
|
||||||
|
case HtlcAcceptedSuccessSecondLevelInputConfirmed:
|
||||||
|
return AcceptedHtlcSuccessWitnessSizeConfirmed, false, nil
|
||||||
|
|
||||||
// An HTLC on the commitment transaction of the remote party,
|
// An HTLC on the commitment transaction of the remote party,
|
||||||
// that has had its absolute timelock expire.
|
// that has had its absolute timelock expire.
|
||||||
case HtlcOfferedRemoteTimeout:
|
case HtlcOfferedRemoteTimeout:
|
||||||
|
427
lntest/itest/lnd_multi-hop_htlc_aggregation_test.go
Normal file
427
lntest/itest/lnd_multi-hop_htlc_aggregation_test.go
Normal file
@ -0,0 +1,427 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/lncfg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/invoicesrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// testMultiHopHtlcAggregation tests that in a multi-hop HTLC scenario, if we
|
||||||
|
// force close a channel with both incoming and outgoing HTLCs, we can properly
|
||||||
|
// resolve them using the second level timeout and success transactions. In
|
||||||
|
// case of anchor channels, the second-level spends can also be aggregated and
|
||||||
|
// properly feebumped, so we'll check that as well.
|
||||||
|
func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest,
|
||||||
|
alice, bob *lntest.HarnessNode, c commitType) {
|
||||||
|
|
||||||
|
const finalCltvDelta = 40
|
||||||
|
ctxb := context.Background()
|
||||||
|
|
||||||
|
// First, we'll create a three hop network: Alice -> Bob -> Carol.
|
||||||
|
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
|
||||||
|
t, net, alice, bob, false, c,
|
||||||
|
)
|
||||||
|
defer shutdownAndAssert(net, t, carol)
|
||||||
|
|
||||||
|
// To ensure we have capacity in both directions of the route, we'll
|
||||||
|
// make a fairly large payment Alice->Carol and settle it.
|
||||||
|
const reBalanceAmt = 500_000
|
||||||
|
invoice := &lnrpc.Invoice{
|
||||||
|
Value: reBalanceAmt,
|
||||||
|
}
|
||||||
|
ctxt, _ := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
resp, err := carol.AddInvoice(ctxt, invoice)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
sendReq := &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: resp.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
}
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
stream, err := alice.RouterClient.SendPaymentV2(ctxt, sendReq)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
result, err := getPaymentResult(stream)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
require.Equal(t.t, result.Status, lnrpc.Payment_SUCCEEDED)
|
||||||
|
|
||||||
|
// With the network active, we'll now add a new hodl invoices at both
|
||||||
|
// Alice's and Carol's end. Make sure the cltv expiry delta is large
|
||||||
|
// enough, otherwise Bob won't send out the outgoing htlc.
|
||||||
|
const numInvoices = 5
|
||||||
|
const invoiceAmt = 50_000
|
||||||
|
|
||||||
|
var (
|
||||||
|
carolInvoices []*invoicesrpc.AddHoldInvoiceResp
|
||||||
|
aliceInvoices []*invoicesrpc.AddHoldInvoiceResp
|
||||||
|
alicePreimages []lntypes.Preimage
|
||||||
|
payHashes [][]byte
|
||||||
|
alicePayHashes [][]byte
|
||||||
|
carolPayHashes [][]byte
|
||||||
|
)
|
||||||
|
|
||||||
|
// Add Carol invoices.
|
||||||
|
for i := 0; i < numInvoices; i++ {
|
||||||
|
preimage := lntypes.Preimage{1, 1, 1, byte(i)}
|
||||||
|
payHash := preimage.Hash()
|
||||||
|
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||||
|
Value: invoiceAmt,
|
||||||
|
CltvExpiry: finalCltvDelta,
|
||||||
|
Hash: payHash[:],
|
||||||
|
}
|
||||||
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
carolInvoice, err := carol.AddHoldInvoice(ctxt, invoiceReq)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
carolInvoices = append(carolInvoices, carolInvoice)
|
||||||
|
payHashes = append(payHashes, payHash[:])
|
||||||
|
carolPayHashes = append(carolPayHashes, payHash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll give Alice's invoices a longer CLTV expiry, to ensure the
|
||||||
|
// channel Bob<->Carol will be closed first.
|
||||||
|
for i := 0; i < numInvoices; i++ {
|
||||||
|
preimage := lntypes.Preimage{2, 2, 2, byte(i)}
|
||||||
|
payHash := preimage.Hash()
|
||||||
|
invoiceReq := &invoicesrpc.AddHoldInvoiceRequest{
|
||||||
|
Value: invoiceAmt,
|
||||||
|
CltvExpiry: 2 * finalCltvDelta,
|
||||||
|
Hash: payHash[:],
|
||||||
|
}
|
||||||
|
ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
aliceInvoice, err := alice.AddHoldInvoice(ctxt, invoiceReq)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
aliceInvoices = append(aliceInvoices, aliceInvoice)
|
||||||
|
alicePreimages = append(alicePreimages, preimage)
|
||||||
|
payHashes = append(payHashes, payHash[:])
|
||||||
|
alicePayHashes = append(alicePayHashes, payHash[:])
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we've created the invoices, we'll pay them all from
|
||||||
|
// Alice<->Carol, going through Bob. We won't wait for the response
|
||||||
|
// however, as neither will immediately settle the payment.
|
||||||
|
ctx, cancel := context.WithCancel(ctxb)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Alice will pay all of Carol's invoices.
|
||||||
|
for _, carolInvoice := range carolInvoices {
|
||||||
|
_, err = alice.RouterClient.SendPaymentV2(
|
||||||
|
ctx, &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: carolInvoice.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// And Carol will pay Alice's.
|
||||||
|
for _, aliceInvoice := range aliceInvoices {
|
||||||
|
_, err = carol.RouterClient.SendPaymentV2(
|
||||||
|
ctx, &routerrpc.SendPaymentRequest{
|
||||||
|
PaymentRequest: aliceInvoice.PaymentRequest,
|
||||||
|
TimeoutSeconds: 60,
|
||||||
|
FeeLimitMsat: noFeeLimitMsat,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// At this point, all 3 nodes should now the HTLCs active on their
|
||||||
|
// channels.
|
||||||
|
nodes := []*lntest.HarnessNode{alice, bob, carol}
|
||||||
|
err = wait.NoError(func() error {
|
||||||
|
return assertActiveHtlcs(nodes, payHashes...)
|
||||||
|
}, defaultTimeout)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Wait for Alice and Carol to mark the invoices as accepted. There is
|
||||||
|
// a small gap to bridge between adding the htlc to the channel and
|
||||||
|
// executing the exit hop logic.
|
||||||
|
for _, payHash := range carolPayHashes {
|
||||||
|
h := lntypes.Hash{}
|
||||||
|
copy(h[:], payHash)
|
||||||
|
waitForInvoiceAccepted(t, carol, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, payHash := range alicePayHashes {
|
||||||
|
h := lntypes.Hash{}
|
||||||
|
copy(h[:], payHash)
|
||||||
|
waitForInvoiceAccepted(t, alice, h)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the fee estimate so that the following force close tx will
|
||||||
|
// be cpfp'ed.
|
||||||
|
net.SetFeeEstimate(30000)
|
||||||
|
|
||||||
|
// We'll now mine enough blocks to trigger Bob's broadcast of his
|
||||||
|
// commitment transaction due to the fact that the Carol's HTLCs are
|
||||||
|
// about to timeout. With the default outgoing broadcast delta of zero,
|
||||||
|
// this will be the same height as the htlc expiry height.
|
||||||
|
numBlocks := padCLTV(
|
||||||
|
uint32(finalCltvDelta - lncfg.DefaultOutgoingBroadcastDelta),
|
||||||
|
)
|
||||||
|
_, err = net.Miner.Node.Generate(numBlocks)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Bob's force close transaction should now be found in the mempool. If
|
||||||
|
// there are anchors, we also expect Bob's anchor sweep.
|
||||||
|
expectedTxes := 1
|
||||||
|
if c == commitTypeAnchors {
|
||||||
|
expectedTxes = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
bobFundingTxid, err := lnd.GetChanPointFundingTxid(bobChanPoint)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
_, err = waitForNTxsInMempool(
|
||||||
|
net.Miner.Node, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
closeTx := getSpendingTxInMempool(
|
||||||
|
t, net.Miner.Node, minerMempoolTimeout, wire.OutPoint{
|
||||||
|
Hash: *bobFundingTxid,
|
||||||
|
Index: bobChanPoint.OutputIndex,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
closeTxid := closeTx.TxHash()
|
||||||
|
|
||||||
|
// Go through the closing transaction outputs, and make an index for the HTLC outputs.
|
||||||
|
successOuts := make(map[wire.OutPoint]struct{})
|
||||||
|
timeoutOuts := make(map[wire.OutPoint]struct{})
|
||||||
|
for i, txOut := range closeTx.TxOut {
|
||||||
|
op := wire.OutPoint{
|
||||||
|
Hash: closeTxid,
|
||||||
|
Index: uint32(i),
|
||||||
|
}
|
||||||
|
|
||||||
|
switch txOut.Value {
|
||||||
|
// If this HTLC goes towards Carol, Bob will claim it with a
|
||||||
|
// timeout Tx. In this case the value will be the invoice
|
||||||
|
// amount.
|
||||||
|
case invoiceAmt:
|
||||||
|
timeoutOuts[op] = struct{}{}
|
||||||
|
|
||||||
|
// If the HTLC has direction towards Alice, Bob will
|
||||||
|
// claim it with the success TX when he learns the preimage. In
|
||||||
|
// this case one extra sat will be on the output, because of
|
||||||
|
// the routing fee.
|
||||||
|
case invoiceAmt + 1:
|
||||||
|
successOuts[op] = struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mine a block to confirm the closing transaction.
|
||||||
|
mineBlocks(t, net, 1, expectedTxes)
|
||||||
|
|
||||||
|
time.Sleep(1 * time.Second)
|
||||||
|
|
||||||
|
// Let Alice settle her invoices. When Bob now gets the preimages, he
|
||||||
|
// has no other option than to broadcast his second-level transactions
|
||||||
|
// to claim the money.
|
||||||
|
for _, preimage := range alicePreimages {
|
||||||
|
ctx, cancel = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
defer cancel()
|
||||||
|
_, err = alice.SettleInvoice(ctx, &invoicesrpc.SettleInvoiceMsg{
|
||||||
|
Preimage: preimage[:],
|
||||||
|
})
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the closing transaction confirmed, we should expect Bob's HTLC
|
||||||
|
// timeout transactions to be broadcast due to the expiry being reached.
|
||||||
|
// We will also expect the success transactions, since he learnt the
|
||||||
|
// preimages from Alice. We also expect Carol to sweep her commitment
|
||||||
|
// output.
|
||||||
|
expectedTxes = 2*numInvoices + 1
|
||||||
|
|
||||||
|
// In case of anchors, all success transactions will be aggregated into
|
||||||
|
// one, the same is the case for the timeout transactions. In this case
|
||||||
|
// Carol will also sweep her anchor output in a separate tx (since it
|
||||||
|
// will be low fee).
|
||||||
|
if c == commitTypeAnchors {
|
||||||
|
expectedTxes = 4
|
||||||
|
}
|
||||||
|
|
||||||
|
txes, err := getNTxsFromMempool(
|
||||||
|
net.Miner.Node, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Since Bob can aggregate the transactions, we expect a single
|
||||||
|
// transaction, that have multiple spends from the commitment.
|
||||||
|
var (
|
||||||
|
timeoutTxs []*chainhash.Hash
|
||||||
|
successTxs []*chainhash.Hash
|
||||||
|
)
|
||||||
|
for _, tx := range txes {
|
||||||
|
txid := tx.TxHash()
|
||||||
|
|
||||||
|
for i := range tx.TxIn {
|
||||||
|
prevOp := tx.TxIn[i].PreviousOutPoint
|
||||||
|
if _, ok := successOuts[prevOp]; ok {
|
||||||
|
successTxs = append(successTxs, &txid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := timeoutOuts[prevOp]; ok {
|
||||||
|
timeoutTxs = append(timeoutTxs, &txid)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// In case of anchor we expect all the timeout and success second
|
||||||
|
// levels to be aggregated into one tx. For earlier channel types, they
|
||||||
|
// will be separate transactions.
|
||||||
|
if c == commitTypeAnchors {
|
||||||
|
require.Len(t.t, timeoutTxs, 1)
|
||||||
|
require.Len(t.t, successTxs, 1)
|
||||||
|
} else {
|
||||||
|
require.Len(t.t, timeoutTxs, numInvoices)
|
||||||
|
require.Len(t.t, successTxs, numInvoices)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
// All mempool transactions should be spending from the commitment
|
||||||
|
// transaction.
|
||||||
|
assertAllTxesSpendFrom(t, txes, closeTxid)
|
||||||
|
|
||||||
|
// Mine a block to confirm the transactions.
|
||||||
|
block := mineBlocks(t, net, 1, expectedTxes)[0]
|
||||||
|
require.Len(t.t, block.Transactions, expectedTxes+1)
|
||||||
|
|
||||||
|
// At this point, Bob should have broadcast his second layer success
|
||||||
|
// transaction, and should have sent it to the nursery for incubation,
|
||||||
|
// or to the sweeper for sweeping.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = waitForNumChannelPendingForceClose(
|
||||||
|
ctxt, bob, 1, func(c *lnrpcForceCloseChannel) error {
|
||||||
|
if c.Channel.LocalBalance != 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.PendingHtlcs) != 1 {
|
||||||
|
return fmt.Errorf("bob should have pending " +
|
||||||
|
"htlc but doesn't")
|
||||||
|
}
|
||||||
|
|
||||||
|
if c.PendingHtlcs[0].Stage != 1 {
|
||||||
|
return fmt.Errorf("bob's htlc should have "+
|
||||||
|
"advanced to the first stage but was "+
|
||||||
|
"stage: %v", c.PendingHtlcs[0].Stage)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// If we then mine additional blocks, Bob can sweep his commitment
|
||||||
|
// output.
|
||||||
|
_, err = net.Miner.Node.Generate(defaultCSV - 2)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Find the commitment sweep.
|
||||||
|
bobCommitSweepHash, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
bobCommitSweep, err := net.Miner.Node.GetRawTransaction(bobCommitSweepHash)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
require.Equal(
|
||||||
|
t.t, closeTxid, bobCommitSweep.MsgTx().TxIn[0].PreviousOutPoint.Hash,
|
||||||
|
)
|
||||||
|
|
||||||
|
// Also ensure it is not spending from any of the HTLC output.
|
||||||
|
for _, txin := range bobCommitSweep.MsgTx().TxIn {
|
||||||
|
for _, timeoutTx := range timeoutTxs {
|
||||||
|
if *timeoutTx == txin.PreviousOutPoint.Hash {
|
||||||
|
t.Fatalf("found unexpected spend of timeout tx")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, successTx := range successTxs {
|
||||||
|
if *successTx == txin.PreviousOutPoint.Hash {
|
||||||
|
t.Fatalf("found unexpected spend of success tx")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
|
||||||
|
// Mining one additional block, Bob's second level tx is mature, and he
|
||||||
|
// can sweep the output.
|
||||||
|
case c == commitTypeAnchors:
|
||||||
|
_ = mineBlocks(t, net, 1, 1)
|
||||||
|
|
||||||
|
// In case this is a non-anchor channel type, we must mine 2 blocks, as
|
||||||
|
// the nursery waits an extra block before sweeping.
|
||||||
|
default:
|
||||||
|
_ = mineBlocks(t, net, 2, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
bobSweep, err := waitForTxInMempool(net.Miner.Node, minerMempoolTimeout)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// Make sure it spends from the second level tx.
|
||||||
|
secondLevelSweep, err := net.Miner.Node.GetRawTransaction(bobSweep)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// It should be sweeping all the second-level outputs.
|
||||||
|
var secondLvlSpends int
|
||||||
|
for _, txin := range secondLevelSweep.MsgTx().TxIn {
|
||||||
|
for _, timeoutTx := range timeoutTxs {
|
||||||
|
if *timeoutTx == txin.PreviousOutPoint.Hash {
|
||||||
|
secondLvlSpends++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, successTx := range successTxs {
|
||||||
|
if *successTx == txin.PreviousOutPoint.Hash {
|
||||||
|
secondLvlSpends++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
require.Equal(t.t, 2*numInvoices, secondLvlSpends)
|
||||||
|
|
||||||
|
// When we mine one additional block, that will confirm Bob's second
|
||||||
|
// level sweep. Now Bob should have no pending channels anymore, as
|
||||||
|
// this just resolved it by the confirmation of the sweep transaction.
|
||||||
|
block = mineBlocks(t, net, 1, 1)[0]
|
||||||
|
assertTxInBlock(t, block, bobSweep)
|
||||||
|
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
|
err = waitForNumChannelPendingForceClose(ctxt, bob, 0, nil)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
|
||||||
|
// THe channel with Alice is still open.
|
||||||
|
assertNodeNumChannels(t, bob, 1)
|
||||||
|
|
||||||
|
// Carol should have no channels left (open nor pending).
|
||||||
|
err = waitForNumChannelPendingForceClose(ctxt, carol, 0, nil)
|
||||||
|
require.NoError(t.t, err)
|
||||||
|
assertNodeNumChannels(t, carol, 0)
|
||||||
|
|
||||||
|
// Coop close channel, expect no anchors.
|
||||||
|
ctxt, _ = context.WithTimeout(ctxb, channelCloseTimeout)
|
||||||
|
closeChannelAndAssertType(
|
||||||
|
ctxt, t, net, alice, aliceChanPoint, false, false,
|
||||||
|
)
|
||||||
|
}
|
@ -184,8 +184,24 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
block = mineBlocks(t, net, 1, expectedTxes)[0]
|
block = mineBlocks(t, net, 1, expectedTxes)[0]
|
||||||
require.Len(t.t, block.Transactions, expectedTxes+1)
|
require.Len(t.t, block.Transactions, expectedTxes+1)
|
||||||
|
|
||||||
|
var secondLevelMaturity uint32
|
||||||
|
switch c {
|
||||||
|
|
||||||
|
// If this is a channel of the anchor type, we will subtract one block
|
||||||
|
// from the default CSV, as the Sweeper will handle the input, and the Sweeper
|
||||||
|
// sweeps the input as soon as the lock expires.
|
||||||
|
case commitTypeAnchors:
|
||||||
|
secondLevelMaturity = defaultCSV - 1
|
||||||
|
|
||||||
|
// For non-anchor channel types, the nursery will handle sweeping the
|
||||||
|
// second level output, and it will wait one extra block before
|
||||||
|
// sweeping it.
|
||||||
|
default:
|
||||||
|
secondLevelMaturity = defaultCSV
|
||||||
|
}
|
||||||
|
|
||||||
// Keep track of the second level tx maturity.
|
// Keep track of the second level tx maturity.
|
||||||
carolSecondLevelCSV := uint32(defaultCSV)
|
carolSecondLevelCSV := secondLevelMaturity
|
||||||
|
|
||||||
// When Bob notices Carol's second level transaction in the block, he
|
// When Bob notices Carol's second level transaction in the block, he
|
||||||
// will extract the preimage and broadcast a second level tx to claim
|
// will extract the preimage and broadcast a second level tx to claim
|
||||||
@ -236,7 +252,7 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
|
|
||||||
// Keep track of Bob's second level maturity, and decrement our track
|
// Keep track of Bob's second level maturity, and decrement our track
|
||||||
// of Carol's.
|
// of Carol's.
|
||||||
bobSecondLevelCSV := uint32(defaultCSV)
|
bobSecondLevelCSV := secondLevelMaturity
|
||||||
carolSecondLevelCSV--
|
carolSecondLevelCSV--
|
||||||
|
|
||||||
// Now that the preimage from Bob has hit the chain, restart Alice to
|
// Now that the preimage from Bob has hit the chain, restart Alice to
|
||||||
|
@ -43,7 +43,7 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
// while the second will be a proper fully valued HTLC.
|
// while the second will be a proper fully valued HTLC.
|
||||||
const (
|
const (
|
||||||
dustHtlcAmt = btcutil.Amount(100)
|
dustHtlcAmt = btcutil.Amount(100)
|
||||||
htlcAmt = btcutil.Amount(30000)
|
htlcAmt = btcutil.Amount(300_000)
|
||||||
finalCltvDelta = 40
|
finalCltvDelta = 40
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -36,7 +36,7 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
|
|||||||
// opens up the base for out tests.
|
// opens up the base for out tests.
|
||||||
const (
|
const (
|
||||||
finalCltvDelta = 40
|
finalCltvDelta = 40
|
||||||
htlcAmt = btcutil.Amount(30000)
|
htlcAmt = btcutil.Amount(300_000)
|
||||||
)
|
)
|
||||||
ctx, cancel := context.WithCancel(ctxb)
|
ctx, cancel := context.WithCancel(ctxb)
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
@ -61,6 +61,11 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
|
|||||||
name: "remote chain claim",
|
name: "remote chain claim",
|
||||||
test: testMultiHopHtlcRemoteChainClaim,
|
test: testMultiHopHtlcRemoteChainClaim,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
// bob: outgoing and incoming, sweep all on chain
|
||||||
|
name: "local htlc aggregation",
|
||||||
|
test: testMultiHopHtlcAggregation,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
commitTypes := []commitType{
|
commitTypes := []commitType{
|
||||||
|
@ -3640,6 +3640,16 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
htlcCsvMaturityHeight = padCLTV(startHeight + defaultCLTV + 1 + defaultCSV)
|
htlcCsvMaturityHeight = padCLTV(startHeight + defaultCLTV + 1 + defaultCSV)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// If we are dealing with an anchor channel type, the sweeper will
|
||||||
|
// sweep the HTLC second level output one block earlier (than the
|
||||||
|
// nursery that waits an additional block, and handles non-anchor
|
||||||
|
// channels). So we set a maturity height that is one less.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
htlcCsvMaturityHeight = padCLTV(
|
||||||
|
startHeight + defaultCLTV + defaultCSV,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
ctxt, _ = context.WithTimeout(ctxb, defaultTimeout)
|
||||||
aliceChan, err := getChanInfo(ctxt, alice)
|
aliceChan, err := getChanInfo(ctxt, alice)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -4157,79 +4167,125 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
|
|
||||||
// Since Alice had numInvoices (6) htlcs extended to Carol before force
|
// Since Alice had numInvoices (6) htlcs extended to Carol before force
|
||||||
// closing, we expect Alice to broadcast an htlc timeout txn for each
|
// closing, we expect Alice to broadcast an htlc timeout txn for each
|
||||||
// one. Wait for them all to show up in the mempool.
|
// one.
|
||||||
htlcTxIDs, err := waitForNTxsInMempool(net.Miner.Node, numInvoices,
|
expectedTxes = numInvoices
|
||||||
minerMempoolTimeout)
|
|
||||||
|
// In case of anchors, the timeout txs will be aggregated into one.
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
expectedTxes = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for them all to show up in the mempool.
|
||||||
|
htlcTxIDs, err := waitForNTxsInMempool(
|
||||||
|
net.Miner.Node, expectedTxes, minerMempoolTimeout,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
|
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Retrieve each htlc timeout txn from the mempool, and ensure it is
|
// Retrieve each htlc timeout txn from the mempool, and ensure it is
|
||||||
// well-formed. This entails verifying that each only spends from
|
// well-formed. This entails verifying that each only spends from
|
||||||
// output, and that that output is from the commitment txn. We do not
|
// output, and that that output is from the commitment txn. In case
|
||||||
// the sweeper check for these timeout transactions because they are
|
// this is an anchor channel, the transactions are aggregated by the
|
||||||
// not swept by the sweeper; the nursery broadcasts the pre-signed
|
// sweeper into one.
|
||||||
// transaction.
|
numInputs := 1
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
numInputs = numInvoices + 1
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construct a map of the already confirmed htlc timeout outpoints,
|
||||||
|
// that will count the number of times each is spent by the sweep txn.
|
||||||
|
// We prepopulate it in this way so that we can later detect if we are
|
||||||
|
// spending from an output that was not a confirmed htlc timeout txn.
|
||||||
|
var htlcTxOutpointSet = make(map[wire.OutPoint]int)
|
||||||
|
|
||||||
var htlcLessFees uint64
|
var htlcLessFees uint64
|
||||||
for _, htlcTxID := range htlcTxIDs {
|
for _, htlcTxID := range htlcTxIDs {
|
||||||
// Fetch the sweep transaction, all input it's spending should
|
// Fetch the sweep transaction, all input it's spending should
|
||||||
// be from the commitment transaction which was broadcast
|
// be from the commitment transaction which was broadcast
|
||||||
// on-chain.
|
// on-chain. In case of an anchor type channel, we expect one
|
||||||
|
// extra input that is not spending from the commitment, that
|
||||||
|
// is added for fees.
|
||||||
htlcTx, err := net.Miner.Node.GetRawTransaction(htlcTxID)
|
htlcTx, err := net.Miner.Node.GetRawTransaction(htlcTxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to fetch sweep tx: %v", err)
|
t.Fatalf("unable to fetch sweep tx: %v", err)
|
||||||
}
|
}
|
||||||
// Ensure the htlc transaction only has one input.
|
|
||||||
|
// Ensure the htlc transaction has the expected number of
|
||||||
|
// inputs.
|
||||||
inputs := htlcTx.MsgTx().TxIn
|
inputs := htlcTx.MsgTx().TxIn
|
||||||
if len(inputs) != 1 {
|
if len(inputs) != numInputs {
|
||||||
t.Fatalf("htlc transaction should only have one txin, "+
|
t.Fatalf("htlc transaction should only have %d txin, "+
|
||||||
"has %d", len(htlcTx.MsgTx().TxIn))
|
"has %d", numInputs, len(htlcTx.MsgTx().TxIn))
|
||||||
}
|
|
||||||
// Ensure the htlc transaction is spending from the commitment
|
|
||||||
// transaction.
|
|
||||||
txIn := inputs[0]
|
|
||||||
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
|
||||||
t.Fatalf("htlc transaction not spending from commit "+
|
|
||||||
"tx %v, instead spending %v",
|
|
||||||
closingTxID, txIn.PreviousOutPoint)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The number of outputs should be the same.
|
||||||
outputs := htlcTx.MsgTx().TxOut
|
outputs := htlcTx.MsgTx().TxOut
|
||||||
if len(outputs) != 1 {
|
if len(outputs) != numInputs {
|
||||||
t.Fatalf("htlc transaction should only have one "+
|
t.Fatalf("htlc transaction should only have %d"+
|
||||||
"txout, has: %v", len(outputs))
|
"txout, has: %v", numInputs, len(outputs))
|
||||||
}
|
}
|
||||||
|
|
||||||
// For each htlc timeout transaction, we expect a resolver
|
// Ensure all the htlc transaction inputs are spending from the
|
||||||
// report recording this on chain resolution for both alice and
|
// commitment transaction, except if this is an extra input
|
||||||
// carol.
|
// added to pay for fees for anchor channels.
|
||||||
outpoint := txIn.PreviousOutPoint
|
nonCommitmentInputs := 0
|
||||||
resolutionOutpoint := &lnrpc.OutPoint{
|
for i, txIn := range inputs {
|
||||||
TxidBytes: outpoint.Hash[:],
|
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
||||||
TxidStr: outpoint.Hash.String(),
|
nonCommitmentInputs++
|
||||||
OutputIndex: outpoint.Index,
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect alice to have a timeout tx resolution with an
|
if nonCommitmentInputs > 1 {
|
||||||
// amount equal to the payment amount.
|
t.Fatalf("htlc transaction not "+
|
||||||
aliceReports[outpoint.String()] = &lnrpc.Resolution{
|
"spending from commit "+
|
||||||
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
"tx %v, instead spending %v",
|
||||||
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
|
closingTxID,
|
||||||
SweepTxid: htlcTx.Hash().String(),
|
txIn.PreviousOutPoint)
|
||||||
Outpoint: resolutionOutpoint,
|
}
|
||||||
AmountSat: uint64(paymentAmt),
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect carol to have a resolution with an incoming htlc
|
// This was an extra input added to pay fees,
|
||||||
// timeout which reflects the full amount of the htlc. It has
|
// continue to the next one.
|
||||||
// no spend tx, because carol stops monitoring the htlc once
|
continue
|
||||||
// it has timed out.
|
}
|
||||||
carolReports[outpoint.String()] = &lnrpc.Resolution{
|
|
||||||
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
|
// For each htlc timeout transaction, we expect a
|
||||||
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
// resolver report recording this on chain resolution
|
||||||
SweepTxid: "",
|
// for both alice and carol.
|
||||||
Outpoint: resolutionOutpoint,
|
outpoint := txIn.PreviousOutPoint
|
||||||
AmountSat: uint64(paymentAmt),
|
resolutionOutpoint := &lnrpc.OutPoint{
|
||||||
|
TxidBytes: outpoint.Hash[:],
|
||||||
|
TxidStr: outpoint.Hash.String(),
|
||||||
|
OutputIndex: outpoint.Index,
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect alice to have a timeout tx resolution with
|
||||||
|
// an amount equal to the payment amount.
|
||||||
|
aliceReports[outpoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_OUTGOING_HTLC,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_FIRST_STAGE,
|
||||||
|
SweepTxid: htlcTx.Hash().String(),
|
||||||
|
Outpoint: resolutionOutpoint,
|
||||||
|
AmountSat: uint64(paymentAmt),
|
||||||
|
}
|
||||||
|
|
||||||
|
// We expect carol to have a resolution with an
|
||||||
|
// incoming htlc timeout which reflects the full amount
|
||||||
|
// of the htlc. It has no spend tx, because carol stops
|
||||||
|
// monitoring the htlc once it has timed out.
|
||||||
|
carolReports[outpoint.String()] = &lnrpc.Resolution{
|
||||||
|
ResolutionType: lnrpc.ResolutionType_INCOMING_HTLC,
|
||||||
|
Outcome: lnrpc.ResolutionOutcome_TIMEOUT,
|
||||||
|
SweepTxid: "",
|
||||||
|
Outpoint: resolutionOutpoint,
|
||||||
|
AmountSat: uint64(paymentAmt),
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recorf the HTLC outpoint, such that we can later
|
||||||
|
// check whether it gets swept
|
||||||
|
op := wire.OutPoint{
|
||||||
|
Hash: *htlcTxID,
|
||||||
|
Index: uint32(i),
|
||||||
|
}
|
||||||
|
htlcTxOutpointSet[op] = 0
|
||||||
}
|
}
|
||||||
|
|
||||||
// We record the htlc amount less fees here, so that we know
|
// We record the htlc amount less fees here, so that we know
|
||||||
@ -4260,7 +4316,13 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Advance the chain until just before the 2nd-layer CSV delays expire.
|
// Advance the chain until just before the 2nd-layer CSV delays expire.
|
||||||
blockHash, err = net.Miner.Node.Generate(defaultCSV - 1)
|
// For anchor channels thhis is one block earlier.
|
||||||
|
numBlocks := uint32(defaultCSV - 1)
|
||||||
|
if channelType == commitTypeAnchors {
|
||||||
|
numBlocks = defaultCSV - 2
|
||||||
|
|
||||||
|
}
|
||||||
|
_, err = net.Miner.Node.Generate(numBlocks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unable to generate block: %v", err)
|
t.Fatalf("unable to generate block: %v", err)
|
||||||
}
|
}
|
||||||
@ -4327,15 +4389,6 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
t.Fatalf("failed to get sweep tx from mempool: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construct a map of the already confirmed htlc timeout txids, that
|
|
||||||
// will count the number of times each is spent by the sweep txn. We
|
|
||||||
// prepopulate it in this way so that we can later detect if we are
|
|
||||||
// spending from an output that was not a confirmed htlc timeout txn.
|
|
||||||
var htlcTxIDSet = make(map[chainhash.Hash]int)
|
|
||||||
for _, htlcTxID := range htlcTxIDs {
|
|
||||||
htlcTxIDSet[*htlcTxID] = 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Fetch the htlc sweep transaction from the mempool.
|
// Fetch the htlc sweep transaction from the mempool.
|
||||||
htlcSweepTx, err := net.Miner.Node.GetRawTransaction(htlcSweepTxID)
|
htlcSweepTx, err := net.Miner.Node.GetRawTransaction(htlcSweepTxID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -4353,19 +4406,19 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
"%v", outputCount)
|
"%v", outputCount)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that each output spends from exactly one htlc timeout txn.
|
// Ensure that each output spends from exactly one htlc timeout output.
|
||||||
for _, txIn := range htlcSweepTx.MsgTx().TxIn {
|
for _, txIn := range htlcSweepTx.MsgTx().TxIn {
|
||||||
outpoint := txIn.PreviousOutPoint.Hash
|
outpoint := txIn.PreviousOutPoint
|
||||||
// Check that the input is a confirmed htlc timeout txn.
|
// Check that the input is a confirmed htlc timeout txn.
|
||||||
if _, ok := htlcTxIDSet[outpoint]; !ok {
|
if _, ok := htlcTxOutpointSet[outpoint]; !ok {
|
||||||
t.Fatalf("htlc sweep output not spending from htlc "+
|
t.Fatalf("htlc sweep output not spending from htlc "+
|
||||||
"tx, instead spending output %v", outpoint)
|
"tx, instead spending output %v", outpoint)
|
||||||
}
|
}
|
||||||
// Increment our count for how many times this output was spent.
|
// Increment our count for how many times this output was spent.
|
||||||
htlcTxIDSet[outpoint]++
|
htlcTxOutpointSet[outpoint]++
|
||||||
|
|
||||||
// Check that each is only spent once.
|
// Check that each is only spent once.
|
||||||
if htlcTxIDSet[outpoint] > 1 {
|
if htlcTxOutpointSet[outpoint] > 1 {
|
||||||
t.Fatalf("htlc sweep tx has multiple spends from "+
|
t.Fatalf("htlc sweep tx has multiple spends from "+
|
||||||
"outpoint %v", outpoint)
|
"outpoint %v", outpoint)
|
||||||
}
|
}
|
||||||
@ -4386,6 +4439,13 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that each HTLC output was spent exactly onece.
|
||||||
|
for op, num := range htlcTxOutpointSet {
|
||||||
|
if num != 1 {
|
||||||
|
t.Fatalf("HTLC outpoint %v was spent %v times", op, num)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Check that we can find the htlc sweep in our set of sweeps using
|
// Check that we can find the htlc sweep in our set of sweeps using
|
||||||
// the verbose output of the listsweeps output.
|
// the verbose output of the listsweeps output.
|
||||||
assertSweepFound(ctxb, t.t, alice, htlcSweepTx.Hash().String(), true)
|
assertSweepFound(ctxb, t.t, alice, htlcSweepTx.Hash().String(), true)
|
||||||
@ -11019,11 +11079,12 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error {
|
|||||||
// Record all payment hashes active for this channel.
|
// Record all payment hashes active for this channel.
|
||||||
htlcHashes := make(map[string]struct{})
|
htlcHashes := make(map[string]struct{})
|
||||||
for _, htlc := range channel.PendingHtlcs {
|
for _, htlc := range channel.PendingHtlcs {
|
||||||
_, ok := htlcHashes[string(htlc.HashLock)]
|
h := hex.EncodeToString(htlc.HashLock)
|
||||||
|
_, ok := htlcHashes[h]
|
||||||
if ok {
|
if ok {
|
||||||
return fmt.Errorf("duplicate HashLock")
|
return fmt.Errorf("duplicate HashLock")
|
||||||
}
|
}
|
||||||
htlcHashes[string(htlc.HashLock)] = struct{}{}
|
htlcHashes[h] = struct{}{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Channel should have exactly the payHashes active.
|
// Channel should have exactly the payHashes active.
|
||||||
@ -11035,12 +11096,13 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error {
|
|||||||
|
|
||||||
// Make sure all the payHashes are active.
|
// Make sure all the payHashes are active.
|
||||||
for _, payHash := range payHashes {
|
for _, payHash := range payHashes {
|
||||||
if _, ok := htlcHashes[string(payHash)]; ok {
|
h := hex.EncodeToString(payHash)
|
||||||
|
if _, ok := htlcHashes[h]; ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
return fmt.Errorf("node %x didn't have the "+
|
return fmt.Errorf("node %x didn't have the "+
|
||||||
"payHash %v active", node.PubKey[:],
|
"payHash %v active", node.PubKey[:],
|
||||||
payHash)
|
h)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3034,16 +3034,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
|||||||
// Finally, we'll generate a sign descriptor to generate a
|
// Finally, we'll generate a sign descriptor to generate a
|
||||||
// signature to give to the remote party for this commitment
|
// signature to give to the remote party for this commitment
|
||||||
// transaction. Note we use the raw HTLC amount.
|
// transaction. Note we use the raw HTLC amount.
|
||||||
|
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
|
||||||
sigJob.SignDesc = input.SignDescriptor{
|
sigJob.SignDesc = input.SignDescriptor{
|
||||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||||
WitnessScript: htlc.theirWitnessScript,
|
WitnessScript: htlc.theirWitnessScript,
|
||||||
Output: &wire.TxOut{
|
Output: txOut,
|
||||||
Value: int64(htlc.Amount.ToSatoshis()),
|
HashType: sigHashType,
|
||||||
},
|
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||||
HashType: sigHashType,
|
InputIndex: 0,
|
||||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
|
||||||
InputIndex: 0,
|
|
||||||
}
|
}
|
||||||
sigJob.OutputIndex = htlc.remoteOutputIndex
|
sigJob.OutputIndex = htlc.remoteOutputIndex
|
||||||
|
|
||||||
@ -3087,16 +3086,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
|||||||
// Finally, we'll generate a sign descriptor to generate a
|
// Finally, we'll generate a sign descriptor to generate a
|
||||||
// signature to give to the remote party for this commitment
|
// signature to give to the remote party for this commitment
|
||||||
// transaction. Note we use the raw HTLC amount.
|
// transaction. Note we use the raw HTLC amount.
|
||||||
|
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
|
||||||
sigJob.SignDesc = input.SignDescriptor{
|
sigJob.SignDesc = input.SignDescriptor{
|
||||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||||
WitnessScript: htlc.theirWitnessScript,
|
WitnessScript: htlc.theirWitnessScript,
|
||||||
Output: &wire.TxOut{
|
Output: txOut,
|
||||||
Value: int64(htlc.Amount.ToSatoshis()),
|
HashType: sigHashType,
|
||||||
},
|
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||||
HashType: sigHashType,
|
InputIndex: 0,
|
||||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
|
||||||
InputIndex: 0,
|
|
||||||
}
|
}
|
||||||
sigJob.OutputIndex = htlc.remoteOutputIndex
|
sigJob.OutputIndex = htlc.remoteOutputIndex
|
||||||
|
|
||||||
@ -5396,7 +5394,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si
|
|||||||
htlcResolutions, err := extractHtlcResolutions(
|
htlcResolutions, err := extractHtlcResolutions(
|
||||||
chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer,
|
chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer,
|
||||||
remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
||||||
&chanState.RemoteChanCfg, *commitSpend.SpenderTxHash,
|
&chanState.RemoteChanCfg, commitSpend.SpendingTx,
|
||||||
chanState.ChanType,
|
chanState.ChanType,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -5519,6 +5517,14 @@ type IncomingHtlcResolution struct {
|
|||||||
// claimed directly from the outpoint listed below.
|
// claimed directly from the outpoint listed below.
|
||||||
SignedSuccessTx *wire.MsgTx
|
SignedSuccessTx *wire.MsgTx
|
||||||
|
|
||||||
|
// SignDetails is non-nil if SignedSuccessTx is non-nil, and the
|
||||||
|
// channel is of the anchor type. As the above HTLC transaction will be
|
||||||
|
// signed by the channel peer using SINGLE|ANYONECANPAY for such
|
||||||
|
// channels, we can use the sign details to add the input-output pair
|
||||||
|
// of the HTLC transaction to another transaction, thereby aggregating
|
||||||
|
// multiple HTLC transactions together, and adding fees as needed.
|
||||||
|
SignDetails *input.SignDetails
|
||||||
|
|
||||||
// CsvDelay is the relative time lock (expressed in blocks) that must
|
// CsvDelay is the relative time lock (expressed in blocks) that must
|
||||||
// pass after the SignedSuccessTx is confirmed in the chain before the
|
// pass after the SignedSuccessTx is confirmed in the chain before the
|
||||||
// output can be swept.
|
// output can be swept.
|
||||||
@ -5560,6 +5566,14 @@ type OutgoingHtlcResolution struct {
|
|||||||
// claimed directly from the outpoint listed below.
|
// claimed directly from the outpoint listed below.
|
||||||
SignedTimeoutTx *wire.MsgTx
|
SignedTimeoutTx *wire.MsgTx
|
||||||
|
|
||||||
|
// SignDetails is non-nil if SignedTimeoutTx is non-nil, and the
|
||||||
|
// channel is of the anchor type. As the above HTLC transaction will be
|
||||||
|
// signed by the channel peer using SINGLE|ANYONECANPAY for such
|
||||||
|
// channels, we can use the sign details to add the input-output pair
|
||||||
|
// of the HTLC transaction to another transaction, thereby aggregating
|
||||||
|
// multiple HTLC transactions together, and adding fees as needed.
|
||||||
|
SignDetails *input.SignDetails
|
||||||
|
|
||||||
// CsvDelay is the relative time lock (expressed in blocks) that must
|
// CsvDelay is the relative time lock (expressed in blocks) that must
|
||||||
// pass after the SignedTimeoutTx is confirmed in the chain before the
|
// pass after the SignedTimeoutTx is confirmed in the chain before the
|
||||||
// output can be swept.
|
// output can be swept.
|
||||||
@ -5599,13 +5613,13 @@ type HtlcResolutions struct {
|
|||||||
// allowing the caller to sweep an outgoing HTLC present on either their, or
|
// allowing the caller to sweep an outgoing HTLC present on either their, or
|
||||||
// the remote party's commitment transaction.
|
// the remote party's commitment transaction.
|
||||||
func newOutgoingHtlcResolution(signer input.Signer,
|
func newOutgoingHtlcResolution(signer input.Signer,
|
||||||
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
|
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
|
||||||
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||||
feePerKw chainfee.SatPerKWeight, csvDelay uint32,
|
feePerKw chainfee.SatPerKWeight, csvDelay uint32,
|
||||||
localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) {
|
localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) {
|
||||||
|
|
||||||
op := wire.OutPoint{
|
op := wire.OutPoint{
|
||||||
Hash: commitHash,
|
Hash: commitTx.TxHash(),
|
||||||
Index: uint32(htlc.OutputIndex),
|
Index: uint32(htlc.OutputIndex),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5664,16 +5678,15 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
|||||||
// With the transaction created, we can generate a sign descriptor
|
// With the transaction created, we can generate a sign descriptor
|
||||||
// that's capable of generating the signature required to spend the
|
// that's capable of generating the signature required to spend the
|
||||||
// HTLC output using the timeout transaction.
|
// HTLC output using the timeout transaction.
|
||||||
|
txOut := commitTx.TxOut[htlc.OutputIndex]
|
||||||
timeoutSignDesc := input.SignDescriptor{
|
timeoutSignDesc := input.SignDescriptor{
|
||||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||||
WitnessScript: htlcScript,
|
WitnessScript: htlcScript,
|
||||||
Output: &wire.TxOut{
|
Output: txOut,
|
||||||
Value: int64(htlc.Amt.ToSatoshis()),
|
HashType: txscript.SigHashAll,
|
||||||
},
|
SigHashes: txscript.NewTxSigHashes(timeoutTx),
|
||||||
HashType: txscript.SigHashAll,
|
InputIndex: 0,
|
||||||
SigHashes: txscript.NewTxSigHashes(timeoutTx),
|
|
||||||
InputIndex: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
||||||
@ -5692,6 +5705,12 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
|||||||
}
|
}
|
||||||
timeoutTx.TxIn[0].Witness = timeoutWitness
|
timeoutTx.TxIn[0].Witness = timeoutWitness
|
||||||
|
|
||||||
|
// If this is an anchor type channel, the sign details will let us
|
||||||
|
// re-sign an aggregated tx later.
|
||||||
|
txSignDetails := HtlcSignDetails(
|
||||||
|
chanType, timeoutSignDesc, sigHashType, htlcSig,
|
||||||
|
)
|
||||||
|
|
||||||
// Finally, we'll generate the script output that the timeout
|
// Finally, we'll generate the script output that the timeout
|
||||||
// transaction creates so we can generate the signDesc required to
|
// transaction creates so we can generate the signDesc required to
|
||||||
// complete the claim process after a delay period.
|
// complete the claim process after a delay period.
|
||||||
@ -5712,6 +5731,7 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
|||||||
return &OutgoingHtlcResolution{
|
return &OutgoingHtlcResolution{
|
||||||
Expiry: htlc.RefundTimeout,
|
Expiry: htlc.RefundTimeout,
|
||||||
SignedTimeoutTx: timeoutTx,
|
SignedTimeoutTx: timeoutTx,
|
||||||
|
SignDetails: txSignDetails,
|
||||||
CsvDelay: csvDelay,
|
CsvDelay: csvDelay,
|
||||||
ClaimOutpoint: wire.OutPoint{
|
ClaimOutpoint: wire.OutPoint{
|
||||||
Hash: timeoutTx.TxHash(),
|
Hash: timeoutTx.TxHash(),
|
||||||
@ -5738,13 +5758,13 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
|||||||
//
|
//
|
||||||
// TODO(roasbeef) consolidate code with above func
|
// TODO(roasbeef) consolidate code with above func
|
||||||
func newIncomingHtlcResolution(signer input.Signer,
|
func newIncomingHtlcResolution(signer input.Signer,
|
||||||
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
|
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
|
||||||
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||||
feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool,
|
feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool,
|
||||||
chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) {
|
chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) {
|
||||||
|
|
||||||
op := wire.OutPoint{
|
op := wire.OutPoint{
|
||||||
Hash: commitHash,
|
Hash: commitTx.TxHash(),
|
||||||
Index: uint32(htlc.OutputIndex),
|
Index: uint32(htlc.OutputIndex),
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -5795,16 +5815,15 @@ func newIncomingHtlcResolution(signer input.Signer,
|
|||||||
|
|
||||||
// Once we've created the second-level transaction, we'll generate the
|
// Once we've created the second-level transaction, we'll generate the
|
||||||
// SignDesc needed spend the HTLC output using the success transaction.
|
// SignDesc needed spend the HTLC output using the success transaction.
|
||||||
|
txOut := commitTx.TxOut[htlc.OutputIndex]
|
||||||
successSignDesc := input.SignDescriptor{
|
successSignDesc := input.SignDescriptor{
|
||||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||||
WitnessScript: htlcScript,
|
WitnessScript: htlcScript,
|
||||||
Output: &wire.TxOut{
|
Output: txOut,
|
||||||
Value: int64(htlc.Amt.ToSatoshis()),
|
HashType: txscript.SigHashAll,
|
||||||
},
|
SigHashes: txscript.NewTxSigHashes(successTx),
|
||||||
HashType: txscript.SigHashAll,
|
InputIndex: 0,
|
||||||
SigHashes: txscript.NewTxSigHashes(successTx),
|
|
||||||
InputIndex: 0,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
||||||
@ -5825,6 +5844,12 @@ func newIncomingHtlcResolution(signer input.Signer,
|
|||||||
}
|
}
|
||||||
successTx.TxIn[0].Witness = successWitness
|
successTx.TxIn[0].Witness = successWitness
|
||||||
|
|
||||||
|
// If this is an anchor type channel, the sign details will let us
|
||||||
|
// re-sign an aggregated tx later.
|
||||||
|
txSignDetails := HtlcSignDetails(
|
||||||
|
chanType, successSignDesc, sigHashType, htlcSig,
|
||||||
|
)
|
||||||
|
|
||||||
// Finally, we'll generate the script that the second-level transaction
|
// Finally, we'll generate the script that the second-level transaction
|
||||||
// creates so we can generate the proper signDesc to sweep it after the
|
// creates so we can generate the proper signDesc to sweep it after the
|
||||||
// CSV delay has passed.
|
// CSV delay has passed.
|
||||||
@ -5844,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer,
|
|||||||
)
|
)
|
||||||
return &IncomingHtlcResolution{
|
return &IncomingHtlcResolution{
|
||||||
SignedSuccessTx: successTx,
|
SignedSuccessTx: successTx,
|
||||||
|
SignDetails: txSignDetails,
|
||||||
CsvDelay: csvDelay,
|
CsvDelay: csvDelay,
|
||||||
ClaimOutpoint: wire.OutPoint{
|
ClaimOutpoint: wire.OutPoint{
|
||||||
Hash: successTx.TxHash(),
|
Hash: successTx.TxHash(),
|
||||||
@ -5892,7 +5918,7 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint {
|
|||||||
func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
||||||
signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing,
|
signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||||
localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
|
localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
|
||||||
commitHash chainhash.Hash, chanType channeldb.ChannelType) (
|
commitTx *wire.MsgTx, chanType channeldb.ChannelType) (
|
||||||
*HtlcResolutions, error) {
|
*HtlcResolutions, error) {
|
||||||
|
|
||||||
// TODO(roasbeef): don't need to swap csv delay?
|
// TODO(roasbeef): don't need to swap csv delay?
|
||||||
@ -5924,7 +5950,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
|||||||
// Otherwise, we'll create an incoming HTLC resolution
|
// Otherwise, we'll create an incoming HTLC resolution
|
||||||
// as we can satisfy the contract.
|
// as we can satisfy the contract.
|
||||||
ihr, err := newIncomingHtlcResolution(
|
ihr, err := newIncomingHtlcResolution(
|
||||||
signer, localChanCfg, commitHash, &htlc,
|
signer, localChanCfg, commitTx, &htlc,
|
||||||
keyRing, feePerKw, uint32(csvDelay), ourCommit,
|
keyRing, feePerKw, uint32(csvDelay), ourCommit,
|
||||||
chanType,
|
chanType,
|
||||||
)
|
)
|
||||||
@ -5937,7 +5963,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
|||||||
}
|
}
|
||||||
|
|
||||||
ohr, err := newOutgoingHtlcResolution(
|
ohr, err := newOutgoingHtlcResolution(
|
||||||
signer, localChanCfg, commitHash, &htlc, keyRing,
|
signer, localChanCfg, commitTx, &htlc, keyRing,
|
||||||
feePerKw, uint32(csvDelay), ourCommit, chanType,
|
feePerKw, uint32(csvDelay), ourCommit, chanType,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -6138,12 +6164,11 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
|
|||||||
// outgoing HTLC's that we'll need to claim as well. If this is after
|
// outgoing HTLC's that we'll need to claim as well. If this is after
|
||||||
// recovery there is not much we can do with HTLCs, so we'll always
|
// recovery there is not much we can do with HTLCs, so we'll always
|
||||||
// use what we have in our latest state when extracting resolutions.
|
// use what we have in our latest state when extracting resolutions.
|
||||||
txHash := commitTx.TxHash()
|
|
||||||
localCommit := chanState.LocalCommitment
|
localCommit := chanState.LocalCommitment
|
||||||
htlcResolutions, err := extractHtlcResolutions(
|
htlcResolutions, err := extractHtlcResolutions(
|
||||||
chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer,
|
chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer,
|
||||||
localCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
localCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
||||||
&chanState.RemoteChanCfg, txHash, chanState.ChanType,
|
&chanState.RemoteChanCfg, commitTx, chanState.ChanType,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
@ -235,6 +235,24 @@ func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType {
|
|||||||
return txscript.SigHashAll
|
return txscript.SigHashAll
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HtlcSignDetails converts the passed parameters to a SignDetails valid for
|
||||||
|
// this channel type. For non-anchor channels this will return nil.
|
||||||
|
func HtlcSignDetails(chanType channeldb.ChannelType, signDesc input.SignDescriptor,
|
||||||
|
sigHash txscript.SigHashType, peerSig input.Signature) *input.SignDetails {
|
||||||
|
|
||||||
|
// Non-anchor channels don't need sign details, as the HTLC second
|
||||||
|
// level cannot be altered.
|
||||||
|
if !chanType.HasAnchors() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &input.SignDetails{
|
||||||
|
SignDesc: signDesc,
|
||||||
|
SigHashType: sigHash,
|
||||||
|
PeerSig: peerSig,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// HtlcSecondLevelInputSequence dictates the sequence number we must use on the
|
// HtlcSecondLevelInputSequence dictates the sequence number we must use on the
|
||||||
// input to a second level HTLC transaction.
|
// input to a second level HTLC transaction.
|
||||||
func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {
|
func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {
|
||||||
|
@ -13,5 +13,5 @@ var (
|
|||||||
//
|
//
|
||||||
// To speed up integration tests waiting for a sweep to happen, the
|
// To speed up integration tests waiting for a sweep to happen, the
|
||||||
// batch window is shortened.
|
// batch window is shortened.
|
||||||
DefaultBatchWindowDuration = 2 * time.Second
|
DefaultBatchWindowDuration = 8 * time.Second
|
||||||
)
|
)
|
||||||
|
@ -293,10 +293,10 @@ func (t *txInputSet) add(input input.Input, constraints addConstraints) bool {
|
|||||||
// minimizing any negative externalities we cause for the Bitcoin system as a
|
// minimizing any negative externalities we cause for the Bitcoin system as a
|
||||||
// whole.
|
// whole.
|
||||||
func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
|
func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
|
||||||
for _, input := range sweepableInputs {
|
for i, inp := range sweepableInputs {
|
||||||
// Apply relaxed constraints for force sweeps.
|
// Apply relaxed constraints for force sweeps.
|
||||||
constraints := constraintsRegular
|
constraints := constraintsRegular
|
||||||
if input.parameters().Force {
|
if inp.parameters().Force {
|
||||||
constraints = constraintsForce
|
constraints = constraintsForce
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -304,16 +304,26 @@ func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
|
|||||||
// succeed because it wouldn't increase the output value,
|
// succeed because it wouldn't increase the output value,
|
||||||
// return. Assuming inputs are sorted by yield, any further
|
// return. Assuming inputs are sorted by yield, any further
|
||||||
// inputs wouldn't increase the output value either.
|
// inputs wouldn't increase the output value either.
|
||||||
if !t.add(input, constraints) {
|
if !t.add(inp, constraints) {
|
||||||
|
var rem []input.Input
|
||||||
|
for j := i; j < len(sweepableInputs); j++ {
|
||||||
|
rem = append(rem, sweepableInputs[j])
|
||||||
|
}
|
||||||
|
log.Debugf("%d negative yield inputs not added to "+
|
||||||
|
"input set: %v", len(rem),
|
||||||
|
inputTypeSummary(rem))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.Debugf("Added positive yield input %v to input set",
|
||||||
|
inputTypeSummary([]input.Input{inp}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// We managed to add all inputs to the set.
|
// We managed to add all inputs to the set.
|
||||||
}
|
}
|
||||||
|
|
||||||
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding as
|
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding
|
||||||
// many as required to bring the tx output value above the given minimum.
|
// as many as required to bring the tx output value above the given minimum.
|
||||||
func (t *txInputSet) tryAddWalletInputsIfNeeded() error {
|
func (t *txInputSet) tryAddWalletInputsIfNeeded() error {
|
||||||
// If we've already have enough to pay the transaction fees and have at
|
// If we've already have enough to pay the transaction fees and have at
|
||||||
// least one output materialize, no action is needed.
|
// least one output materialize, no action is needed.
|
||||||
|
@ -222,6 +222,9 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
|
|||||||
PkScript: outputPkScript,
|
PkScript: outputPkScript,
|
||||||
Value: int64(changeAmt),
|
Value: int64(changeAmt),
|
||||||
})
|
})
|
||||||
|
} else {
|
||||||
|
log.Infof("Change amt %v below dustlimit %v, not adding "+
|
||||||
|
"change output", changeAmt, dustLimit)
|
||||||
}
|
}
|
||||||
|
|
||||||
// We'll default to using the current block height as locktime, if none
|
// We'll default to using the current block height as locktime, if none
|
||||||
|
Loading…
Reference in New Issue
Block a user