diff --git a/breacharbiter.go b/breacharbiter.go index df19bff1..3a68220b 100644 --- a/breacharbiter.go +++ b/breacharbiter.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/htlcswitch" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/sweep" ) var ( @@ -748,33 +749,6 @@ func (b *breachArbiter) handleBreachHandoff(breachEvent *ContractBreachEvent) { go b.exactRetribution(cfChan, retInfo) } -// SpendableOutput an interface which can be used by the breach arbiter to -// construct a transaction spending from outputs we control. -type SpendableOutput interface { - // Amount returns the number of satoshis contained within the output. - Amount() btcutil.Amount - - // Outpoint returns the reference to the output being spent, used to - // construct the corresponding transaction input. - OutPoint() *wire.OutPoint - - // WitnessType returns an enum specifying the type of witness that must - // be generated in order to spend this output. - WitnessType() lnwallet.WitnessType - - // SignDesc returns a reference to a spendable output's sign descriptor, - // which is used during signing to compute a valid witness that spends - // this output. - SignDesc() *lnwallet.SignDescriptor - - // BuildWitness returns a valid witness allowing this output to be - // spent, the witness should be attached to the transaction at the - // location determined by the given `txinIdx`. - BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx, - hashCache *txscript.TxSigHashes, - txinIdx int) ([][]byte, error) -} - // breachedOutput contains all the information needed to sweep a breached // output. A breached output is an output that we are now entitled to due to a // revoked commitment transaction being broadcast. @@ -850,9 +824,16 @@ func (bo *breachedOutput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx, return bo.witnessFunc(txn, hashCache, txinIdx) } -// Add compile-time constraint ensuring breachedOutput implements -// SpendableOutput. -var _ SpendableOutput = (*breachedOutput)(nil) +// BlocksToMaturity returns the relative timelock, as a number of blocks, that +// must be built on top of the confirmation height before the output can be +// spent. +func (bo *breachedOutput) BlocksToMaturity() uint32 { + return 0 +} + +// Add compile-time constraint ensuring breachedOutput implements the Input +// interface. +var _ sweep.Input = (*breachedOutput)(nil) // retributionInfo encapsulates all the data needed to sweep all the contested // funds within a channel whose contract has been breached by the prior @@ -963,13 +944,13 @@ func (b *breachArbiter) createJusticeTx( // outputs, while simultaneously computing the estimated weight of the // transaction. var ( - spendableOutputs []SpendableOutput + spendableOutputs []sweep.Input weightEstimate lnwallet.TxWeightEstimator ) // Allocate enough space to potentially hold each of the breached // outputs in the retribution info. - spendableOutputs = make([]SpendableOutput, 0, len(r.breachedOutputs)) + spendableOutputs = make([]sweep.Input, 0, len(r.breachedOutputs)) // The justice transaction we construct will be a segwit transaction // that pays to a p2wkh output. Components such as the version, @@ -1023,7 +1004,7 @@ func (b *breachArbiter) createJusticeTx( // sweepSpendableOutputsTxn creates a signed transaction from a sequence of // spendable outputs by sweeping the funds into a single p2wkh output. func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, - inputs ...SpendableOutput) (*wire.MsgTx, error) { + inputs ...sweep.Input) (*wire.MsgTx, error) { // First, we obtain a new public key script from the wallet which we'll // sweep the funds to. @@ -1037,7 +1018,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, // Compute the total amount contained in the inputs. var totalAmt btcutil.Amount for _, input := range inputs { - totalAmt += input.Amount() + totalAmt += btcutil.Amount(input.SignDesc().Output.Value) } // We'll actually attempt to target inclusion within the next two @@ -1085,7 +1066,7 @@ func (b *breachArbiter) sweepSpendableOutputsTxn(txWeight int64, // witness, and attaching it to the transaction. This function accepts // an integer index representing the intended txin index, and the // breached output from which it will spend. - addWitness := func(idx int, so SpendableOutput) error { + addWitness := func(idx int, so sweep.Input) error { // First, we construct a valid witness for this outpoint and // transaction using the SpendableOutput's witness generation // function. diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go index db401735..5f8b161d 100644 --- a/contractcourt/chain_arbitrator.go +++ b/contractcourt/chain_arbitrator.go @@ -3,6 +3,7 @@ package contractcourt import ( "errors" "fmt" + "github.com/lightningnetwork/lnd/sweep" "sync" "sync/atomic" @@ -130,6 +131,9 @@ type ChainArbitratorConfig struct { // DisableChannel disables a channel, resulting in it not being able to // forward payments. DisableChannel func(wire.OutPoint) error + + // Sweeper allows resolvers to sweep their final outputs. + Sweeper *sweep.UtxoSweeper } // ChainArbitrator is a sub-system that oversees the on-chain resolution of all diff --git a/contractcourt/contract_resolvers.go b/contractcourt/contract_resolvers.go index 3ecc6be9..56e035e6 100644 --- a/contractcourt/contract_resolvers.go +++ b/contractcourt/contract_resolvers.go @@ -5,10 +5,10 @@ import ( "crypto/sha256" "encoding/binary" "fmt" + "github.com/lightningnetwork/lnd/sweep" "io" "io/ioutil" - "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/davecgh/go-spew/spew" "github.com/lightningnetwork/lnd/chainntnfs" @@ -446,59 +446,19 @@ func (h *htlcSuccessResolver) Resolve() (ContractResolver, error) { "incoming+remote htlc confirmed", h, h.payHash[:]) - // In this case, we can sweep it directly from the - // commitment output. We'll first grab a fresh address - // from the wallet to sweep the output. - addr, err := h.NewSweepAddr() - if err != nil { - return nil, err - } - - // With our address obtained, we'll query for an - // estimate to be confirmed at ease. - // - // TODO(roasbeef): signal up if fee would be too large - // to sweep singly, need to batch - feePerKw, err := h.FeeEstimator.EstimateFeePerKW(6) - if err != nil { - return nil, err - } - - log.Debugf("%T(%x): using %v sat/kw to sweep htlc"+ - "incoming+remote htlc confirmed", h, - h.payHash[:], int64(feePerKw)) - - // Using a weight estimator, we'll compute the total - // fee required, and from that the value we'll end up - // with. - totalWeight := (&lnwallet.TxWeightEstimator{}). - AddWitnessInput(lnwallet.OfferedHtlcSuccessWitnessSize). - AddP2WKHOutput().Weight() - totalFees := feePerKw.FeeForWeight(int64(totalWeight)) - sweepAmt := h.htlcResolution.SweepSignDesc.Output.Value - - int64(totalFees) - - // With the fee computation finished, we'll now - // construct the sweep transaction. - htlcPoint := h.htlcResolution.ClaimOutpoint - h.sweepTx = wire.NewMsgTx(2) - h.sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: htlcPoint, - }) - h.sweepTx.AddTxOut(&wire.TxOut{ - PkScript: addr, - Value: sweepAmt, - }) - - // With the transaction fully assembled, we can now - // generate a valid witness for the transaction. - h.htlcResolution.SweepSignDesc.SigHashes = txscript.NewTxSigHashes( - h.sweepTx, - ) - h.sweepTx.TxIn[0].Witness, err = lnwallet.SenderHtlcSpendRedeem( - h.Signer, &h.htlcResolution.SweepSignDesc, h.sweepTx, + input := sweep.MakeHtlcSucceedInput( + &h.htlcResolution.ClaimOutpoint, + &h.htlcResolution.SweepSignDesc, h.htlcResolution.Preimage[:], ) + + var err error + + // TODO: Set tx lock time to current block height instead of + // zero. Will be taken care of once sweeper implementation is + // complete. + h.sweepTx, err = h.Sweeper.CreateSweepTx( + []sweep.Input{&input}, 0) if err != nil { return nil, err } @@ -1256,46 +1216,16 @@ func (c *commitSweepResolver) Resolve() (ContractResolver, error) { // If the sweep transaction isn't already generated, and the remote // party broadcast the commitment transaction then we'll create it now. case c.sweepTx == nil && !isLocalCommitTx: - // Now that the commitment transaction has confirmed, we'll - // craft a transaction to sweep this output into the wallet. - signDesc := c.commitResolution.SelfOutputSignDesc + input := sweep.MakeBaseInput( + &c.commitResolution.SelfOutPoint, + lnwallet.CommitmentNoDelay, + &c.commitResolution.SelfOutputSignDesc) - // First, we'll estimate the total weight so we can compute - // fees properly. We'll use a lax estimate, as this output is - // in no immediate danger. - feePerKw, err := c.FeeEstimator.EstimateFeePerKW(6) - if err != nil { - return nil, err - } - - log.Debugf("%T(%v): using %v sat/kw for sweep tx", c, - c.chanPoint, int64(feePerKw)) - - totalWeight := (&lnwallet.TxWeightEstimator{}). - AddP2WKHInput(). - AddP2WKHOutput().Weight() - totalFees := feePerKw.FeeForWeight(int64(totalWeight)) - sweepAmt := signDesc.Output.Value - int64(totalFees) - - c.sweepTx = wire.NewMsgTx(2) - c.sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: c.commitResolution.SelfOutPoint, - }) - sweepAddr, err := c.NewSweepAddr() - if err != nil { - return nil, err - } - c.sweepTx.AddTxOut(&wire.TxOut{ - PkScript: sweepAddr, - Value: sweepAmt, - }) - - // With the transaction fully assembled, we can now generate a - // valid witness for the transaction. - signDesc.SigHashes = txscript.NewTxSigHashes(c.sweepTx) - c.sweepTx.TxIn[0].Witness, err = lnwallet.CommitSpendNoDelay( - c.Signer, &signDesc, c.sweepTx, - ) + // TODO: Set tx lock time to current block height instead of + // zero. Will be taken care of once sweeper implementation is + // complete. + c.sweepTx, err = c.Sweeper.CreateSweepTx( + []sweep.Input{&input}, 0) if err != nil { return nil, err } diff --git a/log.go b/log.go index 1fe1b658..a041cbe8 100644 --- a/log.go +++ b/log.go @@ -1,11 +1,9 @@ package main import ( - "os" - - "io" - "fmt" + "io" + "os" "path/filepath" "github.com/btcsuite/btcd/connmgr" @@ -24,6 +22,7 @@ import ( "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/routing" "github.com/lightningnetwork/lnd/signal" + "github.com/lightningnetwork/lnd/sweep" ) // Loggers per subsystem. A single backend logger is created and all subsystem @@ -65,6 +64,7 @@ var ( atplLog = build.NewSubLogger("ATPL", backendLog.Logger) cnctLog = build.NewSubLogger("CNCT", backendLog.Logger) sphxLog = build.NewSubLogger("SPHX", backendLog.Logger) + swprLog = build.NewSubLogger("SWPR", backendLog.Logger) ) // Initialize package-global logger variables. @@ -81,6 +81,7 @@ func init() { contractcourt.UseLogger(cnctLog) sphinx.UseLogger(sphxLog) signal.UseLogger(ltndLog) + sweep.UseLogger(swprLog) } // subsystemLoggers maps each subsystem identifier to its associated logger. @@ -103,6 +104,7 @@ var subsystemLoggers = map[string]btclog.Logger{ "ATPL": atplLog, "CNCT": cnctLog, "SPHX": sphxLog, + "SWPR": swprLog, } // initLogRotator initializes the logging rotator to write logs to logFile and diff --git a/server.go b/server.go index 6c2ee8ba..7ebe5189 100644 --- a/server.go +++ b/server.go @@ -6,6 +6,7 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "github.com/lightningnetwork/lnd/sweep" "image/color" "math/big" "net" @@ -58,6 +59,10 @@ const ( // durations exceeding this value will be eligible to have their // backoffs reduced. defaultStableConnDuration = 10 * time.Minute + + // sweepTxConfirmationTarget assigns a confirmation target for sweep + // txes on which the fee calculation will be based. + sweepTxConfirmationTarget = 6 ) var ( @@ -581,19 +586,24 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, return nil, err } + sweeper := sweep.New(&sweep.UtxoSweeperConfig{ + Estimator: cc.feeEstimator, + GenSweepScript: func() ([]byte, error) { + return newSweepPkScript(cc.wallet) + }, + Signer: cc.wallet.Cfg.Signer, + ConfTarget: sweepTxConfirmationTarget, + }) + s.utxoNursery = newUtxoNursery(&NurseryConfig{ ChainIO: cc.chainIO, ConfDepth: 1, FetchClosedChannels: chanDB.FetchClosedChannels, FetchClosedChannel: chanDB.FetchClosedChannel, - Estimator: cc.feeEstimator, - GenSweepScript: func() ([]byte, error) { - return newSweepPkScript(cc.wallet) - }, - Notifier: cc.chainNotifier, - PublishTransaction: cc.wallet.PublishTransaction, - Signer: cc.wallet.Cfg.Signer, - Store: utxnStore, + Notifier: cc.chainNotifier, + PublishTransaction: cc.wallet.PublishTransaction, + Store: utxnStore, + Sweeper: sweeper, }) // Construct a closure that wraps the htlcswitch's CloseLink method. @@ -685,6 +695,7 @@ func newServer(listenAddrs []net.Addr, chanDB *channeldb.DB, cc *chainControl, DisableChannel: func(op wire.OutPoint) error { return s.announceChanStatus(op, true) }, + Sweeper: sweeper, }, chanDB) s.breachArbiter = newBreachArbiter(&BreachConfig{ diff --git a/sweep/input.go b/sweep/input.go new file mode 100644 index 00000000..795adc09 --- /dev/null +++ b/sweep/input.go @@ -0,0 +1,154 @@ +package sweep + +import ( + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwallet" +) + +// Input contains all data needed to construct a sweep tx input. +type Input interface { + // Outpoint returns the reference to the output being spent, used to + // construct the corresponding transaction input. + OutPoint() *wire.OutPoint + + // WitnessType returns an enum specifying the type of witness that must + // be generated in order to spend this output. + WitnessType() lnwallet.WitnessType + + // SignDesc returns a reference to a spendable output's sign descriptor, + // which is used during signing to compute a valid witness that spends + // this output. + SignDesc() *lnwallet.SignDescriptor + + // BuildWitness returns a valid witness allowing this output to be + // spent, the witness should be attached to the transaction at the + // location determined by the given `txinIdx`. + BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, + txinIdx int) ([][]byte, error) + + // BlocksToMaturity returns the relative timelock, as a number of + // blocks, that must be built on top of the confirmation height before + // the output can be spent. For non-CSV locked inputs this is always + // zero. + BlocksToMaturity() uint32 +} + +type inputKit struct { + outpoint wire.OutPoint + witnessType lnwallet.WitnessType + signDesc lnwallet.SignDescriptor +} + +// OutPoint returns the breached output's identifier that is to be included as a +// transaction input. +func (i *inputKit) OutPoint() *wire.OutPoint { + return &i.outpoint +} + +// WitnessType returns the type of witness that must be generated to spend the +// breached output. +func (i *inputKit) WitnessType() lnwallet.WitnessType { + return i.witnessType +} + +// SignDesc returns the breached output's SignDescriptor, which is used during +// signing to compute the witness. +func (i *inputKit) SignDesc() *lnwallet.SignDescriptor { + return &i.signDesc +} + +// BaseInput contains all the information needed to sweep a basic output +// (CSV/CLTV/no time lock) +type BaseInput struct { + inputKit +} + +// MakeBaseInput assembles a new BaseInput that can be used to construct a +// sweep transaction. +func MakeBaseInput(outpoint *wire.OutPoint, + witnessType lnwallet.WitnessType, + signDescriptor *lnwallet.SignDescriptor) BaseInput { + + return BaseInput{ + inputKit{ + outpoint: *outpoint, + witnessType: witnessType, + signDesc: *signDescriptor, + }, + } +} + +// BuildWitness computes a valid witness that allows us to spend from the +// breached output. It does so by generating the witness generation function, +// which is parameterized primarily by the witness type and sign descriptor. The +// method then returns the witness computed by invoking this function. +func (bi *BaseInput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) { + + witnessFunc := bi.witnessType.GenWitnessFunc( + signer, bi.SignDesc(), + ) + + return witnessFunc(txn, hashCache, txinIdx) +} + +// BlocksToMaturity returns the relative timelock, as a number of blocks, that +// must be built on top of the confirmation height before the output can be +// spent. For non-CSV locked inputs this is always zero. +func (bi *BaseInput) BlocksToMaturity() uint32 { + return 0 +} + +// HtlcSucceedInput constitutes a sweep input that needs a pre-image. The input +// is expected to reside on the commitment tx of the remote party and should not +// be a second level tx output. +type HtlcSucceedInput struct { + inputKit + + preimage []byte +} + +// MakeHtlcSucceedInput assembles a new redeem input that can be used to +// construct a sweep transaction. +func MakeHtlcSucceedInput(outpoint *wire.OutPoint, + signDescriptor *lnwallet.SignDescriptor, + preimage []byte) HtlcSucceedInput { + + return HtlcSucceedInput{ + inputKit: inputKit{ + outpoint: *outpoint, + witnessType: lnwallet.HtlcAcceptedRemoteSuccess, + signDesc: *signDescriptor, + }, + preimage: preimage, + } +} + +// BuildWitness computes a valid witness that allows us to spend from the +// breached output. For HtlcSpendInput it will need to make the preimage part of +// the witness. +func (h *HtlcSucceedInput) BuildWitness(signer lnwallet.Signer, txn *wire.MsgTx, + hashCache *txscript.TxSigHashes, txinIdx int) ([][]byte, error) { + + desc := h.signDesc + desc.SigHashes = hashCache + desc.InputIndex = txinIdx + + return lnwallet.SenderHtlcSpendRedeem( + signer, &desc, txn, + h.preimage, + ) +} + +// BlocksToMaturity returns the relative timelock, as a number of blocks, that +// must be built on top of the confirmation height before the output can be +// spent. +func (h *HtlcSucceedInput) BlocksToMaturity() uint32 { + return 0 +} + +// Add compile-time constraint ensuring input structs implement Input interface. +var _ Input = (*BaseInput)(nil) +var _ Input = (*HtlcSucceedInput)(nil) diff --git a/sweep/log.go b/sweep/log.go new file mode 100644 index 00000000..a5112dfb --- /dev/null +++ b/sweep/log.go @@ -0,0 +1,45 @@ +package sweep + +import ( + "github.com/btcsuite/btclog" + "github.com/lightningnetwork/lnd/build" +) + +// log is a logger that is initialized with no output filters. This +// means the package will not perform any logging by default until the caller +// requests it. +var log btclog.Logger + +// The default amount of logging is none. +func init() { + UseLogger(build.NewSubLogger("SWPR", nil)) +} + +// DisableLog disables all library log output. Logging output is disabled +// by default until UseLogger is called. +func DisableLog() { + UseLogger(btclog.Disabled) +} + +// UseLogger uses a specified Logger to output package logging info. +// This should be used in preference to SetLogWriter if the caller is also +// using btclog. +func UseLogger(logger btclog.Logger) { + log = logger +} + +// logClosure is used to provide a closure over expensive logging operations so +// don't have to be performed when the logging level doesn't warrant it. +type logClosure func() string + +// String invokes the underlying function and returns the result. +func (c logClosure) String() string { + return c() +} + +// newLogClosure returns a new closure over a function that returns a string +// which itself provides a Stringer interface so that it can be used with the +// logging system. +func newLogClosure(c func() string) logClosure { + return logClosure(c) +} diff --git a/sweep/sweeper.go b/sweep/sweeper.go new file mode 100644 index 00000000..0bd101b2 --- /dev/null +++ b/sweep/sweeper.go @@ -0,0 +1,234 @@ +package sweep + +import ( + "github.com/btcsuite/btcd/blockchain" + "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btcutil" + "github.com/lightningnetwork/lnd/lnwallet" +) + +// UtxoSweeper provides the functionality to generate sweep txes. The plan is to +// extend UtxoSweeper in the future to also manage the actual sweeping process +// by itself. +type UtxoSweeper struct { + cfg *UtxoSweeperConfig +} + +// UtxoSweeperConfig contains dependencies of UtxoSweeper. +type UtxoSweeperConfig struct { + // GenSweepScript generates a P2WKH script belonging to the wallet where + // funds can be swept. + GenSweepScript func() ([]byte, error) + + // Estimator is used when crafting sweep transactions to estimate the + // necessary fee relative to the expected size of the sweep transaction. + Estimator lnwallet.FeeEstimator + + // Signer is used by the sweeper to generate valid witnesses at the + // time the incubated outputs need to be spent. + Signer lnwallet.Signer + + // ConfTarget specifies a target for the number of blocks until an + // initial confirmation. + ConfTarget uint32 +} + +// New returns a new UtxoSweeper instance. +func New(cfg *UtxoSweeperConfig) *UtxoSweeper { + return &UtxoSweeper{ + cfg: cfg, + } +} + +// CreateSweepTx accepts a list of inputs and signs and generates a txn that +// spends from them. This method also makes an accurate fee estimate before +// generating the required witnesses. +// +// The created transaction has a single output sending all the funds back to the +// source wallet, after accounting for the fee estimate. +// +// The value of currentBlockHeight argument will be set as the tx locktime. This +// function assumes that all CLTV inputs will be unlocked after +// currentBlockHeight. Reasons not to use the maximum of all actual CLTV expiry +// values of the inputs: +// +// - Make handling re-orgs easier. +// - Thwart future possible fee sniping attempts. +// - Make us blend in with the bitcoind wallet. +func (s *UtxoSweeper) CreateSweepTx(inputs []Input, + currentBlockHeight uint32) (*wire.MsgTx, error) { + + // Generate the receiving script to which the funds will be swept. + pkScript, err := s.cfg.GenSweepScript() + if err != nil { + return nil, err + } + + txWeight, csvCount, cltvCount := s.getWeightEstimate(inputs) + + // Using the txn weight estimate, compute the required txn fee. + feePerKw, err := s.cfg.Estimator.EstimateFeePerKW(s.cfg.ConfTarget) + if err != nil { + return nil, err + } + + log.Infof("Creating sweep transaction for %v inputs (%v CSV, %v CLTV) "+ + "using %v sat/kw", len(inputs), csvCount, cltvCount, + int64(feePerKw)) + + txFee := feePerKw.FeeForWeight(txWeight) + + // Sum up the total value contained in the inputs. + var totalSum btcutil.Amount + for _, o := range inputs { + totalSum += btcutil.Amount(o.SignDesc().Output.Value) + } + + // Sweep as much possible, after subtracting txn fees. + sweepAmt := int64(totalSum - txFee) + + // Create the sweep transaction that we will be building. We use + // version 2 as it is required for CSV. The txn will sweep the amount + // after fees to the pkscript generated above. + sweepTx := wire.NewMsgTx(2) + sweepTx.AddTxOut(&wire.TxOut{ + PkScript: pkScript, + Value: sweepAmt, + }) + + sweepTx.LockTime = currentBlockHeight + + // Add all inputs to the sweep transaction. Ensure that for each + // csvInput, we set the sequence number properly. + + for _, input := range inputs { + sweepTx.AddTxIn(&wire.TxIn{ + PreviousOutPoint: *input.OutPoint(), + Sequence: input.BlocksToMaturity(), + }) + } + + // Before signing the transaction, check to ensure that it meets some + // basic validity requirements. + // TODO(conner): add more control to sanity checks, allowing us to delay + // spending "problem" outputs, e.g. possibly batching with other classes + // if fees are too low. + btx := btcutil.NewTx(sweepTx) + if err := blockchain.CheckTransactionSanity(btx); err != nil { + return nil, err + } + + hashCache := txscript.NewTxSigHashes(sweepTx) + + // With all the inputs in place, use each output's unique witness + // function to generate the final witness required for spending. + addWitness := func(idx int, tso Input) error { + witness, err := tso.BuildWitness( + s.cfg.Signer, sweepTx, hashCache, idx, + ) + if err != nil { + return err + } + + sweepTx.TxIn[idx].Witness = witness + + return nil + } + + // Finally we'll attach a valid witness to each csv and cltv input + // within the sweeping transaction. + for i, input := range inputs { + if err := addWitness(i, input); err != nil { + return nil, err + } + } + + return sweepTx, nil +} + +// getWeightEstimate returns a weight estimate for the given inputs. +// Additionally, it returns counts for the number of csv and cltv inputs. +func (s *UtxoSweeper) getWeightEstimate(inputs []Input) (int64, int, int) { + + // Create a transaction which sweeps all the newly mature outputs into + // an output controlled by the wallet. + + // TODO(roasbeef): can be more intelligent about buffering outputs to + // be more efficient on-chain. + + var weightEstimate lnwallet.TxWeightEstimator + + // Our sweep transaction will pay to a single segwit p2wkh address, + // ensure it contributes to our weight estimate. + weightEstimate.AddP2WKHOutput() + + // For each output, use its witness type to determine the estimate + // weight of its witness, and add it to the proper set of spendable + // outputs. + csvCount := 0 + cltvCount := 0 + + for i := range inputs { + input := inputs[i] + + switch input.WitnessType() { + + // Outputs on a remote commitment transaction that pay directly + // to us. + case lnwallet.CommitmentNoDelay: + weightEstimate.AddP2WKHInput() + + // Outputs on a past commitment transaction that pay directly + // to us. + case lnwallet.CommitmentTimeLock: + weightEstimate.AddWitnessInput( + lnwallet.ToLocalTimeoutWitnessSize, + ) + csvCount++ + + // Outgoing second layer HTLC's that have confirmed within the + // chain, and the output they produced is now mature enough to + // sweep. + case lnwallet.HtlcOfferedTimeoutSecondLevel: + weightEstimate.AddWitnessInput( + lnwallet.ToLocalTimeoutWitnessSize, + ) + csvCount++ + + // Incoming second layer HTLC's that have confirmed within the + // chain, and the output they produced is now mature enough to + // sweep. + case lnwallet.HtlcAcceptedSuccessSecondLevel: + weightEstimate.AddWitnessInput( + lnwallet.ToLocalTimeoutWitnessSize, + ) + csvCount++ + + // An HTLC on the commitment transaction of the remote party, + // that has had its absolute timelock expire. + case lnwallet.HtlcOfferedRemoteTimeout: + weightEstimate.AddWitnessInput( + lnwallet.AcceptedHtlcTimeoutWitnessSize, + ) + cltvCount++ + + // An HTLC on the commitment transaction of the remote party, + // that can be swept with the preimage. + case lnwallet.HtlcAcceptedRemoteSuccess: + weightEstimate.AddWitnessInput( + lnwallet.OfferedHtlcSuccessWitnessSize, + ) + + default: + log.Warnf("kindergarten output in nursery store "+ + "contains unexpected witness type: %v", + input.WitnessType()) + continue + } + } + + txWeight := int64(weightEstimate.Weight()) + + return txWeight, csvCount, cltvCount +} diff --git a/utxonursery.go b/utxonursery.go index 8eed7cb5..8557ad55 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -4,11 +4,11 @@ import ( "bytes" "encoding/binary" "fmt" + "github.com/lightningnetwork/lnd/sweep" "io" "sync" "sync/atomic" - "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -192,14 +192,6 @@ type NurseryConfig struct { FetchClosedChannel func(chanID *wire.OutPoint) ( *channeldb.ChannelCloseSummary, error) - // Estimator is used when crafting sweep transactions to estimate the - // necessary fee relative to the expected size of the sweep transaction. - Estimator lnwallet.FeeEstimator - - // GenSweepScript generates a P2WKH script belonging to the wallet where - // funds can be swept. - GenSweepScript func() ([]byte, error) - // Notifier provides the utxo nursery the ability to subscribe to // transaction confirmation events, which advance outputs through their // persistence state transitions. @@ -209,13 +201,13 @@ type NurseryConfig struct { // transaction to the appropriate network. PublishTransaction func(*wire.MsgTx) error - // Signer is used by the utxo nursery to generate valid witnesses at the - // time the incubated outputs need to be spent. - Signer lnwallet.Signer - // Store provides access to and modification of the persistent state // maintained about the utxo nursery's incubating outputs. Store NurseryStore + + // Sweeper provides functionality to generate sweep transactions. + // Nursery uses this to sweep final outputs back into the wallet. + Sweeper *sweep.UtxoSweeper } // utxoNursery is a system dedicated to incubating time-locked outputs created @@ -879,7 +871,15 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { // generated a sweep txn for this height. Generate one if there // are kindergarten outputs or cltv crib outputs to be spent. if len(kgtnOutputs) > 0 { - finalTx, err = u.createSweepTx(kgtnOutputs, classHeight) + sweepInputs := make([]sweep.Input, + len(kgtnOutputs)) + for i := range kgtnOutputs { + sweepInputs[i] = &kgtnOutputs[i] + } + + finalTx, err = u.cfg.Sweeper.CreateSweepTx( + sweepInputs, classHeight) + if err != nil { utxnLog.Errorf("Failed to create sweep txn at "+ "height=%d", classHeight) @@ -935,203 +935,6 @@ func (u *utxoNursery) graduateClass(classHeight uint32) error { return u.cfg.Store.GraduateHeight(classHeight) } -// craftSweepTx accepts a list of kindergarten outputs, and baby -// outputs which don't require a second-layer claim, and signs and generates a -// signed txn that spends from them. This method also makes an accurate fee -// estimate before generating the required witnesses. -func (u *utxoNursery) createSweepTx(kgtnOutputs []kidOutput, - classHeight uint32) (*wire.MsgTx, error) { - - // Create a transaction which sweeps all the newly mature outputs into - // an output controlled by the wallet. - - // TODO(roasbeef): can be more intelligent about buffering outputs to - // be more efficient on-chain. - - // Assemble the kindergarten class into a slice csv spendable outputs, - // and also a set of regular spendable outputs. The set of regular - // outputs are CLTV locked outputs that have had their timelocks - // expire. - var ( - csvOutputs []CsvSpendableOutput - cltvOutputs []SpendableOutput - weightEstimate lnwallet.TxWeightEstimator - ) - - // Allocate enough room for both types of kindergarten outputs. - csvOutputs = make([]CsvSpendableOutput, 0, len(kgtnOutputs)) - cltvOutputs = make([]SpendableOutput, 0, len(kgtnOutputs)) - - // Our sweep transaction will pay to a single segwit p2wkh address, - // ensure it contributes to our weight estimate. - weightEstimate.AddP2WKHOutput() - - // For each kindergarten output, use its witness type to determine the - // estimate weight of its witness, and add it to the proper set of - // spendable outputs. - for i := range kgtnOutputs { - input := &kgtnOutputs[i] - - switch input.WitnessType() { - - // Outputs on a past commitment transaction that pay directly - // to us. - case lnwallet.CommitmentTimeLock: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - csvOutputs = append(csvOutputs, input) - - // Outgoing second layer HTLC's that have confirmed within the - // chain, and the output they produced is now mature enough to - // sweep. - case lnwallet.HtlcOfferedTimeoutSecondLevel: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - csvOutputs = append(csvOutputs, input) - - // Incoming second layer HTLC's that have confirmed within the - // chain, and the output they produced is now mature enough to - // sweep. - case lnwallet.HtlcAcceptedSuccessSecondLevel: - weightEstimate.AddWitnessInput( - lnwallet.ToLocalTimeoutWitnessSize, - ) - csvOutputs = append(csvOutputs, input) - - // An HTLC on the commitment transaction of the remote party, - // that has had its absolute timelock expire. - case lnwallet.HtlcOfferedRemoteTimeout: - weightEstimate.AddWitnessInput( - lnwallet.AcceptedHtlcTimeoutWitnessSize, - ) - cltvOutputs = append(cltvOutputs, input) - - default: - utxnLog.Warnf("kindergarten output in nursery store "+ - "contains unexpected witness type: %v", - input.WitnessType()) - continue - } - } - - utxnLog.Infof("Creating sweep transaction for %v CSV inputs, %v CLTV "+ - "inputs", len(csvOutputs), len(cltvOutputs)) - - txWeight := int64(weightEstimate.Weight()) - return u.populateSweepTx(txWeight, classHeight, csvOutputs, cltvOutputs) -} - -// populateSweepTx populate the final sweeping transaction with all witnesses -// in place for all inputs using the provided txn fee. The created transaction -// has a single output sending all the funds back to the source wallet, after -// accounting for the fee estimate. -func (u *utxoNursery) populateSweepTx(txWeight int64, classHeight uint32, - csvInputs []CsvSpendableOutput, - cltvInputs []SpendableOutput) (*wire.MsgTx, error) { - - // Generate the receiving script to which the funds will be swept. - pkScript, err := u.cfg.GenSweepScript() - if err != nil { - return nil, err - } - - // Sum up the total value contained in the inputs. - var totalSum btcutil.Amount - for _, o := range csvInputs { - totalSum += o.Amount() - } - for _, o := range cltvInputs { - totalSum += o.Amount() - } - - // Using the txn weight estimate, compute the required txn fee. - feePerKw, err := u.cfg.Estimator.EstimateFeePerKW(6) - if err != nil { - return nil, err - } - txFee := feePerKw.FeeForWeight(txWeight) - - // Sweep as much possible, after subtracting txn fees. - sweepAmt := int64(totalSum - txFee) - - // Create the sweep transaction that we will be building. We use - // version 2 as it is required for CSV. The txn will sweep the amount - // after fees to the pkscript generated above. - sweepTx := wire.NewMsgTx(2) - sweepTx.AddTxOut(&wire.TxOut{ - PkScript: pkScript, - Value: sweepAmt, - }) - - // We'll also ensure that the transaction has the required lock time if - // we're sweeping any cltvInputs. - if len(cltvInputs) > 0 { - sweepTx.LockTime = classHeight - } - - // Add all inputs to the sweep transaction. Ensure that for each - // csvInput, we set the sequence number properly. - for _, input := range csvInputs { - sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: *input.OutPoint(), - Sequence: input.BlocksToMaturity(), - }) - } - for _, input := range cltvInputs { - sweepTx.AddTxIn(&wire.TxIn{ - PreviousOutPoint: *input.OutPoint(), - }) - } - - // Before signing the transaction, check to ensure that it meets some - // basic validity requirements. - // TODO(conner): add more control to sanity checks, allowing us to delay - // spending "problem" outputs, e.g. possibly batching with other classes - // if fees are too low. - btx := btcutil.NewTx(sweepTx) - if err := blockchain.CheckTransactionSanity(btx); err != nil { - return nil, err - } - - hashCache := txscript.NewTxSigHashes(sweepTx) - - // With all the inputs in place, use each output's unique witness - // function to generate the final witness required for spending. - addWitness := func(idx int, tso SpendableOutput) error { - witness, err := tso.BuildWitness( - u.cfg.Signer, sweepTx, hashCache, idx, - ) - if err != nil { - return err - } - - sweepTx.TxIn[idx].Witness = witness - - return nil - } - - // Finally we'll attach a valid witness to each csv and cltv input - // within the sweeping transaction. - for i, input := range csvInputs { - if err := addWitness(i, input); err != nil { - return nil, err - } - } - - // Add offset to relative indexes so cltv witnesses don't overwrite csv - // witnesses. - offset := len(csvInputs) - for i, input := range cltvInputs { - if err := addWitness(offset+i, input); err != nil { - return nil, err - } - } - - return sweepTx, nil -} - // sweepMatureOutputs generates and broadcasts the transaction that transfers // control of funds from a prior channel commitment transaction to the user's // wallet. The outputs swept were previously time locked (either absolute or @@ -1635,29 +1438,6 @@ func newSweepPkScript(wallet lnwallet.WalletController) ([]byte, error) { return txscript.PayToAddrScript(sweepAddr) } -// CsvSpendableOutput is a SpendableOutput that contains all of the information -// necessary to construct, sign, and sweep an output locked with a CSV delay. -type CsvSpendableOutput interface { - SpendableOutput - - // ConfHeight returns the height at which this output was confirmed. - // A zero value indicates that the output has not been confirmed. - ConfHeight() uint32 - - // SetConfHeight marks the height at which the output is confirmed in - // the chain. - SetConfHeight(height uint32) - - // BlocksToMaturity returns the relative timelock, as a number of - // blocks, that must be built on top of the confirmation height before - // the output can be spent. - BlocksToMaturity() uint32 - - // OriginChanPoint returns the outpoint of the channel from which this - // output is derived. - OriginChanPoint() *wire.OutPoint -} - // babyOutput represents a two-stage CSV locked output, and is used to track // htlc outputs through incubation. The first stage requires broadcasting a // presigned timeout txn that spends from the CLTV locked output on the @@ -1969,7 +1749,7 @@ func readTxOut(r io.Reader, txo *wire.TxOut) error { return nil } -// Compile-time constraint to ensure kidOutput and babyOutput implement the -// CsvSpendableOutput interface. -var _ CsvSpendableOutput = (*kidOutput)(nil) -var _ CsvSpendableOutput = (*babyOutput)(nil) +// Compile-time constraint to ensure kidOutput implements the +// Input interface. + +var _ sweep.Input = (*kidOutput)(nil) diff --git a/utxonursery_test.go b/utxonursery_test.go index 0d5668a7..a7249495 100644 --- a/utxonursery_test.go +++ b/utxonursery_test.go @@ -6,6 +6,7 @@ import ( "bytes" "fmt" "github.com/lightningnetwork/lnd/channeldb" + "github.com/lightningnetwork/lnd/sweep" "io/ioutil" "math" "reflect" @@ -431,6 +432,14 @@ func createNurseryTestContext(t *testing.T, notifier := newNurseryMockNotifier(t) + sweeper := sweep.New(&sweep.UtxoSweeperConfig{ + GenSweepScript: func() ([]byte, error) { + return []byte{}, nil + }, + Estimator: &mockFeeEstimator{}, + Signer: &nurseryMockSigner{}, + }) + cfg := NurseryConfig{ Notifier: notifier, FetchClosedChannels: func(pendingOnly bool) ( @@ -445,11 +454,7 @@ func createNurseryTestContext(t *testing.T, }, Store: storeIntercepter, ChainIO: &mockChainIO{}, - GenSweepScript: func() ([]byte, error) { - return []byte{}, nil - }, - Estimator: &mockFeeEstimator{}, - Signer: &nurseryMockSigner{}, + Sweeper: sweeper, } publishChan := make(chan wire.MsgTx, 1)