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..a3b52904 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. 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..db39838c --- /dev/null +++ b/sweep/sweeper.go @@ -0,0 +1,249 @@ +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 outputs and signs and generates a txn that +// spends from them. This method also makes an accurate fee estimate before +// generating the required witnesses. +// +// 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 []CsvSpendableOutput, + currentBlockHeight 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 inputs 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 outputs. + csvOutputs = make([]CsvSpendableOutput, 0, len(inputs)) + cltvOutputs = make([]SpendableOutput, 0, len(inputs)) + + // 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. + for i := range inputs { + input := inputs[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: + log.Warnf("kindergarten output in nursery store "+ + "contains unexpected witness type: %v", + input.WitnessType()) + continue + } + } + + log.Infof("Creating sweep transaction for %v CSV inputs, %v CLTV "+ + "inputs", len(csvOutputs), len(cltvOutputs)) + + txWeight := int64(weightEstimate.Weight()) + return s.populateSweepTx(txWeight, currentBlockHeight, 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 (s *UtxoSweeper) populateSweepTx(txWeight int64, currentBlockHeight uint32, + csvInputs []CsvSpendableOutput, + cltvInputs []SpendableOutput) (*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 + } + + // 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 := s.cfg.Estimator.EstimateFeePerKW(s.cfg.ConfTarget) + if err != nil { + return nil, err + } + + log.Debugf("Using %v sat/kw for sweep tx", int64(feePerKw)) + + 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 = currentBlockHeight + } + + // 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( + 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 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 +} diff --git a/utxonursery.go b/utxonursery.go index 37ebdf11..15783bfe 100644 --- a/utxonursery.go +++ b/utxonursery.go @@ -9,7 +9,6 @@ import ( "sync" "sync/atomic" - "github.com/btcsuite/btcd/blockchain" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcutil" @@ -193,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. @@ -210,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 @@ -880,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) + csvSpendableOutputs := make([]sweep.CsvSpendableOutput, + len(kgtnOutputs)) + for i := range kgtnOutputs { + csvSpendableOutputs[i] = &kgtnOutputs[i] + } + + finalTx, err = u.cfg.Sweeper.CreateSweepTx( + csvSpendableOutputs, classHeight) + if err != nil { utxnLog.Errorf("Failed to create sweep txn at "+ "height=%d", classHeight) @@ -936,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 []sweep.CsvSpendableOutput - cltvOutputs []sweep.SpendableOutput - weightEstimate lnwallet.TxWeightEstimator - ) - - // Allocate enough room for both types of kindergarten outputs. - csvOutputs = make([]sweep.CsvSpendableOutput, 0, len(kgtnOutputs)) - cltvOutputs = make([]sweep.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 []sweep.CsvSpendableOutput, - cltvInputs []sweep.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 sweep.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 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)