diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index cac40bac..ef391ff9 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -20,6 +20,10 @@ const ( // sweepConfTarget is the default number of blocks that we'll use as a // confirmation target when sweeping. sweepConfTarget = 6 + + // secondLevelConfTarget is the confirmation target we'll use when + // adding fees to our second-level HTLC transactions. + secondLevelConfTarget = 6 ) // ContractResolver is an interface which packages a state machine which is diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go index db1706bd..37b4d42b 100644 --- a/contractcourt/htlc_success_resolver.go +++ b/contractcourt/htlc_success_resolver.go @@ -3,11 +3,13 @@ package contractcourt import ( "encoding/binary" "io" + "sync" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" "github.com/davecgh/go-spew/spew" + "github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/labels" @@ -29,7 +31,12 @@ type htlcSuccessResolver struct { htlcResolution lnwallet.IncomingHtlcResolution // outputIncubating returns true if we've sent the output to the output - // incubator (utxo nursery). + // incubator (utxo nursery). In case the htlcResolution has non-nil + // SignDetails, it means we will let the Sweeper handle broadcasting + // the secondd-level transaction, and sweeping its output. In this case + // we let this field indicate whether we need to broadcast the + // second-level tx (false) or if it has confirmed and we must sweep the + // second-level output (true). outputIncubating bool // resolved reflects if the contract has been fully resolved or not. @@ -50,6 +57,15 @@ type htlcSuccessResolver struct { // htlc contains information on the htlc that we are resolving on-chain. htlc channeldb.HTLC + // currentReport stores the current state of the resolver for reporting + // over the rpc interface. This should only be reported in case we have + // a non-nil SignDetails on the htlcResolution, otherwise the nursery + // will produce reports. + currentReport ContractReport + + // reportLock prevents concurrent access to the resolver report. + reportLock sync.Mutex + contractResolverKit } @@ -58,12 +74,16 @@ func newSuccessResolver(res lnwallet.IncomingHtlcResolution, broadcastHeight uint32, htlc channeldb.HTLC, resCfg ResolverConfig) *htlcSuccessResolver { - return &htlcSuccessResolver{ + h := &htlcSuccessResolver{ contractResolverKit: *newContractResolverKit(resCfg), htlcResolution: res, broadcastHeight: broadcastHeight, htlc: htlc, } + + h.initReport() + + return h } // ResolverKey returns an identifier which should be globally unique for this @@ -129,6 +149,11 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { return nil, err } + h.reportLock.Lock() + h.currentReport.RecoveredBalance = h.currentReport.LimboBalance + h.currentReport.LimboBalance = 0 + h.reportLock.Unlock() + h.resolved = true return nil, h.checkpointClaim( spend.SpenderTxHash, channeldb.ResolverOutcomeClaimed, @@ -140,6 +165,18 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { // outpoint of the second-level tx, that we must wait to be spent for the // resolver to be fully resolved. func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) { + // If we have non-nil SignDetails, this means that have a 2nd level + // HTLC transaction that is signed using sighash SINGLE|ANYONECANPAY + // (the case for anchor type channels). In this case we can re-sign it + // and attach fees at will. We let the sweeper handle this job. + // We use the checkpointed outputIncubating field to determine if we + // already swept the HTLC output into the second level transaction. + if h.htlcResolution.SignDetails != nil { + return h.broadcastReSignedSuccessTx() + } + + // Otherwise we'll publish the second-level transaction directly and + // offer the resolution to the nursery to handle. log.Infof("%T(%x): broadcasting second-layer transition tx: %v", h, h.htlc.RHash[:], spew.Sdump(h.htlcResolution.SignedSuccessTx)) @@ -181,6 +218,137 @@ func (h *htlcSuccessResolver) broadcastSuccessTx() (*wire.OutPoint, error) { return &h.htlcResolution.ClaimOutpoint, nil } +// broadcastReSignedSuccessTx handles the case where we have non-nil +// SignDetails, and offers the second level transaction to the Sweeper, that +// will re-sign it and attach fees at will. +func (h *htlcSuccessResolver) broadcastReSignedSuccessTx() ( + *wire.OutPoint, error) { + + // Keep track of the tx spending the HTLC output on the commitment, as + // this will be the confirmed second-level tx we'll ultimately sweep. + var commitSpend *chainntnfs.SpendDetail + + // We will have to let the sweeper re-sign the success tx and wait for + // it to confirm, if we haven't already. + if !h.outputIncubating { + log.Infof("%T(%x): offering second-layer transition tx to "+ + "sweeper: %v", h, h.htlc.RHash[:], + spew.Sdump(h.htlcResolution.SignedSuccessTx)) + + secondLevelInput := input.MakeHtlcSecondLevelSuccessAnchorInput( + h.htlcResolution.SignedSuccessTx, + h.htlcResolution.SignDetails, h.htlcResolution.Preimage, + h.broadcastHeight, + ) + _, err := h.Sweeper.SweepInput( + &secondLevelInput, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: secondLevelConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + log.Infof("%T(%x): waiting for second-level HTLC success "+ + "transaction to confirm", h, h.htlc.RHash[:]) + + // Wait for the second level transaction to confirm. + commitSpend, err = waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + + // Now that the second-level transaction has confirmed, we + // checkpoint the state so we'll go to the next stage in case + // of restarts. + h.outputIncubating = true + if err := h.Checkpoint(h); err != nil { + log.Errorf("unable to Checkpoint: %v", err) + return nil, err + } + + log.Infof("%T(%x): second-level HTLC success transaction "+ + "confirmed!", h, h.htlc.RHash[:]) + } + + // If we ended up here after a restart, we must again get the + // spend notification. + if commitSpend == nil { + var err error + commitSpend, err = waitForSpend( + &h.htlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint, + h.htlcResolution.SignDetails.SignDesc.Output.PkScript, + h.broadcastHeight, h.Notifier, h.quit, + ) + if err != nil { + return nil, err + } + } + + // The HTLC success tx has a CSV lock that we must wait for. + waitHeight := uint32(commitSpend.SpendingHeight) + + h.htlcResolution.CsvDelay - 1 + + // Now that the sweeper has broadcasted the second-level transaction, + // it has confirmed, and we have checkpointed our state, we'll sweep + // the second level output. We report the resolver has moved the next + // stage. + h.reportLock.Lock() + h.currentReport.Stage = 2 + h.currentReport.MaturityHeight = waitHeight + h.reportLock.Unlock() + + log.Infof("%T(%x): waiting for CSV lock to expire at height %v", + h, h.htlc.RHash[:], waitHeight) + + err := waitForHeight(waitHeight, h.Notifier, h.quit) + if err != nil { + return nil, err + } + + // We'll use this input index to determine the second-level output + // index on the transaction, as the signatures requires the indexes to + // be the same. We don't look for the second-level output script + // directly, as there might be more than one HTLC output to the same + // pkScript. + op := &wire.OutPoint{ + Hash: *commitSpend.SpenderTxHash, + Index: commitSpend.SpenderInputIndex, + } + + // Finally, let the sweeper sweep the second-level output. + log.Infof("%T(%x): CSV lock expired, offering second-layer "+ + "output to sweeper: %v", h, h.htlc.RHash[:], op) + + inp := input.NewCsvInput( + op, input.HtlcAcceptedSuccessSecondLevel, + &h.htlcResolution.SweepSignDesc, h.broadcastHeight, + h.htlcResolution.CsvDelay, + ) + _, err = h.Sweeper.SweepInput( + inp, + sweep.Params{ + Fee: sweep.FeePreference{ + ConfTarget: sweepConfTarget, + }, + }, + ) + if err != nil { + return nil, err + } + + // Will return this outpoint, when this is spent the resolver is fully + // resolved. + return op, nil +} + // resolveRemoteCommitOutput handles sweeping an HTLC output on the remote // commitment with the preimage. In this case we can sweep the output directly, // and don't have to broadcast a second-level transaction. @@ -337,6 +505,40 @@ func (h *htlcSuccessResolver) IsResolved() bool { return h.resolved } +// report returns a report on the resolution state of the contract. +func (h *htlcSuccessResolver) report() *ContractReport { + // If the sign details are nil, the report will be created by handled + // by the nursery. + if h.htlcResolution.SignDetails == nil { + return nil + } + + h.reportLock.Lock() + defer h.reportLock.Unlock() + copy := h.currentReport + return © +} + +func (h *htlcSuccessResolver) initReport() { + // We create the initial report. This will only be reported for + // resolvers not handled by the nursery. + finalAmt := h.htlc.Amt.ToSatoshis() + if h.htlcResolution.SignedSuccessTx != nil { + finalAmt = btcutil.Amount( + h.htlcResolution.SignedSuccessTx.TxOut[0].Value, + ) + } + + h.currentReport = ContractReport{ + Outpoint: h.htlcResolution.ClaimOutpoint, + Type: ReportOutputIncomingHtlc, + Amount: finalAmt, + MaturityHeight: h.htlcResolution.CsvDelay, + LimboBalance: finalAmt, + Stage: 1, + } +} + // Encode writes an encoded version of the ContractResolver into the passed // Writer. // @@ -411,6 +613,8 @@ func newSuccessResolverFromReader(r io.Reader, resCfg ResolverConfig) ( return nil, err } + h.initReport() + return h, nil } diff --git a/input/input.go b/input/input.go index cac503ce..e5752e88 100644 --- a/input/input.go +++ b/input/input.go @@ -4,6 +4,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lntypes" ) // Input represents an abstract UTXO which is to be spent using a sweeping @@ -257,7 +258,93 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx, }, nil } +// HtlcsSecondLevelAnchorInput is an input type used to spend HTLC outputs +// using a re-signed second level transaction, either via the timeout or success +// paths. +type HtlcSecondLevelAnchorInput struct { + inputKit + + // SignedTx is the original second level transaction signed by the + // channel peer. + SignedTx *wire.MsgTx + + // createWitness creates a witness allowing the passed transaction to + // spend the input. + createWitness func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, txinIdx int) (wire.TxWitness, error) +} + +// RequiredTxOut returns the tx out needed to be present on the sweep tx for +// the spend of the input to be valid. +func (i *HtlcSecondLevelAnchorInput) RequiredTxOut() *wire.TxOut { + return i.SignedTx.TxOut[0] +} + +// RequiredLockTime returns the locktime needed for the sweep tx for the spend +// of the input to be valid. For a second level HTLC timeout this will be the +// CLTV expiry, for HTLC success it will be zero. +func (i *HtlcSecondLevelAnchorInput) RequiredLockTime() (uint32, bool) { + return i.SignedTx.LockTime, true +} + +// CraftInputScript returns a valid set of input scripts allowing this output +// to be spent. The returns input scripts should target the input at location +// txIndex within the passed transaction. The input scripts generated by this +// method support spending p2wkh, p2wsh, and also nested p2sh outputs. +func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer, + txn *wire.MsgTx, hashCache *txscript.TxSigHashes, + txinIdx int) (*Script, error) { + + witness, err := i.createWitness(signer, txn, hashCache, txinIdx) + if err != nil { + return nil, err + } + + return &Script{ + Witness: witness, + }, nil +} + +// MakeHtlcSecondLevelSuccessAnchorInput creates an input allowing the sweeper +// to spend the HTLC output on our commit using the second level success +// transaction. +func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx, + signDetails *SignDetails, preimage lntypes.Preimage, + heightHint uint32) HtlcSecondLevelAnchorInput { + + // Spend an HTLC output on our local commitment tx using the 2nd + // success transaction. + createWitness := func(signer Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, + txinIdx int) (wire.TxWitness, error) { + + desc := signDetails.SignDesc + desc.SigHashes = hashCache + desc.InputIndex = txinIdx + + return ReceiverHtlcSpendRedeem( + signDetails.PeerSig, signDetails.SigHashType, + preimage[:], signer, &desc, txn, + ) + } + + return HtlcSecondLevelAnchorInput{ + inputKit: inputKit{ + outpoint: signedTx.TxIn[0].PreviousOutPoint, + witnessType: HtlcAcceptedSuccessSecondLevelInputConfirmed, + signDesc: signDetails.SignDesc, + heightHint: heightHint, + + // CSV delay is always 1 for these inputs. + blockToMaturity: 1, + }, + SignedTx: signedTx, + createWitness: createWitness, + } +} + // Compile-time constraints to ensure each input struct implement the Input // interface. var _ Input = (*BaseInput)(nil) var _ Input = (*HtlcSucceedInput)(nil) +var _ Input = (*HtlcSecondLevelAnchorInput)(nil)