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.
This commit is contained in:
Johan T. Halseth 2021-01-14 11:49:27 +01:00
parent eaf97418be
commit 185ba77f8e
No known key found for this signature in database
GPG Key ID: 15BAADA29DA20D26
7 changed files with 80 additions and 33 deletions

@ -1256,7 +1256,7 @@ func (r *rpcServer) SendCoins(ctx context.Context,
// safe manner, so no need to worry about locking. // safe manner, so no need to worry about locking.
sweepTxPkg, err := sweep.CraftSweepAllTx( sweepTxPkg, err := sweep.CraftSweepAllTx(
feePerKw, lnwallet.DefaultDustLimit(), feePerKw, lnwallet.DefaultDustLimit(),
uint32(bestHeight), targetAddr, wallet, uint32(bestHeight), nil, targetAddr, wallet,
wallet.WalletController, wallet.WalletController, wallet.WalletController, wallet.WalletController,
r.server.cc.FeeEstimator, r.server.cc.Signer, r.server.cc.FeeEstimator, r.server.cc.Signer,
) )

@ -1192,8 +1192,8 @@ func (s *UtxoSweeper) sweep(inputs inputSet, feeRate chainfee.SatPerKWeight,
// Create sweep tx. // Create sweep tx.
tx, err := createSweepTx( tx, err := createSweepTx(
inputs, s.currentOutputScript, uint32(currentHeight), feeRate, inputs, nil, s.currentOutputScript, uint32(currentHeight),
dustLimit(s.relayFeeRate), s.cfg.Signer, feeRate, dustLimit(s.relayFeeRate), s.cfg.Signer,
) )
if err != nil { if err != nil {
return fmt.Errorf("create sweep tx: %v", err) return fmt.Errorf("create sweep tx: %v", err)
@ -1487,7 +1487,7 @@ func (s *UtxoSweeper) CreateSweepTx(inputs []input.Input, feePref FeePreference,
} }
return createSweepTx( return createSweepTx(
inputs, pkScript, currentBlockHeight, feePerKw, inputs, nil, pkScript, currentBlockHeight, feePerKw,
dustLimit(s.relayFeeRate), s.cfg.Signer, dustLimit(s.relayFeeRate), s.cfg.Signer,
) )
} }

@ -354,7 +354,7 @@ func assertTxFeeRate(t *testing.T, tx *wire.MsgTx,
outputAmt := tx.TxOut[0].Value outputAmt := tx.TxOut[0].Value
fee := btcutil.Amount(inputAmt - outputAmt) fee := btcutil.Amount(inputAmt - outputAmt)
_, estimator := getWeightEstimate(inputs, 0) _, estimator := getWeightEstimate(inputs, nil, 0)
txWeight := estimator.weight() txWeight := estimator.weight()
expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight)) expectedFee := expectedFeeRate.FeeForWeight(int64(txWeight))

