Skip to content

Commit

Permalink
multi: add new pullanchor command
Browse files Browse the repository at this point in the history
  • Loading branch information
guggero committed Dec 14, 2023
1 parent a5a884b commit 8b187d5
Show file tree
Hide file tree
Showing 5 changed files with 389 additions and 1 deletion.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,7 @@ Available Commands:
forceclose Force-close the last state that is in the channel.db provided
genimportscript Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
migratedb Apply all recent lnd channel database migrations
pullanchor Attempt to CPFP an anchor output of a channel
removechannel Remove a single channel from the given channel DB
rescueclosed Try finding the private keys for funds that are in outputs of remotely force-closed channels
rescuefunding Rescue funds locked in a funding multisig output that never resulted in a proper channel; this is the command the initiator of the channel needs to run
Expand Down Expand Up @@ -481,6 +482,7 @@ Legend:
| [forceclose](doc/chantools_forceclose.md) | :pencil: (:skull: :warning:) Publish an old channel state from a `channel.db` file |
| [genimportscript](doc/chantools_genimportscript.md) | :pencil: Create a script/text file that can be used to import `lnd` keys into other software |
| [migratedb](doc/chantools_migratedb.md) | Upgrade the `channel.db` file to the latest version |
| [pullanchor](doc/chantools_pullanchor.md) | :pencil: Attempt to CPFP an anchor output of a channel |
| [recoverloopin](doc/chantools_recoverloopin.md) | :pencil: Recover funds from a failed Lightning Loop inbound swap |
| [removechannel](doc/chantools_removechannel.md) | (:skull: :warning:) Remove a single channel from a `channel.db` file |
| [rescueclosed](doc/chantools_rescueclosed.md) | :pencil: (:pushpin:) Rescue funds in a legacy (pre `STATIC_REMOTE_KEY`) channel output |
Expand Down
334 changes: 334 additions & 0 deletions cmd/chantools/pullanchor.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,334 @@
package main

import (
"bytes"
"encoding/hex"
"fmt"
"math"

"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/btcutil/hdkeychain"
"github.com/btcsuite/btcd/btcutil/psbt"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/lightninglabs/chantools/btc"
"github.com/lightninglabs/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/spf13/cobra"
)

type pullAnchorCommand struct {
APIURL string
SponsorInput string
AnchorAddr string
ChangeAddr string
FeeRate uint32

rootKey *rootKey
cmd *cobra.Command
}

func newPullAnchorCommand() *cobra.Command {
cc := &pullAnchorCommand{}
cc.cmd = &cobra.Command{
Use: "pullanchor",
Short: "Attempt to CPFP an anchor output of a channel",
Long: `Use this command to confirm a channel force close
transaction of an anchor output channel type. This will attempt to CPFP the
330 byte anchor output created for your node.`,
Example: `chantools pullanchor \
--sponsorinput txid:vout \
--anchoraddr bc1q..... \
--changeaddr bc1q..... \
--feerate 10 \
--publish`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.SponsorInput, "sponsorinput", "", "the input to use to "+
"sponsor the CPFP transaction; must be owned by the "+
"lnd node that owns the anchor output",
)
cc.cmd.Flags().StringVar(
&cc.AnchorAddr, "anchoraddr", "", "the address of the anchor "+
"output (p2wsh output with 330 satoshis)",
)
cc.cmd.Flags().StringVar(
&cc.ChangeAddr, "changeaddr", "", "the change address to "+
"send the remaining funds to",
)
cc.cmd.Flags().Uint32Var(
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
"use for the sweep transaction in sat/vByte",
)

cc.rootKey = newRootKey(cc.cmd, "deriving keys")

return cc.cmd
}

func (c *pullAnchorCommand) Execute(_ *cobra.Command, _ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %w", err)
}

// Make sure all input is provided.
if c.SponsorInput == "" {
return fmt.Errorf("sponsor input is required")
}
if c.AnchorAddr == "" {
return fmt.Errorf("anchor addr is required")
}
if c.ChangeAddr == "" {
return fmt.Errorf("change addr is required")
}

outpoint, err := lnd.ParseOutpoint(c.SponsorInput)
if err != nil {
return fmt.Errorf("error parsing sponsor input outpoint: %w",
err)
}

// Make sure the anchor addr is a P2WSH address, so we can do accurate
// fee estimation.
anchorScript, err := lnd.GetP2WSHScript(c.AnchorAddr, chainParams)
if err != nil {
return fmt.Errorf("error parsing anchor addr: %w", 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, anchorScript, c.AnchorAddr,
changeScript, c.FeeRate,
)
}

