mirror of https://github.com/guggero/chantools
You cannot select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
215 lines
6.4 KiB
Go
215 lines
6.4 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcutil"
|
|
"github.com/btcsuite/btcutil/hdkeychain"
|
|
"github.com/btcsuite/btcutil/psbt"
|
|
"github.com/guggero/chantools/lnd"
|
|
"github.com/lightningnetwork/lnd/channeldb"
|
|
"github.com/lightningnetwork/lnd/input"
|
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
|
"path"
|
|
)
|
|
|
|
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 {
|
|
RootKey string `long:"rootkey" description:"BIP32 HD root key to use. Leave empty to prompt for lnd 24 word aezeed."`
|
|
ChannelDB string `long:"channeldb" description:"The lnd channel.db file to rescue a channel from. Must contain the pending channel specified with --channelpoint."`
|
|
ChannelPoint string `long:"channelpoint" description:"The funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is recorded in the DB."`
|
|
ConfirmedOutPoint string `long:"confirmedchannelpoint" description:"The channel outpoint that got confirmed on chain (<txid>:<txindex>). Normally this is the same as the --channelpoint so it will be set to that value if this is left empty."`
|
|
SweepAddr string `long:"sweepaddr" description:"The address to sweep the rescued funds to."`
|
|
SatPerByte int64 `long:"satperbyte" description:"The fee rate to use in satoshis/vByte."`
|
|
}
|
|
|
|
func (c *rescueFundingCommand) Execute(_ []string) error {
|
|
setupChainParams(cfg)
|
|
|
|
var (
|
|
extendedKey *hdkeychain.ExtendedKey
|
|
chainOp *wire.OutPoint
|
|
err error
|
|
)
|
|
|
|
// Check that root key is valid or fall back to console input.
|
|
switch {
|
|
case c.RootKey != "":
|
|
extendedKey, err = hdkeychain.NewKeyFromString(c.RootKey)
|
|
|
|
default:
|
|
extendedKey, _, err = lnd.ReadAezeed(
|
|
chainParams,
|
|
)
|
|
}
|
|
if err != nil {
|
|
return fmt.Errorf("error reading root key: %v", err)
|
|
}
|
|
|
|
signer := &lnd.Signer{
|
|
ExtendedKey: extendedKey,
|
|
ChainParams: chainParams,
|
|
}
|
|
|
|
// Check that we have a channel DB.
|
|
if c.ChannelDB == "" {
|
|
return fmt.Errorf("channel DB is required")
|
|
}
|
|
db, err := channeldb.Open(
|
|
path.Dir(c.ChannelDB), path.Base(c.ChannelDB),
|
|
channeldb.OptionSetSyncFreelist(true),
|
|
channeldb.OptionReadOnly(true),
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error opening rescue DB: %v", err)
|
|
}
|
|
|
|
// Parse channel point of channel to rescue as known to the DB.
|
|
dbOp, err := lnd.ParseOutpoint(c.ChannelPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing channel point: %v", err)
|
|
}
|
|
|
|
// Parse channel point of channel to rescue as confirmed on chain (if
|
|
// different).
|
|
if len(c.ConfirmedOutPoint) == 0 {
|
|
chainOp = dbOp
|
|
} else {
|
|
chainOp, err = lnd.ParseOutpoint(c.ConfirmedOutPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing confirmed channel "+
|
|
"point: %v", 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: %v", err)
|
|
}
|
|
|
|
if c.SatPerByte < 0 {
|
|
return fmt.Errorf("satperbyte must be greater than 0")
|
|
}
|
|
|
|
return rescueFunding(
|
|
db, signer, dbOp, chainOp, sweepScript,
|
|
btcutil.Amount(c.SatPerByte),
|
|
)
|
|
}
|
|
|
|
func rescueFunding(db *channeldb.DB, signer *lnd.Signer, dbFundingPoint,
|
|
chainPoint *wire.OutPoint, sweepPKScript []byte,
|
|
feeRate btcutil.Amount) error {
|
|
|
|
// First of all make sure the channel can be found in the DB.
|
|
pendingChan, err := db.FetchChannel(*dbFundingPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error loading pending channel %s from DB: "+
|
|
"%v", dbFundingPoint, err)
|
|
}
|
|
|
|
// 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.
|
|
utxo := pendingChan.FundingTxn.TxOut[dbFundingPoint.Index]
|
|
|
|
// We should also be able to create the funding script from the two
|
|
// multisig keys.
|
|
localKey := pendingChan.LocalChanCfg.MultiSigKey.PubKey
|
|
remoteKey := pendingChan.RemoteChanCfg.MultiSigKey.PubKey
|
|
witnessScript, fundingTxOut, err := input.GenFundingPkScript(
|
|
localKey.SerializeCompressed(), remoteKey.SerializeCompressed(),
|
|
utxo.Value,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("could not derive funding script: %v", 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: %v", err)
|
|
}
|
|
packet.Inputs[0] = pIn
|
|
|
|
// Now we add our partial signature.
|
|
err = signer.AddPartialSignature(
|
|
packet, pendingChan.LocalChanCfg.MultiSigKey, utxo,
|
|
witnessScript, 0,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error adding partial signature: %v", err)
|
|
}
|
|
|
|
// We're done, we can now output the finished PSBT.
|
|
base64, err := packet.B64Encode()
|
|
if err != nil {
|
|
return fmt.Errorf("error encoding PSBT: %v", 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
|
|
}
|