Merge pull request #107 from lightninglabs/cli-cleanup

Support P2TR as sweep/change address everywhere
pull/114/head v0.12.1
Oliver Gugger 5 months ago committed by GitHub
commit 399a23adba
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -196,13 +196,17 @@ func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
url := fmt.Sprintf("%s/tx", a.BaseURL) url := fmt.Sprintf("%s/tx", a.BaseURL)
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex)) resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("error posting data to API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body := new(bytes.Buffer) body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body) _, err = body.ReadFrom(resp.Body)
if err != nil { if err != nil {
return "", err return "", fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
} }
return body.String(), nil return body.String(), nil
} }
@ -210,20 +214,29 @@ func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
func fetchJSON(url string, target interface{}) error { func fetchJSON(url string, target interface{}) error {
resp, err := http.Get(url) resp, err := http.Get(url)
if err != nil { if err != nil {
return err return fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
} }
defer resp.Body.Close() defer resp.Body.Close()
body := new(bytes.Buffer) body := new(bytes.Buffer)
_, err = body.ReadFrom(resp.Body) _, err = body.ReadFrom(resp.Body)
if err != nil { if err != nil {
return err return fmt.Errorf("error fetching data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
} }
err = json.Unmarshal(body.Bytes(), target) err = json.Unmarshal(body.Bytes(), target)
if err != nil { if err != nil {
if body.String() == "Transaction not found" { if body.String() == "Transaction not found" {
return ErrTxNotFound return ErrTxNotFound
} }
return fmt.Errorf("error decoding data from API '%s', "+
"server might be experiencing temporary issues, try "+
"again later; error details: %w", url, err)
} }
return err
return nil
} }

@ -7,13 +7,12 @@ import (
"github.com/lightninglabs/chantools/dataformat" "github.com/lightninglabs/chantools/dataformat"
) )
func SummarizeChannels(apiURL string, channels []*dataformat.SummaryEntry, func SummarizeChannels(api *ExplorerAPI, channels []*dataformat.SummaryEntry,
log btclog.Logger) (*dataformat.SummaryEntryFile, error) { log btclog.Logger) (*dataformat.SummaryEntryFile, error) {
summaryFile := &dataformat.SummaryEntryFile{ summaryFile := &dataformat.SummaryEntryFile{
Channels: channels, Channels: channels,
} }
api := &ExplorerAPI{BaseURL: apiURL}
for idx, channel := range channels { for idx, channel := range channels {
tx, err := api.Transaction(channel.FundingTXID) tx, err := api.Transaction(channel.FundingTXID)

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/pool/account" "github.com/lightninglabs/pool/account"
"github.com/lightninglabs/pool/poolscript" "github.com/lightninglabs/pool/poolscript"
@ -89,7 +88,9 @@ obtained by running 'pool accounts list' `,
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -125,8 +126,12 @@ func (c *closePoolAccountCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Parse account outpoint and auctioneer key. // Parse account outpoint and auctioneer key.
@ -161,11 +166,21 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
sweepAddr string, publish bool, feeRate uint32, minExpiry, sweepAddr string, publish bool, feeRate uint32, minExpiry,
maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error { maxNumBlocks, maxNumAccounts, maxNumBatchKeys uint32) error {
signer := &lnd.Signer{ var (
ExtendedKey: extendedKey, estimator input.TxWeightEstimator
ChainParams: chainParams, 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 := &btc.ExplorerAPI{BaseURL: apiURL}
tx, err := api.Transaction(outpoint.Hash.String()) tx, err := api.Transaction(outpoint.Hash.String())
if err != nil { if err != nil {
@ -241,7 +256,6 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
var ( var (
estimator input.TxWeightEstimator
prevOutFetcher = txscript.NewCannedPrevOutputFetcher( prevOutFetcher = txscript.NewCannedPrevOutputFetcher(
pkScript, sweepValue, pkScript, sweepValue,
) )
@ -277,15 +291,10 @@ func closePoolAccount(extendedKey *hdkeychain.ExtendedKey, apiURL string,
signDesc.HashType = txscript.SigHashDefault signDesc.HashType = txscript.SigHashDefault
signDesc.SignMethod = input.TaprootScriptSpendSignMethod signDesc.SignMethod = input.TaprootScriptSpendSignMethod
} }
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Add our sweep destination output. // Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{ sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee), Value: sweepValue - int64(totalFee),
PkScript: sweepScript, PkScript: sweepScript,

@ -13,7 +13,6 @@ import (
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/decred/dcrd/dcrec/secp256k1/v4" "github.com/decred/dcrd/dcrec/secp256k1/v4"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -35,16 +34,16 @@ type doubleSpendInputs struct {
func newDoubleSpendInputsCommand() *cobra.Command { func newDoubleSpendInputsCommand() *cobra.Command {
cc := &doubleSpendInputs{} cc := &doubleSpendInputs{}
cc.cmd = &cobra.Command{ cc.cmd = &cobra.Command{
Use: "doublespendinputs", Use: "doublespendinputs",
Short: "Tries to double spend the given inputs by deriving the " + Short: "Replace a transaction by double spending its input",
"private for the address and sweeping the funds to the given " + Long: `Tries to double spend the given inputs by deriving the
"address. This can only be used with inputs that belong to " + private for the address and sweeping the funds to the given address. This can
"an lnd wallet.", only be used with inputs that belong to an lnd wallet.`,
Example: `chantools doublespendinputs \ Example: `chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \ --sweepaddr bc1q..... \
--feerate 10 \ --feerate 10 \
--publish`, --publish`,
RunE: cc.Execute, RunE: cc.Execute,
} }
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
@ -56,7 +55,9 @@ func newDoubleSpendInputsCommand() *cobra.Command {
"list of outpoints to double spend in the format txid:vout", "list of outpoints to double spend in the format txid:vout",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -84,8 +85,12 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Make sure we have at least one input. // Make sure we have at least one input.
@ -93,15 +98,15 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("inputoutpoints are required") return fmt.Errorf("inputoutpoints are required")
} }
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
addresses := make([]btcutil.Address, 0, len(c.InputOutpoints)) addresses := make([]btcutil.Address, 0, len(c.InputOutpoints))
outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints)) outpoints := make([]*wire.OutPoint, 0, len(c.InputOutpoints))
privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints)) privKeys := make([]*secp256k1.PrivateKey, 0, len(c.InputOutpoints))
// Get the addresses for the inputs. // Get the addresses for the inputs.
for _, input := range c.InputOutpoints { for _, inputOutpoint := range c.InputOutpoints {
addrString, err := api.Address(input) addrString, err := api.Address(inputOutpoint)
if err != nil { if err != nil {
return err return err
} }
@ -113,12 +118,12 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
addresses = append(addresses, addr) addresses = append(addresses, addr)
txHash, err := chainhash.NewHashFromStr(input[:64]) txHash, err := chainhash.NewHashFromStr(inputOutpoint[:64])
if err != nil { if err != nil {
return err return err
} }
vout, err := strconv.Atoi(input[65:]) vout, err := strconv.Atoi(inputOutpoint[65:])
if err != nil { if err != nil {
return err return err
} }
@ -139,7 +144,13 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Start with the txweight estimator. // 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 // Find the key for the given addresses and add their
// output weight to the tx estimator. // output weight to the tx estimator.
@ -164,7 +175,9 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
return err return err
} }
estimator.AddTaprootKeySpendInput(txscript.SigHashDefault) estimator.AddTaprootKeySpendInput(
txscript.SigHashDefault,
)
default: default:
return fmt.Errorf("address type %T not supported", addr) return fmt.Errorf("address type %T not supported", addr)
@ -184,47 +197,32 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
// Next get the full value of the inputs. // Next get the full value of the inputs.
var totalInput btcutil.Amount var totalInput btcutil.Amount
for _, input := range outpoints { for _, outpoint := range outpoints {
// Get the transaction. // Get the transaction.
tx, err := api.Transaction(input.Hash.String()) tx, err := api.Transaction(outpoint.Hash.String())
if err != nil { if err != nil {
return err return err
} }
value := tx.Vout[input.Index].Value value := tx.Vout[outpoint.Index].Value
// Get the output index. // Get the output index.
totalInput += btcutil.Amount(value) 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 { if err != nil {
return err return err
} }
// Add the output to the map. // Add the output to the map.
prevOuts[*input] = &wire.TxOut{ prevOuts[*outpoint] = &wire.TxOut{
Value: int64(value), Value: int64(value),
PkScript: scriptPubkey, 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. // Calculate the fee.
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
@ -233,14 +231,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
tx := wire.NewMsgTx(2) tx := wire.NewMsgTx(2)
// Add the inputs. // Add the inputs.
for _, input := range outpoints { for _, outpoint := range outpoints {
tx.AddTxIn(wire.NewTxIn(input, nil, nil)) tx.AddTxIn(wire.NewTxIn(outpoint, nil, nil))
}
// Add the output.
sweepScript, err := txscript.PayToAddrScript(sweepAddr)
if err != nil {
return err
} }
tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript)) tx.AddTxOut(wire.NewTxOut(int64(totalInput-totalFee), sweepScript))
@ -280,7 +272,8 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
default: default:
return fmt.Errorf("address type %T not supported", addresses[i]) return fmt.Errorf("address type %T not supported",
addresses[i])
} }
} }
@ -291,7 +284,7 @@ func (c *doubleSpendInputs) Execute(_ *cobra.Command, _ []string) error {
} }
// Print the transaction. // 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. // Publish the transaction.
if c.Publish { if c.Publish {

@ -11,7 +11,6 @@ import (
"github.com/btcsuite/btcd/btcutil/hdkeychain" "github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat" "github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/channeldb" "github.com/lightningnetwork/lnd/channeldb"
@ -105,7 +104,7 @@ func forceCloseChannels(apiURL string, extendedKey *hdkeychain.ExtendedKey,
if err != nil { if err != nil {
return err return err
} }
api := &btc.ExplorerAPI{BaseURL: apiURL} api := newExplorerAPI(apiURL)
signer := &lnd.Signer{ signer := &lnd.Signer{
ExtendedKey: extendedKey, ExtendedKey: extendedKey,
ChainParams: chainParams, ChainParams: chainParams,

@ -65,7 +65,9 @@ transaction of an anchor output channel type. This will attempt to CPFP the
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.ChangeAddr, "changeaddr", "", "the change address to "+ &cc.ChangeAddr, "changeaddr", "", "the change address to "+
"send the remaining funds to", "send the remaining funds back to; specify '"+
lnd.AddressDeriveFromWallet+"' to derive a new "+
"address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -90,8 +92,21 @@ func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
if len(c.AnchorAddrs) == 0 { if len(c.AnchorAddrs) == 0 {
return fmt.Errorf("at least one anchor addr is required") return fmt.Errorf("at least one anchor addr is required")
} }
if c.ChangeAddr == "" { for _, anchorAddr := range c.AnchorAddrs {
return fmt.Errorf("change addr is required") err = lnd.CheckAddress(
anchorAddr, chainParams, true, "anchor",
lnd.AddrTypeP2WSH, lnd.AddrTypeP2TR,
)
if err != nil {
return err
}
}
err = lnd.CheckAddress(
c.ChangeAddr, chainParams, true, "change", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
outpoint, err := lnd.ParseOutpoint(c.SponsorInput) outpoint, err := lnd.ParseOutpoint(c.SponsorInput)
@ -100,17 +115,12 @@ func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
err) err)
} }
changeScript, err := lnd.GetP2WPKHScript(c.ChangeAddr, chainParams)
if err != nil {
return fmt.Errorf("error parsing change addr: %w", err)
}
// Set default values. // Set default values.
if c.FeeRate == 0 { if c.FeeRate == 0 {
c.FeeRate = defaultFeeSatPerVByte c.FeeRate = defaultFeeSatPerVByte
} }
return createPullTransactionTemplate( return createPullTransactionTemplate(
extendedKey, c.APIURL, outpoint, c.AnchorAddrs, changeScript, extendedKey, c.APIURL, outpoint, c.AnchorAddrs, c.ChangeAddr,
c.FeeRate, c.FeeRate,
) )
} }
@ -126,14 +136,23 @@ type targetAnchor struct {
func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey, func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string, apiURL string, sponsorOutpoint *wire.OutPoint, anchorAddrs []string,
changeScript []byte, feeRate uint32) error { changeAddr string, feeRate uint32) error {
signer := &lnd.Signer{ var (
ExtendedKey: rootKey, signer = &lnd.Signer{
ChainParams: chainParams, 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 := &btc.ExplorerAPI{BaseURL: apiURL}
estimator := input.TxWeightEstimator{}
// Make sure the sponsor input is a P2WPKH or P2TR input and is known // 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. // to the block explorer, so we can fetch the witness utxo.
@ -194,7 +213,6 @@ func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
} }
// Now we can calculate the fee and add the change output. // Now we can calculate the fee and add the change output.
estimator.AddP2WKHOutput()
anchorAmt := uint64(len(anchorAddrs)) * 330 anchorAmt := uint64(len(anchorAddrs)) * 330
totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt) totalOutputValue := btcutil.Amount(sponsorTxOut.Value + anchorAmt)
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()

@ -5,11 +5,9 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightninglabs/loop" "github.com/lightninglabs/loop"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
@ -70,8 +68,9 @@ func newRecoverLoopInCommand() *cobra.Command {
"database directory, where the loop.db file is located", "database directory, where the loop.db file is located",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweep_addr", "", "address to recover "+ &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"the funds to", "to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", 0, "fee rate to "+ &cc.FeeRate, "feerate", 0, "fee rate to "+
@ -117,12 +116,15 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
return fmt.Errorf("loop_db_dir is required") return fmt.Errorf("loop_db_dir is required")
} }
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep_addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
signer := &lnd.Signer{ signer := &lnd.Signer{
ExtendedKey: extendedKey, ExtendedKey: extendedKey,
ChainParams: chainParams, ChainParams: chainParams,
@ -162,29 +164,20 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Get the destination address. // 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 { if err != nil {
return err return err
} }
// Calculate the sweep fee. // Calculate the sweep fee.
estimator := &input.TxWeightEstimator{} err = htlc.AddTimeoutToEstimator(&estimator)
err = htlc.AddTimeoutToEstimator(estimator)
if err != nil { if err != nil {
return err 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( feeRateKWeight := chainfee.SatPerKVByte(
1000 * c.FeeRate, 1000 * c.FeeRate,
).FeePerKWeight() ).FeePerKWeight()
@ -213,13 +206,8 @@ func (c *recoverLoopInCommand) Execute(_ *cobra.Command, _ []string) error {
}) })
// Add output for the destination address. // Add output for the destination address.
sweepPkScript, err := txscript.PayToAddrScript(sweepAddr)
if err != nil {
return err
}
sweepTx.AddTxOut(&wire.TxOut{ sweepTx.AddTxOut(&wire.TxOut{
PkScript: sweepPkScript, PkScript: sweepScript,
Value: int64(loopIn.Contract.AmountRequested) - int64(fee), Value: int64(loopIn.Contract.AmountRequested) - int64(fee),
}) })

@ -9,7 +9,6 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -113,7 +112,9 @@ chantools rescuefunding \
"specified manually", "specified manually",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -227,35 +228,46 @@ func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
} }
} }
// Make sure the sweep addr is a P2WKH address so we can do accurate err = lnd.CheckAddress(
// fee estimation. c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
sweepScript, err := lnd.GetP2WPKHScript(c.SweepAddr, chainParams) lnd.AddrTypeP2TR,
)
if err != nil { if err != nil {
return fmt.Errorf("error parsing sweep addr: %w", err) return err
} }
return rescueFunding( return rescueFunding(
localKeyDesc, remotePubKey, signer, chainOp, localKeyDesc, remotePubKey, signer, chainOp, c.SweepAddr,
sweepScript, btcutil.Amount(c.FeeRate), c.APIURL, btcutil.Amount(c.FeeRate), c.APIURL,
) )
} }
func rescueFunding(localKeyDesc *keychain.KeyDescriptor, func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
remoteKey *btcec.PublicKey, signer *lnd.Signer, 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 { 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. // Prepare the wire part of the PSBT.
txIn := &wire.TxIn{ txIn := &wire.TxIn{
PreviousOutPoint: *chainPoint, PreviousOutPoint: *chainPoint,
Sequence: 0, Sequence: 0,
} }
txOut := &wire.TxOut{ txOut := &wire.TxOut{
PkScript: sweepPKScript, PkScript: sweepScript,
} }
// Locate the output in the funding TX. // Locate the output in the funding TX.
api := &btc.ExplorerAPI{BaseURL: apiURL}
tx, err := api.Transaction(chainPoint.Hash.String()) tx, err := api.Transaction(chainPoint.Hash.String())
if err != nil { if err != nil {
return fmt.Errorf("error fetching UTXO info for outpoint %s: "+ return fmt.Errorf("error fetching UTXO info for outpoint %s: "+
@ -294,17 +306,15 @@ func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
WitnessScript: witnessScript, WitnessScript: witnessScript,
Unknowns: []*psbt.Unknown{{ Unknowns: []*psbt.Unknown{{
// We add the public key the other party needs to sign // 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. // out with the signrescuefunding command.
Key: PsbtKeyTypeOutputMissingSigPubkey, Key: PsbtKeyTypeOutputMissingSigPubkey,
Value: remoteKey.SerializeCompressed(), Value: remoteKey.SerializeCompressed(),
}}, }},
} }
// Estimate the transaction weight so we can do the fee estimation. // Estimate the transaction weight, so we can do the fee estimation.
var estimator input.TxWeightEstimator
estimator.AddWitnessInput(MultiSigWitnessSize) estimator.AddWitnessInput(MultiSigWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
txOut.Value = utxo.Value - int64(totalFee) txOut.Value = utxo.Value - int64(totalFee)

@ -26,7 +26,9 @@ import (
) )
const ( const (
defaultAPIURL = "https://blockstream.info/api" defaultAPIURL = "https://blockstream.info/api"
defaultTestnetAPIURL = "https://blockstream.info/testnet/api"
defaultRegtestAPIURL = "http://localhost:3004"
// version is the current version of the tool. It is set during build. // version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the // NOTE: When changing this, please also update the version in the
@ -56,7 +58,8 @@ var rootCmd = &cobra.Command{
Short: "Chantools helps recover funds from lightning channels", Short: "Chantools helps recover funds from lightning channels",
Long: `This tool provides helper functions that can be used rescue Long: `This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore. funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/lightninglabs/chantools/.`, Complete documentation is available at
https://github.com/lightninglabs/chantools/.`,
Version: fmt.Sprintf("v%s, commit %s", version, Commit), Version: fmt.Sprintf("v%s, commit %s", version, Commit),
PersistentPreRun: func(cmd *cobra.Command, args []string) { PersistentPreRun: func(cmd *cobra.Command, args []string) {
switch { switch {
@ -326,3 +329,22 @@ func setSubLogger(subsystem string, logger btclog.Logger,
func noConsole() ([]byte, error) { func noConsole() ([]byte, error) {
return nil, fmt.Errorf("wallet db requires console access") return nil, fmt.Errorf("wallet db requires console access")
} }
func newExplorerAPI(apiURL string) *btc.ExplorerAPI {
// Override for testnet if default is used.
if apiURL == defaultAPIURL &&
chainParams.Name == chaincfg.TestNet3Params.Name {
return &btc.ExplorerAPI{BaseURL: defaultTestnetAPIURL}
}
// Also override for regtest if default is used.
if apiURL == defaultAPIURL &&
chainParams.Name == chaincfg.RegressionNetParams.Name {
return &btc.ExplorerAPI{BaseURL: defaultRegtestAPIURL}
}
// Otherwise use the provided URL.
return &btc.ExplorerAPI{BaseURL: apiURL}
}

@ -53,7 +53,8 @@ func (c *summaryCommand) Execute(_ *cobra.Command, _ []string) error {
func summarizeChannels(apiURL string, func summarizeChannels(apiURL string,
channels []*dataformat.SummaryEntry) error { channels []*dataformat.SummaryEntry) error {
summaryFile, err := btc.SummarizeChannels(apiURL, channels, log) api := newExplorerAPI(apiURL)
summaryFile, err := btc.SummarizeChannels(api, channels, log)
if err != nil { if err != nil {
return fmt.Errorf("error running summary: %w", err) return fmt.Errorf("error running summary: %w", err)
} }

@ -76,7 +76,9 @@ Supported remote force-closed channel types are:
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint32Var( cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
@ -95,8 +97,12 @@ func (c *sweepRemoteClosedCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Set default values. // Set default values.
@ -127,9 +133,17 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
sweepAddr string, recoveryWindow uint32, feeRate uint32, sweepAddr string, recoveryWindow uint32, feeRate uint32,
publish bool) error { publish bool) error {
var estimator input.TxWeightEstimator
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil {
return err
}
var ( var (
targets []*targetAddr targets []*targetAddr
api = &btc.ExplorerAPI{BaseURL: apiURL} api = newExplorerAPI(apiURL)
) )
for index := uint32(0); index < recoveryWindow; index++ { for index := uint32(0); index < recoveryWindow; index++ {
path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d", path := fmt.Sprintf("m/1017'/%d'/%d'/0/%d",
@ -171,7 +185,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
// Create estimator and transaction template. // Create estimator and transaction template.
var ( var (
estimator input.TxWeightEstimator
signDescs []*input.SignDescriptor signDescs []*input.SignDescriptor
sweepTx = wire.NewMsgTx(2) sweepTx = wire.NewMsgTx(2)
totalOutputValue = uint64(0) totalOutputValue = uint64(0)
@ -286,13 +299,6 @@ func sweepRemoteClosed(extendedKey *hdkeychain.ExtendedKey, apiURL,
len(targets), totalOutputValue, sweepDustLimit) 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 // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
@ -423,7 +429,7 @@ func queryAddressBalances(pubKey *btcec.PublicKey, path string,
return nil, err return nil, err
} }
p2tr, scriptTree, err := lnd.P2TaprootStaticRemove(pubKey, chainParams) p2tr, scriptTree, err := lnd.P2TaprootStaticRemote(pubKey, chainParams)
if err != nil { if err != nil {
return nil, err return nil, err
} }

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/dataformat" "github.com/lightninglabs/chantools/dataformat"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
@ -65,7 +64,9 @@ parameter to 144.`,
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint16Var( cc.cmd.Flags().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+ &cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
@ -89,8 +90,12 @@ func (c *sweepTimeLockCommand) Execute(_ *cobra.Command, _ []string) error {
} }
// Make sure sweep addr is set. // Make sure sweep addr is set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
// Parse channel entries from any of the possible input files. // Parse channel entries from any of the possible input files.
@ -216,18 +221,26 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
publish bool, feeRate uint32) error { publish bool, feeRate uint32) error {
// Create signer and transaction template. // Create signer and transaction template.
signer := &lnd.Signer{ var (
ExtendedKey: extendedKey, estimator input.TxWeightEstimator
ChainParams: chainParams, 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 := &btc.ExplorerAPI{BaseURL: apiURL}
var ( var (
sweepTx = wire.NewMsgTx(2) sweepTx = wire.NewMsgTx(2)
totalOutputValue = int64(0) totalOutputValue = int64(0)
signDescs = make([]*input.SignDescriptor, 0) signDescs = make([]*input.SignDescriptor, 0)
prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil) prevOutFetcher = txscript.NewMultiPrevOutFetcher(nil)
estimator input.TxWeightEstimator
) )
for _, target := range targets { for _, target := range targets {
// We can't rely on the CSV delay of the channel DB to be // We can't rely on the CSV delay of the channel DB to be
@ -242,8 +255,8 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
), target.lockScript, 0, maxCsvTimeout, ), target.lockScript, 0, maxCsvTimeout,
) )
if err != nil { if err != nil {
log.Errorf("Could not create matching script for %s "+ log.Errorf("could not create matching script for %s "+
"or csv too high: %w", target.channelPoint, err) "or csv too high: %v", target.channelPoint, err)
continue continue
} }
@ -283,13 +296,6 @@ func sweepTimeLock(extendedKey *hdkeychain.ExtendedKey, apiURL string,
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) 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 // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -90,7 +89,9 @@ chantools sweeptimelockmanual \
"API instead of just printing the TX", "API instead of just printing the TX",
) )
cc.cmd.Flags().StringVar( cc.cmd.Flags().StringVar(
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to", &cc.SweepAddr, "sweepaddr", "", "address to recover the funds "+
"to; specify '"+lnd.AddressDeriveFromWallet+"' to "+
"derive a new address from the seed automatically",
) )
cc.cmd.Flags().Uint16Var( cc.cmd.Flags().Uint16Var(
&cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+ &cc.MaxCsvLimit, "maxcsvlimit", defaultCsvLimit, "maximum CSV "+
@ -143,11 +144,20 @@ func (c *sweepTimeLockManualCommand) Execute(_ *cobra.Command, _ []string) error
} }
// Make sure the sweep and time lock addrs are set. // Make sure the sweep and time lock addrs are set.
if c.SweepAddr == "" { err = lnd.CheckAddress(
return fmt.Errorf("sweep addr is required") c.SweepAddr, chainParams, true, "sweep", lnd.AddrTypeP2WKH,
lnd.AddrTypeP2TR,
)
if err != nil {
return err
} }
if c.TimeLockAddr == "" {
return fmt.Errorf("time lock addr is required") err = lnd.CheckAddress(
c.TimeLockAddr, chainParams, true, "time lock",
lnd.AddrTypeP2WSH,
)
if err != nil {
return err
} }
var ( var (
@ -238,12 +248,30 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
maxCsvTimeout, startNumChannels, maxNumChannels, maxCsvTimeout, startNumChannels, maxNumChannels,
maxNumChanUpdates) 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 // 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 // brute force the script with the information we have. If not, we can't
// continue anyway. // continue anyway.
lockScript, err := lnd.GetP2WSHScript(timeLockAddr, chainParams) lockScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, nil, extendedKey, "time lock",
)
if err != nil {
return err
}
sweepScript, err := lnd.PrepareWalletAddress(
sweepAddr, chainParams, &estimator, extendedKey, "sweep",
)
if err != nil { if err != nil {
return fmt.Errorf("invalid time lock addr: %w", err) return err
} }
// We need to go through a lot of our keys so it makes sense to // We need to go through a lot of our keys so it makes sense to
@ -293,13 +321,6 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
return fmt.Errorf("target script not derived") return fmt.Errorf("target script not derived")
} }
// Create signer and transaction template.
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
api := &btc.ExplorerAPI{BaseURL: apiURL}
// We now know everything we need to construct the sweep transaction, // 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 // except for what outpoint to sweep. We'll ask the chain API to give
// us this information. // us this information.
@ -329,17 +350,11 @@ func sweepTimeLockManual(extendedKey *hdkeychain.ExtendedKey, apiURL string,
// Calculate the fee based on the given fee rate and our weight // Calculate the fee based on the given fee rate and our weight
// estimation. // estimation.
var estimator input.TxWeightEstimator
estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize) estimator.AddWitnessInput(input.ToLocalTimeoutWitnessSize)
estimator.AddP2WKHOutput()
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight() feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight())) totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
// Add our sweep destination output. // Add our sweep destination output.
sweepScript, err := lnd.GetP2WPKHScript(sweepAddr, chainParams)
if err != nil {
return err
}
sweepTx.TxOut = []*wire.TxOut{{ sweepTx.TxOut = []*wire.TxOut{{
Value: sweepValue - int64(totalFee), Value: sweepValue - int64(totalFee),
PkScript: sweepScript, PkScript: sweepScript,

@ -10,7 +10,6 @@ import (
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr" "github.com/btcsuite/btcd/connmgr"
"github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd" "github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/brontide" "github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
@ -152,7 +151,7 @@ func (c *triggerForceCloseCommand) Execute(_ *cobra.Command, _ []string) error {
log.Infof("Message sent, waiting for force close transaction to " + log.Infof("Message sent, waiting for force close transaction to " +
"appear in mempool") "appear in mempool")
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
channelAddress, err := api.Address(c.ChannelPoint) channelAddress, err := api.Address(c.ChannelPoint)
if err != nil { if err != nil {
return fmt.Errorf("error getting channel address: %w", err) return fmt.Errorf("error getting channel address: %w", err)

@ -14,7 +14,6 @@ import (
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/hasura/go-graphql-client" "github.com/hasura/go-graphql-client"
"github.com/lightninglabs/chantools/btc"
"github.com/spf13/cobra" "github.com/spf13/cobra"
"golang.org/x/oauth2" "golang.org/x/oauth2"
) )
@ -188,7 +187,7 @@ func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
log.Infof("%s: %s", groups[1], groups[2]) log.Infof("%s: %s", groups[1], groups[2])
} }
api := &btc.ExplorerAPI{BaseURL: c.APIURL} api := newExplorerAPI(c.APIURL)
src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AmbossKey}) src := oauth2.StaticTokenSource(&oauth2.Token{AccessToken: c.AmbossKey})
httpClient := oauth2.NewClient(context.Background(), src) httpClient := oauth2.NewClient(context.Background(), src)
client := graphql.NewClient( client := graphql.NewClient(

@ -6,7 +6,8 @@ Chantools helps recover funds from lightning channels
This tool provides helper functions that can be used rescue This tool provides helper functions that can be used rescue
funds locked in lnd channels in case lnd itself cannot run properly anymore. funds locked in lnd channels in case lnd itself cannot run properly anymore.
Complete documentation is available at https://github.com/lightninglabs/chantools/. Complete documentation is available at
https://github.com/lightninglabs/chantools/.
### Options ### Options
@ -24,7 +25,7 @@ Complete documentation is available at https://github.com/lightninglabs/chantool
* [chantools compactdb](chantools_compactdb.md) - Create a copy of a channel.db file in safe/read-only mode * [chantools compactdb](chantools_compactdb.md) - Create a copy of a channel.db file in safe/read-only mode
* [chantools deletepayments](chantools_deletepayments.md) - Remove all (failed) payments from a channel DB * [chantools deletepayments](chantools_deletepayments.md) - Remove all (failed) payments from a channel DB
* [chantools derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path * [chantools derivekey](chantools_derivekey.md) - Derive a key with a specific derivation path
* [chantools doublespendinputs](chantools_doublespendinputs.md) - Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address. This can only be used with inputs that belong to an lnd wallet. * [chantools doublespendinputs](chantools_doublespendinputs.md) - Replace a transaction by double spending its input
* [chantools dropchannelgraph](chantools_dropchannelgraph.md) - Remove all graph related data from a channel DB * [chantools dropchannelgraph](chantools_dropchannelgraph.md) - Remove all graph related data from a channel DB
* [chantools dropgraphzombies](chantools_dropgraphzombies.md) - Remove all channels identified as zombies from the graph to force a re-sync of the graph * [chantools dropgraphzombies](chantools_dropgraphzombies.md) - Remove all channels identified as zombies from the graph to force a re-sync of the graph
* [chantools dumpbackup](chantools_dumpbackup.md) - Dump the content of a channel.backup file * [chantools dumpbackup](chantools_dumpbackup.md) - Dump the content of a channel.backup file

@ -41,7 +41,7 @@ chantools closepoolaccount \
--outpoint string last account outpoint of the account to close (<txid>:<txindex>) --outpoint string last account outpoint of the account to close (<txid>:<txindex>)
--publish publish sweep TX to the chain API instead of just printing the TX --publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

@ -1,6 +1,12 @@
## chantools doublespendinputs ## chantools doublespendinputs
Tries to double spend the given inputs by deriving the private for the address and sweeping the funds to the given address. This can only be used with inputs that belong to an lnd wallet. Replace a transaction by double spending its input
### Synopsis
Tries to double spend the given inputs by deriving the
private for the address and sweeping the funds to the given address. This can
only be used with inputs that belong to an lnd wallet.
``` ```
chantools doublespendinputs [flags] chantools doublespendinputs [flags]
@ -10,10 +16,10 @@ chantools doublespendinputs [flags]
``` ```
chantools doublespendinputs \ chantools doublespendinputs \
--inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \ --inputoutpoints xxxxxxxxx:y,xxxxxxxxx:y \
--sweepaddr bc1q..... \ --sweepaddr bc1q..... \
--feerate 10 \ --feerate 10 \
--publish --publish
``` ```
### Options ### Options
@ -27,7 +33,7 @@ chantools doublespendinputs \
--publish publish replacement TX to the chain API instead of just printing the TX --publish publish replacement TX to the chain API instead of just printing the TX
--recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500) --recoverywindow uint32 number of keys to scan per internal/external branch; output will consist of double this amount of keys (default 2500)
--rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving the input keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

@ -28,7 +28,7 @@ chantools pullanchor \
--anchoraddr stringArray the address of the anchor output (p2wsh or p2tr output with 330 satoshis) that should be pulled; can be specified multiple times per command to pull multiple anchors with a single transaction --anchoraddr stringArray the address of the anchor output (p2wsh or p2tr output with 330 satoshis) that should be pulled; can be specified multiple times per command to pull multiple anchors with a single transaction
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api") --apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag --bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--changeaddr string the change address to send the remaining funds to --changeaddr string the change address to send the remaining funds back to; specify 'fromseed' to derive a new address from the seed automatically
--feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30) --feerate uint32 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for pullanchor -h, --help help for pullanchor
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed

@ -31,7 +31,7 @@ chantools recoverloopin \
--rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving starting key; leave empty to prompt for lnd 24 word aezeed
--start_key_index int start key index to try to find the correct key index --start_key_index int start key index to try to find the correct key index
--swap_hash string swap hash of the loop in swap --swap_hash string swap hash of the loop in swap
--sweep_addr string address to recover the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--txid string transaction id of the on-chain transaction that created the HTLC --txid string transaction id of the on-chain transaction that created the HTLC
--vout uint32 output index of the on-chain transaction that created the HTLC --vout uint32 output index of the on-chain transaction that created the HTLC
``` ```

@ -49,7 +49,7 @@ chantools rescuefunding \
--localkeyindex uint32 in case a channel DB is not available (but perhaps a channel backup file), the derivation index of the local multisig public key can be specified manually --localkeyindex uint32 in case a channel DB is not available (but perhaps a channel backup file), the derivation index of the local multisig public key can be specified manually
--remotepubkey string in case a channel DB is not available (but perhaps a channel backup file), the remote multisig public key can be specified manually --remotepubkey string in case a channel DB is not available (but perhaps a channel backup file), the remote multisig public key can be specified manually
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

@ -40,7 +40,7 @@ chantools sweepremoteclosed \
--publish publish sweep TX to the chain API instead of just printing the TX --publish publish sweep TX to the chain API instead of just printing the TX
--recoverywindow uint32 number of keys to scan per derivation path (default 200) --recoverywindow uint32 number of keys to scan per derivation path (default 200)
--rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for sweeping the wallet; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

@ -40,7 +40,7 @@ chantools sweeptimelock \
--pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin --pendingchannels string channel input is in the format of lncli's pendingchannels format; specify '-' to read from stdin
--publish publish sweep TX to the chain API instead of just printing the TX --publish publish sweep TX to the chain API instead of just printing the TX
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
``` ```
### Options inherited from parent commands ### Options inherited from parent commands

@ -61,7 +61,7 @@ chantools sweeptimelockmanual \
--publish publish sweep TX to the chain API instead of just printing the TX --publish publish sweep TX to the chain API instead of just printing the TX
--remoterevbasepoint string remote node's revocation base point, can be found in a channel.backup file --remoterevbasepoint string remote node's revocation base point, can be found in a channel.backup file
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed --rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to --sweepaddr string address to recover the funds to; specify 'fromseed' to derive a new address from the seed automatically
--timelockaddr string address of the time locked commitment output where the funds are stuck in --timelockaddr string address of the time locked commitment output where the funds are stuck in
``` ```

@ -13,6 +13,7 @@ import (
"github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcwallet/waddrmgr"
"github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain" "github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/shachain" "github.com/lightningnetwork/lnd/shachain"
@ -24,6 +25,16 @@ const (
WalletBIP49DerivationPath = "m/49'/0'/0'" WalletBIP49DerivationPath = "m/49'/0'/0'"
WalletBIP86DerivationPath = "m/86'/0'/0'" WalletBIP86DerivationPath = "m/86'/0'/0'"
LndDerivationPath = "m/1017'/%d'/%d'" LndDerivationPath = "m/1017'/%d'/%d'"
AddressDeriveFromWallet = "fromseed"
)
type AddrType int
const (
AddrTypeP2WKH AddrType = iota
AddrTypeP2WSH
AddrTypeP2TR
) )
func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) ( func DeriveChildren(key *hdkeychain.ExtendedKey, path []uint32) (
@ -383,7 +394,7 @@ func P2AnchorStaticRemote(pubKey *btcec.PublicKey,
return p2wsh, commitScript, err return p2wsh, commitScript, err
} }
func P2TaprootStaticRemove(pubKey *btcec.PublicKey, func P2TaprootStaticRemote(pubKey *btcec.PublicKey,
params *chaincfg.Params) (*btcutil.AddressTaproot, params *chaincfg.Params) (*btcutil.AddressTaproot,
*input.CommitScriptTree, error) { *input.CommitScriptTree, error) {
@ -398,6 +409,140 @@ func P2TaprootStaticRemove(pubKey *btcec.PublicKey,
return addr, scriptTree, err return addr, scriptTree, err
} }
func CheckAddress(addr string, chainParams *chaincfg.Params, allowDerive bool,
hint string, allowedTypes ...AddrType) error {
// We generally always want an address to be specified. If one should
// be derived from the wallet automatically, the user should specify
// "derive" as the address.
if addr == "" {
return fmt.Errorf("%s address cannot be empty", hint)
}
// If we're allowed to derive an address from the wallet, we can skip
// the rest of the checks.
if allowDerive && addr == AddressDeriveFromWallet {
return nil
}
parsedAddr, err := ParseAddress(addr, chainParams)
if err != nil {
return fmt.Errorf("%s address is invalid: %w", hint, err)
}
if !matchAddrType(parsedAddr, allowedTypes...) {
return fmt.Errorf("%s address is of wrong type, allowed "+
"types: %s", hint, addrTypesToString(allowedTypes))
}
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 {
if allowedType == addrType {
return true
}
}
return false
}
switch addr.(type) {
case *btcutil.AddressWitnessPubKeyHash:
return contains(allowedTypes, AddrTypeP2WKH)
case *btcutil.AddressWitnessScriptHash:
return contains(allowedTypes, AddrTypeP2WSH)
case *btcutil.AddressTaproot:
return contains(allowedTypes, AddrTypeP2TR)
default:
return false
}
}
func addrTypesToString(allowedTypes []AddrType) string {
var types []string
for _, allowedType := range allowedTypes {
switch allowedType {
case AddrTypeP2WKH:
types = append(types, "P2WKH")
case AddrTypeP2WSH:
types = append(types, "P2WSH")
case AddrTypeP2TR:
types = append(types, "P2TR")
}
}
return strings.Join(types, ", ")
}
type HDKeyRing struct { type HDKeyRing struct {
ExtendedKey *hdkeychain.ExtendedKey ExtendedKey *hdkeychain.ExtendedKey
ChainParams *chaincfg.Params ChainParams *chaincfg.Params

Loading…
Cancel
Save