func createPullTransactionTemplate(rootKey *hdkeychain.ExtendedKey,
apiURL string, sponsorOutpoint *wire.OutPoint, anchorPkScript []byte,
anchorAddr string, changeScript []byte, feeRate uint32) error {

signer := &lnd.Signer{
ExtendedKey: rootKey,
ChainParams: chainParams,
}
api := &btc.ExplorerAPI{BaseURL: 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.
sponsorTx, err := api.Transaction(sponsorOutpoint.Hash.String())
if err != nil {
return fmt.Errorf("error fetching sponsor tx: %w", err)
}
sponsorTxOut := sponsorTx.Vout[sponsorOutpoint.Index]
sponsorPkScript, err := hex.DecodeString(sponsorTxOut.ScriptPubkey)
if err != nil {
return fmt.Errorf("error decoding sponsor pkscript: %w", err)
}

sponsorType, err := txscript.ParsePkScript(sponsorPkScript)
if err != nil {
return fmt.Errorf("error parsing sponsor pkscript: %w", err)
}
var sponsorSigHashType txscript.SigHashType
switch sponsorType.Class() {
case txscript.WitnessV0PubKeyHashTy:
estimator.AddP2WKHInput()
sponsorSigHashType = txscript.SigHashAll

case txscript.WitnessV1TaprootTy:
sponsorSigHashType = txscript.SigHashDefault
estimator.AddTaprootKeySpendInput(sponsorSigHashType)

default:
return fmt.Errorf("unsupported sponsor input type: %v",
sponsorType.Class())
}

// Fetch the additional info we need for the anchor output as well.
anchorTx, anchorIndex, err := api.Outpoint(anchorAddr)
if err != nil {
return fmt.Errorf("error fetching anchor outpoint: %w", err)
}
anchorTxHash, err := chainhash.NewHashFromStr(anchorTx.TXID)
if err != nil {
return fmt.Errorf("error decoding anchor txid: %w", err)
}
estimator.AddWitnessInput(input.AnchorWitnessSize)

// First, we need to derive the correct branch from the local root key.
localMultisig, err := lnd.DeriveChildren(rootKey, []uint32{
lnd.HardenedKeyStart + uint32(keychain.BIP0043Purpose),
lnd.HardenedKeyStart + chainParams.HDCoinType,
lnd.HardenedKeyStart + uint32(keychain.KeyFamilyMultiSig),
0,
})
if err != nil {
return fmt.Errorf("could not derive local multisig key: %w",
err)
}

anchorKeyDesc, anchorWitnessScript, err := findAnchorKey(
localMultisig, anchorPkScript,
)
if err != nil {
return fmt.Errorf("could not find anchor key: %w", err)
}

log.Infof("Found multisig key %x for anchor pk script %x",
anchorKeyDesc.PubKey.SerializeCompressed(), anchorPkScript)

tx := wire.NewMsgTx(2)
packet, err := psbt.NewFromUnsignedTx(tx)
if err != nil {
return fmt.Errorf("error creating PSBT: %w", err)
}

// Let's add the inputs to the PSBT.
packet.UnsignedTx.TxIn = append(packet.UnsignedTx.TxIn, &wire.TxIn{
PreviousOutPoint: *sponsorOutpoint,
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: &wire.TxOut{
Value: int64(sponsorTxOut.Value),
PkScript: sponsorPkScript,
},
SighashType: sponsorSigHashType,
})

anchorUtxo := &wire.TxOut{
Value: 330,
PkScript: anchorPkScript,
}
packet.UnsignedTx.TxIn = append(packet.UnsignedTx.TxIn, &wire.TxIn{
PreviousOutPoint: wire.OutPoint{
Hash: *anchorTxHash,
Index: uint32(anchorIndex),
},
})
packet.Inputs = append(packet.Inputs, psbt.PInput{
WitnessUtxo: anchorUtxo,
WitnessScript: anchorWitnessScript,
})

// Now we can calculate the fee and add the change output.
estimator.AddP2WKHOutput()
totalOutputValue := btcutil.Amount(sponsorTxOut.Value + 330)
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))

log.Infof("Fee %d sats of %d total amount (estimated weight %d)",
totalFee, totalOutputValue, estimator.Weight())

packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, &wire.TxOut{
Value: int64(totalOutputValue - totalFee),
PkScript: changeScript,
})
packet.Outputs = append(packet.Outputs, psbt.POutput{})