@ -131,12 +131,14 @@ func generateInputPartitionings(sweepableInputs []txInput,
return sets, nil return sets, nil
} }
// createSweepTx builds a signed tx spending the inputs to a the output script. // createSweepTx builds a signed tx spending the inputs to the given outputs,
func createSweepTx(inputs []input.Input, outputPkScript []byte, // sending any leftover change to the change script.
currentBlockHeight uint32, feePerKw chainfee.SatPerKWeight, func createSweepTx(inputs []input.Input, outputs []*wire.TxOut,
dustLimit btcutil.Amount, signer input.Signer) (*wire.MsgTx, error) { 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() txFee := estimator.fee()
var ( var (
@ -210,6 +212,16 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
totalInput += btcutil.Amount(o.SignDesc().Output.Value) 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 // 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 // change. Not that this fee is what we would have to pay in case the
// sweep tx has a change output. // sweep tx has a change output.
@ -219,7 +231,7 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
// above. // above.
if changeAmt >= dustLimit { if changeAmt >= dustLimit {
sweepTx.AddTxOut(&wire.TxOut{ sweepTx.AddTxOut(&wire.TxOut{
PkScript: outputPkScript, PkScript: changePkScript,
Value: int64(changeAmt), Value: int64(changeAmt),
}) })
} else { } else {
@ -287,8 +299,8 @@ func createSweepTx(inputs []input.Input, outputPkScript []byte,
// getWeightEstimate returns a weight estimate for the given inputs. // getWeightEstimate returns a weight estimate for the given inputs.
// Additionally, it returns counts for the number of csv and cltv inputs. // Additionally, it returns counts for the number of csv and cltv inputs.
func getWeightEstimate(inputs []input.Input, feeRate chainfee.SatPerKWeight) ( func getWeightEstimate(inputs []input.Input, outputs []*wire.TxOut,
[]input.Input, *weightEstimator) { feeRate chainfee.SatPerKWeight) ([]input.Input, *weightEstimator) {
// We initialize a weight estimator so we can accurately asses the // We initialize a weight estimator so we can accurately asses the
// amount of fees we need to pay for this sweep transaction. // 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. // be more efficient on-chain.
weightEstimate := newWeightEstimator(feeRate) weightEstimate := newWeightEstimator(feeRate)
// Our sweep transaction will pay to a single segwit p2wkh address, // Our sweep transaction will always pay to the given set of outputs.
// ensure it contributes to our weight estimate. If the inputs we add for _, o := range outputs {
// have required TxOuts, then this will be our change address. Note weightEstimate.addOutput(o)
// 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 // If there is any leftover change after paying to the given outputs
// subtracted from this already dust output, and trimmed. // 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() weightEstimate.addP2WKHOutput()
// For each output, use its witness type to determine the estimate // For each output, use its witness type to determine the estimate

@ -39,7 +39,7 @@ func TestWeightEstimate(t *testing.T) {
)) ))
} }
_, estimator := getWeightEstimate(inputs, 0) _, estimator := getWeightEstimate(inputs, nil, 0)
weight := int64(estimator.weight()) weight := int64(estimator.weight())
if weight != expectedWeight { if weight != expectedWeight {
t.Fatalf("unexpected weight. expected %d but got %d.", t.Fatalf("unexpected weight. expected %d but got %d.",

@ -148,13 +148,24 @@ type WalletSweepPackage struct {
CancelSweepAttempt func() 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 // CraftSweepAllTx attempts to craft a WalletSweepPackage which will allow the
// caller to sweep ALL outputs within the wallet to a single UTXO, as specified // caller to sweep ALL outputs within the wallet to a list of outputs. Any
// by the delivery address. The sweep transaction will be crafted with the // leftover amount after these outputs and transaction fee, is sent to a single
// target fee rate, and will use the utxoSource and outpointLocker as sources // output, as specified by the change address. The sweep transaction will be
// for wallet funds. // 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, func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount,
blockHeight uint32, deliveryAddr btcutil.Address, blockHeight uint32, deliveryAddrs []DeliveryAddr, changeAddr btcutil.Address,
coinSelectLocker CoinSelectionLocker, utxoSource UtxoSource, coinSelectLocker CoinSelectionLocker, utxoSource UtxoSource,
outpointLocker OutpointLocker, feeEstimator chainfee.Estimator, outpointLocker OutpointLocker, feeEstimator chainfee.Estimator,
signer input.Signer) (*WalletSweepPackage, error) { signer input.Signer) (*WalletSweepPackage, error) {
@ -261,9 +272,25 @@ func CraftSweepAllTx(feeRate chainfee.SatPerKWeight, dustLimit btcutil.Amount,
inputsToSweep = append(inputsToSweep, &input) 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. // to create the sweep transaction.
deliveryPkScript, err := txscript.PayToAddrScript(deliveryAddr) changePkScript, err := txscript.PayToAddrScript(changeAddr)
if err != nil { if err != nil {
unlockOutputs() 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 // Finally, we'll ask the sweeper to craft a sweep transaction which
// respects our fee preference and targets all the UTXOs of the wallet. // respects our fee preference and targets all the UTXOs of the wallet.
sweepTx, err := createSweepTx( sweepTx, err := createSweepTx(
inputsToSweep, deliveryPkScript, blockHeight, feeRate, inputsToSweep, txOuts, changePkScript, blockHeight, feeRate,
dustLimit, signer, dustLimit, signer,
) )
if err != nil { if err != nil {

@ -288,7 +288,8 @@ func TestCraftSweepAllTxCoinSelectFail(t *testing.T) {
utxoLocker := newMockOutpointLocker() utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx( _, 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 // Since we instructed the coin select locker to fail above, we should
@ -313,7 +314,8 @@ func TestCraftSweepAllTxUnknownWitnessType(t *testing.T) {
utxoLocker := newMockOutpointLocker() utxoLocker := newMockOutpointLocker()
_, err := CraftSweepAllTx( _, 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 // Since passed in a p2wsh output, which is unknown, we should fail to
@ -347,8 +349,8 @@ func TestCraftSweepAllTx(t *testing.T) {
utxoLocker := newMockOutpointLocker() utxoLocker := newMockOutpointLocker()
sweepPkg, err := CraftSweepAllTx( sweepPkg, err := CraftSweepAllTx(
0, 100, 10, deliveryAddr, coinSelectLocker, utxoSource, utxoLocker, 0, 100, 10, nil, deliveryAddr, coinSelectLocker, utxoSource,
feeEstimator, signer, utxoLocker, feeEstimator, signer,
) )
if err != nil { if err != nil {
t.Fatalf("unable to make sweep tx: %v", err) t.Fatalf("unable to make sweep tx: %v", err)