mirror of
https://github.com/lightningnetwork/lnd.git
synced 2024-11-19 09:53:54 +01:00
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"
|
||||
"io"
|
||||
|
||||
"github.com/btcsuite/btcd/btcec"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||
@ -275,6 +277,13 @@ var (
|
||||
// the full set of resolutions for a channel.
|
||||
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
|
||||
// store the anchor resolution, if any.
|
||||
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
|
||||
// can now write out the resolutions for the incoming and
|
||||
// outgoing HTLC's.
|
||||
@ -668,6 +681,11 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
numOutgoing := uint32(len(c.HtlcResolutions.OutgoingHTLCs))
|
||||
if err := binary.Write(&b, endian, numOutgoing); err != nil {
|
||||
@ -678,13 +696,28 @@ func (b *boltArbitratorLog) LogContractResolutions(c *ContractResolutions) error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = encodeSignDetails(&signDetailsBuf, htlc.SignDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Put the resolutions under the resolutionsKey.
|
||||
err = scopeBucket.Put(resolutionsKey, b.Bytes())
|
||||
if err != nil {
|
||||
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.
|
||||
if c.AnchorResolution != nil {
|
||||
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)
|
||||
if anchorResBytes != nil {
|
||||
c.AnchorResolution = &lnwallet.AnchorResolution{}
|
||||
@ -941,6 +1001,11 @@ func (b *boltArbitratorLog) WipeHistory() error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = scopeBucket.Delete(resolutionsSignDetailsKey)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// We'll delete any chain actions that are still stored by
|
||||
// removing the enclosing bucket.
|
||||
err = scopeBucket.DeleteNestedBucket(actionsBucketKey)
|
||||
@ -980,6 +1045,79 @@ func (b *boltArbitratorLog) checkpointContract(c ContractResolver,
|
||||
}, 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 {
|
||||
if _, err := w.Write(i.Preimage[:]); err != nil {
|
||||
return err
|
||||
|
@ -102,6 +102,58 @@ var (
|
||||
},
|
||||
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) {
|
||||
@ -219,13 +271,13 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver,
|
||||
case *htlcOutgoingContestResolver:
|
||||
diskRes := diskResolver.(*htlcOutgoingContestResolver)
|
||||
assertTimeoutResEqual(
|
||||
&ogRes.htlcTimeoutResolver, &diskRes.htlcTimeoutResolver,
|
||||
ogRes.htlcTimeoutResolver, diskRes.htlcTimeoutResolver,
|
||||
)
|
||||
|
||||
case *htlcIncomingContestResolver:
|
||||
diskRes := diskResolver.(*htlcIncomingContestResolver)
|
||||
assertSuccessResEqual(
|
||||
&ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver,
|
||||
ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver,
|
||||
)
|
||||
|
||||
if ogRes.htlcExpiry != diskRes.htlcExpiry {
|
||||
@ -323,13 +375,13 @@ func TestContractInsertionRetrieval(t *testing.T) {
|
||||
contestTimeout := timeoutResolver
|
||||
contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint()
|
||||
resolvers = append(resolvers, &htlcOutgoingContestResolver{
|
||||
htlcTimeoutResolver: contestTimeout,
|
||||
htlcTimeoutResolver: &contestTimeout,
|
||||
})
|
||||
contestSuccess := successResolver
|
||||
contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint()
|
||||
resolvers = append(resolvers, &htlcIncomingContestResolver{
|
||||
htlcExpiry: 100,
|
||||
htlcSuccessResolver: contestSuccess,
|
||||
htlcSuccessResolver: &contestSuccess,
|
||||
})
|
||||
|
||||
// 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
|
||||
// contest resolver that eventually turns into the timeout resolver.
|
||||
timeoutResolver := htlcTimeoutResolver{
|
||||
timeoutResolver := &htlcTimeoutResolver{
|
||||
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
||||
Expiry: 99,
|
||||
SignedTimeoutTx: nil,
|
||||
@ -497,7 +549,7 @@ func TestContractSwapping(t *testing.T) {
|
||||
|
||||
// With the resolver inserted, we'll now attempt to atomically swap it
|
||||
// for its underlying timeout resolver.
|
||||
err = testLog.SwapContract(contestResolver, &timeoutResolver)
|
||||
err = testLog.SwapContract(contestResolver, timeoutResolver)
|
||||
if err != nil {
|
||||
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.
|
||||
assertResolversEqual(t, &timeoutResolver, dbContracts[0])
|
||||
assertResolversEqual(t, timeoutResolver, dbContracts[0])
|
||||
}
|
||||
|
||||
// TestContractResolutionsStorage tests that we're able to properly store and
|
||||
@ -550,8 +602,38 @@ func TestContractResolutionsStorage(t *testing.T) {
|
||||
ClaimOutpoint: randOutPoint(),
|
||||
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{
|
||||
// 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,
|
||||
SignedTimeoutTx: nil,
|
||||
@ -559,6 +641,15 @@ func TestContractResolutionsStorage(t *testing.T) {
|
||||
ClaimOutpoint: randOutPoint(),
|
||||
SweepSignDesc: testSignDesc,
|
||||
},
|
||||
// Resolution with SignDetails.
|
||||
{
|
||||
Expiry: 103,
|
||||
SignedTimeoutTx: testTx,
|
||||
SignDetails: testSignDetails,
|
||||
CsvDelay: 923923,
|
||||
ClaimOutpoint: randOutPoint(),
|
||||
SweepSignDesc: testSignDesc,
|
||||
},
|
||||
},
|
||||
},
|
||||
AnchorResolution: &lnwallet.AnchorResolution{
|
||||
@ -585,8 +676,15 @@ func TestContractResolutionsStorage(t *testing.T) {
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(&res, diskRes) {
|
||||
t.Fatalf("resolution mismatch: expected %#v\n, got %#v",
|
||||
&res, diskRes)
|
||||
for _, h := range res.HtlcResolutions.IncomingHTLCs {
|
||||
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
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
@ -79,10 +80,12 @@ func (c *commitSweepResolver) ResolverKey() []byte {
|
||||
|
||||
// waitForHeight registers for block notifications and waits for the provided
|
||||
// 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
|
||||
// will be sent on the channel immediately.
|
||||
blockEpochs, err := c.Notifier.RegisterBlockEpochNtfn(nil)
|
||||
blockEpochs, err := notifier.RegisterBlockEpochNtfn(nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -99,12 +102,38 @@ func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
case <-c.quit:
|
||||
case <-quit:
|
||||
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
|
||||
// the confirmation height.
|
||||
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
|
||||
// unlocks the spend path.
|
||||
err := c.waitForHeight(unlockHeight - 1)
|
||||
err := waitForHeight(unlockHeight-1, c.Notifier, c.quit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
@ -103,17 +103,19 @@ func (i *commitSweepResolverTestContext) waitForResult() {
|
||||
}
|
||||
|
||||
type mockSweeper struct {
|
||||
sweptInputs chan input.Input
|
||||
updatedInputs chan wire.OutPoint
|
||||
sweepTx *wire.MsgTx
|
||||
sweepErr error
|
||||
sweptInputs chan input.Input
|
||||
updatedInputs chan wire.OutPoint
|
||||
sweepTx *wire.MsgTx
|
||||
sweepErr error
|
||||
createSweepTxChan chan *wire.MsgTx
|
||||
}
|
||||
|
||||
func newMockSweeper() *mockSweeper {
|
||||
return &mockSweeper{
|
||||
sweptInputs: make(chan input.Input),
|
||||
updatedInputs: make(chan wire.OutPoint),
|
||||
sweepTx: &wire.MsgTx{},
|
||||
sweptInputs: make(chan input.Input),
|
||||
updatedInputs: make(chan wire.OutPoint),
|
||||
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,
|
||||
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 {
|
||||
|
@ -20,6 +20,10 @@ const (
|
||||
// sweepConfTarget is the default number of blocks that we'll use as a
|
||||
// confirmation target when sweeping.
|
||||
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
|
||||
|
@ -31,7 +31,7 @@ type htlcIncomingContestResolver struct {
|
||||
|
||||
// htlcSuccessResolver is the inner resolver that may be utilized if we
|
||||
// learn of the preimage.
|
||||
htlcSuccessResolver
|
||||
*htlcSuccessResolver
|
||||
}
|
||||
|
||||
// newIncomingContestResolver instantiates a new incoming htlc contest resolver.
|
||||
@ -45,7 +45,7 @@ func newIncomingContestResolver(
|
||||
|
||||
return &htlcIncomingContestResolver{
|
||||
htlcExpiry: htlc.RefundTimeout,
|
||||
htlcSuccessResolver: *success,
|
||||
htlcSuccessResolver: success,
|
||||
}
|
||||
}
|
||||
|
||||
@ -189,7 +189,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &h.htlcSuccessResolver, nil
|
||||
return h.htlcSuccessResolver, nil
|
||||
|
||||
// If the htlc was failed, mark the htlc as
|
||||
// resolved.
|
||||
@ -293,7 +293,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &h.htlcSuccessResolver, nil
|
||||
return h.htlcSuccessResolver, nil
|
||||
}
|
||||
|
||||
witnessUpdates = preimageSubscription.WitnessUpdates
|
||||
@ -315,7 +315,7 @@ func (h *htlcIncomingContestResolver) Resolve() (ContractResolver, error) {
|
||||
// We've learned of the preimage and this information
|
||||
// has been added to our inner resolver. We return it so
|
||||
// it can continue contract resolution.
|
||||
return &h.htlcSuccessResolver, nil
|
||||
return h.htlcSuccessResolver, nil
|
||||
|
||||
case hodlItem := <-hodlChan:
|
||||
htlcResolution := hodlItem.(invoices.HtlcResolution)
|
||||
@ -420,7 +420,7 @@ func newIncomingContestResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
h.htlcSuccessResolver = *successResolver
|
||||
h.htlcSuccessResolver = successResolver
|
||||
|
||||
return h, nil
|
||||
}
|
||||
|
@ -342,7 +342,7 @@ func newIncomingResolverTestContext(t *testing.T, isExit bool) *incomingResolver
|
||||
},
|
||||
}
|
||||
resolver := &htlcIncomingContestResolver{
|
||||
htlcSuccessResolver: htlcSuccessResolver{
|
||||
htlcSuccessResolver: &htlcSuccessResolver{
|
||||
contractResolverKit: *newContractResolverKit(cfg),
|
||||
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
||||
htlc: channeldb.HTLC{
|
||||
|
@ -17,7 +17,7 @@ import (
|
||||
type htlcOutgoingContestResolver struct {
|
||||
// htlcTimeoutResolver is the inner solver that this resolver may turn
|
||||
// into. This only happens if the HTLC expires on-chain.
|
||||
htlcTimeoutResolver
|
||||
*htlcTimeoutResolver
|
||||
}
|
||||
|
||||
// newOutgoingContestResolver instantiates a new outgoing contested htlc
|
||||
@ -31,7 +31,7 @@ func newOutgoingContestResolver(res lnwallet.OutgoingHtlcResolution,
|
||||
)
|
||||
|
||||
return &htlcOutgoingContestResolver{
|
||||
htlcTimeoutResolver: *timeout,
|
||||
htlcTimeoutResolver: timeout,
|
||||
}
|
||||
}
|
||||
|
||||
@ -131,7 +131,7 @@ func (h *htlcOutgoingContestResolver) Resolve() (ContractResolver, error) {
|
||||
"into timeout resolver", h,
|
||||
h.htlcResolution.ClaimOutpoint,
|
||||
newHeight, h.htlcResolution.Expiry)
|
||||
return &h.htlcTimeoutResolver, nil
|
||||
return h.htlcTimeoutResolver, nil
|
||||
}
|
||||
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
h.htlcTimeoutResolver = *timeoutResolver
|
||||
h.htlcTimeoutResolver = timeoutResolver
|
||||
return h, nil
|
||||
}
|
||||
|
||||
|
@ -177,7 +177,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext {
|
||||
}
|
||||
|
||||
resolver := &htlcOutgoingContestResolver{
|
||||
htlcTimeoutResolver: htlcTimeoutResolver{
|
||||
htlcTimeoutResolver: &htlcTimeoutResolver{
|
||||
contractResolverKit: *newContractResolverKit(cfg),
|
||||
htlcResolution: outgoingRes,
|
||||
htlc: channeldb.HTLC{
|
||||
|
@ -3,11 +3,13 @@ package contractcourt
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/labels"
|
||||
@ -29,7 +31,12 @@ type htlcSuccessResolver struct {
|
||||
htlcResolution lnwallet.IncomingHtlcResolution
|
||||
|
||||
// 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
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@ -58,12 +74,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution,
|
||||
broadcastHeight uint32, htlc channeldb.HTLC,
|
||||
resCfg ResolverConfig) *htlcSuccessResolver {
|
||||
|
||||
return &htlcSuccessResolver{
|
||||
h := &htlcSuccessResolver{
|
||||
contractResolverKit: *newContractResolverKit(resCfg),
|
||||
htlcResolution: res,
|
||||
broadcastHeight: broadcastHeight,
|
||||
htlc: htlc,
|
||||
}
|
||||
|
||||
h.initReport()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// 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
|
||||
// an output on the remote party's commitment transaction.
|
||||
if h.htlcResolution.SignedSuccessTx == nil {
|
||||
// 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))
|
||||
|
||||
// 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,
|
||||
)
|
||||
return h.resolveRemoteCommitOutput()
|
||||
}
|
||||
|
||||
// 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",
|
||||
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
|
||||
// been spent, then fully resolve the contract.
|
||||
spendNtfn, err := h.Notifier.RegisterSpendNtfn(
|
||||
&h.htlcResolution.ClaimOutpoint,
|
||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||
h.broadcastHeight,
|
||||
return &h.htlcResolution.ClaimOutpoint, nil
|
||||
}
|
||||
|
||||
// broadcastReSignedSuccessTx handles the case where we have non-nil
|
||||
// SignDetails, and offers the second level transaction to the Sweeper, that
|
||||
// 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 {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+
|
||||
"after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay)
|
||||
// Will return this outpoint, when this is spent the resolver is fully
|
||||
// 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 {
|
||||
case spend, ok := <-spendNtfn.Spend:
|
||||
case _, ok := <-confNtfn.Confirmed:
|
||||
if !ok {
|
||||
return nil, errResolverShuttingDown
|
||||
}
|
||||
spendTxid = spend.SpenderTxHash
|
||||
|
||||
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(
|
||||
spendTxid, channeldb.ResolverOutcomeClaimed,
|
||||
&sweepTXID,
|
||||
channeldb.ResolverOutcomeClaimed,
|
||||
)
|
||||
}
|
||||
|
||||
@ -330,6 +505,40 @@ func (h *htlcSuccessResolver) IsResolved() bool {
|
||||
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
|
||||
// Writer.
|
||||
//
|
||||
@ -355,6 +564,12 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We encode the sign details last for backwards compatibility.
|
||||
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -388,6 +603,18 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -1,13 +1,19 @@
|
||||
package contractcourt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/kvdb"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -15,33 +21,59 @@ import (
|
||||
|
||||
var testHtlcAmt = lnwire.MilliSatoshi(200000)
|
||||
|
||||
type htlcSuccessResolverTestContext struct {
|
||||
resolver *htlcSuccessResolver
|
||||
type htlcResolverTestContext struct {
|
||||
resolver ContractResolver
|
||||
|
||||
checkpoint func(_ ContractResolver,
|
||||
_ ...*channeldb.ResolverReport) error
|
||||
|
||||
notifier *mock.ChainNotifier
|
||||
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{
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch, 1),
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail, 1),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation, 1),
|
||||
}
|
||||
|
||||
checkPointChan := make(chan struct{}, 1)
|
||||
|
||||
testCtx := &htlcSuccessResolverTestContext{
|
||||
notifier: notifier,
|
||||
t: t,
|
||||
testCtx := &htlcResolverTestContext{
|
||||
checkpoint: nil,
|
||||
notifier: notifier,
|
||||
resolutionChan: make(chan ResolutionMsg, 1),
|
||||
t: t,
|
||||
}
|
||||
|
||||
witnessBeacon := newMockWitnessBeacon()
|
||||
chainCfg := ChannelArbitratorConfig{
|
||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||
Notifier: notifier,
|
||||
Notifier: notifier,
|
||||
PreimageDB: witnessBeacon,
|
||||
PublishTx: func(_ *wire.MsgTx, _ string) error {
|
||||
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,
|
||||
report *channeldb.ResolverReport) error {
|
||||
@ -49,31 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon
|
||||
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{
|
||||
ChannelArbitratorConfig: chainCfg,
|
||||
Checkpoint: func(_ ContractResolver,
|
||||
_ ...*channeldb.ResolverReport) error {
|
||||
|
||||
checkPointChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
Checkpoint: checkpointFunc,
|
||||
}
|
||||
|
||||
testCtx.resolver = &htlcSuccessResolver{
|
||||
contractResolverKit: *newContractResolverKit(cfg),
|
||||
htlcResolution: lnwallet.IncomingHtlcResolution{},
|
||||
htlc: channeldb.HTLC{
|
||||
RHash: testResHash,
|
||||
OnionBlob: testOnionBlob,
|
||||
Amt: testHtlcAmt,
|
||||
},
|
||||
htlc := channeldb.HTLC{
|
||||
RHash: testResHash,
|
||||
OnionBlob: testOnionBlob,
|
||||
Amt: testHtlcAmt,
|
||||
}
|
||||
|
||||
testCtx.resolver = newResolver(htlc, cfg)
|
||||
|
||||
return testCtx
|
||||
}
|
||||
|
||||
func (i *htlcSuccessResolverTestContext) resolve() {
|
||||
func (i *htlcResolverTestContext) resolve() {
|
||||
// Start resolver.
|
||||
i.resolverResultChan = make(chan resolveResult, 1)
|
||||
go func() {
|
||||
@ -85,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() {
|
||||
}()
|
||||
}
|
||||
|
||||
func (i *htlcSuccessResolverTestContext) waitForResult() {
|
||||
func (i *htlcResolverTestContext) waitForResult() {
|
||||
i.t.Helper()
|
||||
|
||||
result := <-i.resolverResultChan
|
||||
@ -98,8 +130,9 @@ func (i *htlcSuccessResolverTestContext) waitForResult() {
|
||||
}
|
||||
}
|
||||
|
||||
// TestSingleStageSuccess tests successful sweep of a single stage htlc claim.
|
||||
func TestSingleStageSuccess(t *testing.T) {
|
||||
// TestHtlcSuccessSingleStage tests successful sweep of a single stage htlc
|
||||
// claim.
|
||||
func TestHtlcSuccessSingleStage(t *testing.T) {
|
||||
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||
|
||||
sweepTx := &wire.MsgTx{
|
||||
@ -114,15 +147,6 @@ func TestSingleStageSuccess(t *testing.T) {
|
||||
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()
|
||||
claim := &channeldb.ResolverReport{
|
||||
OutPoint: htlcOutpoint,
|
||||
@ -131,14 +155,46 @@ func TestSingleStageSuccess(t *testing.T) {
|
||||
ResolverOutcome: channeldb.ResolverOutcomeClaimed,
|
||||
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(
|
||||
t, singleStageResolution, resolve, sweepTx, claim,
|
||||
t, singleStageResolution, checkpoints,
|
||||
)
|
||||
}
|
||||
|
||||
// TestSecondStageResolution tests successful sweep of a second stage htlc
|
||||
// claim.
|
||||
func TestSecondStageResolution(t *testing.T) {
|
||||
// claim, going through the Nursery.
|
||||
func TestHtlcSuccessSecondStageResolution(t *testing.T) {
|
||||
commitOutpoint := wire.OutPoint{Index: 2}
|
||||
htlcOutpoint := wire.OutPoint{Index: 3}
|
||||
|
||||
@ -158,20 +214,17 @@ func TestSecondStageResolution(t *testing.T) {
|
||||
PreviousOutPoint: commitOutpoint,
|
||||
},
|
||||
},
|
||||
TxOut: []*wire.TxOut{},
|
||||
TxOut: []*wire.TxOut{
|
||||
{
|
||||
Value: 111,
|
||||
PkScript: []byte{0xaa, 0xaa},
|
||||
},
|
||||
},
|
||||
},
|
||||
ClaimOutpoint: htlcOutpoint,
|
||||
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()
|
||||
firstStage := &channeldb.ResolverReport{
|
||||
OutPoint: commitOutpoint,
|
||||
@ -189,54 +242,373 @@ func TestSecondStageResolution(t *testing.T) {
|
||||
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(
|
||||
t, twoStageResolution, resolve, sweepTx, secondStage, firstStage,
|
||||
t, twoStageResolution, checkpoints,
|
||||
)
|
||||
}
|
||||
|
||||
// testHtlcSuccess tests resolution of a success resolver. It takes a resolve
|
||||
// function which triggers resolution and the sweeptxid that will resolve it.
|
||||
// TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with
|
||||
// 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,
|
||||
resolve func(*htlcSuccessResolverTestContext),
|
||||
sweepTx *wire.MsgTx, reports ...*channeldb.ResolverReport) {
|
||||
checkpoints []checkpoint) {
|
||||
|
||||
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
|
||||
// channel for us to consume. We replace this function on the resolver
|
||||
// itself because it is created by the test context.
|
||||
reportChan := make(chan *channeldb.ResolverReport)
|
||||
ctx.resolver.Checkpoint = func(_ ContractResolver,
|
||||
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 := 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 {
|
||||
|
||||
// Send all of our reports into the channel.
|
||||
for _, report := range reports {
|
||||
reportChan <- report
|
||||
if nextCheckpoint >= len(expectedCheckpoints) {
|
||||
t.Fatal("did not expect more checkpoints")
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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.
|
||||
ctx.resolve()
|
||||
|
||||
// Trigger and event that will resolve our test context.
|
||||
resolve(ctx)
|
||||
// Go through our list of expected checkpoints, so we can run the
|
||||
// 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.
|
||||
ctx.waitForResult()
|
||||
|
||||
if nextCheckpoint < len(expectedCheckpoints) {
|
||||
t.Fatalf("not all checkpoints hit")
|
||||
}
|
||||
|
||||
return checkpointedState
|
||||
}
|
||||
|
@ -4,8 +4,8 @@ import (
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"io"
|
||||
"sync"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
@ -15,6 +15,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
)
|
||||
|
||||
// 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 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
|
||||
}
|
||||
|
||||
@ -54,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution,
|
||||
broadcastHeight uint32, htlc channeldb.HTLC,
|
||||
resCfg ResolverConfig) *htlcTimeoutResolver {
|
||||
|
||||
return &htlcTimeoutResolver{
|
||||
h := &htlcTimeoutResolver{
|
||||
contractResolverKit: *newContractResolverKit(resCfg),
|
||||
htlcResolution: res,
|
||||
broadcastHeight: broadcastHeight,
|
||||
htlc: htlc,
|
||||
}
|
||||
|
||||
h.initReport()
|
||||
|
||||
return h
|
||||
}
|
||||
|
||||
// ResolverKey returns an identifier which should be globally unique for this
|
||||
@ -254,9 +268,83 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// If we haven't already sent the output to the utxo nursery, then
|
||||
// we'll do so now.
|
||||
if !h.outputIncubating {
|
||||
// Start by spending the HTLC output, either by broadcasting the
|
||||
// second-level timeout transaction, or directly if this is the remote
|
||||
// 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,
|
||||
h.htlcResolution.ClaimOutpoint)
|
||||
|
||||
@ -276,51 +364,14 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) {
|
||||
}
|
||||
}
|
||||
|
||||
var spendTxID *chainhash.Hash
|
||||
|
||||
// waitForOutputResolution waits for the HTLC output to be fully
|
||||
// resolved. The output is considered fully resolved once it has been
|
||||
// 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.
|
||||
// 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.
|
||||
// Depending on if this is our commitment, or the remote party's
|
||||
// commitment, we'll be watching a different outpoint and script.
|
||||
outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
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"+
|
||||
"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
|
||||
// commitment transaction has been spent.
|
||||
var (
|
||||
spend *chainntnfs.SpendDetail
|
||||
ok bool
|
||||
spend, err := waitForSpend(
|
||||
outpointToWatch, scriptToWatch, h.broadcastHeight,
|
||||
h.Notifier, h.quit,
|
||||
)
|
||||
select {
|
||||
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 {
|
||||
if err != nil {
|
||||
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
|
||||
// wait for the second-level HTLC output to be spent, and for that
|
||||
// transaction itself to confirm.
|
||||
if h.htlcResolution.SignedTimeoutTx != nil {
|
||||
log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+
|
||||
"output", h, h.htlcResolution.ClaimOutpoint)
|
||||
if err := waitForOutputResolution(); err != nil {
|
||||
case h.htlcResolution.SignedTimeoutTx != nil:
|
||||
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
||||
"delayed output", h, claimOutpoint)
|
||||
sweep, err := waitForSpend(
|
||||
&claimOutpoint,
|
||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||
h.broadcastHeight, h.Notifier, h.quit,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Once our timeout tx has confirmed, we add a resolution for
|
||||
// our timeoutTx tx first stage transaction.
|
||||
timeoutTx := h.htlcResolution.SignedTimeoutTx
|
||||
spendHash := timeoutTx.TxHash()
|
||||
// Update the spend txid to the hash of the sweep transaction.
|
||||
spendTxID = sweep.SpenderTxHash
|
||||
|
||||
// Once our sweep of the timeout tx has confirmed, we add a
|
||||
// resolution for our timeoutTx tx first stage transaction.
|
||||
timeoutTx := commitSpend.SpendingTx
|
||||
index := commitSpend.SpenderInputIndex
|
||||
spendHash := commitSpend.SpenderTxHash
|
||||
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
||||
OutPoint: timeoutTx.TxIn[index].PreviousOutPoint,
|
||||
Amount: h.htlc.Amt.ToSatoshis(),
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||
SpendTxID: &spendHash,
|
||||
SpendTxID: spendHash,
|
||||
})
|
||||
}
|
||||
|
||||
// With the clean up message sent, we'll now mark the contract
|
||||
// resolved, record the timeout and the sweep txid on disk, and wait.
|
||||
// resolved, update the recovered balance, record the timeout and the
|
||||
// sweep txid on disk, and wait.
|
||||
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)
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: h.htlcResolution.ClaimOutpoint,
|
||||
OutPoint: claimOutpoint,
|
||||
Amount: amt,
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: channeldb.ResolverOutcomeTimeout,
|
||||
@ -428,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
|
||||
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
|
||||
// Writer.
|
||||
//
|
||||
@ -455,6 +617,12 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// We encode the sign details last for backwards compatibility.
|
||||
err := encodeSignDetails(w, h.htlcResolution.SignDetails)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -490,6 +658,18 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) (
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -3,10 +3,12 @@ package contractcourt
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
@ -17,6 +19,7 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
type mockWitnessBeacon struct {
|
||||
@ -127,6 +130,16 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To avoid triggering the race detector by
|
||||
// setting the witness the second time this
|
||||
// method is called during tests, we return
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
return templateTx, nil
|
||||
}
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
@ -148,6 +161,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To avoid triggering the race detector by
|
||||
// setting the witness the second time this
|
||||
// method is called during tests, we return
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
return templateTx, nil
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
|
||||
// Set the outpoint to be on our commitment, since
|
||||
@ -174,6 +198,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To avoid triggering the race detector by
|
||||
// setting the witness the second time this
|
||||
// method is called during tests, we return
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
return templateTx, nil
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
@ -196,6 +231,17 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// To avoid triggering the race detector by
|
||||
// setting the witness the second time this
|
||||
// method is called during tests, we return
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
return templateTx, nil
|
||||
}
|
||||
|
||||
templateTx.TxIn[0].Witness = witness
|
||||
return templateTx, nil
|
||||
},
|
||||
@ -282,16 +328,19 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
// broadcast, then we'll set the timeout commit to a fake
|
||||
// transaction to force the code path.
|
||||
if !testCase.remoteCommit {
|
||||
resolver.htlcResolution.SignedTimeoutTx = sweepTx
|
||||
timeoutTx, err := testCase.txToBroadcast()
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
||||
|
||||
if testCase.timeout {
|
||||
success := sweepTx.TxHash()
|
||||
timeoutTxID := timeoutTx.TxHash()
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: sweepTx.TxIn[0].PreviousOutPoint,
|
||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
||||
Amount: testHtlcAmt.ToSatoshis(),
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||
SpendTxID: &success,
|
||||
SpendTxID: &timeoutTxID,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -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/wire"
|
||||
"github.com/btcsuite/btcutil"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
)
|
||||
|
||||
// Input represents an abstract UTXO which is to be spent using a sweeping
|
||||
@ -67,6 +68,22 @@ type TxInfo struct {
|
||||
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 {
|
||||
outpoint wire.OutPoint
|
||||
witnessType WitnessType
|
||||
@ -241,7 +258,130 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
|
||||
}, 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
|
||||
// interface.
|
||||
var _ Input = (*BaseInput)(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 +
|
||||
33 + 5*1 + 4 + 8*1
|
||||
|
||||
// AcceptedHtlcScriptSizeConfirmed 143 bytes
|
||||
//
|
||||
// TODO(halseth): the non-confirmed version currently includes the
|
||||
// overhead.
|
||||
AcceptedHtlcScriptSizeConfirmed = AcceptedHtlcScriptSize // + HtlcConfirmedScriptOverhead
|
||||
|
||||
// AcceptedHtlcTimeoutWitnessSize 219
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - sender_sig_length: 1 byte
|
||||
@ -361,6 +367,12 @@ const (
|
||||
AcceptedHtlcSuccessWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + 32 + 1 +
|
||||
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
|
||||
// - OP_DUP: 1 byte
|
||||
// - OP_HASH160: 1 byte
|
||||
@ -398,6 +410,12 @@ const (
|
||||
// - OP_ENDIF: 1 byte
|
||||
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
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - receiver_sig_length: 1 byte
|
||||
@ -420,6 +438,12 @@ const (
|
||||
// - witness_script (offered_htlc_script)
|
||||
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
|
||||
// - number_of_witness_elements: 1 byte
|
||||
// - revocation_sig_length: 1 byte
|
||||
|
@ -80,6 +80,14 @@ const (
|
||||
// result, we can only spend this after a CSV delay.
|
||||
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
|
||||
// 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
|
||||
@ -87,6 +95,14 @@ const (
|
||||
// transaction. As a result, we can only spend this after a CSV delay.
|
||||
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
|
||||
// HTLC that we offered to the remote party which lies in the
|
||||
// commitment transaction of the remote party. We can spend this output
|
||||
@ -163,9 +179,15 @@ func (wt StandardWitnessType) String() string {
|
||||
case HtlcOfferedTimeoutSecondLevel:
|
||||
return "HtlcOfferedTimeoutSecondLevel"
|
||||
|
||||
case HtlcOfferedTimeoutSecondLevelInputConfirmed:
|
||||
return "HtlcOfferedTimeoutSecondLevelInputConfirmed"
|
||||
|
||||
case HtlcAcceptedSuccessSecondLevel:
|
||||
return "HtlcAcceptedSuccessSecondLevel"
|
||||
|
||||
case HtlcAcceptedSuccessSecondLevelInputConfirmed:
|
||||
return "HtlcAcceptedSuccessSecondLevelInputConfirmed"
|
||||
|
||||
case HtlcOfferedRemoteTimeout:
|
||||
return "HtlcOfferedRemoteTimeout"
|
||||
|
||||
@ -375,12 +397,20 @@ func (wt StandardWitnessType) SizeUpperBound() (int, bool, error) {
|
||||
case HtlcOfferedTimeoutSecondLevel:
|
||||
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
|
||||
// chain, and the output they produced is now mature enough to
|
||||
// sweep.
|
||||
case HtlcAcceptedSuccessSecondLevel:
|
||||
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,
|
||||
// that has had its absolute timelock expire.
|
||||
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]
|
||||
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.
|
||||
carolSecondLevelCSV := uint32(defaultCSV)
|
||||
carolSecondLevelCSV := secondLevelMaturity
|
||||
|
||||
// When Bob notices Carol's second level transaction in the block, he
|
||||
// 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
|
||||
// of Carol's.
|
||||
bobSecondLevelCSV := uint32(defaultCSV)
|
||||
bobSecondLevelCSV := secondLevelMaturity
|
||||
carolSecondLevelCSV--
|
||||
|
||||
// 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.
|
||||
const (
|
||||
dustHtlcAmt = btcutil.Amount(100)
|
||||
htlcAmt = btcutil.Amount(30000)
|
||||
htlcAmt = btcutil.Amount(300_000)
|
||||
finalCltvDelta = 40
|
||||
)
|
||||
|
||||
|
@ -36,7 +36,7 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
|
||||
// opens up the base for out tests.
|
||||
const (
|
||||
finalCltvDelta = 40
|
||||
htlcAmt = btcutil.Amount(30000)
|
||||
htlcAmt = btcutil.Amount(300_000)
|
||||
)
|
||||
ctx, cancel := context.WithCancel(ctxb)
|
||||
defer cancel()
|
||||
|
@ -61,6 +61,11 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
|
||||
name: "remote chain claim",
|
||||
test: testMultiHopHtlcRemoteChainClaim,
|
||||
},
|
||||
{
|
||||
// bob: outgoing and incoming, sweep all on chain
|
||||
name: "local htlc aggregation",
|
||||
test: testMultiHopHtlcAggregation,
|
||||
},
|
||||
}
|
||||
|
||||
commitTypes := []commitType{
|
||||
|
@ -3640,6 +3640,16 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
||||
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)
|
||||
aliceChan, err := getChanInfo(ctxt, alice)
|
||||
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
|
||||
// closing, we expect Alice to broadcast an htlc timeout txn for each
|
||||
// one. Wait for them all to show up in the mempool.
|
||||
htlcTxIDs, err := waitForNTxsInMempool(net.Miner.Node, numInvoices,
|
||||
minerMempoolTimeout)
|
||||
// one.
|
||||
expectedTxes = numInvoices
|
||||
|
||||
// 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 {
|
||||
t.Fatalf("unable to find htlc timeout txns in mempool: %v", err)
|
||||
}
|
||||
|
||||
// Retrieve each htlc timeout txn from the mempool, and ensure it is
|
||||
// well-formed. This entails verifying that each only spends from
|
||||
// output, and that that output is from the commitment txn. We do not
|
||||
// the sweeper check for these timeout transactions because they are
|
||||
// not swept by the sweeper; the nursery broadcasts the pre-signed
|
||||
// transaction.
|
||||
// output, and that that output is from the commitment txn. In case
|
||||
// this is an anchor channel, the transactions are aggregated by the
|
||||
// sweeper into one.
|
||||
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
|
||||
for _, htlcTxID := range htlcTxIDs {
|
||||
// Fetch the sweep transaction, all input it's spending should
|
||||
// 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)
|
||||
if err != nil {
|
||||
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
|
||||
if len(inputs) != 1 {
|
||||
t.Fatalf("htlc transaction should only have one txin, "+
|
||||
"has %d", 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)
|
||||
if len(inputs) != numInputs {
|
||||
t.Fatalf("htlc transaction should only have %d txin, "+
|
||||
"has %d", numInputs, len(htlcTx.MsgTx().TxIn))
|
||||
}
|
||||
|
||||
// The number of outputs should be the same.
|
||||
outputs := htlcTx.MsgTx().TxOut
|
||||
if len(outputs) != 1 {
|
||||
t.Fatalf("htlc transaction should only have one "+
|
||||
"txout, has: %v", len(outputs))
|
||||
if len(outputs) != numInputs {
|
||||
t.Fatalf("htlc transaction should only have %d"+
|
||||
"txout, has: %v", numInputs, len(outputs))
|
||||
}
|
||||
|
||||
// For each htlc timeout transaction, we expect a resolver
|
||||
// report recording this on chain resolution for both alice and
|
||||
// carol.
|
||||
outpoint := txIn.PreviousOutPoint
|
||||
resolutionOutpoint := &lnrpc.OutPoint{
|
||||
TxidBytes: outpoint.Hash[:],
|
||||
TxidStr: outpoint.Hash.String(),
|
||||
OutputIndex: outpoint.Index,
|
||||
}
|
||||
// Ensure all the htlc transaction inputs are spending from the
|
||||
// commitment transaction, except if this is an extra input
|
||||
// added to pay for fees for anchor channels.
|
||||
nonCommitmentInputs := 0
|
||||
for i, txIn := range inputs {
|
||||
if !closingTxID.IsEqual(&txIn.PreviousOutPoint.Hash) {
|
||||
nonCommitmentInputs++
|
||||
|
||||
// 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),
|
||||
}
|
||||
if nonCommitmentInputs > 1 {
|
||||
t.Fatalf("htlc transaction not "+
|
||||
"spending from commit "+
|
||||
"tx %v, instead spending %v",
|
||||
closingTxID,
|
||||
txIn.PreviousOutPoint)
|
||||
}
|
||||
|
||||
// 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),
|
||||
// This was an extra input added to pay fees,
|
||||
// continue to the next one.
|
||||
continue
|
||||
}
|
||||
|
||||
// For each htlc timeout transaction, we expect a
|
||||
// resolver report recording this on chain resolution
|
||||
// for both alice and carol.
|
||||
outpoint := txIn.PreviousOutPoint
|
||||
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
|
||||
@ -4260,7 +4316,13 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
htlcSweepTx, err := net.Miner.Node.GetRawTransaction(htlcSweepTxID)
|
||||
if err != nil {
|
||||
@ -4353,19 +4406,19 @@ func channelForceClosureTest(net *lntest.NetworkHarness, t *harnessTest,
|
||||
"%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 {
|
||||
outpoint := txIn.PreviousOutPoint.Hash
|
||||
outpoint := txIn.PreviousOutPoint
|
||||
// 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 "+
|
||||
"tx, instead spending output %v", outpoint)
|
||||
}
|
||||
// Increment our count for how many times this output was spent.
|
||||
htlcTxIDSet[outpoint]++
|
||||
htlcTxOutpointSet[outpoint]++
|
||||
|
||||
// Check that each is only spent once.
|
||||
if htlcTxIDSet[outpoint] > 1 {
|
||||
if htlcTxOutpointSet[outpoint] > 1 {
|
||||
t.Fatalf("htlc sweep tx has multiple spends from "+
|
||||
"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
|
||||
// the verbose output of the listsweeps output.
|
||||
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.
|
||||
htlcHashes := make(map[string]struct{})
|
||||
for _, htlc := range channel.PendingHtlcs {
|
||||
_, ok := htlcHashes[string(htlc.HashLock)]
|
||||
h := hex.EncodeToString(htlc.HashLock)
|
||||
_, ok := htlcHashes[h]
|
||||
if ok {
|
||||
return fmt.Errorf("duplicate HashLock")
|
||||
}
|
||||
htlcHashes[string(htlc.HashLock)] = struct{}{}
|
||||
htlcHashes[h] = struct{}{}
|
||||
}
|
||||
|
||||
// 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.
|
||||
for _, payHash := range payHashes {
|
||||
if _, ok := htlcHashes[string(payHash)]; ok {
|
||||
h := hex.EncodeToString(payHash)
|
||||
if _, ok := htlcHashes[h]; ok {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("node %x didn't have the "+
|
||||
"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
|
||||
// signature to give to the remote party for this commitment
|
||||
// transaction. Note we use the raw HTLC amount.
|
||||
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
|
||||
sigJob.SignDesc = input.SignDescriptor{
|
||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||
WitnessScript: htlc.theirWitnessScript,
|
||||
Output: &wire.TxOut{
|
||||
Value: int64(htlc.Amount.ToSatoshis()),
|
||||
},
|
||||
HashType: sigHashType,
|
||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||
InputIndex: 0,
|
||||
Output: txOut,
|
||||
HashType: sigHashType,
|
||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||
InputIndex: 0,
|
||||
}
|
||||
sigJob.OutputIndex = htlc.remoteOutputIndex
|
||||
|
||||
@ -3087,16 +3086,15 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
|
||||
// Finally, we'll generate a sign descriptor to generate a
|
||||
// signature to give to the remote party for this commitment
|
||||
// transaction. Note we use the raw HTLC amount.
|
||||
txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
|
||||
sigJob.SignDesc = input.SignDescriptor{
|
||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||
WitnessScript: htlc.theirWitnessScript,
|
||||
Output: &wire.TxOut{
|
||||
Value: int64(htlc.Amount.ToSatoshis()),
|
||||
},
|
||||
HashType: sigHashType,
|
||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||
InputIndex: 0,
|
||||
Output: txOut,
|
||||
HashType: sigHashType,
|
||||
SigHashes: txscript.NewTxSigHashes(sigJob.Tx),
|
||||
InputIndex: 0,
|
||||
}
|
||||
sigJob.OutputIndex = htlc.remoteOutputIndex
|
||||
|
||||
@ -5396,7 +5394,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, signer input.Si
|
||||
htlcResolutions, err := extractHtlcResolutions(
|
||||
chainfee.SatPerKWeight(remoteCommit.FeePerKw), false, signer,
|
||||
remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
||||
&chanState.RemoteChanCfg, *commitSpend.SpenderTxHash,
|
||||
&chanState.RemoteChanCfg, commitSpend.SpendingTx,
|
||||
chanState.ChanType,
|
||||
)
|
||||
if err != nil {
|
||||
@ -5519,6 +5517,14 @@ type IncomingHtlcResolution struct {
|
||||
// claimed directly from the outpoint listed below.
|
||||
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
|
||||
// pass after the SignedSuccessTx is confirmed in the chain before the
|
||||
// output can be swept.
|
||||
@ -5560,6 +5566,14 @@ type OutgoingHtlcResolution struct {
|
||||
// claimed directly from the outpoint listed below.
|
||||
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
|
||||
// pass after the SignedTimeoutTx is confirmed in the chain before the
|
||||
// output can be swept.
|
||||
@ -5599,13 +5613,13 @@ type HtlcResolutions struct {
|
||||
// allowing the caller to sweep an outgoing HTLC present on either their, or
|
||||
// the remote party's commitment transaction.
|
||||
func newOutgoingHtlcResolution(signer input.Signer,
|
||||
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
|
||||
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
|
||||
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||
feePerKw chainfee.SatPerKWeight, csvDelay uint32,
|
||||
localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) {
|
||||
|
||||
op := wire.OutPoint{
|
||||
Hash: commitHash,
|
||||
Hash: commitTx.TxHash(),
|
||||
Index: uint32(htlc.OutputIndex),
|
||||
}
|
||||
|
||||
@ -5664,16 +5678,15 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
||||
// With the transaction created, we can generate a sign descriptor
|
||||
// that's capable of generating the signature required to spend the
|
||||
// HTLC output using the timeout transaction.
|
||||
txOut := commitTx.TxOut[htlc.OutputIndex]
|
||||
timeoutSignDesc := input.SignDescriptor{
|
||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||
WitnessScript: htlcScript,
|
||||
Output: &wire.TxOut{
|
||||
Value: int64(htlc.Amt.ToSatoshis()),
|
||||
},
|
||||
HashType: txscript.SigHashAll,
|
||||
SigHashes: txscript.NewTxSigHashes(timeoutTx),
|
||||
InputIndex: 0,
|
||||
Output: txOut,
|
||||
HashType: txscript.SigHashAll,
|
||||
SigHashes: txscript.NewTxSigHashes(timeoutTx),
|
||||
InputIndex: 0,
|
||||
}
|
||||
|
||||
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
||||
@ -5692,6 +5705,12 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
||||
}
|
||||
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
|
||||
// transaction creates so we can generate the signDesc required to
|
||||
// complete the claim process after a delay period.
|
||||
@ -5712,6 +5731,7 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
||||
return &OutgoingHtlcResolution{
|
||||
Expiry: htlc.RefundTimeout,
|
||||
SignedTimeoutTx: timeoutTx,
|
||||
SignDetails: txSignDetails,
|
||||
CsvDelay: csvDelay,
|
||||
ClaimOutpoint: wire.OutPoint{
|
||||
Hash: timeoutTx.TxHash(),
|
||||
@ -5738,13 +5758,13 @@ func newOutgoingHtlcResolution(signer input.Signer,
|
||||
//
|
||||
// TODO(roasbeef) consolidate code with above func
|
||||
func newIncomingHtlcResolution(signer input.Signer,
|
||||
localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash,
|
||||
localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx,
|
||||
htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||
feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool,
|
||||
chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) {
|
||||
|
||||
op := wire.OutPoint{
|
||||
Hash: commitHash,
|
||||
Hash: commitTx.TxHash(),
|
||||
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
|
||||
// SignDesc needed spend the HTLC output using the success transaction.
|
||||
txOut := commitTx.TxOut[htlc.OutputIndex]
|
||||
successSignDesc := input.SignDescriptor{
|
||||
KeyDesc: localChanCfg.HtlcBasePoint,
|
||||
SingleTweak: keyRing.LocalHtlcKeyTweak,
|
||||
WitnessScript: htlcScript,
|
||||
Output: &wire.TxOut{
|
||||
Value: int64(htlc.Amt.ToSatoshis()),
|
||||
},
|
||||
HashType: txscript.SigHashAll,
|
||||
SigHashes: txscript.NewTxSigHashes(successTx),
|
||||
InputIndex: 0,
|
||||
Output: txOut,
|
||||
HashType: txscript.SigHashAll,
|
||||
SigHashes: txscript.NewTxSigHashes(successTx),
|
||||
InputIndex: 0,
|
||||
}
|
||||
|
||||
htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256())
|
||||
@ -5825,6 +5844,12 @@ func newIncomingHtlcResolution(signer input.Signer,
|
||||
}
|
||||
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
|
||||
// creates so we can generate the proper signDesc to sweep it after the
|
||||
// CSV delay has passed.
|
||||
@ -5844,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer,
|
||||
)
|
||||
return &IncomingHtlcResolution{
|
||||
SignedSuccessTx: successTx,
|
||||
SignDetails: txSignDetails,
|
||||
CsvDelay: csvDelay,
|
||||
ClaimOutpoint: wire.OutPoint{
|
||||
Hash: successTx.TxHash(),
|
||||
@ -5892,7 +5918,7 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint {
|
||||
func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
||||
signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing,
|
||||
localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
|
||||
commitHash chainhash.Hash, chanType channeldb.ChannelType) (
|
||||
commitTx *wire.MsgTx, chanType channeldb.ChannelType) (
|
||||
*HtlcResolutions, error) {
|
||||
|
||||
// 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
|
||||
// as we can satisfy the contract.
|
||||
ihr, err := newIncomingHtlcResolution(
|
||||
signer, localChanCfg, commitHash, &htlc,
|
||||
signer, localChanCfg, commitTx, &htlc,
|
||||
keyRing, feePerKw, uint32(csvDelay), ourCommit,
|
||||
chanType,
|
||||
)
|
||||
@ -5937,7 +5963,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool,
|
||||
}
|
||||
|
||||
ohr, err := newOutgoingHtlcResolution(
|
||||
signer, localChanCfg, commitHash, &htlc, keyRing,
|
||||
signer, localChanCfg, commitTx, &htlc, keyRing,
|
||||
feePerKw, uint32(csvDelay), ourCommit, chanType,
|
||||
)
|
||||
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
|
||||
// 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.
|
||||
txHash := commitTx.TxHash()
|
||||
localCommit := chanState.LocalCommitment
|
||||
htlcResolutions, err := extractHtlcResolutions(
|
||||
chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer,
|
||||
localCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
|
||||
&chanState.RemoteChanCfg, txHash, chanState.ChanType,
|
||||
&chanState.RemoteChanCfg, commitTx, chanState.ChanType,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
@ -235,6 +235,24 @@ func HtlcSigHashType(chanType channeldb.ChannelType) txscript.SigHashType {
|
||||
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
|
||||
// input to a second level HTLC transaction.
|
||||
func HtlcSecondLevelInputSequence(chanType channeldb.ChannelType) uint32 {
|
||||
|
@ -13,5 +13,5 @@ var (
|
||||
//
|
||||
// To speed up integration tests waiting for a sweep to happen, the
|
||||
// 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
|
||||
// whole.
|
||||
func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
|
||||
for _, input := range sweepableInputs {
|
||||
for i, inp := range sweepableInputs {
|
||||
// Apply relaxed constraints for force sweeps.
|
||||
constraints := constraintsRegular
|
||||
if input.parameters().Force {
|
||||
if inp.parameters().Force {
|
||||
constraints = constraintsForce
|
||||
}
|
||||
|
||||
@ -304,16 +304,26 @@ func (t *txInputSet) addPositiveYieldInputs(sweepableInputs []txInput) {
|
||||
// succeed because it wouldn't increase the output value,
|
||||
// return. Assuming inputs are sorted by yield, any further
|
||||
// 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
|
||||
}
|
||||
|
||||
log.Debugf("Added positive yield input %v to input set",
|
||||
inputTypeSummary([]input.Input{inp}))
|
||||
}
|
||||
|
||||
// We managed to add all inputs to the set.
|
||||
}
|
||||
|
||||
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding as
|
||||
// many as required to bring the tx output value above the given minimum.
|
||||
// tryAddWalletInputsIfNeeded retrieves utxos from the wallet and tries adding
|
||||
// as many as required to bring the tx output value above the given minimum.
|
||||
func (t *txInputSet) tryAddWalletInputsIfNeeded() error {
|
||||
// If we've already have enough to pay the transaction fees and have at
|
||||
// least one output materialize, no action is needed.
|
||||
|
@ -222,6 +222,9 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
|
||||
PkScript: outputPkScript,
|
||||
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
|
||||
|
Loading…
Reference in New Issue
Block a user