From a05962e03e9f82f7d6c8d6ce60a72b58b46b57bc Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Fri, 29 Dec 2023 10:22:43 +0100 Subject: [PATCH] multi: standardize sweep/change addr support --- cmd/chantools/closepoolaccount.go | 24 ++++++----- cmd/chantools/doublespendinputs.go | 64 +++++++++++----------------- cmd/chantools/pullanchor.go | 29 +++++++------ cmd/chantools/recoverloopin.go | 27 +++--------- cmd/chantools/rescuefunding.go | 33 +++++++------- cmd/chantools/sweepremoteclosed.go | 16 +++---- cmd/chantools/sweeptimelock.go | 29 +++++++------ cmd/chantools/sweeptimelockmanual.go | 35 ++++++++------- lnd/hdkeychain.go | 62 +++++++++++++++++++++++++++ 9 files changed, 184 insertions(+), 135 deletions(-) diff --git a/cmd/chantools/closepoolaccount.go b/cmd/chantools/closepoolaccount.go index 0503539..6307363 100644 --- a/cmd/chantools/closepoolaccount.go +++ b/cmd/chantools/closepoolaccount.go @@ -166,11 +166,21 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string, sweepAddr string, publish bool, feeRate uint32, minExpiry, maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error { - signer := &lnd.Signer{ - ExtendedKey: extendedKey, - ChainParams: chainParams, + var ( + estimator input.TxWeightEstimator + signer = &lnd.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + api = newExplorerAPI(apiURL) + ) + + sweepScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) + if err != nil { + return err } - api := newExplorerAPI(apiURL) tx, err := api.Transaction(outpoint.Hash.String()) if err != nil { @@ -246,7 +256,6 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string, // Calculate the fee based on the given fee rate and our weight // estimation. var ( - estimator input.TxWeightEstimator prevOutFetcher = txscript.NewCannedPrevOutputFetcher( pkScript, sweepValue, ) @@ -282,15 +291,10 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string, signDesc.HashType = txscript.SigHashDefault signDesc.SignMethod = input.TaprootScriptSpendSignMethod } - estimator.AddP2WKHOutput() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) // Add our sweep destination output. - sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams) - if err != nil { - return err - } sweepTx.TxOut = []*wire.TxOut{{ Value: sweepValue - int64(totalFee), PkScript: sweepScript, diff --git a/cmd/chantools/doublespendinputs.go b/cmd/chantools/doublespendinputs.go index c36c5a6..440283c 100644 --- a/cmd/chantools/doublespendinputs.go +++ b/cmd/chantools/doublespendinputs.go @@ -105,8 +105,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints)) // Get the addresses for the inputs. - for _, input := range c.InputOutpoints { - addrString, err := api.Address(input) + for _, inputOutpoint := range c.InputOutpoints { + addrString, err := api.Address(inputOutpoint) if err != nil { return err } @@ -118,12 +118,12 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { addresses = append(addresses, addr) - txHash, err := chainhash.NewHashFromStr(input[:64]) + txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64]) if err != nil { return err } - vout, err := strconv.Atoi(input[65:]) + vout, err := strconv.Atoi(inputOutpoint[65:]) if err != nil { return err } @@ -144,7 +144,13 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { } // Start with the txweight estimator. - estimator := input.TxWeightEstimator{} + var estimator input.TxWeightEstimator + sweepScript, err := lnd.PrepareWalletAddress( + c.SweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) + if err != nil { + return err + } // Find the key for the given addresses and add their // output weight to the tx estimator. @@ -169,7 +175,9 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { return err } - estimator.AddTaprootKeySpendInput(txscript.SigHashDefault) + estimator.AddTaprootKeySpendInput( + txscript.SigHashDefault, + ) default: return fmt.Errorf("address type %T not supported", addr) @@ -189,47 +197,32 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { // Next get the full value of the inputs. var totalInput btcutil.Amount - for _, input := range outpoints { + for _, outpoint := range outpoints { // Get the transaction. - tx, err := api.Transaction(input.Hash.String()) + tx, err := api.Transaction(outpoint.Hash.String()) if err != nil { return err } - value := tx.Vout[input.Index].Value + value := tx.Vout[outpoint.Index].Value // Get the output index. totalInput += btcutil.Amount(value) - scriptPubkey, err := hex.DecodeString(tx.Vout[input.Index].ScriptPubkey) + scriptPubkey, err := hex.DecodeString( + tx.Vout[outpoint.Index].ScriptPubkey, + ) if err != nil { return err } // Add the output to the map. - prevOuts[*input] = &wire.TxOut{ + prevOuts[*outpoint] = &wire.TxOut{ Value: int64(value), PkScript: scriptPubkey, } } - // Calculate the fee. - sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams) - if err != nil { - return err - } - - switch sweepAddr.(type) { - case *btcutil.AddressWitnessPubKeyHash: - estimator.AddP2WKHOutput() - - case *btcutil.AddressTaproot: - estimator.AddP2TROutput() - - default: - return fmt.Errorf("address type %T not supported", sweepAddr) - } - // Calculate the fee. feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight() totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) @@ -238,14 +231,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { tx := wire.NewMsgTx(2) // Add the inputs. - for _, input := range outpoints { - tx.AddTxIn(wire.NewTxIn(input, nil, nil)) - } - - // Add the output. - sweepScript, err := txscript.PayToAddrScript(sweepAddr) - if err != nil { - return err + for _, outpoint := range outpoints { + tx.AddTxIn(wire.NewTxIn(outpoint, nil, nil)) } tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript)) @@ -285,7 +272,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { } default: - return fmt.Errorf("address type %T not supported", addresses[i]) + return fmt.Errorf("address type %T not supported", + addresses[i]) } } @@ -296,7 +284,7 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error { } // Print the transaction. - fmt.Printf("Sweeping transaction:\n%s\n", hex.EncodeToString(txBuf.Bytes())) + fmt.Printf("Sweeping transaction:\n%x\n", txBuf.Bytes()) // Publish the transaction. if c.Publish { diff --git a/cmd/chantools/pullanchor.go b/cmd/chantools/pullanchor.go index 2fa32da..46b8f55 100644 --- a/cmd/chantools/pullanchor.go +++ b/cmd/chantools/pullanchor.go @@ -115,17 +115,12 @@ func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error { err) } - changeScript, err := lnd.GetP2WPKHScript(c.ChangeAddr, chainParams) - if err != nil { - return fmt.Errorf("error parsing change addr: %w", err) - } - // Set default values. if c.FeeRate == 0 { c.FeeRate = defaultFeeSatPerVByte } return createPullTransactionTemplate( - extendedKey, c.APIURL, outpoint, c.AnchorAddrs, changeScript, + extendedKey, c.APIURL, outpoint, c.AnchorAddrs, c.ChangeAddr, c.FeeRate, ) } @@ -141,14 +136,23 @@ type targetAnchor struct { func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey, apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string, - changeScript []byte, feeRate uint32) error { + changeAddr string, feeRate uint32) error { - signer := &lnd.Signer{ - ExtendedKey: rootKey, - ChainParams: chainParams, + var ( + signer = &lnd.Signer{ + ExtendedKey: rootKey, + ChainParams: chainParams, + } + api = newExplorerAPI(apiURL) + estimator input.TxWeightEstimator + ) + + changeScript, err := lnd.PrepareWalletAddress( + changeAddr, chainParams, &estimator, rootKey, "change", + ) + if err != nil { + return err } - api := newExplorerAPI(apiURL) - estimator := input.TxWeightEstimator{} // Make sure the sponsor input is a P2WPKH or P2TR input and is known // to the block explorer, so we can fetch the witness utxo. @@ -209,7 +213,6 @@ func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey, } // Now we can calculate the fee and add the change output. - estimator.AddP2WKHOutput() anchorAmt := uint64(len(anchorAddrs)) * 330 totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt) feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() diff --git a/cmd/chantools/recoverloopin.go b/cmd/chantools/recoverloopin.go index 18dfbdf..8c372a9 100644 --- a/cmd/chantools/recoverloopin.go +++ b/cmd/chantools/recoverloopin.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "fmt" - "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -165,29 +164,20 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error { } // Get the destination address. - sweepAddr, err := btcutil.DecodeAddress(c.SweepAddr, chainParams) + var estimator input.TxWeightEstimator + sweepScript, err := lnd.PrepareWalletAddress( + c.SweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) if err != nil { return err } // Calculate the sweep fee. - estimator := &input.TxWeightEstimator{} - err = htlc.AddTimeoutToEstimator(estimator) + err = htlc.AddTimeoutToEstimator(&estimator) if err != nil { return err } - switch sweepAddr.(type) { - case *btcutil.AddressWitnessPubKeyHash: - estimator.AddP2WKHOutput() - - case *btcutil.AddressTaproot: - estimator.AddP2TROutput() - - default: - return fmt.Errorf("unsupported address type") - } - feeRateKWeight := chainfee.SatPerKVByte( 1000 * c.FeeRate, ).FeePerKWeight() @@ -216,13 +206,8 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error { }) // Add output for the destination address. - sweepPkScript, err := txscript.PayToAddrScript(sweepAddr) - if err != nil { - return err - } - sweepTx.AddTxOut(&wire.TxOut{ - PkScript: sweepPkScript, + PkScript: sweepScript, Value: int64(loopIn.Contract.AmountRequested) - int64(fee), }) diff --git a/cmd/chantools/rescuefunding.go b/cmd/chantools/rescuefunding.go index a5e92cb..3fbea6e 100644 --- a/cmd/chantools/rescuefunding.go +++ b/cmd/chantools/rescuefunding.go @@ -236,35 +236,38 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error { return err } - // Make sure the sweep addr is a P2WKH address so we can do accurate - // fee estimation. - sweepScript, err := lnd.GetP2WPKHScript(c.SweepAddr, chainParams) - if err != nil { - return fmt.Errorf("error parsing sweep addr: %w", err) - } - return rescueFunding( - localKeyDesc, remotePubKey, signer, chainOp, - sweepScript, btcutil.Amount(c.FeeRate), c.APIURL, + localKeyDesc, remotePubKey, signer, chainOp, c.SweepAddr, + btcutil.Amount(c.FeeRate), c.APIURL, ) } func rescueFunding(localKeyDesc *keychain.KeyDescriptor, remoteKey *btcec.PublicKey, signer *lnd.Signer, - chainPoint *wire.OutPoint, sweepPKScript []byte, feeRate btcutil.Amount, + chainPoint *wire.OutPoint, sweepAddr string, feeRate btcutil.Amount, apiURL string) error { + var ( + estimator input.TxWeightEstimator + api = newExplorerAPI(apiURL) + ) + sweepScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, &estimator, signer.ExtendedKey, "sweep", + ) + if err != nil { + return err + } + // Prepare the wire part of the PSBT. txIn := &wire.TxIn{ PreviousOutPoint: *chainPoint, Sequence: 0, } txOut := &wire.TxOut{ - PkScript: sweepPKScript, + PkScript: sweepScript, } // Locate the output in the funding TX. - api := newExplorerAPI(apiURL) tx, err := api.Transaction(chainPoint.Hash.String()) if err != nil { return fmt.Errorf("error fetching UTXO info for outpoint %s: "+ @@ -303,17 +306,15 @@ func rescueFunding(localKeyDesc *keychain.KeyDescriptor, WitnessScript: witnessScript, Unknowns: []*psbt.Unknown{{ // We add the public key the other party needs to sign - // with as a proprietary field so we can easily read it + // with as a proprietary field, so we can easily read it // out with the signrescuefunding command. Key: PsbtKeyTypeOutputMissingSigPubkey, Value: remoteKey.SerializeCompressed(), }}, } - // Estimate the transaction weight so we can do the fee estimation. - var estimator input.TxWeightEstimator + // Estimate the transaction weight, so we can do the fee estimation. estimator.AddWitnessInput(MultiSigWitnessSize) - estimator.AddP2WKHOutput() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) txOut.Value = utxo.Value - int64(totalFee) diff --git a/cmd/chantools/sweepremoteclosed.go b/cmd/chantools/sweepremoteclosed.go index f8cd6ef..bc1dddc 100644 --- a/cmd/chantools/sweepremoteclosed.go +++ b/cmd/chantools/sweepremoteclosed.go @@ -133,6 +133,14 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL, sweepAddr string, recoveryWindow uint32, feeRate uint32, publish bool) error { + var estimator input.TxWeightEstimator + sweepScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) + if err != nil { + return err + } + var ( targets []*targetAddr api = newExplorerAPI(apiURL) @@ -177,7 +185,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL, // Create estimator and transaction template. var ( - estimator input.TxWeightEstimator signDescs []*input.SignDescriptor sweepTx = wire.NewMsgTx(2) totalOutputValue = uint64(0) @@ -292,13 +299,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL, len(targets), totalOutputValue, sweepDustLimit) } - // Add our sweep destination output. - sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams) - if err != nil { - return err - } - estimator.AddP2WKHOutput() - // Calculate the fee based on the given fee rate and our weight // estimation. feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() diff --git a/cmd/chantools/sweeptimelock.go b/cmd/chantools/sweeptimelock.go index d6571ef..5650d70 100644 --- a/cmd/chantools/sweeptimelock.go +++ b/cmd/chantools/sweeptimelock.go @@ -221,18 +221,26 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string, publish bool, feeRate uint32) error { // Create signer and transaction template. - signer := &lnd.Signer{ - ExtendedKey: extendedKey, - ChainParams: chainParams, + var ( + estimator input.TxWeightEstimator + signer = &lnd.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + api = newExplorerAPI(apiURL) + ) + sweepScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) + if err != nil { + return err } - api := newExplorerAPI(apiURL) var ( sweepTx = wire.NewMsgTx(2) totalOutputValue = int64(0) signDescs = make([]*input.SignDescriptor, 0) prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil) - estimator input.TxWeightEstimator ) for _, target := range targets { // We can't rely on the CSV delay of the channel DB to be @@ -247,8 +255,8 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string, ), target.lockScript, 0, maxCsvTimeout, ) if err != nil { - log.Errorf("Could not create matching script for %s "+ - "or csv too high: %w", target.channelPoint, err) + log.Errorf("could not create matching script for %s "+ + "or csv too high: %v", target.channelPoint, err) continue } @@ -288,13 +296,6 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string, estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) } - // Add our sweep destination output. - sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams) - if err != nil { - return err - } - estimator.AddP2WKHOutput() - // Calculate the fee based on the given fee rate and our weight // estimation. feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() diff --git a/cmd/chantools/sweeptimelockmanual.go b/cmd/chantools/sweeptimelockmanual.go index 7bdf748..25ee652 100644 --- a/cmd/chantools/sweeptimelockmanual.go +++ b/cmd/chantools/sweeptimelockmanual.go @@ -248,12 +248,30 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, maxCsvTimeout, startNumChannels, maxNumChannels, maxNumChanUpdates) + // Create signer and transaction template. + var ( + estimator input.TxWeightEstimator + signer = &lnd.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } + api = newExplorerAPI(apiURL) + ) + // First of all, we need to parse the lock addr and make sure we can // brute force the script with the information we have. If not, we can't // continue anyway. - lockScript, err := lnd.GetP2WSHScript(timeLockAddr, chainParams) + lockScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, nil, extendedKey, "time lock", + ) if err != nil { - return fmt.Errorf("invalid time lock addr: %w", err) + return err + } + sweepScript, err := lnd.PrepareWalletAddress( + sweepAddr, chainParams, &estimator, extendedKey, "sweep", + ) + if err != nil { + return err } // We need to go through a lot of our keys so it makes sense to @@ -303,13 +321,6 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, return fmt.Errorf("target script not derived") } - // Create signer and transaction template. - signer := &lnd.Signer{ - ExtendedKey: extendedKey, - ChainParams: chainParams, - } - api := newExplorerAPI(apiURL) - // We now know everything we need to construct the sweep transaction, // except for what outpoint to sweep. We'll ask the chain API to give // us this information. @@ -339,17 +350,11 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string, // Calculate the fee based on the given fee rate and our weight // estimation. - var estimator input.TxWeightEstimator estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) - estimator.AddP2WKHOutput() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) // Add our sweep destination output. - sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams) - if err != nil { - return err - } sweepTx.TxOut = []*wire.TxOut{{ Value: sweepValue - int64(totalFee), PkScript: sweepScript, diff --git a/lnd/hdkeychain.go b/lnd/hdkeychain.go index f67fea8..51caeb4 100644 --- a/lnd/hdkeychain.go +++ b/lnd/hdkeychain.go @@ -13,6 +13,7 @@ import ( "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcwallet/waddrmgr" "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/shachain" @@ -437,6 +438,67 @@ func CheckAddress(addr string, chainParams *chaincfg.Params, allowDerive bool, return nil } +func PrepareWalletAddress(addr string, chainParams *chaincfg.Params, + estimator *input.TxWeightEstimator, rootKey *hdkeychain.ExtendedKey, + hint string) ([]byte, error) { + + // We already checked if deriving a new address is allowed in a previous + // step, so we can just go ahead and do it now if requested. + if addr == AddressDeriveFromWallet { + // To maximize compatibility and recoverability, we always + // derive the very first P2WKH address from the wallet. + // This corresponds to the derivation path: m/84'/0'/0'/0/0. + derivedKey, err := DeriveChildren(rootKey, []uint32{ + HardenedKeyStart + waddrmgr.KeyScopeBIP0084.Purpose, + HardenedKeyStart + chainParams.HDCoinType, + HardenedKeyStart + 0, 0, 0, + }) + if err != nil { + return nil, err + } + + derivedPubKey, err := derivedKey.ECPubKey() + if err != nil { + return nil, err + } + + p2wkhAddr, err := P2WKHAddr(derivedPubKey, chainParams) + if err != nil { + return nil, err + } + + return txscript.PayToAddrScript(p2wkhAddr) + } + + parsedAddr, err := ParseAddress(addr, chainParams) + if err != nil { + return nil, fmt.Errorf("%s address is invalid: %w", hint, err) + } + + // Exit early if we don't need to estimate the weight. + if estimator == nil { + return txscript.PayToAddrScript(parsedAddr) + } + + // These are the three address types that we support in general. We + // should have checked that we get the correct type in a previous step. + switch parsedAddr.(type) { + case *btcutil.AddressWitnessPubKeyHash: + estimator.AddP2WKHOutput() + + case *btcutil.AddressWitnessScriptHash: + estimator.AddP2WSHOutput() + + case *btcutil.AddressTaproot: + estimator.AddP2TROutput() + + default: + return nil, fmt.Errorf("%s address is of wrong type", hint) + } + + return txscript.PayToAddrScript(parsedAddr) +} + func matchAddrType(addr btcutil.Address, allowedTypes ...AddrType) bool { contains := func(allowedTypes []AddrType, addrType AddrType) bool { for _, allowedType := range allowedTypes {