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..98d30ba9 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) { @@ -219,13 +271,13 @@ func assertResolversEqual(t *testing.T, originalResolver ContractResolver, case *htlcOutgoingContestResolver: diskRes := diskResolver.(*htlcOutgoingContestResolver) assertTimeoutResEqual( - &ogRes.htlcTimeoutResolver, &diskRes.htlcTimeoutResolver, + ogRes.htlcTimeoutResolver, diskRes.htlcTimeoutResolver, ) case *htlcIncomingContestResolver: diskRes := diskResolver.(*htlcIncomingContestResolver) assertSuccessResEqual( - &ogRes.htlcSuccessResolver, &diskRes.htlcSuccessResolver, + ogRes.htlcSuccessResolver, diskRes.htlcSuccessResolver, ) if ogRes.htlcExpiry != diskRes.htlcExpiry { @@ -323,13 +375,13 @@ func TestContractInsertionRetrieval(t *testing.T) { contestTimeout := timeoutResolver contestTimeout.htlcResolution.ClaimOutpoint = randOutPoint() resolvers = append(resolvers, &htlcOutgoingContestResolver{ - htlcTimeoutResolver: contestTimeout, + htlcTimeoutResolver: &contestTimeout, }) contestSuccess := successResolver contestSuccess.htlcResolution.ClaimOutpoint = randOutPoint() resolvers = append(resolvers, &htlcIncomingContestResolver{ htlcExpiry: 100, - htlcSuccessResolver: contestSuccess, + htlcSuccessResolver: &contestSuccess, }) // For quick lookup during the test, we'll create this map which allow @@ -469,7 +521,7 @@ func TestContractSwapping(t *testing.T) { // We'll create two resolvers, a regular timeout resolver, and the // contest resolver that eventually turns into the timeout resolver. - timeoutResolver := htlcTimeoutResolver{ + timeoutResolver := &htlcTimeoutResolver{ htlcResolution: lnwallet.OutgoingHtlcResolution{ Expiry: 99, SignedTimeoutTx: nil, @@ -497,7 +549,7 @@ func TestContractSwapping(t *testing.T) { // With the resolver inserted, we'll now attempt to atomically swap it // for its underlying timeout resolver. - err = testLog.SwapContract(contestResolver, &timeoutResolver) + err = testLog.SwapContract(contestResolver, timeoutResolver) if err != nil { t.Fatalf("unable to swap contracts: %v", err) } @@ -514,7 +566,7 @@ func TestContractSwapping(t *testing.T) { } // That single contract should be the underlying timeout resolver. - assertResolversEqual(t, &timeoutResolver, dbContracts[0]) + assertResolversEqual(t, timeoutResolver, dbContracts[0]) } // TestContractResolutionsStorage tests that we're able to properly store and @@ -550,8 +602,38 @@ func TestContractResolutionsStorage(t *testing.T) { ClaimOutpoint: randOutPoint(), SweepSignDesc: testSignDesc, }, + + // We add a resolution with SignDetails. + { + Preimage: testPreimage, + SignedSuccessTx: testTx, + SignDetails: testSignDetails, + CsvDelay: 900, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + + // We add a resolution with a signed tx, but no + // SignDetails. + { + Preimage: testPreimage, + SignedSuccessTx: testTx, + CsvDelay: 900, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, }, OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{ + // We add a resolution with a signed tx, but no + // SignDetails. + { + Expiry: 103, + SignedTimeoutTx: testTx, + CsvDelay: 923923, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, + // Resolution without signed tx. { Expiry: 103, SignedTimeoutTx: nil, @@ -559,6 +641,15 @@ func TestContractResolutionsStorage(t *testing.T) { ClaimOutpoint: randOutPoint(), SweepSignDesc: testSignDesc, }, + // Resolution with SignDetails. + { + Expiry: 103, + SignedTimeoutTx: testTx, + SignDetails: testSignDetails, + CsvDelay: 923923, + ClaimOutpoint: randOutPoint(), + SweepSignDesc: testSignDesc, + }, }, }, AnchorResolution: &lnwallet.AnchorResolution{ @@ -585,8 +676,15 @@ func TestContractResolutionsStorage(t *testing.T) { } if !reflect.DeepEqual(&res, diskRes) { - t.Fatalf("resolution mismatch: expected %#v\n, got %#v", - &res, diskRes) + for _, h := range res.HtlcResolutions.IncomingHTLCs { + h.SweepSignDesc.KeyDesc.PubKey.Curve = nil + } + for _, h := range diskRes.HtlcResolutions.IncomingHTLCs { + h.SweepSignDesc.KeyDesc.PubKey.Curve = nil + } + + t.Fatalf("resolution mismatch: expected %v\n, got %v", + spew.Sdump(&res), spew.Sdump(diskRes)) } // We'll now delete the state, then attempt to retrieve the set of diff --git a/contractcourt/commit_sweep_resolver.go b/contractcourt/commit_sweep_resolver.go index 06bdfd53..1ce3f322 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,12 +102,38 @@ func (c *commitSweepResolver) waitForHeight(waitHeight uint32) error { return nil } - case <-c.quit: + case <-quit: return errResolverShuttingDown } } } +// waitForSpend waits for the given outpoint to be spent, and returns the +// details of the spending tx. +func waitForSpend(op *wire.OutPoint, pkScript []byte, heightHint uint32, + notifier chainntnfs.ChainNotifier, quit <-chan struct{}) ( + *chainntnfs.SpendDetail, error) { + + spendNtfn, err := notifier.RegisterSpendNtfn( + op, pkScript, heightHint, + ) + if err != nil { + return nil, err + } + + select { + case spendDetail, ok := <-spendNtfn.Spend: + if !ok { + return nil, errResolverShuttingDown + } + + return spendDetail, nil + + case <-quit: + return nil, errResolverShuttingDown + } +} + // getCommitTxConfHeight waits for confirmation of the commitment tx and returns // the confirmation height. func (c *commitSweepResolver) getCommitTxConfHeight() (uint32, error) { @@ -169,7 +198,7 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // We only need to wait for the block before the block that // unlocks the spend path. - err := c.waitForHeight(unlockHeight - 1) + err := waitForHeight(unlockHeight-1, c.Notifier, c.quit) if err != nil { return nil, err } 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/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_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{ diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index 1a99cc3b..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 @@ -105,104 +125,58 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // If we don't have a success transaction, then this means that this is // an output on the remote party's commitment transaction. if h.htlcResolution.SignedSuccessTx == nil { - // If we don't already have the sweep transaction constructed, - // we'll do so and broadcast it. - if h.sweepTx == nil { - log.Infof("%T(%x): crafting sweep tx for "+ - "incoming+remote htlc confirmed", h, - h.htlc.RHash[:]) - - // Before we can craft out sweeping transaction, we - // need to create an input which contains all the items - // required to add this input to a sweeping transaction, - // and generate a witness. - inp := input.MakeHtlcSucceedInput( - &h.htlcResolution.ClaimOutpoint, - &h.htlcResolution.SweepSignDesc, - h.htlcResolution.Preimage[:], - h.broadcastHeight, - h.htlcResolution.CsvDelay, - ) - - // With the input created, we can now generate the full - // sweep transaction, that we'll use to move these - // coins back into the backing wallet. - // - // TODO: Set tx lock time to current block height - // instead of zero. Will be taken care of once sweeper - // implementation is complete. - // - // TODO: Use time-based sweeper and result chan. - var err error - h.sweepTx, err = h.Sweeper.CreateSweepTx( - []input.Input{&inp}, - sweep.FeePreference{ - ConfTarget: sweepConfTarget, - }, 0, - ) - if err != nil { - return nil, err - } - - log.Infof("%T(%x): crafted sweep tx=%v", h, - h.htlc.RHash[:], spew.Sdump(h.sweepTx)) - - // With the sweep transaction signed, we'll now - // Checkpoint our state. - if err := h.Checkpoint(h); err != nil { - log.Errorf("unable to Checkpoint: %v", err) - return nil, err - } - } - - // Regardless of whether an existing transaction was found or newly - // constructed, we'll broadcast the sweep transaction to the - // network. - label := labels.MakeLabel( - labels.LabelTypeChannelClose, &h.ShortChanID, - ) - err := h.PublishTx(h.sweepTx, label) - if err != nil { - log.Infof("%T(%x): unable to publish tx: %v", - h, h.htlc.RHash[:], err) - return nil, err - } - - // With the sweep transaction broadcast, we'll wait for its - // confirmation. - sweepTXID := h.sweepTx.TxHash() - sweepScript := h.sweepTx.TxOut[0].PkScript - confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( - &sweepTXID, sweepScript, 1, h.broadcastHeight, - ) - if err != nil { - return nil, err - } - - log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+ - "confirmed", h, h.htlc.RHash[:], sweepTXID) - - select { - case _, ok := <-confNtfn.Confirmed: - if !ok { - return nil, errResolverShuttingDown - } - - case <-h.quit: - return nil, errResolverShuttingDown - } - - // Once the transaction has received a sufficient number of - // confirmations, we'll mark ourselves as fully resolved and exit. - h.resolved = true - - // Checkpoint the resolver, and write the outcome to disk. - return nil, h.checkpointClaim( - &sweepTXID, - channeldb.ResolverOutcomeClaimed, - ) + return h.resolveRemoteCommitOutput() } + // Otherwise this an output on our own commitment, and we must start by + // broadcasting the second-level success transaction. + secondLevelOutpoint, err := h.broadcastSuccessTx() + if err != nil { + return nil, err + } + + // To wrap this up, we'll wait until the second-level transaction has + // been spent, then fully resolve the contract. + log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ + "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) + + spend, err := waitForSpend( + secondLevelOutpoint, + h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + + h.resolved = true + return nil, h.checkpointClaim( + spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed, + ) +} + +// broadcastSuccessTx handles an HTLC output on our local commitment by +// broadcasting the second-level success transaction. It returns the ultimate +// outpoint of the second-level tx, that we must wait to be spent for the +// resolver to be fully resolved. +func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) { + // If we have non-nil SignDetails, this means that have a 2nd level + // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY + // (the case for anchor type channels). In this case we can re-sign it + // and attach fees at will. We let the sweeper handle this job. + // We use the checkpointed outputIncubating field to determine if we + // already swept the HTLC output into the second level transaction. + if h.htlcResolution.SignDetails != nil { + return h.broadcastReSignedSuccessTx() + } + + // Otherwise we'll publish the second-level transaction directly and + // offer the resolution to the nursery to handle. log.Infof("%T(%x): broadcasting second-layer transition tx: %v", h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) @@ -241,35 +215,236 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { } } - // To wrap this up, we'll wait until the second-level transaction has - // been spent, then fully resolve the contract. - spendNtfn, err := h.Notifier.RegisterSpendNtfn( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, + return &h.htlcResolution.ClaimOutpoint, nil +} + +// broadcastReSignedSuccessTx handles the case where we have non-nil +// SignDetails, and offers the second level transaction to the Sweeper, that +// will re-sign it and attach fees at will. +func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() ( + *wire.OutPoint, error) { + + // Keep track of the tx spending the HTLC output on the commitment, as + // this will be the confirmed second-level tx we'll ultimately sweep. + var commitSpend *chainntnfs.SpendDetail + + // We will have to let the sweeper re-sign the success tx and wait for + // it to confirm, if we haven't already. + if !h.outputIncubating { + log.Infof("%T(%x): offering second-layer transition tx to "+ + "sweeper: %v", h, h.htlc.RHash[:], + spew.Sdump(h.htlcResolution.SignedSuccessTx)) + + secondLevelInput := input.MakeHtlcSecondLevelSuccessAnchorInput( + h.htlcResolution.SignedSuccessTx, + h.htlcResolution.SignDetails, h.htlcResolution.Preimage, + h.broadcastHeight, + ) + _, err := h.Sweeper.SweepInput( + &secondLevelInput, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: secondLevelConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%x): waiting for second-level HTLC success "+ + "transaction to confirm", h, h.htlc.RHash[:]) + + // Wait for the second level transaction to confirm. + commitSpend, err = waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + // Now that the second-level transaction has confirmed, we + // checkpoint the state so we'll go to the next stage in case + // of restarts. + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err + } + + log.Infof("%T(%x): second-level HTLC success transaction "+ + "confirmed!", h, h.htlc.RHash[:]) + } + + // If we ended up here after a restart, we must again get the + // spend notification. + if commitSpend == nil { + var err error + commitSpend, err = waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + } + + // The HTLC success tx has a CSV lock that we must wait for. + waitHeight := uint32(commitSpend.SpendingHeight) + + h.htlcResolution.CsvDelay - 1 + + // Now that the sweeper has broadcasted the second-level transaction, + // it has confirmed, and we have checkpointed our state, we'll sweep + // the second level output. We report the resolver has moved the next + // stage. + h.reportLock.Lock() + h.currentReport.Stage = 2 + h.currentReport.MaturityHeight = waitHeight + h.reportLock.Unlock() + + log.Infof("%T(%x): waiting for CSV lock to expire at height %v", + h, h.htlc.RHash[:], waitHeight) + + err := waitForHeight(waitHeight, h.Notifier, h.quit) + if err != nil { + return nil, err + } + + // We'll use this input index to determine the second-level output + // index on the transaction, as the signatures requires the indexes to + // be the same. We don't look for the second-level output script + // directly, as there might be more than one HTLC output to the same + // pkScript. + op := &wire.OutPoint{ + Hash: *commitSpend.SpenderTxHash, + Index: commitSpend.SpenderInputIndex, + } + + // Finally, let the sweeper sweep the second-level output. + log.Infof("%T(%x): CSV lock expired, offering second-layer "+ + "output to sweeper: %v", h, h.htlc.RHash[:], op) + + inp := input.NewCsvInput( + op, input.HtlcAcceptedSuccessSecondLevel, + &h.htlcResolution.SweepSignDesc, h.broadcastHeight, + h.htlcResolution.CsvDelay, + ) + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, ) if err != nil { return nil, err } - log.Infof("%T(%x): waiting for second-level HTLC output to be spent "+ - "after csv_delay=%v", h, h.htlc.RHash[:], h.htlcResolution.CsvDelay) + // Will return this outpoint, when this is spent the resolver is fully + // resolved. + return op, nil +} + +// resolveRemoteCommitOutput handles sweeping an HTLC output on the remote +// commitment with the preimage. In this case we can sweep the output directly, +// and don't have to broadcast a second-level transaction. +func (h *htlcSuccessResolver) resolveRemoteCommitOutput() ( + ContractResolver, error) { + + // If we don't already have the sweep transaction constructed, we'll do + // so and broadcast it. + if h.sweepTx == nil { + log.Infof("%T(%x): crafting sweep tx for incoming+remote "+ + "htlc confirmed", h, h.htlc.RHash[:]) + + // Before we can craft out sweeping transaction, we need to + // create an input which contains all the items required to add + // this input to a sweeping transaction, and generate a + // witness. + inp := input.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, + h.htlcResolution.Preimage[:], + h.broadcastHeight, + h.htlcResolution.CsvDelay, + ) + + // With the input created, we can now generate the full sweep + // transaction, that we'll use to move these coins back into + // the backing wallet. + // + // TODO: Set tx lock time to current block height instead of + // zero. Will be taken care of once sweeper implementation is + // complete. + // + // TODO: Use time-based sweeper and result chan. + var err error + h.sweepTx, err = h.Sweeper.CreateSweepTx( + []input.Input{&inp}, + sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, 0, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%x): crafted sweep tx=%v", h, + h.htlc.RHash[:], spew.Sdump(h.sweepTx)) + + // TODO(halseth): should checkpoint sweep tx to DB? Since after + // a restart we might create a different tx, that will conflict + // with the published one. + } + + // Regardless of whether an existing transaction was found or newly + // constructed, we'll broadcast the sweep transaction to the network. + label := labels.MakeLabel( + labels.LabelTypeChannelClose, &h.ShortChanID, + ) + err := h.PublishTx(h.sweepTx, label) + if err != nil { + log.Infof("%T(%x): unable to publish tx: %v", + h, h.htlc.RHash[:], err) + return nil, err + } + + // With the sweep transaction broadcast, we'll wait for its + // confirmation. + sweepTXID := h.sweepTx.TxHash() + sweepScript := h.sweepTx.TxOut[0].PkScript + confNtfn, err := h.Notifier.RegisterConfirmationsNtfn( + &sweepTXID, sweepScript, 1, h.broadcastHeight, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%x): waiting for sweep tx (txid=%v) to be "+ + "confirmed", h, h.htlc.RHash[:], sweepTXID) - var spendTxid *chainhash.Hash select { - case spend, ok := <-spendNtfn.Spend: + case _, ok := <-confNtfn.Confirmed: if !ok { return nil, errResolverShuttingDown } - spendTxid = spend.SpenderTxHash case <-h.quit: return nil, errResolverShuttingDown } + // Once the transaction has received a sufficient number of + // confirmations, we'll mark ourselves as fully resolved and exit. h.resolved = true + + // Checkpoint the resolver, and write the outcome to disk. return nil, h.checkpointClaim( - spendTxid, channeldb.ResolverOutcomeClaimed, + &sweepTXID, + channeldb.ResolverOutcomeClaimed, ) } @@ -330,6 +505,40 @@ func (h *htlcSuccessResolver) IsResolved() bool { return h.resolved } +// report returns a report on the resolution state of the contract. +func (h *htlcSuccessResolver) report() *ContractReport { + // If the sign details are nil, the report will be created by handled + // by the nursery. + if h.htlcResolution.SignDetails == nil { + return nil + } + + h.reportLock.Lock() + defer h.reportLock.Unlock() + copy := h.currentReport + return © +} + +func (h *htlcSuccessResolver) initReport() { + // We create the initial report. This will only be reported for + // resolvers not handled by the nursery. + finalAmt := h.htlc.Amt.ToSatoshis() + if h.htlcResolution.SignedSuccessTx != nil { + finalAmt = btcutil.Amount( + h.htlcResolution.SignedSuccessTx.TxOut[0].Value, + ) + } + + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputIncomingHtlc, + Amount: finalAmt, + MaturityHeight: h.htlcResolution.CsvDelay, + LimboBalance: finalAmt, + Stage: 1, + } +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // @@ -355,6 +564,12 @@ func (h *htlcSuccessResolver) Encode(w io.Writer) error { return err } + // We encode the sign details last for backwards compatibility. + err := encodeSignDetails(w, h.htlcResolution.SignDetails) + if err != nil { + return err + } + return nil } @@ -388,6 +603,18 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + // Sign details is a new field that was added to the htlc resolution, + // so it is serialized last for backwards compatibility. We try to read + // it, but don't error out if there are not bytes left. + signDetails, err := decodeSignDetails(r) + if err == nil { + h.htlcResolution.SignDetails = signDetails + } else if err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + + h.initReport() + return h, nil } diff --git a/contractcourt/htlc_success_resolver_test.go b/contractcourt/htlc_success_resolver_test.go index 6e44c22c..288aeae5 100644 --- a/contractcourt/htlc_success_resolver_test.go +++ b/contractcourt/htlc_success_resolver_test.go @@ -1,13 +1,19 @@ package contractcourt import ( + "bytes" + "fmt" + "reflect" "testing" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb/kvdb" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" @@ -15,33 +21,59 @@ import ( var testHtlcAmt = lnwire.MilliSatoshi(200000) -type htlcSuccessResolverTestContext struct { - resolver *htlcSuccessResolver +type htlcResolverTestContext struct { + resolver ContractResolver + + checkpoint func(_ ContractResolver, + _ ...*channeldb.ResolverReport) error + notifier *mock.ChainNotifier resolverResultChan chan resolveResult - t *testing.T + resolutionChan chan ResolutionMsg + + t *testing.T } -func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestContext { +func newHtlcResolverTestContext(t *testing.T, + newResolver func(htlc channeldb.HTLC, + cfg ResolverConfig) ContractResolver) *htlcResolverTestContext { + notifier := &mock.ChainNotifier{ - EpochChan: make(chan *chainntnfs.BlockEpoch), - SpendChan: make(chan *chainntnfs.SpendDetail), - ConfChan: make(chan *chainntnfs.TxConfirmation), + EpochChan: make(chan *chainntnfs.BlockEpoch, 1), + SpendChan: make(chan *chainntnfs.SpendDetail, 1), + ConfChan: make(chan *chainntnfs.TxConfirmation, 1), } - checkPointChan := make(chan struct{}, 1) - - testCtx := &htlcSuccessResolverTestContext{ - notifier: notifier, - t: t, + testCtx := &htlcResolverTestContext{ + checkpoint: nil, + notifier: notifier, + resolutionChan: make(chan ResolutionMsg, 1), + t: t, } + witnessBeacon := newMockWitnessBeacon() chainCfg := ChannelArbitratorConfig{ ChainArbitratorConfig: ChainArbitratorConfig{ - Notifier: notifier, + Notifier: notifier, + PreimageDB: witnessBeacon, PublishTx: func(_ *wire.MsgTx, _ string) error { return nil }, + Sweeper: newMockSweeper(), + IncubateOutputs: func(wire.OutPoint, *lnwallet.OutgoingHtlcResolution, + *lnwallet.IncomingHtlcResolution, uint32) error { + return nil + }, + DeliverResolutionMsg: func(msgs ...ResolutionMsg) error { + if len(msgs) != 1 { + return fmt.Errorf("expected 1 "+ + "resolution msg, instead got %v", + len(msgs)) + } + + testCtx.resolutionChan <- msgs[0] + return nil + }, }, PutResolverReport: func(_ kvdb.RwTx, report *channeldb.ResolverReport) error { @@ -49,31 +81,31 @@ func newHtlcSuccessResolverTextContext(t *testing.T) *htlcSuccessResolverTestCon return nil }, } + // Since we want to replace this checkpoint method later in the test, + // we wrap the call to it in a closure. The linter will complain about + // this so set nolint directive. + checkpointFunc := func(c ContractResolver, // nolint + r ...*channeldb.ResolverReport) error { + return testCtx.checkpoint(c, r...) + } cfg := ResolverConfig{ ChannelArbitratorConfig: chainCfg, - Checkpoint: func(_ ContractResolver, - _ ...*channeldb.ResolverReport) error { - - checkPointChan <- struct{}{} - return nil - }, + Checkpoint: checkpointFunc, } - testCtx.resolver = &htlcSuccessResolver{ - contractResolverKit: *newContractResolverKit(cfg), - htlcResolution: lnwallet.IncomingHtlcResolution{}, - htlc: channeldb.HTLC{ - RHash: testResHash, - OnionBlob: testOnionBlob, - Amt: testHtlcAmt, - }, + htlc := channeldb.HTLC{ + RHash: testResHash, + OnionBlob: testOnionBlob, + Amt: testHtlcAmt, } + testCtx.resolver = newResolver(htlc, cfg) + return testCtx } -func (i *htlcSuccessResolverTestContext) resolve() { +func (i *htlcResolverTestContext) resolve() { // Start resolver. i.resolverResultChan = make(chan resolveResult, 1) go func() { @@ -85,7 +117,7 @@ func (i *htlcSuccessResolverTestContext) resolve() { }() } -func (i *htlcSuccessResolverTestContext) waitForResult() { +func (i *htlcResolverTestContext) waitForResult() { i.t.Helper() result := <-i.resolverResultChan @@ -98,8 +130,9 @@ func (i *htlcSuccessResolverTestContext) waitForResult() { } } -// TestSingleStageSuccess tests successful sweep of a single stage htlc claim. -func TestSingleStageSuccess(t *testing.T) { +// TestHtlcSuccessSingleStage tests successful sweep of a single stage htlc +// claim. +func TestHtlcSuccessSingleStage(t *testing.T) { htlcOutpoint := wire.OutPoint{Index: 3} sweepTx := &wire.MsgTx{ @@ -114,15 +147,6 @@ func TestSingleStageSuccess(t *testing.T) { ClaimOutpoint: htlcOutpoint, } - // We send a confirmation for our sweep tx to indicate that our sweep - // succeeded. - resolve := func(ctx *htlcSuccessResolverTestContext) { - ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{ - Tx: ctx.resolver.sweepTx, - BlockHeight: testInitialBlockHeight - 1, - } - } - sweepTxid := sweepTx.TxHash() claim := &channeldb.ResolverReport{ OutPoint: htlcOutpoint, @@ -131,14 +155,46 @@ func TestSingleStageSuccess(t *testing.T) { ResolverOutcome: channeldb.ResolverOutcomeClaimed, SpendTxID: &sweepTxid, } + + checkpoints := []checkpoint{ + { + // We send a confirmation for our sweep tx to indicate + // that our sweep succeeded. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + // The resolver will create and publish a sweep + // tx. + resolver := ctx.resolver.(*htlcSuccessResolver) + resolver.Sweeper.(*mockSweeper). + createSweepTxChan <- sweepTx + + // Confirm the sweep, which should resolve it. + ctx.notifier.ConfChan <- &chainntnfs.TxConfirmation{ + Tx: sweepTx, + BlockHeight: testInitialBlockHeight - 1, + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // report. + resolved: true, + reports: []*channeldb.ResolverReport{ + claim, + }, + }, + } + testHtlcSuccess( - t, singleStageResolution, resolve, sweepTx, claim, + t, singleStageResolution, checkpoints, ) } // TestSecondStageResolution tests successful sweep of a second stage htlc -// claim. -func TestSecondStageResolution(t *testing.T) { +// claim, going through the Nursery. +func TestHtlcSuccessSecondStageResolution(t *testing.T) { commitOutpoint := wire.OutPoint{Index: 2} htlcOutpoint := wire.OutPoint{Index: 3} @@ -158,20 +214,17 @@ func TestSecondStageResolution(t *testing.T) { PreviousOutPoint: commitOutpoint, }, }, - TxOut: []*wire.TxOut{}, + TxOut: []*wire.TxOut{ + { + Value: 111, + PkScript: []byte{0xaa, 0xaa}, + }, + }, }, ClaimOutpoint: htlcOutpoint, SweepSignDesc: testSignDesc, } - // We send a spend notification for our output to resolve our htlc. - resolve := func(ctx *htlcSuccessResolverTestContext) { - ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ - SpendingTx: sweepTx, - SpenderTxHash: &sweepHash, - } - } - successTx := twoStageResolution.SignedSuccessTx.TxHash() firstStage := &channeldb.ResolverReport{ OutPoint: commitOutpoint, @@ -189,54 +242,373 @@ func TestSecondStageResolution(t *testing.T) { SpendTxID: &sweepHash, } + checkpoints := []checkpoint{ + { + // The resolver will send the output to the Nursery. + incubating: true, + }, + { + // It will then wait for the Nursery to spend the + // output. We send a spend notification for our output + // to resolve our htlc. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderTxHash: &sweepHash, + } + + return nil + }, + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + secondStage, + firstStage, + }, + }, + } + testHtlcSuccess( - t, twoStageResolution, resolve, sweepTx, secondStage, firstStage, + t, twoStageResolution, checkpoints, ) } -// testHtlcSuccess tests resolution of a success resolver. It takes a resolve -// function which triggers resolution and the sweeptxid that will resolve it. +// TestHtlcSuccessSecondStageResolutionSweeper test that a resolver with +// non-nil SignDetails will offer the second-level transaction to the sweeper +// for re-signing. +func TestHtlcSuccessSecondStageResolutionSweeper(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + htlcOutpoint := wire.OutPoint{Index: 3} + + successTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 123, + PkScript: []byte{0xff, 0xff}, + }, + }, + } + + reSignedSuccessTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0xaa, 0xbb}, + Index: 0, + }, + }, + successTx.TxIn[0], + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0xaa, 0xbb}, + Index: 2, + }, + }, + }, + + TxOut: []*wire.TxOut{ + { + Value: 111, + PkScript: []byte{0xaa, 0xaa}, + }, + successTx.TxOut[0], + }, + } + reSignedHash := successTx.TxHash() + + sweepTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + + { + PreviousOutPoint: wire.OutPoint{ + Hash: reSignedHash, + Index: 1, + }, + }, + }, + TxOut: []*wire.TxOut{{}}, + } + sweepHash := sweepTx.TxHash() + + // twoStageResolution is a resolution for htlc on our own commitment + // which is spent from the signed success tx. + twoStageResolution := lnwallet.IncomingHtlcResolution{ + Preimage: [32]byte{}, + CsvDelay: 4, + SignedSuccessTx: successTx, + SignDetails: &input.SignDetails{ + SignDesc: testSignDesc, + PeerSig: testSig, + }, + ClaimOutpoint: htlcOutpoint, + SweepSignDesc: testSignDesc, + } + + firstStage := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &reSignedHash, + } + + secondStage := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeIncomingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &sweepHash, + } + + checkpoints := []checkpoint{ + { + // The HTLC output on the commitment should be offered + // to the sweeper. We'll notify that it gets spent. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + + resolver := ctx.resolver.(*htlcSuccessResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + op := inp.OutPoint() + if *op != commitOutpoint { + return fmt.Errorf("outpoint %v swept, "+ + "expected %v", op, + commitOutpoint) + } + + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedSuccessTx, + SpenderTxHash: &reSignedHash, + SpenderInputIndex: 1, + SpendingHeight: 10, + } + return nil + + }, + // incubating=true is used to signal that the + // second-level transaction was confirmed. + incubating: true, + }, + { + // The resolver will wait for the second-level's CSV + // lock to expire. + preCheckpoint: func(ctx *htlcResolverTestContext, + resumed bool) error { + + // If we are resuming from a checkpoint, we + // expect the resolver to re-subscribe to a + // spend, hence we must resend it. + if resumed { + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedSuccessTx, + SpenderTxHash: &reSignedHash, + SpenderInputIndex: 1, + SpendingHeight: 10, + } + } + + ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{ + Height: 13, + } + + // We expect it to sweep the second-level + // transaction we notfied about above. + resolver := ctx.resolver.(*htlcSuccessResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + op := inp.OutPoint() + exp := wire.OutPoint{ + Hash: reSignedHash, + Index: 1, + } + if *op != exp { + return fmt.Errorf("swept outpoint %v, expected %v", + op, exp) + } + + // Notify about the spend, which should resolve + // the resolver. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderTxHash: &sweepHash, + SpendingHeight: 14, + } + + return nil + }, + + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + secondStage, + firstStage, + }, + }, + } + + testHtlcSuccess(t, twoStageResolution, checkpoints) +} + +// checkpoint holds expected data we expect the resolver to checkpoint itself +// to the DB next. +type checkpoint struct { + // preCheckpoint is a method that will be called before we reach the + // checkpoint, to carry out any needed operations to drive the resolver + // in this stage. + preCheckpoint func(*htlcResolverTestContext, bool) error + + // data we expect the resolver to be checkpointed with next. + incubating bool + resolved bool + reports []*channeldb.ResolverReport +} + +// testHtlcSuccess tests resolution of a success resolver. It takes a a list of +// checkpoints that it expects the resolver to go through. And will run the +// resolver all the way through these checkpoints, and also attempt to resume +// the resolver from every checkpoint. func testHtlcSuccess(t *testing.T, resolution lnwallet.IncomingHtlcResolution, - resolve func(*htlcSuccessResolverTestContext), - sweepTx *wire.MsgTx, reports ...*channeldb.ResolverReport) { + checkpoints []checkpoint) { defer timeout(t)() - ctx := newHtlcSuccessResolverTextContext(t) + // We first run the resolver from start to finish, ensuring it gets + // checkpointed at every expected stage. We store the checkpointed data + // for the next portion of the test. + ctx := newHtlcResolverTestContext(t, + func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { + return &htlcSuccessResolver{ + contractResolverKit: *newContractResolverKit(cfg), + htlc: htlc, + htlcResolution: resolution, + } + }, + ) - // Replace our checkpoint with one which will push reports into a - // channel for us to consume. We replace this function on the resolver - // itself because it is created by the test context. - reportChan := make(chan *channeldb.ResolverReport) - ctx.resolver.Checkpoint = func(_ ContractResolver, + checkpointedState := runFromCheckpoint(t, ctx, checkpoints) + + // Now, from every checkpoint created, we re-create the resolver, and + // run the test from that checkpoint. + for i := range checkpointedState { + cp := bytes.NewReader(checkpointedState[i]) + ctx := newHtlcResolverTestContext(t, + func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { + resolver, err := newSuccessResolverFromReader(cp, cfg) + if err != nil { + t.Fatal(err) + } + + resolver.Supplement(htlc) + resolver.htlcResolution = resolution + return resolver + }, + ) + + // Run from the given checkpoint, ensuring we'll hit the rest. + _ = runFromCheckpoint(t, ctx, checkpoints[i+1:]) + } +} + +// runFromCheckpoint executes the Resolve method on the success resolver, and +// asserts that it checkpoints itself according to the expected checkpoints. +func runFromCheckpoint(t *testing.T, ctx *htlcResolverTestContext, + expectedCheckpoints []checkpoint) [][]byte { + + defer timeout(t)() + + var checkpointedState [][]byte + + // Replace our checkpoint method with one which we'll use to assert the + // checkpointed state and reports are equal to what we expect. + nextCheckpoint := 0 + checkpointChan := make(chan struct{}) + ctx.checkpoint = func(resolver ContractResolver, reports ...*channeldb.ResolverReport) error { - // Send all of our reports into the channel. - for _, report := range reports { - reportChan <- report + if nextCheckpoint >= len(expectedCheckpoints) { + t.Fatal("did not expect more checkpoints") } + var resolved, incubating bool + if h, ok := resolver.(*htlcSuccessResolver); ok { + resolved = h.resolved + incubating = h.outputIncubating + } + if h, ok := resolver.(*htlcTimeoutResolver); ok { + resolved = h.resolved + incubating = h.outputIncubating + } + + cp := expectedCheckpoints[nextCheckpoint] + + if resolved != cp.resolved { + t.Fatalf("expected checkpoint to be resolve=%v, had %v", + cp.resolved, resolved) + } + + if !reflect.DeepEqual(incubating, cp.incubating) { + t.Fatalf("expected checkpoint to be have "+ + "incubating=%v, had %v", cp.incubating, + incubating) + + } + + // Check we go the expected reports. + if len(reports) != len(cp.reports) { + t.Fatalf("unexpected number of reports. Expected %v "+ + "got %v", len(cp.reports), len(reports)) + } + + for i, report := range reports { + if !reflect.DeepEqual(report, cp.reports[i]) { + t.Fatalf("expected: %v, got: %v", + spew.Sdump(cp.reports[i]), + spew.Sdump(report)) + } + } + + // Finally encode the resolver, and store it for later use. + b := bytes.Buffer{} + if err := resolver.Encode(&b); err != nil { + t.Fatal(err) + } + + checkpointedState = append(checkpointedState, b.Bytes()) + nextCheckpoint++ + checkpointChan <- struct{}{} return nil } - ctx.resolver.htlcResolution = resolution - - // We set the sweepTx to be non-nil and mark the output as already - // incubating so that we do not need to set test values for crafting - // our own sweep transaction. - ctx.resolver.sweepTx = sweepTx - ctx.resolver.outputIncubating = true - // Start the htlc success resolver. ctx.resolve() - // Trigger and event that will resolve our test context. - resolve(ctx) + // Go through our list of expected checkpoints, so we can run the + // preCheckpoint logic if needed. + resumed := true + for i, cp := range expectedCheckpoints { + if cp.preCheckpoint != nil { + if err := cp.preCheckpoint(ctx, resumed); err != nil { + t.Fatalf("failure at stage %d: %v", i, err) + } - for _, report := range reports { - assertResolverReport(t, reportChan, report) + } + resumed = false + + // Wait for the resolver to have checkpointed its state. + <-checkpointChan } // Wait for the resolver to fully complete. ctx.waitForResult() + + if nextCheckpoint < len(expectedCheckpoints) { + t.Fatalf("not all checkpoints hit") + } + + return checkpointedState } diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go index b85441b1..44be1643 100644 --- a/contractcourt/htlc_timeout_resolver.go +++ b/contractcourt/htlc_timeout_resolver.go @@ -4,8 +4,8 @@ import ( "encoding/binary" "fmt" "io" + "sync" - "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" @@ -15,6 +15,7 @@ import ( "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/lnwire" + "github.com/lightningnetwork/lnd/sweep" ) // htlcTimeoutResolver is a ContractResolver that's capable of resolving an @@ -46,6 +47,15 @@ type htlcTimeoutResolver struct { // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // currentReport stores the current state of the resolver for reporting + // over the rpc interface. This should only be reported in case we have + // a non-nil SignDetails on the htlcResolution, otherwise the nursery + // will produce reports. + currentReport ContractReport + + // reportLock prevents concurrent access to the resolver report. + reportLock sync.Mutex + contractResolverKit } @@ -54,12 +64,16 @@ func newTimeoutResolver(res lnwallet.OutgoingHtlcResolution, broadcastHeight uint32, htlc channeldb.HTLC, resCfg ResolverConfig) *htlcTimeoutResolver { - return &htlcTimeoutResolver{ + h := &htlcTimeoutResolver{ contractResolverKit: *newContractResolverKit(resCfg), htlcResolution: res, broadcastHeight: broadcastHeight, htlc: htlc, } + + h.initReport() + + return h } // ResolverKey returns an identifier which should be globally unique for this @@ -254,9 +268,83 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { return nil, nil } - // If we haven't already sent the output to the utxo nursery, then - // we'll do so now. - if !h.outputIncubating { + // Start by spending the HTLC output, either by broadcasting the + // second-level timeout transaction, or directly if this is the remote + // commitment. + commitSpend, err := h.spendHtlcOutput() + if err != nil { + return nil, err + } + + // If the spend reveals the pre-image, then we'll enter the clean up + // workflow to pass the pre-image back to the incoming link, add it to + // the witness cache, and exit. + if isSuccessSpend(commitSpend, h.htlcResolution.SignedTimeoutTx != nil) { + log.Infof("%T(%v): HTLC has been swept with pre-image by "+ + "remote party during timeout flow! Adding pre-image to "+ + "witness cache", h.htlcResolution.ClaimOutpoint) + + return h.claimCleanUp(commitSpend) + } + + log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+ + "confirmed", h, h.htlcResolution.ClaimOutpoint) + + // At this point, the second-level transaction is sufficiently + // confirmed, or a transaction directly spending the output is. + // Therefore, we can now send back our clean up message, failing the + // HTLC on the incoming link. + failureMsg := &lnwire.FailPermanentChannelFailure{} + if err := h.DeliverResolutionMsg(ResolutionMsg{ + SourceChan: h.ShortChanID, + HtlcIndex: h.htlc.HtlcIndex, + Failure: failureMsg, + }); err != nil { + return nil, err + } + + // Depending on whether this was a local or remote commit, we must + // handle the spending transaction accordingly. + return h.handleCommitSpend(commitSpend) +} + +// spendHtlcOutput handles the initial spend of an HTLC output via the timeout +// clause. If this is our local commitment, the second-level timeout TX will be +// used to spend the output into the next stage. If this is the remote +// commitment, the output will be swept directly without the timeout +// transaction. +func (h *htlcTimeoutResolver) spendHtlcOutput() (*chainntnfs.SpendDetail, error) { + switch { + + // If we have non-nil SignDetails, this means that have a 2nd level + // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY + // (the case for anchor type channels). In this case we can re-sign it + // and attach fees at will. We let the sweeper handle this job. + case h.htlcResolution.SignDetails != nil && !h.outputIncubating: + log.Infof("%T(%x): offering second-layer timeout tx to "+ + "sweeper: %v", h, h.htlc.RHash[:], + spew.Sdump(h.htlcResolution.SignedTimeoutTx)) + + inp := input.MakeHtlcSecondLevelTimeoutAnchorInput( + h.htlcResolution.SignedTimeoutTx, + h.htlcResolution.SignDetails, + h.broadcastHeight, + ) + _, err := h.Sweeper.SweepInput( + &inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: secondLevelConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + // If we have no SignDetails, and we haven't already sent the output to + // the utxo nursery, then we'll do so now. + case h.htlcResolution.SignDetails == nil && !h.outputIncubating: log.Tracef("%T(%v): incubating htlc output", h, h.htlcResolution.ClaimOutpoint) @@ -276,51 +364,14 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { } } - var spendTxID *chainhash.Hash - - // waitForOutputResolution waits for the HTLC output to be fully - // resolved. The output is considered fully resolved once it has been - // spent, and the spending transaction has been fully confirmed. - waitForOutputResolution := func() error { - // We first need to register to see when the HTLC output itself - // has been spent by a confirmed transaction. - spendNtfn, err := h.Notifier.RegisterSpendNtfn( - &h.htlcResolution.ClaimOutpoint, - h.htlcResolution.SweepSignDesc.Output.PkScript, - h.broadcastHeight, - ) - if err != nil { - return err - } - - select { - case spendDetail, ok := <-spendNtfn.Spend: - if !ok { - return errResolverShuttingDown - } - spendTxID = spendDetail.SpenderTxHash - - case <-h.quit: - return errResolverShuttingDown - } - - return nil - } - - // Now that we've handed off the HTLC to the nursery, we'll watch for a - // spend of the output, and make our next move off of that. Depending - // on if this is our commitment, or the remote party's commitment, - // we'll be watching a different outpoint and script. + // Now that we've handed off the HTLC to the nursery or sweeper, we'll + // watch for a spend of the output, and make our next move off of that. + // Depending on if this is our commitment, or the remote party's + // commitment, we'll be watching a different outpoint and script. outpointToWatch, scriptToWatch, err := h.chainDetailsToWatch() if err != nil { return nil, err } - spendNtfn, err := h.Notifier.RegisterSpendNtfn( - outpointToWatch, scriptToWatch, h.broadcastHeight, - ) - if err != nil { - return nil, err - } log.Infof("%T(%v): waiting for HTLC output %v to be spent"+ "fully confirmed", h, h.htlcResolution.ClaimOutpoint, @@ -328,81 +379,158 @@ func (h *htlcTimeoutResolver) Resolve() (ContractResolver, error) { // We'll block here until either we exit, or the HTLC output on the // commitment transaction has been spent. - var ( - spend *chainntnfs.SpendDetail - ok bool + spend, err := waitForSpend( + outpointToWatch, scriptToWatch, h.broadcastHeight, + h.Notifier, h.quit, ) - select { - case spend, ok = <-spendNtfn.Spend: - if !ok { - return nil, errResolverShuttingDown - } - spendTxID = spend.SpenderTxHash - - case <-h.quit: - return nil, errResolverShuttingDown - } - - // If the spend reveals the pre-image, then we'll enter the clean up - // workflow to pass the pre-image back to the incoming link, add it to - // the witness cache, and exit. - if isSuccessSpend(spend, h.htlcResolution.SignedTimeoutTx != nil) { - log.Infof("%T(%v): HTLC has been swept with pre-image by "+ - "remote party during timeout flow! Adding pre-image to "+ - "witness cache", h.htlcResolution.ClaimOutpoint) - - return h.claimCleanUp(spend) - } - - log.Infof("%T(%v): resolving htlc with incoming fail msg, fully "+ - "confirmed", h, h.htlcResolution.ClaimOutpoint) - - // At this point, the second-level transaction is sufficiently - // confirmed, or a transaction directly spending the output is. - // Therefore, we can now send back our clean up message, failing the - // HTLC on the incoming link. - failureMsg := &lnwire.FailPermanentChannelFailure{} - if err := h.DeliverResolutionMsg(ResolutionMsg{ - SourceChan: h.ShortChanID, - HtlcIndex: h.htlc.HtlcIndex, - Failure: failureMsg, - }); err != nil { + if err != nil { return nil, err } - var reports []*channeldb.ResolverReport + // If this was the second level transaction published by the sweeper, + // we can checkpoint the resolver now that it's confirmed. + if h.htlcResolution.SignDetails != nil && !h.outputIncubating { + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err + } + } + + return spend, err +} + +// handleCommitSpend handles the spend of the HTLC output on the commitment +// transaction. If this was our local commitment, the spend will be he +// confirmed second-level timeout transaction, and we'll sweep that into our +// wallet. If the was a remote commitment, the resolver will resolve +// immetiately. +func (h *htlcTimeoutResolver) handleCommitSpend( + commitSpend *chainntnfs.SpendDetail) (ContractResolver, error) { + + var ( + // claimOutpoint will be the outpoint of the second level + // transaction, or on the remote commitment directly. It will + // start out as set in the resolution, but we'll update it if + // the second-level goes through the sweeper and changes its + // txid. + claimOutpoint = h.htlcResolution.ClaimOutpoint + + // spendTxID will be the ultimate spend of the claimOutpoint. + // We set it to the commit spend for now, as this is the + // ultimate spend in case this is a remote commitment. If we go + // through the second-level transaction, we'll update this + // accordingly. + spendTxID = commitSpend.SpenderTxHash + + reports []*channeldb.ResolverReport + ) + + switch { + + // If the sweeper is handling the second level transaction, wait for + // the CSV lock to expire, before sweeping the output on the + // second-level. + case h.htlcResolution.SignDetails != nil: + waitHeight := uint32(commitSpend.SpendingHeight) + + h.htlcResolution.CsvDelay - 1 + + h.reportLock.Lock() + h.currentReport.Stage = 2 + h.currentReport.MaturityHeight = waitHeight + h.reportLock.Unlock() + + log.Infof("%T(%x): waiting for CSV lock to expire at height %v", + h, h.htlc.RHash[:], waitHeight) + + err := waitForHeight(waitHeight, h.Notifier, h.quit) + if err != nil { + return nil, err + } + + // We'll use this input index to determine the second-level + // output index on the transaction, as the signatures requires + // the indexes to be the same. We don't look for the + // second-level output script directly, as there might be more + // than one HTLC output to the same pkScript. + op := &wire.OutPoint{ + Hash: *commitSpend.SpenderTxHash, + Index: commitSpend.SpenderInputIndex, + } + + // Let the sweeper sweep the second-level output now that the + // CSV delay has passed. + log.Infof("%T(%x): CSV lock expired, offering second-layer "+ + "output to sweeper: %v", h, h.htlc.RHash[:], op) + + inp := input.NewCsvInput( + op, input.HtlcOfferedTimeoutSecondLevel, + &h.htlcResolution.SweepSignDesc, + h.broadcastHeight, + h.htlcResolution.CsvDelay, + ) + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + // Update the claim outpoint to point to the second-level + // transaction created by the sweeper. + claimOutpoint = *op + fallthrough // Finally, if this was an output on our commitment transaction, we'll // wait for the second-level HTLC output to be spent, and for that // transaction itself to confirm. - if h.htlcResolution.SignedTimeoutTx != nil { - log.Infof("%T(%v): waiting for nursery to spend CSV delayed "+ - "output", h, h.htlcResolution.ClaimOutpoint) - if err := waitForOutputResolution(); err != nil { + case h.htlcResolution.SignedTimeoutTx != nil: + log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+ + "delayed output", h, claimOutpoint) + sweep, err := waitForSpend( + &claimOutpoint, + h.htlcResolution.SweepSignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { return nil, err } - // Once our timeout tx has confirmed, we add a resolution for - // our timeoutTx tx first stage transaction. - timeoutTx := h.htlcResolution.SignedTimeoutTx - spendHash := timeoutTx.TxHash() + // Update the spend txid to the hash of the sweep transaction. + spendTxID = sweep.SpenderTxHash + + // Once our sweep of the timeout tx has confirmed, we add a + // resolution for our timeoutTx tx first stage transaction. + timeoutTx := commitSpend.SpendingTx + index := commitSpend.SpenderInputIndex + spendHash := commitSpend.SpenderTxHash reports = append(reports, &channeldb.ResolverReport{ - OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, + OutPoint: timeoutTx.TxIn[index].PreviousOutPoint, Amount: h.htlc.Amt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &spendHash, + SpendTxID: spendHash, }) } // With the clean up message sent, we'll now mark the contract - // resolved, record the timeout and the sweep txid on disk, and wait. + // resolved, update the recovered balance, record the timeout and the + // sweep txid on disk, and wait. h.resolved = true + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() amt := btcutil.Amount(h.htlcResolution.SweepSignDesc.Output.Value) reports = append(reports, &channeldb.ResolverReport{ - OutPoint: h.htlcResolution.ClaimOutpoint, + OutPoint: claimOutpoint, Amount: amt, ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeTimeout, @@ -428,6 +556,40 @@ func (h *htlcTimeoutResolver) IsResolved() bool { return h.resolved } +// report returns a report on the resolution state of the contract. +func (h *htlcTimeoutResolver) report() *ContractReport { + // If the sign details are nil, the report will be created by handled + // by the nursery. + if h.htlcResolution.SignDetails == nil { + return nil + } + + h.reportLock.Lock() + defer h.reportLock.Unlock() + copy := h.currentReport + return © +} + +func (h *htlcTimeoutResolver) initReport() { + // We create the initial report. This will only be reported for + // resolvers not handled by the nursery. + finalAmt := h.htlc.Amt.ToSatoshis() + if h.htlcResolution.SignedTimeoutTx != nil { + finalAmt = btcutil.Amount( + h.htlcResolution.SignedTimeoutTx.TxOut[0].Value, + ) + } + + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputOutgoingHtlc, + Amount: finalAmt, + MaturityHeight: h.htlcResolution.Expiry, + LimboBalance: finalAmt, + Stage: 1, + } +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // @@ -455,6 +617,12 @@ func (h *htlcTimeoutResolver) Encode(w io.Writer) error { return err } + // We encode the sign details last for backwards compatibility. + err := encodeSignDetails(w, h.htlcResolution.SignDetails) + if err != nil { + return err + } + return nil } @@ -490,6 +658,18 @@ func newTimeoutResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + // Sign details is a new field that was added to the htlc resolution, + // so it is serialized last for backwards compatibility. We try to read + // it, but don't error out if there are not bytes left. + signDetails, err := decodeSignDetails(r) + if err == nil { + h.htlcResolution.SignDetails = signDetails + } else if err != io.EOF && err != io.ErrUnexpectedEOF { + return nil, err + } + + h.initReport() + return h, nil } diff --git a/contractcourt/htlc_timeout_resolver_test.go b/contractcourt/htlc_timeout_resolver_test.go index 6e41ad79..8aa366ef 100644 --- a/contractcourt/htlc_timeout_resolver_test.go +++ b/contractcourt/htlc_timeout_resolver_test.go @@ -3,10 +3,12 @@ package contractcourt import ( "bytes" "fmt" + "reflect" "sync" "testing" "time" + "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -17,6 +19,7 @@ import ( "github.com/lightningnetwork/lnd/lntest/mock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" ) type mockWitnessBeacon struct { @@ -127,6 +130,16 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -148,6 +161,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness // Set the outpoint to be on our commitment, since @@ -174,6 +198,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -196,6 +231,17 @@ func TestHtlcTimeoutResolver(t *testing.T) { return nil, err } + // To avoid triggering the race detector by + // setting the witness the second time this + // method is called during tests, we return + // immediately if the witness is already set + // correctly. + if reflect.DeepEqual( + templateTx.TxIn[0].Witness, witness, + ) { + return templateTx, nil + } + templateTx.TxIn[0].Witness = witness return templateTx, nil }, @@ -282,16 +328,19 @@ func TestHtlcTimeoutResolver(t *testing.T) { // broadcast, then we'll set the timeout commit to a fake // transaction to force the code path. if !testCase.remoteCommit { - resolver.htlcResolution.SignedTimeoutTx = sweepTx + timeoutTx, err := testCase.txToBroadcast() + require.NoError(t, err) + + resolver.htlcResolution.SignedTimeoutTx = timeoutTx if testCase.timeout { - success := sweepTx.TxHash() + timeoutTxID := timeoutTx.TxHash() reports = append(reports, &channeldb.ResolverReport{ - OutPoint: sweepTx.TxIn[0].PreviousOutPoint, + OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, Amount: testHtlcAmt.ToSatoshis(), ResolverType: channeldb.ResolverTypeOutgoingHtlc, ResolverOutcome: channeldb.ResolverOutcomeFirstStage, - SpendTxID: &success, + SpendTxID: &timeoutTxID, }) } } @@ -433,3 +482,834 @@ func TestHtlcTimeoutResolver(t *testing.T) { } } } + +// NOTE: the following tests essentially checks many of the same scenarios as +// the test above, but they expand on it by checking resuming from checkpoints +// at every stage. + +// TestHtlcTimeoutSingleStage tests a remote commitment confirming, and the +// local node sweeping the HTLC output directly after timeout. +func TestHtlcTimeoutSingleStage(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 3} + + sweepTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{{}}, + } + + // singleStageResolution is a resolution for a htlc on the remote + // party's commitment. + singleStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: commitOutpoint, + SweepSignDesc: testSignDesc, + } + + sweepTxid := sweepTx.TxHash() + claim := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: &sweepTxid, + } + + checkpoints := []checkpoint{ + { + // Output should be handed off to the nursery. + incubating: true, + }, + { + // We send a confirmation the sweep tx from published + // by the nursery. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + // The nursery will create and publish a sweep + // tx. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderTxHash: &sweepTxid, + } + + // The resolver should deliver a failure + // resolition message (indicating we + // successfully timed out the HTLC). + select { + case resolutionMsg := <-ctx.resolutionChan: + if resolutionMsg.Failure == nil { + t.Fatalf("expected failure resolution msg") + } + case <-time.After(time.Second * 5): + t.Fatalf("resolution not sent") + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // report. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + claim, + }, + }, + } + + testHtlcTimeout( + t, singleStageResolution, checkpoints, + ) +} + +// TestHtlcTimeoutSecondStage tests a local commitment being confirmed, and the +// local node claiming the HTLC output using the second-level timeout tx. +func TestHtlcTimeoutSecondStage(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + htlcOutpoint := wire.OutPoint{Index: 3} + + sweepTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{{}}, + } + sweepHash := sweepTx.TxHash() + + timeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 111, + PkScript: []byte{0xaa, 0xaa}, + }, + }, + } + + signer := &mock.DummySigner{} + witness, err := input.SenderHtlcSpendTimeout( + &mock.DummySignature{}, txscript.SigHashAll, + signer, &testSignDesc, timeoutTx, + ) + require.NoError(t, err) + timeoutTx.TxIn[0].Witness = witness + + timeoutTxid := timeoutTx.TxHash() + + // twoStageResolution is a resolution for a htlc on the local + // party's commitment. + twoStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: htlcOutpoint, + SignedTimeoutTx: timeoutTx, + SweepSignDesc: testSignDesc, + } + + firstStage := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &timeoutTxid, + } + + secondState := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: &sweepHash, + } + + checkpoints := []checkpoint{ + { + // Output should be handed off to the nursery. + incubating: true, + }, + { + // We send a confirmation for our sweep tx to indicate + // that our sweep succeeded. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + // The nursery will publish the timeout tx. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: timeoutTx, + SpenderTxHash: &timeoutTxid, + } + + // The resolver should deliver a failure + // resolution message (indicating we + // successfully timed out the HTLC). + select { + case resolutionMsg := <-ctx.resolutionChan: + if resolutionMsg.Failure == nil { + t.Fatalf("expected failure resolution msg") + } + case <-time.After(time.Second * 1): + t.Fatalf("resolution not sent") + } + + // Deliver spend of timeout tx. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderTxHash: &sweepHash, + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // reports. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + firstStage, secondState, + }, + }, + } + + testHtlcTimeout( + t, twoStageResolution, checkpoints, + ) +} + +// TestHtlcTimeoutSingleStageRemoteSpend tests that when a local commitment +// confirms, and the remote spends the HTLC output directly, we detect this and +// extract the preimage. +func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + htlcOutpoint := wire.OutPoint{Index: 3} + + spendTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{{}}, + } + + fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) + var fakePreimage lntypes.Preimage + copy(fakePreimage[:], fakePreimageBytes) + + signer := &mock.DummySigner{} + witness, err := input.SenderHtlcSpendRedeem( + signer, &testSignDesc, spendTx, + fakePreimageBytes, + ) + require.NoError(t, err) + spendTx.TxIn[0].Witness = witness + + spendTxHash := spendTx.TxHash() + + timeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 123, + PkScript: []byte{0xff, 0xff}, + }, + }, + } + + timeoutWitness, err := input.SenderHtlcSpendTimeout( + &mock.DummySignature{}, txscript.SigHashAll, + signer, &testSignDesc, timeoutTx, + ) + require.NoError(t, err) + timeoutTx.TxIn[0].Witness = timeoutWitness + + // twoStageResolution is a resolution for a htlc on the local + // party's commitment. + twoStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: htlcOutpoint, + SignedTimeoutTx: timeoutTx, + SweepSignDesc: testSignDesc, + } + + claim := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &spendTxHash, + } + + checkpoints := []checkpoint{ + { + // Output should be handed off to the nursery. + incubating: true, + }, + { + // We send a spend notification for a remote spend with + // the preimage. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + + witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) + + // The remote spends the output direcly with + // the preimage. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: spendTx, + SpenderTxHash: &spendTxHash, + } + + // We should extract the preimage. + select { + case newPreimage := <-witnessBeacon.newPreimages: + if newPreimage[0] != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, newPreimage) + } + + case <-time.After(time.Second * 5): + t.Fatalf("pre-image not added") + } + + // Finally, we should get a resolution message + // with the pre-image set within the message. + select { + case resolutionMsg := <-ctx.resolutionChan: + if *resolutionMsg.PreImage != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, resolutionMsg.PreImage) + } + case <-time.After(time.Second * 5): + t.Fatalf("resolution not sent") + } + + return nil + }, + + // After the success tx has confirmed, we expect the + // checkpoint to be resolved, and with the above + // report. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + claim, + }, + }, + } + + testHtlcTimeout( + t, twoStageResolution, checkpoints, + ) +} + +// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment +// confirms, and the remote spends the output using the success tx, we +// properly detect this and extract the preimage. +func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + + remoteSuccessTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{}, + } + + fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) + var fakePreimage lntypes.Preimage + copy(fakePreimage[:], fakePreimageBytes) + + signer := &mock.DummySigner{} + witness, err := input.ReceiverHtlcSpendRedeem( + &mock.DummySignature{}, txscript.SigHashAll, + fakePreimageBytes, signer, + &testSignDesc, remoteSuccessTx, + ) + require.NoError(t, err) + + remoteSuccessTx.TxIn[0].Witness = witness + successTxid := remoteSuccessTx.TxHash() + + // singleStageResolution allwoing the local node to sweep HTLC output + // directly from the remote commitment after timeout. + singleStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: commitOutpoint, + SweepSignDesc: testSignDesc, + } + + claim := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &successTxid, + } + + checkpoints := []checkpoint{ + { + // Output should be handed off to the nursery. + incubating: true, + }, + { + // We send a confirmation for the remote's second layer + // success transcation. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: remoteSuccessTx, + SpenderTxHash: &successTxid, + } + + witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) + + // We expect the preimage to be extracted, + select { + case newPreimage := <-witnessBeacon.newPreimages: + if newPreimage[0] != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, newPreimage) + } + + case <-time.After(time.Second * 5): + t.Fatalf("pre-image not added") + } + + // Finally, we should get a resolution message with the + // pre-image set within the message. + select { + case resolutionMsg := <-ctx.resolutionChan: + if *resolutionMsg.PreImage != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, resolutionMsg.PreImage) + } + case <-time.After(time.Second * 5): + t.Fatalf("resolution not sent") + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // report. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + claim, + }, + }, + } + + testHtlcTimeout( + t, singleStageResolution, checkpoints, + ) +} + +// TestHtlcTimeoutSecondStageSweeper tests that for anchor channels, when a +// local commitment confirms, the timeout tx is handed to the sweeper to claim +// the HTLC output. +func TestHtlcTimeoutSecondStageSweeper(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + htlcOutpoint := wire.OutPoint{Index: 3} + + sweepTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{{}}, + } + sweepHash := sweepTx.TxHash() + + timeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 123, + PkScript: []byte{0xff, 0xff}, + }, + }, + } + + // We set the timeout witness since the script is used when subscribing + // to spends. + signer := &mock.DummySigner{} + timeoutWitness, err := input.SenderHtlcSpendTimeout( + &mock.DummySignature{}, txscript.SigHashAll, + signer, &testSignDesc, timeoutTx, + ) + require.NoError(t, err) + timeoutTx.TxIn[0].Witness = timeoutWitness + + reSignedTimeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0xaa, 0xbb}, + Index: 0, + }, + }, + timeoutTx.TxIn[0], + { + PreviousOutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0xaa, 0xbb}, + Index: 2, + }, + }, + }, + + TxOut: []*wire.TxOut{ + { + Value: 111, + PkScript: []byte{0xaa, 0xaa}, + }, + timeoutTx.TxOut[0], + }, + } + reSignedHash := reSignedTimeoutTx.TxHash() + reSignedOutPoint := wire.OutPoint{ + Hash: reSignedHash, + Index: 1, + } + + // twoStageResolution is a resolution for a htlc on the local + // party's commitment, where the timout tx can be re-signed. + twoStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: htlcOutpoint, + SignedTimeoutTx: timeoutTx, + SignDetails: &input.SignDetails{ + SignDesc: testSignDesc, + PeerSig: testSig, + }, + SweepSignDesc: testSignDesc, + } + + firstStage := &channeldb.ResolverReport{ + OutPoint: commitOutpoint, + Amount: testHtlcAmt.ToSatoshis(), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeFirstStage, + SpendTxID: &reSignedHash, + } + + secondState := &channeldb.ResolverReport{ + OutPoint: reSignedOutPoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeTimeout, + SpendTxID: &sweepHash, + } + + checkpoints := []checkpoint{ + { + // The output should be given to the sweeper. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + + resolver := ctx.resolver.(*htlcTimeoutResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + op := inp.OutPoint() + if *op != commitOutpoint { + return fmt.Errorf("outpoint %v swept, "+ + "expected %v", op, + commitOutpoint) + } + + // Emulat the sweeper spending using the + // re-signed timeout tx. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedTimeoutTx, + SpenderInputIndex: 1, + SpenderTxHash: &reSignedHash, + SpendingHeight: 10, + } + + return nil + }, + // incubating=true is used to signal that the + // second-level transaction was confirmed. + incubating: true, + }, + { + // We send a confirmation for our sweep tx to indicate + // that our sweep succeeded. + preCheckpoint: func(ctx *htlcResolverTestContext, + resumed bool) error { + + // If we are resuming from a checkpoing, we + // expect the resolver to re-subscribe to a + // spend, hence we must resend it. + if resumed { + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: reSignedTimeoutTx, + SpenderInputIndex: 1, + SpenderTxHash: &reSignedHash, + SpendingHeight: 10, + } + } + + // The resolver should deliver a failure + // resolution message (indicating we + // successfully timed out the HTLC). + select { + case resolutionMsg := <-ctx.resolutionChan: + if resolutionMsg.Failure == nil { + t.Fatalf("expected failure resolution msg") + } + case <-time.After(time.Second * 1): + t.Fatalf("resolution not sent") + } + + // Mimic CSV lock expiring. + ctx.notifier.EpochChan <- &chainntnfs.BlockEpoch{ + Height: 13, + } + + // The timout tx output should now be given to + // the sweeper. + resolver := ctx.resolver.(*htlcTimeoutResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + op := inp.OutPoint() + exp := wire.OutPoint{ + Hash: reSignedHash, + Index: 1, + } + if *op != exp { + return fmt.Errorf("wrong outpoint swept") + } + + // Notify about the spend, which should resolve + // the resolver. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: sweepTx, + SpenderTxHash: &sweepHash, + SpendingHeight: 14, + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // reports. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + firstStage, + secondState, + }, + }, + } + + testHtlcTimeout( + t, twoStageResolution, checkpoints, + ) +} + +// TestHtlcTimeoutSecondStageSweeperRemoteSpend tests that if a local timeout +// tx is offered to the sweeper, but the output is swept by the remote node, we +// properly detect this and extract the preimage. +func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) { + commitOutpoint := wire.OutPoint{Index: 2} + htlcOutpoint := wire.OutPoint{Index: 3} + + timeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + PreviousOutPoint: commitOutpoint, + }, + }, + TxOut: []*wire.TxOut{ + { + Value: 123, + PkScript: []byte{0xff, 0xff}, + }, + }, + } + + // We set the timeout witness since the script is used when subscribing + // to spends. + signer := &mock.DummySigner{} + timeoutWitness, err := input.SenderHtlcSpendTimeout( + &mock.DummySignature{}, txscript.SigHashAll, + signer, &testSignDesc, timeoutTx, + ) + require.NoError(t, err) + timeoutTx.TxIn[0].Witness = timeoutWitness + + spendTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{{}}, + TxOut: []*wire.TxOut{{}}, + } + + fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize) + var fakePreimage lntypes.Preimage + copy(fakePreimage[:], fakePreimageBytes) + + witness, err := input.SenderHtlcSpendRedeem( + signer, &testSignDesc, spendTx, + fakePreimageBytes, + ) + require.NoError(t, err) + spendTx.TxIn[0].Witness = witness + + spendTxHash := spendTx.TxHash() + + // twoStageResolution is a resolution for a htlc on the local + // party's commitment, where the timout tx can be re-signed. + twoStageResolution := lnwallet.OutgoingHtlcResolution{ + ClaimOutpoint: htlcOutpoint, + SignedTimeoutTx: timeoutTx, + SignDetails: &input.SignDetails{ + SignDesc: testSignDesc, + PeerSig: testSig, + }, + SweepSignDesc: testSignDesc, + } + + claim := &channeldb.ResolverReport{ + OutPoint: htlcOutpoint, + Amount: btcutil.Amount(testSignDesc.Output.Value), + ResolverType: channeldb.ResolverTypeOutgoingHtlc, + ResolverOutcome: channeldb.ResolverOutcomeClaimed, + SpendTxID: &spendTxHash, + } + + checkpoints := []checkpoint{ + { + // The output should be given to the sweeper. + preCheckpoint: func(ctx *htlcResolverTestContext, + _ bool) error { + + resolver := ctx.resolver.(*htlcTimeoutResolver) + inp := <-resolver.Sweeper.(*mockSweeper).sweptInputs + op := inp.OutPoint() + if *op != commitOutpoint { + return fmt.Errorf("outpoint %v swept, "+ + "expected %v", op, + commitOutpoint) + } + + // Emulate the remote sweeping the output with the preimage. + // re-signed timeout tx. + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: spendTx, + SpenderTxHash: &spendTxHash, + } + + return nil + }, + // incubating=true is used to signal that the + // second-level transaction was confirmed. + incubating: true, + }, + { + // We send a confirmation for our sweep tx to indicate + // that our sweep succeeded. + preCheckpoint: func(ctx *htlcResolverTestContext, + resumed bool) error { + + // If we are resuming from a checkpoing, we + // expect the resolver to re-subscribe to a + // spend, hence we must resend it. + if resumed { + fmt.Println("resumed") + ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{ + SpendingTx: spendTx, + SpenderTxHash: &spendTxHash, + } + } + + witnessBeacon := ctx.resolver.(*htlcTimeoutResolver).PreimageDB.(*mockWitnessBeacon) + + // We should extract the preimage. + select { + case newPreimage := <-witnessBeacon.newPreimages: + if newPreimage[0] != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, newPreimage) + } + + case <-time.After(time.Second * 5): + t.Fatalf("pre-image not added") + } + + // Finally, we should get a resolution message + // with the pre-image set within the message. + select { + case resolutionMsg := <-ctx.resolutionChan: + if *resolutionMsg.PreImage != fakePreimage { + t.Fatalf("wrong pre-image: "+ + "expected %v, got %v", + fakePreimage, resolutionMsg.PreImage) + } + case <-time.After(time.Second * 5): + t.Fatalf("resolution not sent") + } + + return nil + }, + + // After the sweep has confirmed, we expect the + // checkpoint to be resolved, and with the above + // reports. + incubating: true, + resolved: true, + reports: []*channeldb.ResolverReport{ + claim, + }, + }, + } + + testHtlcTimeout( + t, twoStageResolution, checkpoints, + ) +} + +func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution, + checkpoints []checkpoint) { + + defer timeout(t)() + + // We first run the resolver from start to finish, ensuring it gets + // checkpointed at every expected stage. We store the checkpointed data + // for the next portion of the test. + ctx := newHtlcResolverTestContext(t, + func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { + return &htlcTimeoutResolver{ + contractResolverKit: *newContractResolverKit(cfg), + htlc: htlc, + htlcResolution: resolution, + } + }, + ) + + checkpointedState := runFromCheckpoint(t, ctx, checkpoints) + + // Now, from every checkpoint created, we re-create the resolver, and + // run the test from that checkpoint. + for i := range checkpointedState { + cp := bytes.NewReader(checkpointedState[i]) + ctx := newHtlcResolverTestContext(t, + func(htlc channeldb.HTLC, cfg ResolverConfig) ContractResolver { + resolver, err := newTimeoutResolverFromReader(cp, cfg) + if err != nil { + t.Fatal(err) + } + + resolver.Supplement(htlc) + resolver.htlcResolution = resolution + return resolver + }, + ) + + // Run from the given checkpoint, ensuring we'll hit the rest. + _ = runFromCheckpoint(t, ctx, checkpoints[i+1:]) + } +} diff --git a/input/input.go b/input/input.go index 7c0d79f6..87a6339e 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 @@ -67,6 +68,22 @@ type TxInfo struct { Weight int64 } +// SignDetails is a struct containing information needed to resign certain +// inputs. It is used to re-sign 2nd level HTLC transactions that uses the +// SINGLE|ANYONECANPAY sighash type, as we have a signature provided by our +// peer, but we can aggregate multiple of these 2nd level transactions into a +// new transaction, that needs to be signed by us. +type SignDetails struct { + // SignDesc is the sign descriptor needed for us to sign the input. + SignDesc SignDescriptor + + // PeerSig is the peer's signature for this input. + PeerSig Signature + + // SigHashType is the sighash signed by the peer. + SigHashType txscript.SigHashType +} + type inputKit struct { outpoint wire.OutPoint witnessType WitnessType @@ -241,7 +258,130 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx, }, nil } +// HtlcsSecondLevelAnchorInput is an input type used to spend HTLC outputs +// using a re-signed second level transaction, either via the timeout or success +// paths. +type HtlcSecondLevelAnchorInput struct { + inputKit + + // SignedTx is the original second level transaction signed by the + // channel peer. + SignedTx *wire.MsgTx + + // createWitness creates a witness allowing the passed transaction to + // spend the input. + createWitness func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, txinIdx int) (wire.TxWitness, error) +} + +// RequiredTxOut returns the tx out needed to be present on the sweep tx for +// the spend of the input to be valid. +func (i *HtlcSecondLevelAnchorInput) RequiredTxOut() *wire.TxOut { + return i.SignedTx.TxOut[0] +} + +// RequiredLockTime returns the locktime needed for the sweep tx for the spend +// of the input to be valid. For a second level HTLC timeout this will be the +// CLTV expiry, for HTLC success it will be zero. +func (i *HtlcSecondLevelAnchorInput) RequiredLockTime() (uint32, bool) { + return i.SignedTx.LockTime, true +} + +// CraftInputScript returns a valid set of input scripts allowing this output +// to be spent. The returns input scripts should target the input at location +// txIndex within the passed transaction. The input scripts generated by this +// method support spending p2wkh, p2wsh, and also nested p2sh outputs. +func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer, + txn *wire.MsgTx, hashCache *txscript.TxSigHashes, + txinIdx int) (*Script, error) { + + witness, err := i.createWitness(signer, txn, hashCache, txinIdx) + if err != nil { + return nil, err + } + + return &Script{ + Witness: witness, + }, nil +} + +// MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper +// to spend the HTLC output on our commit using the second level timeout +// transaction. +func MakeHtlcSecondLevelTimeoutAnchorInput(signedTx *wire.MsgTx, + signDetails *SignDetails, heightHint uint32) HtlcSecondLevelAnchorInput { + + // Spend an HTLC output on our local commitment tx using the + // 2nd timeout transaction. + createWitness := func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, + txinIdx int) (wire.TxWitness, error) { + + desc := signDetails.SignDesc + desc.SigHashes = txscript.NewTxSigHashes(txn) + desc.InputIndex = txinIdx + + return SenderHtlcSpendTimeout( + signDetails.PeerSig, signDetails.SigHashType, signer, + &desc, txn, + ) + } + + return HtlcSecondLevelAnchorInput{ + inputKit: inputKit{ + outpoint: signedTx.TxIn[0].PreviousOutPoint, + witnessType: HtlcOfferedTimeoutSecondLevelInputConfirmed, + signDesc: signDetails.SignDesc, + heightHint: heightHint, + + // CSV delay is always 1 for these inputs. + blockToMaturity: 1, + }, + SignedTx: signedTx, + createWitness: createWitness, + } +} + +// MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper +// to spend the HTLC output on our commit using the second level success +// transaction. +func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx, + signDetails *SignDetails, preimage lntypes.Preimage, + heightHint uint32) HtlcSecondLevelAnchorInput { + + // Spend an HTLC output on our local commitment tx using the 2nd + // success transaction. + createWitness := func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, + txinIdx int) (wire.TxWitness, error) { + + desc := signDetails.SignDesc + desc.SigHashes = hashCache + desc.InputIndex = txinIdx + + return ReceiverHtlcSpendRedeem( + signDetails.PeerSig, signDetails.SigHashType, + preimage[:], signer, &desc, txn, + ) + } + + return HtlcSecondLevelAnchorInput{ + inputKit: inputKit{ + outpoint: signedTx.TxIn[0].PreviousOutPoint, + witnessType: HtlcAcceptedSuccessSecondLevelInputConfirmed, + signDesc: signDetails.SignDesc, + heightHint: heightHint, + + // CSV delay is always 1 for these inputs. + blockToMaturity: 1, + }, + SignedTx: signedTx, + createWitness: createWitness, + } +} + // Compile-time constraints to ensure each input struct implement the Input // interface. var _ Input = (*BaseInput)(nil) var _ Input = (*HtlcSucceedInput)(nil) +var _ Input = (*HtlcSecondLevelAnchorInput)(nil) 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 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: 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_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 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() 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{ diff --git a/lntest/itest/lnd_test.go b/lntest/itest/lnd_test.go index 68845c9f..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) @@ -11019,11 +11079,12 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error { // Record all payment hashes active for this channel. htlcHashes := make(map[string]struct{}) for _, htlc := range channel.PendingHtlcs { - _, ok := htlcHashes[string(htlc.HashLock)] + h := hex.EncodeToString(htlc.HashLock) + _, ok := htlcHashes[h] if ok { return fmt.Errorf("duplicate HashLock") } - htlcHashes[string(htlc.HashLock)] = struct{}{} + htlcHashes[h] = struct{}{} } // Channel should have exactly the payHashes active. @@ -11035,12 +11096,13 @@ func assertActiveHtlcs(nodes []*lntest.HarnessNode, payHashes ...[]byte) error { // Make sure all the payHashes are active. for _, payHash := range payHashes { - if _, ok := htlcHashes[string(payHash)]; ok { + h := hex.EncodeToString(payHash) + if _, ok := htlcHashes[h]; ok { continue } return fmt.Errorf("node %x didn't have the "+ "payHash %v active", node.PubKey[:], - payHash) + h) } } } diff --git a/lnwallet/channel.go b/lnwallet/channel.go index 4ab00e28..90f30e58 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 { @@ -5519,6 +5517,14 @@ type IncomingHtlcResolution struct { // claimed directly from the outpoint listed below. SignedSuccessTx *wire.MsgTx + // SignDetails is non-nil if SignedSuccessTx is non-nil, and the + // channel is of the anchor type. As the above HTLC transaction will be + // signed by the channel peer using SINGLE|ANYONECANPAY for such + // channels, we can use the sign details to add the input-output pair + // of the HTLC transaction to another transaction, thereby aggregating + // multiple HTLC transactions together, and adding fees as needed. + SignDetails *input.SignDetails + // CsvDelay is the relative time lock (expressed in blocks) that must // pass after the SignedSuccessTx is confirmed in the chain before the // output can be swept. @@ -5560,6 +5566,14 @@ type OutgoingHtlcResolution struct { // claimed directly from the outpoint listed below. SignedTimeoutTx *wire.MsgTx + // SignDetails is non-nil if SignedTimeoutTx is non-nil, and the + // channel is of the anchor type. As the above HTLC transaction will be + // signed by the channel peer using SINGLE|ANYONECANPAY for such + // channels, we can use the sign details to add the input-output pair + // of the HTLC transaction to another transaction, thereby aggregating + // multiple HTLC transactions together, and adding fees as needed. + SignDetails *input.SignDetails + // CsvDelay is the relative time lock (expressed in blocks) that must // pass after the SignedTimeoutTx is confirmed in the chain before the // output can be swept. @@ -5599,13 +5613,13 @@ type HtlcResolutions struct { // allowing the caller to sweep an outgoing HTLC present on either their, or // the remote party's commitment transaction. func newOutgoingHtlcResolution(signer input.Signer, - localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, + localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool, chanType channeldb.ChannelType) (*OutgoingHtlcResolution, error) { op := wire.OutPoint{ - Hash: commitHash, + Hash: commitTx.TxHash(), Index: uint32(htlc.OutputIndex), } @@ -5664,16 +5678,15 @@ func newOutgoingHtlcResolution(signer input.Signer, // With the transaction created, we can generate a sign descriptor // that's capable of generating the signature required to spend the // HTLC output using the timeout transaction. + txOut := commitTx.TxOut[htlc.OutputIndex] timeoutSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, WitnessScript: htlcScript, - Output: &wire.TxOut{ - Value: int64(htlc.Amt.ToSatoshis()), - }, - HashType: txscript.SigHashAll, - SigHashes: txscript.NewTxSigHashes(timeoutTx), - InputIndex: 0, + Output: txOut, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(timeoutTx), + InputIndex: 0, } htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256()) @@ -5692,6 +5705,12 @@ func newOutgoingHtlcResolution(signer input.Signer, } timeoutTx.TxIn[0].Witness = timeoutWitness + // If this is an anchor type channel, the sign details will let us + // re-sign an aggregated tx later. + txSignDetails := HtlcSignDetails( + chanType, timeoutSignDesc, sigHashType, htlcSig, + ) + // Finally, we'll generate the script output that the timeout // transaction creates so we can generate the signDesc required to // complete the claim process after a delay period. @@ -5712,6 +5731,7 @@ func newOutgoingHtlcResolution(signer input.Signer, return &OutgoingHtlcResolution{ Expiry: htlc.RefundTimeout, SignedTimeoutTx: timeoutTx, + SignDetails: txSignDetails, CsvDelay: csvDelay, ClaimOutpoint: wire.OutPoint{ Hash: timeoutTx.TxHash(), @@ -5738,13 +5758,13 @@ func newOutgoingHtlcResolution(signer input.Signer, // // TODO(roasbeef) consolidate code with above func func newIncomingHtlcResolution(signer input.Signer, - localChanCfg *channeldb.ChannelConfig, commitHash chainhash.Hash, + localChanCfg *channeldb.ChannelConfig, commitTx *wire.MsgTx, htlc *channeldb.HTLC, keyRing *CommitmentKeyRing, feePerKw chainfee.SatPerKWeight, csvDelay uint32, localCommit bool, chanType channeldb.ChannelType) (*IncomingHtlcResolution, error) { op := wire.OutPoint{ - Hash: commitHash, + Hash: commitTx.TxHash(), Index: uint32(htlc.OutputIndex), } @@ -5795,16 +5815,15 @@ func newIncomingHtlcResolution(signer input.Signer, // Once we've created the second-level transaction, we'll generate the // SignDesc needed spend the HTLC output using the success transaction. + txOut := commitTx.TxOut[htlc.OutputIndex] successSignDesc := input.SignDescriptor{ KeyDesc: localChanCfg.HtlcBasePoint, SingleTweak: keyRing.LocalHtlcKeyTweak, WitnessScript: htlcScript, - Output: &wire.TxOut{ - Value: int64(htlc.Amt.ToSatoshis()), - }, - HashType: txscript.SigHashAll, - SigHashes: txscript.NewTxSigHashes(successTx), - InputIndex: 0, + Output: txOut, + HashType: txscript.SigHashAll, + SigHashes: txscript.NewTxSigHashes(successTx), + InputIndex: 0, } htlcSig, err := btcec.ParseDERSignature(htlc.Signature, btcec.S256()) @@ -5825,6 +5844,12 @@ func newIncomingHtlcResolution(signer input.Signer, } successTx.TxIn[0].Witness = successWitness + // If this is an anchor type channel, the sign details will let us + // re-sign an aggregated tx later. + txSignDetails := HtlcSignDetails( + chanType, successSignDesc, sigHashType, htlcSig, + ) + // Finally, we'll generate the script that the second-level transaction // creates so we can generate the proper signDesc to sweep it after the // CSV delay has passed. @@ -5844,6 +5869,7 @@ func newIncomingHtlcResolution(signer input.Signer, ) return &IncomingHtlcResolution{ SignedSuccessTx: successTx, + SignDetails: txSignDetails, CsvDelay: csvDelay, ClaimOutpoint: wire.OutPoint{ Hash: successTx.TxHash(), @@ -5892,7 +5918,7 @@ func (r *OutgoingHtlcResolution) HtlcPoint() wire.OutPoint { func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, signer input.Signer, htlcs []channeldb.HTLC, keyRing *CommitmentKeyRing, localChanCfg, remoteChanCfg *channeldb.ChannelConfig, - commitHash chainhash.Hash, chanType channeldb.ChannelType) ( + commitTx *wire.MsgTx, chanType channeldb.ChannelType) ( *HtlcResolutions, error) { // TODO(roasbeef): don't need to swap csv delay? @@ -5924,7 +5950,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, // Otherwise, we'll create an incoming HTLC resolution // as we can satisfy the contract. ihr, err := newIncomingHtlcResolution( - signer, localChanCfg, commitHash, &htlc, + signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), ourCommit, chanType, ) @@ -5937,7 +5963,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight, ourCommit bool, } ohr, err := newOutgoingHtlcResolution( - signer, localChanCfg, commitHash, &htlc, keyRing, + signer, localChanCfg, commitTx, &htlc, keyRing, feePerKw, uint32(csvDelay), ourCommit, chanType, ) if err != nil { @@ -6138,12 +6164,11 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel, // outgoing HTLC's that we'll need to claim as well. If this is after // recovery there is not much we can do with HTLCs, so we'll always // use what we have in our latest state when extracting resolutions. - txHash := commitTx.TxHash() localCommit := chanState.LocalCommitment htlcResolutions, err := extractHtlcResolutions( chainfee.SatPerKWeight(localCommit.FeePerKw), true, signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg, - &chanState.RemoteChanCfg, txHash, chanState.ChanType, + &chanState.RemoteChanCfg, commitTx, chanState.ChanType, ) if err != nil { return nil, err 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 { 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 ) 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. 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