From 185ba77f8e05c497ab3edec742811a16bfc981b7 Mon Sep 17 00:00:00 2001 From: "Johan T. Halseth" Date: Thu, 14 Jan 2021 11:49:27 +0100 Subject: [PATCH] sweep: allow specified outputs to sweep tx We'll use this to attach an output for the value reserved for anchor commitments fee bumping if the user requests a send_all transaction. --- rpcserver.go | 2 +- sweep/sweeper.go | 6 ++--- sweep/sweeper_test.go | 2 +- sweep/txgenerator.go | 48 +++++++++++++++++++++++++++------------ sweep/txgenerator_test.go | 2 +- sweep/walletsweep.go | 43 ++++++++++++++++++++++++++++------- sweep/walletsweep_test.go | 10 ++++---- 7 files changed, 80 insertions(+), 33 deletions(-) diff --git a/rpcserver.go b/rpcserver.go index 1d0c707c..19072226 100644 --- a/rpcserver.go +++ b/rpcserver.go @@ -1256,7 +1256,7 @@ func (r *rpcServer) SendCoins(ctx context.Context, // safe manner, so no need to worry about locking. sweepTxPkg, err := sweep.CraftSweepAllTx( feePerKw, lnwallet.DefaultDustLimit(), - uint32(bestHeight), targetAddr, wallet, + uint32(bestHeight), nil, targetAddr, wallet, wallet.WalletController, wallet.WalletController, r.server.cc.FeeEstimator, r.server.cc.Signer, ) diff --git a/sweep/sweeper.go b/sweep/sweeper.go index b7f20274..5869e456 100644 --- a/sweep/sweeper.go +++ b/sweep/sweeper.go @@ -1192,8 +1192,8 @@ func (s *UtxoSweeper) sweep(inputs inputSet, feeRate chainfee.SatPerKWeight, // Create sweep tx. tx, err := createSweepTx( - inputs, s.currentOutputScript, uint32(currentHeight), feeRate, - dustLimit(s.relayFeeRate), s.cfg.Signer, + inputs, nil, s.currentOutputScript, uint32(currentHeight), + feeRate, dustLimit(s.relayFeeRate), s.cfg.Signer, ) if err != nil { return fmt.Errorf("create sweep tx: %v", err) @@ -1487,7 +1487,7 @@ func (s *UtxoSweeper) CreateSweepTx(inputs []input.Input, feePref FeePreference, } return createSweepTx( - inputs, pkScript, currentBlockHeight, feePerKw, + inputs, nil, pkScript, currentBlockHeight, feePerKw, dustLimit(s.relayFeeRate), s.cfg.Signer, ) } diff --git a/sweep/sweeper_test.go b/sweep/sweeper_test.go index 10cbd05a..2851a643 100644 --- a/sweep/sweeper_test.go +++ b/sweep/sweeper_test.go @@ -354,7 +354,7 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx, outputAmt := tx.TxOut[0].Value fee := btcutil.Amount(inputAmt - outputAmt) - _, estimator := getWeightEstimate(inputs, 0) + _, estimator := getWeightEstimate(inputs, nil, 0) txWeight := estimator.weight() expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight)) diff --git a/sweep/txgenerator.go b/sweep/txgenerator.go index 0afe04bf..10b3c7ee 100644 --- a/sweep/txgenerator.go +++ b/sweep/txgenerator.go @@ -131,12 +131,14 @@ func generateInputPartitionings(sweepableInputs []txInput, return sets, nil } -// createSweepTx builds a signed tx spending the inputs to a the output script. -func createSweepTx(inputs []input.Input, outputPkScript []byte, - currentBlockHeight uint32, feePerKw chainfee.SatPerKWeight, - dustLimit btcutil.Amount, signer input.Signer) (*wire.MsgTx, error) { +// createSweepTx builds a signed tx spending the inputs to the given outputs, +// sending any leftover change to the change script. +func createSweepTx(inputs []input.Input, outputs []*wire.TxOut, + changePkScript []byte, currentBlockHeight uint32, + feePerKw chainfee.SatPerKWeight, dustLimit btcutil.Amount, + signer input.Signer) (*wire.MsgTx, error) { - inputs, estimator := getWeightEstimate(inputs, feePerKw) + inputs, estimator := getWeightEstimate(inputs, outputs, feePerKw) txFee := estimator.fee() var ( @@ -210,6 +212,16 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte, totalInput += btcutil.Amount(o.SignDesc().Output.Value) } + // Add the outputs given, if any. + for _, o := range outputs { + sweepTx.AddTxOut(o) + requiredOutput += btcutil.Amount(o.Value) + } + + if requiredOutput+txFee > totalInput { + return nil, fmt.Errorf("insufficient input to create sweep tx") + } + // The value remaining after the required output and fees, go to // change. Not that this fee is what we would have to pay in case the // sweep tx has a change output. @@ -219,7 +231,7 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte, // above. if changeAmt >= dustLimit { sweepTx.AddTxOut(&wire.TxOut{ - PkScript: outputPkScript, + PkScript: changePkScript, Value: int64(changeAmt), }) } else { @@ -287,8 +299,8 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte, // getWeightEstimate returns a weight estimate for the given inputs. // Additionally, it returns counts for the number of csv and cltv inputs. -func getWeightEstimate(inputs []input.Input, feeRate chainfee.SatPerKWeight) ( - []input.Input, *weightEstimator) { +func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut, + feeRate chainfee.SatPerKWeight) ([]input.Input, *weightEstimator) { // We initialize a weight estimator so we can accurately asses the // amount of fees we need to pay for this sweep transaction. @@ -297,13 +309,19 @@ func getWeightEstimate(inputs []input.Input, feeRate chainfee.SatPerKWeight) ( // be more efficient on-chain. weightEstimate := newWeightEstimator(feeRate) - // Our sweep transaction will pay to a single segwit p2wkh address, - // ensure it contributes to our weight estimate. If the inputs we add - // have required TxOuts, then this will be our change address. Note - // that if we have required TxOuts, we might end up creating a sweep tx - // without a change output. It is okay to add the change output to the - // weight estimate regardless, since the estimated fee will just be - // subtracted from this already dust output, and trimmed. + // Our sweep transaction will always pay to the given set of outputs. + for _, o := range outputs { + weightEstimate.addOutput(o) + } + + // If there is any leftover change after paying to the given outputs + // and required outputs, it will go to a single segwit p2wkh address. + // This will be our change address, so ensure it contributes to our + // weight estimate. Note that if we have other outputs, we might end up + // creating a sweep tx without a change output. It is okay to add the + // change output to the weight estimate regardless, since the estimated + // fee will just be subtracted from this already dust output, and + // trimmed. weightEstimate.addP2WKHOutput() // For each output, use its witness type to determine the estimate diff --git a/sweep/txgenerator_test.go b/sweep/txgenerator_test.go index 377ac24f..0543543d 100644 --- a/sweep/txgenerator_test.go +++ b/sweep/txgenerator_test.go @@ -39,7 +39,7 @@ func TestWeightEstimate(t *testing.T) { )) } - _, estimator := getWeightEstimate(inputs, 0) + _, estimator := getWeightEstimate(inputs, nil, 0) weight := int64(estimator.weight()) if weight != expectedWeight { t.Fatalf("unexpected weight. expected %d but got %d.", diff --git a/sweep/walletsweep.go b/sweep/walletsweep.go index 06c41e5a..920f6cab 100644 --- a/sweep/walletsweep.go +++ b/sweep/walletsweep.go @@ -148,13 +148,24 @@ type WalletSweepPackage struct { CancelSweepAttempt func() } +// DeliveryAddr is a pair of (address, amount) used to craft a transaction +// paying to more than one specified address. +type DeliveryAddr struct { + // Addr is the address to pay to. + Addr btcutil.Address + + // Amt is the amount to pay to the given address. + Amt btcutil.Amount +} + // CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the -// caller to sweep ALL outputs within the wallet to a single UTXO, as specified -// by the delivery address. The sweep transaction will be crafted with the -// target fee rate, and will use the utxoSource and outpointLocker as sources -// for wallet funds. +// caller to sweep ALL outputs within the wallet to a list of outputs. Any +// leftover amount after these outputs and transaction fee, is sent to a single +// output, as specified by the change address. The sweep transaction will be +// crafted with the target fee rate, and will use the utxoSource and +// outpointLocker as sources for wallet funds. func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount, - blockHeight uint32, deliveryAddr btcutil.Address, + blockHeight uint32, deliveryAddrs []DeliveryAddr, changeAddr btcutil.Address, coinSelectLocker CoinSelectionLocker, utxoSource UtxoSource, outpointLocker OutpointLocker, feeEstimator chainfee.Estimator, signer input.Signer) (*WalletSweepPackage, error) { @@ -261,9 +272,25 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount, inputsToSweep = append(inputsToSweep, &input) } - // Next, we'll convert the delivery addr to a pkScript that we can use + // Create a list of TxOuts from the given delivery addresses. + var txOuts []*wire.TxOut + for _, d := range deliveryAddrs { + pkScript, err := txscript.PayToAddrScript(d.Addr) + if err != nil { + unlockOutputs() + + return nil, err + } + + txOuts = append(txOuts, &wire.TxOut{ + PkScript: pkScript, + Value: int64(d.Amt), + }) + } + + // Next, we'll convert the change addr to a pkScript that we can use // to create the sweep transaction. - deliveryPkScript, err := txscript.PayToAddrScript(deliveryAddr) + changePkScript, err := txscript.PayToAddrScript(changeAddr) if err != nil { unlockOutputs() @@ -273,7 +300,7 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount, // Finally, we'll ask the sweeper to craft a sweep transaction which // respects our fee preference and targets all the UTXOs of the wallet. sweepTx, err := createSweepTx( - inputsToSweep, deliveryPkScript, blockHeight, feeRate, + inputsToSweep, txOuts, changePkScript, blockHeight, feeRate, dustLimit, signer, ) if err != nil { diff --git a/sweep/walletsweep_test.go b/sweep/walletsweep_test.go index 032f30c0..dbdab3d1 100644 --- a/sweep/walletsweep_test.go +++ b/sweep/walletsweep_test.go @@ -288,7 +288,8 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) { utxoLocker := newMockOutpointLocker() _, err := CraftSweepAllTx( - 0, 100, 10, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil, + 0, 100, 10, nil, nil, coinSelectLocker, utxoSource, + utxoLocker, nil, nil, ) // Since we instructed the coin select locker to fail above, we should @@ -313,7 +314,8 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) { utxoLocker := newMockOutpointLocker() _, err := CraftSweepAllTx( - 0, 100, 10, nil, coinSelectLocker, utxoSource, utxoLocker, nil, nil, + 0, 100, 10, nil, nil, coinSelectLocker, utxoSource, + utxoLocker, nil, nil, ) // Since passed in a p2wsh output, which is unknown, we should fail to @@ -347,8 +349,8 @@ func TestCraftSweepAllTx(t *testing.T) { utxoLocker := newMockOutpointLocker() sweepPkg, err := CraftSweepAllTx( - 0, 100, 10, deliveryAddr, coinSelectLocker, utxoSource, utxoLocker, - feeEstimator, signer, + 0, 100, 10, nil, deliveryAddr, coinSelectLocker, utxoSource, + utxoLocker, feeEstimator, signer, ) if err != nil { t.Fatalf("unable to make sweep tx: %v", err)