|
|
|
package main
|
|
|
|
|
|
|
|
import (
|
|
|
|
"bytes"
|
|
|
|
"encoding/hex"
|
|
|
|
"fmt"
|
|
|
|
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
|
|
"github.com/btcsuite/btcd/btcutil"
|
|
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
|
|
"github.com/btcsuite/btcd/wire"
|
|
|
|
"github.com/guggero/chantools/btc"
|
|
|
|
"github.com/guggero/chantools/lnd"
|
|
|
|
"github.com/lightningnetwork/lnd/input"
|
|
|
|
"github.com/lightningnetwork/lnd/keychain"
|
|
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
|
|
"github.com/spf13/cobra"
|
|
|
|
)
|
|
|
|
|
|
|
|
const (
|
|
|
|
MaxChannelLookup = 5000
|
|
|
|
|
|
|
|
// MultiSigWitnessSize 222 bytes
|
|
|
|
// - NumberOfWitnessElements: 1 byte
|
|
|
|
// - NilLength: 1 byte
|
|
|
|
// - sigAliceLength: 1 byte
|
|
|
|
// - sigAlice: 73 bytes
|
|
|
|
// - sigBobLength: 1 byte
|
|
|
|
// - sigBob: 73 bytes
|
|
|
|
// - WitnessScriptLength: 1 byte
|
|
|
|
// - WitnessScript (MultiSig)
|
|
|
|
MultiSigWitnessSize = 1 + 1 + 1 + 73 + 1 + 73 + 1 + input.MultiSigSize
|
|
|
|
)
|
|
|
|
|
|
|
|
var (
|
|
|
|
PsbtKeyTypeOutputMissingSigPubkey = []byte{0xcc}
|
|
|
|
)
|
|
|
|
|
|
|
|
type rescueFundingCommand struct {
|
|
|
|
ChannelDB string
|
|
|
|
DBChannelPoint string
|
|
|
|
ConfirmedOutPoint string
|
|
|
|
|
|
|
|
LocalKeyIndex uint32
|
|
|
|
RemotePubKey string
|
|
|
|
|
|
|
|
SweepAddr string
|
|
|
|
FeeRate uint16
|
|
|
|
APIURL string
|
|
|
|
|
|
|
|
rootKey *rootKey
|
|
|
|
cmd *cobra.Command
|
|
|
|
}
|
|
|
|
|
|
|
|
func newRescueFundingCommand() *cobra.Command {
|
|
|
|
cc := &rescueFundingCommand{}
|
|
|
|
cc.cmd = &cobra.Command{
|
|
|
|
Use: "rescuefunding",
|
|
|
|
Short: "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",
|
|
|
|
Long: `This is part 1 of a two phase process to rescue a channel
|
|
|
|
funding output that was created on chain by accident but never resulted in a
|
|
|
|
proper channel and no commitment transactions exist to spend the funds locked in
|
|
|
|
the 2-of-2 multisig.
|
|
|
|
|
|
|
|
**You need the cooperation of the channel partner (remote node) for this to
|
|
|
|
work**! They need to run the second command of this process: signrescuefunding
|
|
|
|
|
|
|
|
If successful, this will create a PSBT that then has to be sent to the channel
|
|
|
|
partner (remote node operator).`,
|
|
|
|
Example: `chantools rescuefunding \
|
|
|
|
--channeldb ~/.lnd/data/graph/mainnet/channel.db \
|
|
|
|
--dbchannelpoint xxxxxxx:xx \
|
|
|
|
--sweepaddr bc1qxxxxxxxxx \
|
|
|
|
--feerate 10
|
|
|
|
|
|
|
|
chantools rescuefunding \
|
|
|
|
--confirmedchannelpoint xxxxxxx:xx \
|
|
|
|
--localkeyindex x \
|
|
|
|
--remotepubkey 0xxxxxxxxxxxxxxxx \
|
|
|
|
--sweepaddr bc1qxxxxxxxxx \
|
|
|
|
--feerate 10`,
|
|
|
|
RunE: cc.Execute,
|
|
|
|
}
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.ChannelDB, "channeldb", "", "lnd channel.db file to "+
|
|
|
|
"rescue a channel from; must contain the pending "+
|
|
|
|
"channel specified with --channelpoint",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.DBChannelPoint, "dbchannelpoint", "", "funding transaction "+
|
|
|
|
"outpoint of the channel to rescue (<txid>:<txindex>) "+
|
|
|
|
"as it is recorded in the DB",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.ConfirmedOutPoint, "confirmedchannelpoint", "", "channel "+
|
|
|
|
"outpoint that got confirmed on chain "+
|
|
|
|
"(<txid>:<txindex>); normally this is the same as the "+
|
|
|
|
"--dbchannelpoint so it will be set to that value if"+
|
|
|
|
"this is left empty",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().Uint32Var(
|
|
|
|
&cc.LocalKeyIndex, "localkeyindex", 0, "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",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.RemotePubKey, "remotepubkey", "", "in case a channel DB "+
|
|
|
|
"is not available (but perhaps a channel backup "+
|
|
|
|
"file), the remote multisig public key can be "+
|
|
|
|
"specified manually",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.SweepAddr, "sweepaddr", "", "address to sweep the funds to",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().Uint16Var(
|
|
|
|
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
|
|
|
"use for the sweep transaction in sat/vByte",
|
|
|
|
)
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
|
|
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
|
|
|
|
"be esplora compatible)",
|
|
|
|
)
|
|
|
|
|
|
|
|
cc.rootKey = newRootKey(cc.cmd, "deriving keys")
|
|
|
|
|
|
|
|
return cc.cmd
|
|
|
|
}
|
|
|
|
|
|
|
|
func (c *rescueFundingCommand) Execute(_ *cobra.Command, _ []string) error {
|
|
|
|
var (
|
|
|
|
chainOp *wire.OutPoint
|
|
|
|
databaseOp *wire.OutPoint
|
|
|
|
localKeyDesc *keychain.KeyDescriptor
|
|
|
|
remotePubKey *btcec.PublicKey
|
|
|
|
)
|
|
|
|
|
|
|
|
extendedKey, err := c.rootKey.read()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error reading root key: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
signer := &lnd.Signer{
|
|
|
|
ExtendedKey: extendedKey,
|
|
|
|
ChainParams: chainParams,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Check that we have a channel DB or manual keys.
|
|
|
|
switch {
|
|
|
|
case (c.ChannelDB == "" || c.DBChannelPoint == "") &&
|
|
|
|
c.RemotePubKey == "":
|
|
|
|
|
|
|
|
return fmt.Errorf("need to specify either channel DB and " +
|
|
|
|
"channel point or both local and remote pubkey")
|
|
|
|
|
|
|
|
case c.ChannelDB != "" && c.DBChannelPoint != "":
|
|
|
|
db, err := lnd.OpenDB(c.ChannelDB, true)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error opening rescue DB: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse channel point of channel to rescue as known to the DB.
|
|
|
|
databaseOp, err = lnd.ParseOutpoint(c.DBChannelPoint)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error parsing channel point: %w",
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// First, make sure the channel can be found in the DB.
|
|
|
|
pendingChan, err := db.ChannelStateDB().FetchChannel(
|
|
|
|
nil, *databaseOp,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error loading pending channel %s "+
|
|
|
|
"from DB: %w", databaseOp, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil {
|
|
|
|
return fmt.Errorf("invalid channel data in DB, local " +
|
|
|
|
"multisig pubkey is nil")
|
|
|
|
}
|
|
|
|
if pendingChan.LocalChanCfg.MultiSigKey.PubKey == nil {
|
|
|
|
return fmt.Errorf("invalid channel data in DB, remote " +
|
|
|
|
"multisig pubkey is nil")
|
|
|
|
}
|
|
|
|
|
|
|
|
localKeyDesc = &pendingChan.LocalChanCfg.MultiSigKey
|
|
|
|
remotePubKey = pendingChan.RemoteChanCfg.MultiSigKey.PubKey
|
|
|
|
|
|
|
|
case c.RemotePubKey != "":
|
|
|
|
remoteKeyBytes, err := hex.DecodeString(c.RemotePubKey)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error hex decoding remote pubkey: "+
|
|
|
|
"%w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
remotePubKey, err = btcec.ParsePubKey(remoteKeyBytes)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error parsing remote pubkey: %w",
|
|
|
|
err)
|
|
|
|
}
|
|
|
|
|
|
|
|
localKeyDesc = &keychain.KeyDescriptor{
|
|
|
|
KeyLocator: keychain.KeyLocator{
|
|
|
|
Family: keychain.KeyFamilyMultiSig,
|
|
|
|
Index: c.LocalKeyIndex,
|
|
|
|
},
|
|
|
|
}
|
|
|
|
privKey, err := signer.FetchPrivKey(localKeyDesc)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error deriving local key: %w", err)
|
|
|
|
}
|
|
|
|
localKeyDesc.PubKey = privKey.PubKey()
|
|
|
|
}
|
|
|
|
|
|
|
|
// Parse channel point of channel to rescue as confirmed on chain (if
|
|
|
|
// different).
|
|
|
|
if len(c.ConfirmedOutPoint) == 0 {
|
|
|
|
chainOp = databaseOp
|
|
|
|
} else {
|
|
|
|
chainOp, err = lnd.ParseOutpoint(c.ConfirmedOutPoint)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error parsing confirmed channel "+
|
|
|
|
"point: %w", 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,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func rescueFunding(localKeyDesc *keychain.KeyDescriptor,
|
|
|
|
remoteKey *btcec.PublicKey, signer *lnd.Signer,
|
|
|
|
chainPoint *wire.OutPoint, sweepPKScript []byte, feeRate btcutil.Amount,
|
|
|
|
apiURL string) error {
|
|
|
|
|
|
|
|
// Prepare the wire part of the PSBT.
|
|
|
|
txIn := &wire.TxIn{
|
|
|
|
PreviousOutPoint: *chainPoint,
|
|
|
|
Sequence: 0,
|
|
|
|
}
|
|
|
|
txOut := &wire.TxOut{
|
|
|
|
PkScript: sweepPKScript,
|
|
|
|
}
|
|
|
|
|
|
|
|
// Locate the output in the funding TX.
|
|
|
|
api := &btc.ExplorerAPI{BaseURL: apiURL}
|
|
|
|
tx, err := api.Transaction(chainPoint.Hash.String())
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error fetching UTXO info for outpoint %s: "+
|
|
|
|
"%v", chainPoint.String(), err)
|
|
|
|
}
|
|
|
|
apiUtxo := tx.Vout[chainPoint.Index]
|
|
|
|
|
|
|
|
pkScript, err := hex.DecodeString(apiUtxo.ScriptPubkey)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error decoding pk script %s: %w",
|
|
|
|
apiUtxo.ScriptPubkey, err)
|
|
|
|
}
|
|
|
|
utxo := &wire.TxOut{
|
|
|
|
Value: int64(apiUtxo.Value),
|
|
|
|
PkScript: pkScript,
|
|
|
|
}
|
|
|
|
|
|
|
|
// We should also be able to create the funding script from the two
|
|
|
|
// multisig keys.
|
|
|
|
witnessScript, fundingTxOut, err := input.GenFundingPkScript(
|
|
|
|
localKeyDesc.PubKey.SerializeCompressed(),
|
|
|
|
remoteKey.SerializeCompressed(), utxo.Value,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("could not derive funding script: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// Some last sanity check that we're working with the correct data.
|
|
|
|
if !bytes.Equal(fundingTxOut.PkScript, utxo.PkScript) {
|
|
|
|
return fmt.Errorf("funding output script does not match UTXO")
|
|
|
|
}
|
|
|
|
|
|
|
|
// Now the rest of the known data for the PSBT.
|
|
|
|
pIn := psbt.PInput{
|
|
|
|
WitnessUtxo: utxo,
|
|
|
|
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
|
|
|
|
// 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
|
|
|
|
estimator.AddWitnessInput(MultiSigWitnessSize)
|
|
|
|
estimator.AddP2WKHOutput()
|
|
|
|
feeRateKWeight := chainfee.SatPerKVByte(1000 * feeRate).FeePerKWeight()
|
|
|
|
totalFee := feeRateKWeight.FeeForWeight(int64(estimator.Weight()))
|
|
|
|
txOut.Value = utxo.Value - int64(totalFee)
|
|
|
|
|
|
|
|
// Let's now create the PSBT as we have everything we need so far.
|
|
|
|
wireTx := &wire.MsgTx{
|
|
|
|
Version: 2,
|
|
|
|
TxIn: []*wire.TxIn{txIn},
|
|
|
|
TxOut: []*wire.TxOut{txOut},
|
|
|
|
}
|
|
|
|
packet, err := psbt.NewFromUnsignedTx(wireTx)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error creating PSBT: %w", err)
|
|
|
|
}
|
|
|
|
packet.Inputs[0] = pIn
|
|
|
|
|
|
|
|
// Now we add our partial signature.
|
|
|
|
err = signer.AddPartialSignature(
|
|
|
|
packet, *localKeyDesc, utxo, witnessScript, 0,
|
|
|
|
)
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error adding partial signature: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
// We're done, we can now output the finished PSBT.
|
|
|
|
base64, err := packet.B64Encode()
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("error encoding PSBT: %w", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
fmt.Printf("Partially signed transaction created. Send this to the "+
|
|
|
|
"other peer \nand ask them to run the 'chantools "+
|
|
|
|
"signrescuefunding' command: \n\n%s\n\n", base64)
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|