From eb8d22e1943ccec94fa6c060d5ad0ee3cca75d1f Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:01 +0100 Subject: [PATCH 01/25] lnwallet/channel: properly set SignDesc.Output Only value was populated for some, which would cause code to rely on the PkScript being there to fail. --- lnwallet/channel.go | 63 +++++++++++++++++++++------------------------ 1 file changed, 29 insertions(+), 34 deletions(-) diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 4ab00e28..9aa70818 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -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 { @@ -5599,13 +5597,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 +5662,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()) @@ -5738,13 +5735,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 +5792,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()) @@ -5892,7 +5888,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 +5920,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 +5933,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 +6134,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 From 1e68cdc8cfb286dd49ace5190ada3f32ea384fe7 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:01 +0100 Subject: [PATCH 02/25] input+lnwallet+contractcourt: define SignDetails for HTLC resolutions --- contractcourt/briefcase.go | 138 +++++++++++++++++++++++++ contractcourt/briefcase_test.go | 102 +++++++++++++++++- contractcourt/htlc_success_resolver.go | 16 +++ contractcourt/htlc_timeout_resolver.go | 16 +++ input/input.go | 16 +++ lnwallet/channel.go | 30 ++++++ lnwallet/commitment.go | 18 ++++ 7 files changed, 334 insertions(+), 2 deletions(-) diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go index eb1489d5..fea23bf7 100644 --- a/contractcourt/briefcase.go +++ b/contractcourt/briefcase.go @@ -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 diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index b9671239..ae66ea44 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -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) { @@ -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 diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 1a99cc3b..b20f006d 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -355,6 +355,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 +394,16 @@ 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 + } + return h, nil } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index b85441b1..db60efcc 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -455,6 +455,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 +496,16 @@ 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 + } + return h, nil } diff --git a/input/input.go b/input/input.go index 7c0d79f6..cac503ce 100644 --- a/input/input.go +++ b/input/input.go @@ -67,6 +67,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 diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 9aa70818..90f30e58 100644 --- a/lnwallet/channel.go +++ b/lnwallet/channel.go @@ -5517,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. @@ -5558,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. @@ -5689,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. @@ -5709,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(), @@ -5821,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. @@ -5840,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer, ) return &IncomingHtlcResolution{ SignedSuccessTx: successTx, + SignDetails: txSignDetails, CsvDelay: csvDelay, ClaimOutpoint: wire.OutPoint{ Hash: successTx.TxHash(), diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go index 44b3c81c..f5eaf861 100644 --- a/lnwallet/commitment.go +++ b/lnwallet/commitment.go @@ -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 { From 6150995b46e66126233aa98cbec955a429968a61 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:01 +0100 Subject: [PATCH 03/25] sweep/txgenerator: log in case of no change output --- sweep/txgenerator.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index ec58b414..0afe04bf 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -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 From 83f9aaec9873800d2a80bfdce4eb0575fbdb4372 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:01 +0100 Subject: [PATCH 04/25] sweeper/tx_input_set: add logging for input set construction --- sweep/tx_input_set.go | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go index b05b5ab0..2cd5b3e5 100644 --- a/sweep/tx_input_set.go +++ b/sweep/tx_input_set.go @@ -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. From 09f2307d14f412d376062935a7ef072a3fcf7094 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 05/25] itest: increase htlc amt for local force close tests Since the tests set a quite high fee rate before the node goes to chain, the HTLCs wouldn't be economical to sweep at this fee rate. Pre sweeper handling of the second-level transactions this was not a problem, since the fees were set when the second-levels were created, before the fee estimate was increased. --- lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go | 2 +- ...nd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go index e9ef4643..e1230b95 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_timeout_test.go @@ -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 ) diff --git a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go index 83ede6e9..d241743e 100644 --- a/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go +++ b/lntest/itest/lnd_multi-hop_local_force_close_on_chain_htlc_timeout_test.go @@ -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() From 241e21a0d1ffc5ef3b735e116f08460589951a4c Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 06/25] itest: use hex encoded hash in error message --- lntest/itest/lnd_test.go | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 68845c9f..9317f926 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -11019,11 +11019,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 +11036,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) } } } From 8eb6d7cf87f2dd3910c51cb031b1c0c042b9b018 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 07/25] input/size: define witness constants needed We define the witness constanst we need for fee estimation for this HTLC second level type. --- input/size.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/input/size.go b/input/size.go index c1964716..b1ddfe94 100644 --- a/input/size.go +++ b/input/size.go @@ -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 From 65e50f6952faa12f0fc549ea0ea01ef93e0533ff Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 08/25] input/witnessgen: define witness type for HTLC 2nd level inputs These will only be used for size upper bound estimations by the sweeper. --- input/witnessgen.go | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/input/witnessgen.go b/input/witnessgen.go index 1a1fba6e..f5017083 100644 --- a/input/witnessgen.go +++ b/input/witnessgen.go @@ -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: From 5f613147adacc0cdc260aeb1136d972c9b050d74 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 09/25] contractcourt: decouple waitForHeight from commit sweep resolver To make it usable from other resolvers. --- contractcourt/commit_sweep_resolver.go | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 06bdfd53..8320ae63 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -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,7 +102,7 @@ func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error { return nil } - case <-c.quit: + case <-quit: return errResolverShuttingDown } } @@ -169,7 +172,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 } From 4da2b290f925efaa8c9dc050dcf7c9673fe3a192 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 10/25] contractcourt/succes+timeout resolver: extract waitForSpend logic --- contractcourt/commit_sweep_resolver.go | 26 ++++++++++ contractcourt/htlc_success_resolver.go | 24 +++------ contractcourt/htlc_timeout_resolver.go | 67 ++++++-------------------- 3 files changed, 48 insertions(+), 69 deletions(-) diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 8320ae63..1ce3f322 100644 --- a/contractcourt/commit_sweep_resolver.go +++ b/contractcourt/commit_sweep_resolver.go @@ -108,6 +108,32 @@ func waitForHeight(waitHeight uint32, notifier chainntnfs.ChainNotifier, } } +// 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) { diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index b20f006d..172f3279 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -243,33 +243,21 @@ 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( + 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( &h.htlcResolution.ClaimOutpoint, h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, + h.broadcastHeight, h.Notifier, h.quit, ) 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) - - var spendTxid *chainhash.Hash - select { - case spend, ok := <-spendNtfn.Spend: - if !ok { - return nil, errResolverShuttingDown - } - spendTxid = spend.SpenderTxHash - - case <-h.quit: - return nil, errResolverShuttingDown - } - h.resolved = true return nil, h.checkpointClaim( - spendTxid, channeldb.ResolverOutcomeClaimed, + spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed, ) } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index db60efcc..771f0c7b 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -5,7 +5,6 @@ import ( "fmt" "io" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" @@ -276,37 +275,6 @@ 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, @@ -315,12 +283,6 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { 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,21 +290,16 @@ 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 err != nil { + return nil, err } + spendTxID := spend.SpenderTxHash + // If the spend reveals the pre-image, then we'll enter the clean up // workflow to pass the pre-image back to the incoming link, add it to // the witness cache, and exit. @@ -378,10 +335,18 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { 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 { + sweep, err := waitForSpend( + &h.htlcResolution.ClaimOutpoint, + h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { return nil, err } + // Update the spend txid to the hash of the sweep transaction. + spendTxID = sweep.SpenderTxHash + // Once our timeout tx has confirmed, we add a resolution for // our timeoutTx tx first stage transaction. timeoutTx := h.htlcResolution.SignedTimeoutTx From 9b08ef6d4e9469867ca8ecdc159c0ed66bf3393d Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 11/25] contractcourt/[incoming|outgoing]_contest_resolver: make inner resolver pointer To make the linter happy, make a pointer to the inner resolver. Otherwise the linter would complain with copylocks: literal copies lock value since we'll add a mutex to the resolver in following commits. --- contractcourt/briefcase_test.go | 14 +++++++------- contractcourt/htlc_incoming_contest_resolver.go | 12 ++++++------ contractcourt/htlc_incoming_resolver_test.go | 2 +- contractcourt/htlc_outgoing_contest_resolver.go | 8 ++++---- .../htlc_outgoing_contest_resolver_test.go | 2 +- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/contractcourt/briefcase_test.go b/contractcourt/briefcase_test.go index ae66ea44..98d30ba9 100644 --- a/contractcourt/briefcase_test.go +++ b/contractcourt/briefcase_test.go @@ -271,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 { @@ -375,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 @@ -521,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, @@ -549,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) } @@ -566,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 diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go index 442121a6..59f5d9fd 100644 --- a/contractcourt/htlc_incoming_contest_resolver.go +++ b/contractcourt/htlc_incoming_contest_resolver.go @@ -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 } diff --git a/contractcourt/htlc_incoming_resolver_test.go b/contractcourt/htlc_incoming_resolver_test.go index 4ada27ff..da27db72 100644 --- a/contractcourt/htlc_incoming_resolver_test.go +++ b/contractcourt/htlc_incoming_resolver_test.go @@ -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{ diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go index 28d95247..d2813c95 100644 --- a/contractcourt/htlc_outgoing_contest_resolver.go +++ b/contractcourt/htlc_outgoing_contest_resolver.go @@ -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 } diff --git a/contractcourt/htlc_outgoing_contest_resolver_test.go b/contractcourt/htlc_outgoing_contest_resolver_test.go index 987c1a7a..6d1f4e90 100644 --- a/contractcourt/htlc_outgoing_contest_resolver_test.go +++ b/contractcourt/htlc_outgoing_contest_resolver_test.go @@ -177,7 +177,7 @@ func newOutgoingResolverTestContext(t *testing.T) *outgoingResolverTestContext { } resolver := &htlcOutgoingContestResolver{ - htlcTimeoutResolver: htlcTimeoutResolver{ + htlcTimeoutResolver: &htlcTimeoutResolver{ contractResolverKit: *newContractResolverKit(cfg), htlcResolution: outgoingRes, htlc: channeldb.HTLC{ From 9d33b0008271d889a88fe8937efec1d5eb3205f7 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 12/25] contractcourt/success_resolver: extract remoteHTLC sweep into resolveRemoteCommitOutput This move the logic for sweeping the HTLC output on the remote commitment into its own method. --- contractcourt/htlc_success_resolver.go | 199 +++++++++++++------------ 1 file changed, 103 insertions(+), 96 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 172f3279..5fb034c8 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -105,102 +105,7 @@ 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() } log.Infof("%T(%x): broadcasting second-layer transition tx: %v", @@ -261,6 +166,108 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { ) } +// 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)) + + // 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, + ) +} + // checkpointClaim checkpoints the success resolver with the reports it needs. // If this htlc was claimed two stages, it will write reports for both stages, // otherwise it will just write for the single htlc claim. From 0b84d5f976bc8cec8f009b8a4d626fd78e5cb6b0 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:02 +0100 Subject: [PATCH 13/25] contractcourt/success_resolver: extract HTLC success handling into broadcastSuccessTx --- contractcourt/htlc_success_resolver.go | 51 +++++++++++++++++--------- 1 file changed, 33 insertions(+), 18 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 5fb034c8..9e48a351 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -108,6 +108,38 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { 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.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) { log.Infof("%T(%x): broadcasting second-layer transition tx: %v", h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) @@ -146,24 +178,7 @@ 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. - 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( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - h.resolved = true - return nil, h.checkpointClaim( - spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed, - ) + return &h.htlcResolution.ClaimOutpoint, nil } // resolveRemoteCommitOutput handles sweeping an HTLC output on the remote From 7142a302c9eefc9caccc0dbcb8c4fa58a2a0147b Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 14/25] contractcourt/success_resolver: remove sweep tx checkpoint The sweep tx is not actually part of the resolver's encoded data, so the checkpointing was essentially a noop. --- contractcourt/htlc_success_resolver.go | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 9e48a351..db1706bd 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -228,12 +228,9 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( 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 - } + // 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 From d02b486195cf8cd30744582d29cf642060783866 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 15/25] contractcourt: revamp HTLC success unit test We add checkpoint assertions and resume the resolver from every checkpoint to ensure it can handle restarts. --- contractcourt/commit_sweep_resolver_test.go | 20 +- contractcourt/htlc_success_resolver_test.go | 286 +++++++++++++++----- 2 files changed, 232 insertions(+), 74 deletions(-) diff --git a/contractcourt/commit_sweep_resolver_test.go b/contractcourt/commit_sweep_resolver_test.go index eb54dbf3..93c0b8b5 100644 --- a/contractcourt/commit_sweep_resolver_test.go +++ b/contractcourt/commit_sweep_resolver_test.go @@ -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 { diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 6e44c22c..0366853a 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -1,10 +1,14 @@ package contractcourt import ( + "bytes" + "io" + "reflect" "testing" "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" @@ -22,11 +26,11 @@ type htlcSuccessResolverTestContext struct { t *testing.T } -func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestContext { +func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlcSuccessResolverTestContext { 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) @@ -42,6 +46,11 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon PublishTx: func(_ *wire.MsgTx, _ string) error { return nil }, + Sweeper: newMockSweeper(), + IncubateOutputs: func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution, + *lnwallet.IncomingHtlcResolution, uint32) error { + return nil + }, }, PutResolverReport: func(_ kvdb.RwTx, report *channeldb.ResolverReport) error { @@ -59,15 +68,27 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon return nil }, } + htlc := channeldb.HTLC{ + RHash: testResHash, + OnionBlob: testOnionBlob, + Amt: testHtlcAmt, + } + if checkpoint != nil { + var err error + testCtx.resolver, err = newSuccessResolverFromReader(checkpoint, cfg) + if err != nil { + t.Fatal(err) + } - testCtx.resolver = &htlcSuccessResolver{ - contractResolverKit: *newContractResolverKit(cfg), - htlcResolution: lnwallet.IncomingHtlcResolution{}, - htlc: channeldb.HTLC{ - RHash: testResHash, - OnionBlob: testOnionBlob, - Amt: testHtlcAmt, - }, + testCtx.resolver.Supplement(htlc) + + } else { + + testCtx.resolver = &htlcSuccessResolver{ + contractResolverKit: *newContractResolverKit(cfg), + htlcResolution: lnwallet.IncomingHtlcResolution{}, + htlc: htlc, + } } return testCtx @@ -98,8 +119,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 +136,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 +144,45 @@ 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 *htlcSuccessResolverTestContext, + _ bool) error { + // The resolver will create and publish a sweep + // tx. + ctx.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 +202,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 +230,167 @@ 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 *htlcSuccessResolverTestContext, + _ 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. +// 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(*htlcSuccessResolverTestContext, 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) - - // 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, - reports ...*channeldb.ResolverReport) error { - - // Send all of our reports into the channel. - for _, report := range reports { - reportChan <- report - } - - return nil - } - + // 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 := newHtlcSuccessResolverTextContext(t, 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 + 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 := newHtlcSuccessResolverTextContext(t, cp) + ctx.resolver.htlcResolution = resolution + + // 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 *htlcSuccessResolverTestContext, + 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.resolver.Checkpoint = func(resolver ContractResolver, + reports ...*channeldb.ResolverReport) error { + + if nextCheckpoint >= len(expectedCheckpoints) { + t.Fatal("did not expect more checkpoints") + } + + h := resolver.(*htlcSuccessResolver) + cp := expectedCheckpoints[nextCheckpoint] + + if h.resolved != cp.resolved { + t.Fatalf("expected checkpoint to be resolve=%v, had %v", + cp.resolved, h.resolved) + } + + if !reflect.DeepEqual(h.outputIncubating, cp.incubating) { + t.Fatalf("expected checkpoint to be have "+ + "incubating=%v, had %v", cp.incubating, + h.outputIncubating) + + } + + // 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 + } // 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 } From 85ea181d67a6948afedc60ed176ce1a6cfb33459 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 16/25] contraccourt+input: create HtlcSecondLevelAnchorInput and resolver for success tx This commit makes the HTLC resolutions having non-nil SignDetails (meaning we can re-sign the second-level transactions) go through the sweeper. They will be offered to the sweeper which will cluster them and arrange them on its sweep transaction. When that is done we will further sweep the output on this sweep transaction as any other second-level tx. In this commit we do this for the HTLC success resolver and the accompanying HTLC success transaction. --- contractcourt/contract_resolvers.go | 4 + contractcourt/htlc_success_resolver.go | 208 ++++++++++++++++++++++++- input/input.go | 87 +++++++++++ 3 files changed, 297 insertions(+), 2 deletions(-) diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index cac40bac..ef391ff9 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -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 diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index db1706bd..37b4d42b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -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 @@ -129,6 +149,11 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { 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, @@ -140,6 +165,18 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // 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)) @@ -181,6 +218,137 @@ func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) { 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 + } + + // 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. @@ -337,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. // @@ -411,6 +613,8 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } diff --git a/input/input.go b/input/input.go index cac503ce..e5752e88 100644 --- a/input/input.go +++ b/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 @@ -257,7 +258,93 @@ 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 +} + +// 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) From aabba32b34643d521340c4429a65ec1559c527ba Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 17/25] contractcourt: add TestHtlcSuccessSecondStageResolutionSweeper Test success resolvers going through the sweeper. --- contractcourt/htlc_success_resolver_test.go | 183 ++++++++++++++++++++ 1 file changed, 183 insertions(+) diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 0366853a..f4f7c25e 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -2,16 +2,19 @@ package contractcourt import ( "bytes" + "fmt" "io" "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" @@ -262,6 +265,186 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) { ) } +// 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 *htlcSuccessResolverTestContext, + _ bool) error { + + inp := <-ctx.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 *htlcSuccessResolverTestContext, + 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. + inp := <-ctx.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 { From 2f3342550955c09fa496ba66ea9a1a1137eb710c Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 18/25] contractcourt/timeout_resolver: extract logic into spendHtlcOutput This commit moves the code doing the initial spend of the HTLC output of the commit tx into its own method. --- contractcourt/htlc_timeout_resolver.go | 105 ++++++++++++++----------- 1 file changed, 61 insertions(+), 44 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 771f0c7b..09088839 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -253,62 +253,25 @@ 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 { - log.Tracef("%T(%v): incubating htlc output", h, - h.htlcResolution.ClaimOutpoint) - - err := h.IncubateOutputs( - h.ChanPoint, &h.htlcResolution, nil, - h.broadcastHeight, - ) - if err != nil { - return nil, err - } - - h.outputIncubating = true - - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } - } - - // Now that we've handed off the HTLC to the nursery, we'll watch for a - // spend of the output, and make our next move off of that. Depending - // on if this is our commitment, or the remote party's commitment, - // we'll be watching a different outpoint and script. - outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch() + // 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 } - log.Infof("%T(%v): waiting for HTLC output %v to be spent"+ - "fully confirmed", h, h.htlcResolution.ClaimOutpoint, - outpointToWatch) - - // We'll block here until either we exit, or the HTLC output on the - // commitment transaction has been spent. - spend, err := waitForSpend( - outpointToWatch, scriptToWatch, h.broadcastHeight, - h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - spendTxID := spend.SpenderTxHash + spendTxID := commitSpend.SpenderTxHash // If the spend reveals the pre-image, then we'll enter the clean up // workflow to pass the pre-image back to the incoming link, add it to // the witness cache, and exit. - if isSuccessSpend(spend, h.htlcResolution.SignedTimeoutTx != nil) { + 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(spend) + return h.claimCleanUp(commitSpend) } log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+ @@ -377,6 +340,60 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, h.Checkpoint(h, reports...) } +// 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) { + // If we haven't already sent the output to the utxo nursery, then + // we'll do so now. + if !h.outputIncubating { + log.Tracef("%T(%v): incubating htlc output", h, + h.htlcResolution.ClaimOutpoint) + + err := h.IncubateOutputs( + h.ChanPoint, &h.htlcResolution, nil, + h.broadcastHeight, + ) + if err != nil { + return nil, err + } + + h.outputIncubating = true + + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err + } + } + + // Now that we've handed off the HTLC to the nursery, we'll watch for a + // spend of the output, and make our next move off of that. Depending + // on if this is our commitment, or the remote party's commitment, + // we'll be watching a different outpoint and script. + outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch() + if err != nil { + return nil, err + } + + log.Infof("%T(%v): waiting for HTLC output %v to be spent"+ + "fully confirmed", h, h.htlcResolution.ClaimOutpoint, + outpointToWatch) + + // We'll block here until either we exit, or the HTLC output on the + // commitment transaction has been spent. + spend, err := waitForSpend( + outpointToWatch, scriptToWatch, h.broadcastHeight, + h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + return spend, err +} + // Stop signals the resolver to cancel any current resolution processes, and // suspend. // From 0c3b64a3cd139147d05c151114d2fa40699df6f8 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 19/25] contractcourt/timeout_resolver: extract logic into sweepSecondLevelTransaction This commit moves the logic for sweeping the confirmed second-level timeout transaction into its own method. We do a small change to the logic: When setting the spending tx in the report, we use the detected commitspend instead of the presigned tiemout tx. This is to prepare for the coming change where the spending transaction might actually be a re-signed timeout tx, and will therefore have a different txid. --- contractcourt/htlc_timeout_resolver.go | 116 +++++++++++--------- contractcourt/htlc_timeout_resolver_test.go | 56 +++++++++- 2 files changed, 118 insertions(+), 54 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 09088839..366e4fd5 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -261,8 +261,6 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - spendTxID := commitSpend.SpenderTxHash - // If the spend reveals the pre-image, then we'll enter the clean up // workflow to pass the pre-image back to the incoming link, add it to // the witness cache, and exit. @@ -290,54 +288,7 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - var reports []*channeldb.ResolverReport - - // Finally, if this was an output on our commitment transaction, we'll - // wait for the second-level HTLC output to be spent, and for that - // transaction itself to confirm. - if h.htlcResolution.SignedTimeoutTx != nil { - log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ - "output", h, h.htlcResolution.ClaimOutpoint) - sweep, err := waitForSpend( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, h.Notifier, h.quit, - ) - if err != nil { - return nil, err - } - - // Update the spend txid to the hash of the sweep transaction. - spendTxID = sweep.SpenderTxHash - - // Once our timeout tx has confirmed, we add a resolution for - // our timeoutTx tx first stage transaction. - timeoutTx := h.htlcResolution.SignedTimeoutTx - spendHash := timeoutTx.TxHash() - - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, - Amount: h.htlc.Amt.ToSatoshis(), - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &spendHash, - }) - } - - // With the clean up message sent, we'll now mark the contract - // resolved, record the timeout and the sweep txid on disk, and wait. - h.resolved = true - - amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) - reports = append(reports, &channeldb.ResolverReport{ - OutPoint: h.htlcResolution.ClaimOutpoint, - Amount: amt, - ResolverType: channeldb.ResolverTypeOutgoingHtlc, - ResolverOutcome: channeldb.ResolverOutcomeTimeout, - SpendTxID: spendTxID, - }) - - return nil, h.Checkpoint(h, reports...) + return h.sweepSecondLevelTransaction(commitSpend) } // spendHtlcOutput handles the initial spend of an HTLC output via the timeout @@ -394,6 +345,71 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) return spend, err } +// sweepSecondLevelTransaction sweeps the output of the confirmed second-level +// timeout transaction into our wallet. The given SpendDetail should be the +// confirmed timeout tx spending the HTLC output on the commitment tx. +func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( + commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { + + var ( + // spendTxID will be the ultimate spend of the claimOutpoint. + // We set it to the commit spend for now, as this is the + // ultimate spend in case this is a remote commitment. If we go + // through the second-level transaction, we'll update this + // accordingly. + spendTxID = commitSpend.SpenderTxHash + + reports []*channeldb.ResolverReport + ) + + // Finally, if this was an output on our commitment transaction, we'll + // wait for the second-level HTLC output to be spent, and for that + // transaction itself to confirm. + if h.htlcResolution.SignedTimeoutTx != nil { + log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ + "output", h, h.htlcResolution.ClaimOutpoint) + sweep, err := waitForSpend( + &h.htlcResolution.ClaimOutpoint, + h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + // Update the spend txid to the hash of the sweep transaction. + spendTxID = sweep.SpenderTxHash + + // Once our sweep of the timeout tx has confirmed, we add a + // resolution for our timeoutTx tx first stage transaction. + timeoutTx := commitSpend.SpendingTx + spendHash := timeoutTx.TxHash() + + reports = append(reports, &channeldb.ResolverReport{ + OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, + Amount: h.htlc.Amt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &spendHash, + }) + } + + // With the clean up message sent, we'll now mark the contract + // resolved, record the timeout and the sweep txid on disk, and wait. + h.resolved = true + + amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) + reports = append(reports, &channeldb.ResolverReport{ + OutPoint: h.htlcResolution.ClaimOutpoint, + Amount: amt, + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: spendTxID, + }) + + return nil, h.Checkpoint(h, reports...) +} + // Stop signals the resolver to cancel any current resolution processes, and // suspend. // diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 6e41ad79..ffcee1e2 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -3,6 +3,7 @@ package contractcourt import ( "bytes" "fmt" + "reflect" "sync" "testing" "time" @@ -17,6 +18,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" ) type mockWitnessBeacon struct { @@ -127,6 +129,16 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -148,6 +160,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness // Set the outpoint to be on our commitment, since @@ -174,6 +197,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -196,6 +230,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -282,16 +327,19 @@ func TestHtlcTimeoutResolver(t *testing.T) { // broadcast, then we'll set the timeout commit to a fake // transaction to force the code path. if !testCase.remoteCommit { - resolver.htlcResolution.SignedTimeoutTx = sweepTx + timeoutTx, err := testCase.txToBroadcast() + require.NoError(t, err) + + resolver.htlcResolution.SignedTimeoutTx = timeoutTx if testCase.timeout { - success := sweepTx.TxHash() + timeoutTxID := timeoutTx.TxHash() reports = append(reports, &channeldb.ResolverReport{ - OutPoint: sweepTx.TxIn[0].PreviousOutPoint, + OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, Amount: testHtlcAmt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &success, + SpendTxID: &timeoutTxID, }) } } From 4992e414393fd243199023b2f654981686eb240a Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 20/25] contraccourt+input: create resolver for timeout second level In this commit we make the sweeper handle second level transactions for HTLC timeout resolvers for anchor channels. --- contractcourt/htlc_timeout_resolver.go | 210 ++++++++++++++++++++++--- input/input.go | 37 +++++ 2 files changed, 225 insertions(+), 22 deletions(-) diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index 366e4fd5..44be1643 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -4,6 +4,7 @@ import ( "encoding/binary" "fmt" "io" + "sync" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -14,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 @@ -45,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 } @@ -53,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 @@ -288,7 +303,9 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, err } - return h.sweepSecondLevelTransaction(commitSpend) + // 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 @@ -297,9 +314,37 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // commitment, the output will be swept directly without the timeout // transaction. func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) { - // If we haven't already sent the output to the utxo nursery, then - // we'll do so now. - if !h.outputIncubating { + 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) @@ -319,10 +364,10 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) } } - // 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 @@ -342,16 +387,35 @@ func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) return nil, err } + // 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 } -// sweepSecondLevelTransaction sweeps the output of the confirmed second-level -// timeout transaction into our wallet. The given SpendDetail should be the -// confirmed timeout tx spending the HTLC output on the commitment tx. -func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( +// 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 @@ -362,14 +426,74 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( 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) + case h.htlcResolution.SignedTimeoutTx != nil: + log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ + "delayed output", h, claimOutpoint) sweep, err := waitForSpend( - &h.htlcResolution.ClaimOutpoint, + &claimOutpoint, h.htlcResolution.SweepSignDesc.Output.PkScript, h.broadcastHeight, h.Notifier, h.quit, ) @@ -383,24 +507,30 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTransaction( // Once our sweep of the timeout tx has confirmed, we add a // resolution for our timeoutTx tx first stage transaction. timeoutTx := commitSpend.SpendingTx - spendHash := timeoutTx.TxHash() + 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, @@ -426,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. // @@ -504,6 +668,8 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } diff --git a/input/input.go b/input/input.go index e5752e88..87a6339e 100644 --- a/input/input.go +++ b/input/input.go @@ -305,6 +305,43 @@ func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer, }, 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. From bb406c82a901330b76666fde758abc9d07aed120 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:03 +0100 Subject: [PATCH 21/25] contractcourt/htlc_timeout_test: expand timeout tests --- contractcourt/htlc_success_resolver_test.go | 153 ++-- contractcourt/htlc_timeout_resolver_test.go | 832 ++++++++++++++++++++ 2 files changed, 926 insertions(+), 59 deletions(-) diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index f4f7c25e..288aeae5 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -3,7 +3,6 @@ package contractcourt import ( "bytes" "fmt" - "io" "reflect" "testing" @@ -22,30 +21,41 @@ 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, checkpoint io.Reader) *htlcSuccessResolverTestContext { +func newHtlcResolverTestContext(t *testing.T, + newResolver func(htlc channeldb.HTLC, + cfg ResolverConfig) ContractResolver) *htlcResolverTestContext { + notifier := &mock.ChainNotifier{ 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 }, @@ -54,6 +64,16 @@ func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlc *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 { @@ -61,43 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T, checkpoint io.Reader) *htlc 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, } + htlc := channeldb.HTLC{ RHash: testResHash, OnionBlob: testOnionBlob, Amt: testHtlcAmt, } - if checkpoint != nil { - var err error - testCtx.resolver, err = newSuccessResolverFromReader(checkpoint, cfg) - if err != nil { - t.Fatal(err) - } - testCtx.resolver.Supplement(htlc) - - } else { - - testCtx.resolver = &htlcSuccessResolver{ - contractResolverKit: *newContractResolverKit(cfg), - htlcResolution: lnwallet.IncomingHtlcResolution{}, - htlc: htlc, - } - } + 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() { @@ -109,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() { }() } -func (i *htlcSuccessResolverTestContext) waitForResult() { +func (i *htlcResolverTestContext) waitForResult() { i.t.Helper() result := <-i.resolverResultChan @@ -152,11 +160,12 @@ func TestHtlcSuccessSingleStage(t *testing.T) { { // We send a confirmation for our sweep tx to indicate // that our sweep succeeded. - preCheckpoint: func(ctx *htlcSuccessResolverTestContext, + preCheckpoint: func(ctx *htlcResolverTestContext, _ bool) error { // The resolver will create and publish a sweep // tx. - ctx.resolver.Sweeper.(*mockSweeper). + resolver := ctx.resolver.(*htlcSuccessResolver) + resolver.Sweeper.(*mockSweeper). createSweepTxChan <- sweepTx // Confirm the sweep, which should resolve it. @@ -242,7 +251,7 @@ func TestHtlcSuccessSecondStageResolution(t *testing.T) { // 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 *htlcSuccessResolverTestContext, + preCheckpoint: func(ctx *htlcResolverTestContext, _ bool) error { ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ SpendingTx: sweepTx, @@ -361,11 +370,11 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { { // The HTLC output on the commitment should be offered // to the sweeper. We'll notify that it gets spent. - preCheckpoint: func(ctx *htlcSuccessResolverTestContext, + preCheckpoint: func(ctx *htlcResolverTestContext, _ bool) error { - inp := <-ctx.resolver.Sweeper.(*mockSweeper). - sweptInputs + resolver := ctx.resolver.(*htlcSuccessResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs op := inp.OutPoint() if *op != commitOutpoint { return fmt.Errorf("outpoint %v swept, "+ @@ -389,7 +398,7 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { { // The resolver will wait for the second-level's CSV // lock to expire. - preCheckpoint: func(ctx *htlcSuccessResolverTestContext, + preCheckpoint: func(ctx *htlcResolverTestContext, resumed bool) error { // If we are resuming from a checkpoint, we @@ -410,8 +419,8 @@ func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { // We expect it to sweep the second-level // transaction we notfied about above. - inp := <-ctx.resolver.Sweeper.(*mockSweeper). - sweptInputs + resolver := ctx.resolver.(*htlcSuccessResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs op := inp.OutPoint() exp := wire.OutPoint{ Hash: reSignedHash, @@ -451,7 +460,7 @@ 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(*htlcSuccessResolverTestContext, bool) error + preCheckpoint func(*htlcResolverTestContext, bool) error // data we expect the resolver to be checkpointed with next. incubating bool @@ -471,8 +480,15 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, // 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 := newHtlcSuccessResolverTextContext(t, nil) - ctx.resolver.htlcResolution = resolution + ctx := newHtlcResolverTestContext(t, + func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { + return &htlcSuccessResolver{ + contractResolverKit: *newContractResolverKit(cfg), + htlc: htlc, + htlcResolution: resolution, + } + }, + ) checkpointedState := runFromCheckpoint(t, ctx, checkpoints) @@ -480,8 +496,18 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, // run the test from that checkpoint. for i := range checkpointedState { cp := bytes.NewReader(checkpointedState[i]) - ctx := newHtlcSuccessResolverTextContext(t, cp) - ctx.resolver.htlcResolution = resolution + 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:]) @@ -490,7 +516,7 @@ func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, // 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 *htlcSuccessResolverTestContext, +func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, expectedCheckpoints []checkpoint) [][]byte { defer timeout(t)() @@ -501,25 +527,34 @@ func runFromCheckpoint(t *testing.T, ctx *htlcSuccessResolverTestContext, // checkpointed state and reports are equal to what we expect. nextCheckpoint := 0 checkpointChan := make(chan struct{}) - ctx.resolver.Checkpoint = func(resolver ContractResolver, + ctx.checkpoint = func(resolver ContractResolver, reports ...*channeldb.ResolverReport) error { if nextCheckpoint >= len(expectedCheckpoints) { t.Fatal("did not expect more checkpoints") } - h := resolver.(*htlcSuccessResolver) - cp := expectedCheckpoints[nextCheckpoint] - - if h.resolved != cp.resolved { - t.Fatalf("expected checkpoint to be resolve=%v, had %v", - cp.resolved, h.resolved) + 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 } - if !reflect.DeepEqual(h.outputIncubating, cp.incubating) { + 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, - h.outputIncubating) + incubating) } diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index ffcee1e2..8aa366ef 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -8,6 +8,7 @@ import ( "testing" "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -481,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:]) + } +} From 70eb52643cb0c21d09e3eb8e630ee08179ec5fa8 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:04 +0100 Subject: [PATCH 22/25] rpctest: increase sweeper BatchWindow during itests Since we are checking HTLC aggregation, we must give the sweeper a bit more time to aggregate them to avoid flakes. --- sweep/defaults_rpctest.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sweep/defaults_rpctest.go b/sweep/defaults_rpctest.go index 6d027ab6..0d185e74 100644 --- a/sweep/defaults_rpctest.go +++ b/sweep/defaults_rpctest.go @@ -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 ) From 4b9fbe21461e89597667983e7ef5400cfab728db Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:04 +0100 Subject: [PATCH 23/25] itest/local_chain_claim test: mine one less blocks for anchor sweeps In case of anchor channel types, we mine one less block before we expect the second level sweep to appear in the mempool, since the sweeper sweeps one block earlier than the nursery. --- ...d_multi-hop_htlc_local_chain_claim_test.go | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go index d9422762..44a07456 100644 --- a/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go +++ b/lntest/itest/lnd_multi-hop_htlc_local_chain_claim_test.go @@ -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 From 42c51b662a354d6f5432d33127c45fe833cba493 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:04 +0100 Subject: [PATCH 24/25] itest/channel_force_close test: handle HTLCs going through sweeper Now that the HTLC second-level transactions are going through the sweeper instead of the nursery, there are a few things we must account for. 1. The sweeper sweeps the CSV locked HTLC output one block earlier than the nursery. 2. The sweeper aggregates several HTLC second levels into one transaction. This also means it is not enough to check txids of the transactions spent by the final sweep, but we must use the actual outpoint to distinguish. --- lntest/itest/lnd_test.go | 192 +++++++++++++++++++++++++-------------- 1 file changed, 126 insertions(+), 66 deletions(-) diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 9317f926..b22bcf54 100644 --- a/lntest/itest/lnd_test.go +++ b/lntest/itest/lnd_test.go @@ -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) From 1627310fb06272d266e6c1e3e07d93e48f2de78d Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Wed, 9 Dec 2020 12:24:04 +0100 Subject: [PATCH 25/25] itest: add HTLC aggregation test --- .../lnd_multi-hop_htlc_aggregation_test.go | 427 ++++++++++++++++++ lntest/itest/lnd_multi-hop_test.go | 5 + 2 files changed, 432 insertions(+) create mode 100644 lntest/itest/lnd_multi-hop_htlc_aggregation_test.go diff --git a/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go new file mode 100644 index 00000000..fa6a46b5 --- /dev/null +++ b/lntest/itest/lnd_multi-hop_htlc_aggregation_test.go @@ -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, + ) +} diff --git a/lntest/itest/lnd_multi-hop_test.go b/lntest/itest/lnd_multi-hop_test.go index a7086310..9a4819db 100644 --- a/lntest/itest/lnd_multi-hop_test.go +++ b/lntest/itest/lnd_multi-hop_test.go @@ -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{