// And now we sign the anchor input.
anchorSig, err := signer.SignOutputRaw(
packet.UnsignedTx, &input.SignDescriptor{
KeyDesc: *anchorKeyDesc,
WitnessScript: anchorWitnessScript,
SignMethod: input.WitnessV0SignMethod,
Output: anchorUtxo,
HashType: txscript.SigHashAll,
PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
anchorUtxo.PkScript, anchorUtxo.Value,
),
InputIndex: 1,
},
)
if err != nil {
return fmt.Errorf("error signing anchor input: %w", err)
}

anchorWitness := make(wire.TxWitness, 2)
anchorWitness[0] = append(
anchorSig.Serialize(), byte(txscript.SigHashAll),
)
anchorWitness[1] = anchorWitnessScript

var witnessBuf bytes.Buffer
if err = psbt.WriteTxWitness(&witnessBuf, anchorWitness); err != nil {
return fmt.Errorf("error serializing witness: %w", err)
}

packet.Inputs[1].FinalScriptWitness = witnessBuf.Bytes()

packetBase64, err := packet.B64Encode()
if err != nil {
return fmt.Errorf("error encoding PSBT: %w", err)
}

log.Infof("Prepared PSBT follows, please now call\n" +
"'lncli wallet psbt finalize <psbt>' to finalize the\n" +
"transaction, then publish it manually or by using\n" +
"'lncli wallet publishtx <final_tx>':\n\n" + packetBase64 +
"\n")

return nil
}

func findAnchorKey(multisigBranch *hdkeychain.ExtendedKey,
targetScript []byte) (*keychain.KeyDescriptor, []byte, error) {

// Loop through the local multisig keys to find the target anchor
// script.
for index := uint32(0); index < math.MaxInt16; index++ {
currentKey, err := multisigBranch.DeriveNonStandard(index)
if err != nil {
return nil, nil, fmt.Errorf("error deriving child "+
"key: %w", err)
}

currentPubkey, err := currentKey.ECPubKey()
if err != nil {
return nil, nil, fmt.Errorf("error deriving public "+
"key: %w", err)
}

script, err := input.CommitScriptAnchor(currentPubkey)
if err != nil {
return nil, nil, fmt.Errorf("error deriving script: "+
"%w", err)
}

pkScript, err := input.WitnessScriptHash(script)
if err != nil {
return nil, nil, fmt.Errorf("error deriving script "+
"hash: %w", err)
}

if !bytes.Equal(pkScript, targetScript) {
continue
}

return &keychain.KeyDescriptor{
PubKey: currentPubkey,
KeyLocator: keychain.KeyLocator{
Family: keychain.KeyFamilyMultiSig,
Index: index,
},
}, script, nil
}

return nil, nil, fmt.Errorf("no matching pubkeys found")
}
3 changes: 2 additions & 1 deletion cmd/chantools/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ const (
// version is the current version of the tool. It is set during build.
// NOTE: When changing this, please also update the version in the
// download link shown in the README.
version = "0.12.0"
version = "0.12.1"
na = "n/a"

// lndVersion is the current version of lnd that we support. This is
Expand Down Expand Up @@ -113,6 +113,7 @@ func main() {
newForceCloseCommand(),
newGenImportScriptCommand(),
newMigrateDBCommand(),
newPullAnchorCommand(),
newRecoverLoopInCommand(),
newRemoveChannelCommand(),
newRescueClosedCommand(),
Expand Down
1 change: 1 addition & 0 deletions doc/chantools.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Complete documentation is available at https://github.com/lightninglabs/chantool
* [chantools forceclose](chantools_forceclose.md) - Force-close the last state that is in the channel.db provided
* [chantools genimportscript](chantools_genimportscript.md) - Generate a script containing the on-chain keys of an lnd wallet that can be imported into other software like bitcoind
* [chantools migratedb](chantools_migratedb.md) - Apply all recent lnd channel database migrations
* [chantools pullanchor](chantools_pullanchor.md) - Attempt to CPFP an anchor output of a channel
* [chantools recoverloopin](chantools_recoverloopin.md) - Recover a loop in swap that the loop daemon is not able to sweep
* [chantools removechannel](chantools_removechannel.md) - Remove a single channel from the given channel DB
* [chantools rescueclosed](chantools_rescueclosed.md) - Try finding the private keys for funds that are in outputs of remotely force-closed channels
Expand Down
Loading

0 comments on commit 8b187d5

Please sign in to comment.