-
Notifications
You must be signed in to change notification settings - Fork 34
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
390 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,336 @@ | ||
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/mempool" | ||
"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 30`, | ||
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, | ||
Sequence: mempool.MaxRBFSequence, | ||
}) | ||
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), | ||
}, | ||
Sequence: mempool.MaxRBFSequence, | ||
}) | ||
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") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.