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.
508 lines
14 KiB
Go
508 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/btcsuite/btcd/btcec/v2"
|
|
"github.com/btcsuite/btcd/btcutil/psbt"
|
|
"github.com/btcsuite/btcd/chaincfg"
|
|
"github.com/btcsuite/btcd/wire"
|
|
"github.com/btcsuite/btcwallet/wallet/txrules"
|
|
"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"
|
|
)
|
|
|
|
type zombieRecoveryMakeOfferCommand struct {
|
|
Node1 string
|
|
Node2 string
|
|
FeeRate uint16
|
|
|
|
rootKey *rootKey
|
|
cmd *cobra.Command
|
|
}
|
|
|
|
func newZombieRecoveryMakeOfferCommand() *cobra.Command {
|
|
cc := &zombieRecoveryMakeOfferCommand{}
|
|
cc.cmd = &cobra.Command{
|
|
Use: "makeoffer",
|
|
Short: "[2/3] Make an offer on how to split the funds to " +
|
|
"recover",
|
|
Long: `After both parties have prepared their keys with the
|
|
'preparekeys' command and have exchanged the files generated from that step,
|
|
one party has to create an offer on how to split the funds that are in the
|
|
channels to be rescued.
|
|
If the other party agrees with the offer, they can sign and publish the offer
|
|
with the 'signoffer' command. If the other party does not agree, they can create
|
|
a counter offer.`,
|
|
Example: `chantools zombierecovery makeoffer \
|
|
--node1_keys preparedkeys-xxxx-xx-xx-<pubkey1>.json \
|
|
--node2_keys preparedkeys-xxxx-xx-xx-<pubkey2>.json \
|
|
--feerate 15`,
|
|
RunE: cc.Execute,
|
|
}
|
|
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.Node1, "node1_keys", "", "the JSON file generated in the"+
|
|
"previous step ('preparekeys') command of node 1",
|
|
)
|
|
cc.cmd.Flags().StringVar(
|
|
&cc.Node2, "node2_keys", "", "the JSON file generated in the"+
|
|
"previous step ('preparekeys') command of node 2",
|
|
)
|
|
cc.cmd.Flags().Uint16Var(
|
|
&cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+
|
|
"use for the sweep transaction in sat/vByte",
|
|
)
|
|
|
|
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
|
|
|
|
return cc.cmd
|
|
}
|
|
|
|
func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, // nolint:gocyclo
|
|
_ []string) error {
|
|
|
|
extendedKey, err := c.rootKey.read()
|
|
if err != nil {
|
|
return fmt.Errorf("error reading root key: %v", err)
|
|
}
|
|
|
|
if c.FeeRate == 0 {
|
|
c.FeeRate = defaultFeeSatPerVByte
|
|
}
|
|
|
|
node1Bytes, err := ioutil.ReadFile(c.Node1)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading node1 key file %s: %v",
|
|
c.Node1, err)
|
|
}
|
|
node2Bytes, err := ioutil.ReadFile(c.Node2)
|
|
if err != nil {
|
|
return fmt.Errorf("error reading node2 key file %s: %v",
|
|
c.Node2, err)
|
|
}
|
|
keys1, keys2 := &match{}, &match{}
|
|
decoder := json.NewDecoder(bytes.NewReader(node1Bytes))
|
|
if err := decoder.Decode(&keys1); err != nil {
|
|
return fmt.Errorf("error decoding node1 key file %s: %v",
|
|
c.Node1, err)
|
|
}
|
|
decoder = json.NewDecoder(bytes.NewReader(node2Bytes))
|
|
if err := decoder.Decode(&keys2); err != nil {
|
|
return fmt.Errorf("error decoding node2 key file %s: %v",
|
|
c.Node2, err)
|
|
}
|
|
|
|
// Make sure the key files were filled correctly.
|
|
if keys1.Node1 == nil || keys1.Node2 == nil {
|
|
return fmt.Errorf("invalid node1 file, node info missing")
|
|
}
|
|
if keys2.Node1 == nil || keys2.Node2 == nil {
|
|
return fmt.Errorf("invalid node2 file, node info missing")
|
|
}
|
|
if keys1.Node1.PubKey != keys2.Node1.PubKey {
|
|
return fmt.Errorf("invalid files, node 1 pubkey doesn't match")
|
|
}
|
|
if keys1.Node2.PubKey != keys2.Node2.PubKey {
|
|
return fmt.Errorf("invalid files, node 2 pubkey doesn't match")
|
|
}
|
|
if len(keys1.Node1.MultisigKeys) == 0 &&
|
|
len(keys1.Node2.MultisigKeys) == 0 {
|
|
|
|
return fmt.Errorf("invalid node1 file, missing multisig keys")
|
|
}
|
|
if len(keys2.Node1.MultisigKeys) == 0 &&
|
|
len(keys2.Node2.MultisigKeys) == 0 {
|
|
|
|
return fmt.Errorf("invalid node2 file, missing multisig keys")
|
|
}
|
|
if len(keys1.Node1.MultisigKeys) == len(keys2.Node1.MultisigKeys) {
|
|
return fmt.Errorf("invalid files, channel info incorrect")
|
|
}
|
|
if len(keys1.Node2.MultisigKeys) == len(keys2.Node2.MultisigKeys) {
|
|
return fmt.Errorf("invalid files, channel info incorrect")
|
|
}
|
|
if len(keys1.Channels) != len(keys2.Channels) {
|
|
return fmt.Errorf("invalid files, channels don't match")
|
|
}
|
|
for idx, node1Channel := range keys1.Channels {
|
|
if keys2.Channels[idx].ChanPoint != node1Channel.ChanPoint {
|
|
return fmt.Errorf("invalid files, channels don't match")
|
|
}
|
|
|
|
if keys2.Channels[idx].Address != node1Channel.Address {
|
|
return fmt.Errorf("invalid files, channels don't match")
|
|
}
|
|
|
|
if keys2.Channels[idx].Address == "" ||
|
|
node1Channel.Address == "" {
|
|
|
|
return fmt.Errorf("invalid files, channel address " +
|
|
"missing")
|
|
}
|
|
}
|
|
|
|
// Make sure one of the nodes is ours.
|
|
_, pubKey, _, err := lnd.DeriveKey(
|
|
extendedKey, lnd.IdentityPath(chainParams), chainParams,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error deriving identity pubkey: %v", err)
|
|
}
|
|
|
|
pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed())
|
|
if keys1.Node1.PubKey != pubKeyStr && keys1.Node2.PubKey != pubKeyStr {
|
|
return fmt.Errorf("derived pubkey %s from seed but that key "+
|
|
"was not found in the match files", pubKeyStr)
|
|
}
|
|
|
|
// Pick the correct list of keys. There are 4 possibilities, given 2
|
|
// files with 2 node slots each.
|
|
var (
|
|
ourKeys []string
|
|
ourPayoutAddr string
|
|
theirKeys []string
|
|
theirPayoutAddr string
|
|
)
|
|
if keys1.Node1.PubKey == pubKeyStr && len(keys1.Node1.MultisigKeys) > 0 {
|
|
ourKeys = keys1.Node1.MultisigKeys
|
|
ourPayoutAddr = keys1.Node1.PayoutAddr
|
|
theirKeys = keys2.Node2.MultisigKeys
|
|
theirPayoutAddr = keys2.Node2.PayoutAddr
|
|
}
|
|
if keys1.Node2.PubKey == pubKeyStr && len(keys1.Node2.MultisigKeys) > 0 {
|
|
ourKeys = keys1.Node2.MultisigKeys
|
|
ourPayoutAddr = keys1.Node2.PayoutAddr
|
|
theirKeys = keys2.Node1.MultisigKeys
|
|
theirPayoutAddr = keys2.Node1.PayoutAddr
|
|
}
|
|
if keys2.Node1.PubKey == pubKeyStr && len(keys2.Node1.MultisigKeys) > 0 {
|
|
ourKeys = keys2.Node1.MultisigKeys
|
|
ourPayoutAddr = keys2.Node1.PayoutAddr
|
|
theirKeys = keys1.Node2.MultisigKeys
|
|
theirPayoutAddr = keys1.Node2.PayoutAddr
|
|
}
|
|
if keys2.Node2.PubKey == pubKeyStr && len(keys2.Node2.MultisigKeys) > 0 {
|
|
ourKeys = keys2.Node2.MultisigKeys
|
|
ourPayoutAddr = keys2.Node2.PayoutAddr
|
|
theirKeys = keys1.Node1.MultisigKeys
|
|
theirPayoutAddr = keys1.Node1.PayoutAddr
|
|
}
|
|
if len(ourKeys) == 0 || len(theirKeys) == 0 {
|
|
return fmt.Errorf("couldn't find necessary keys")
|
|
}
|
|
if ourPayoutAddr == "" || theirPayoutAddr == "" {
|
|
return fmt.Errorf("payout address missing")
|
|
}
|
|
|
|
ourPubKeys := make([]*btcec.PublicKey, len(ourKeys))
|
|
theirPubKeys := make([]*btcec.PublicKey, len(theirKeys))
|
|
for idx, pubKeyHex := range ourKeys {
|
|
ourPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing our pubKey: %v", err)
|
|
}
|
|
}
|
|
for idx, pubKeyHex := range theirKeys {
|
|
theirPubKeys[idx], err = pubKeyFromHex(pubKeyHex)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing their pubKey: %v", err)
|
|
}
|
|
}
|
|
|
|
// Loop through all channels and all keys now, this will definitely take
|
|
// a while.
|
|
channelLoop:
|
|
for _, channel := range keys1.Channels {
|
|
for ourKeyIndex, ourKey := range ourPubKeys {
|
|
for _, theirKey := range theirPubKeys {
|
|
match, witnessScript, err := matchScript(
|
|
channel.Address, ourKey, theirKey,
|
|
chainParams,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error matching "+
|
|
"keys to script: %v", err)
|
|
}
|
|
|
|
if match {
|
|
channel.ourKeyIndex = uint32(ourKeyIndex)
|
|
channel.ourKey = ourKey
|
|
channel.theirKey = theirKey
|
|
channel.witnessScript = witnessScript
|
|
|
|
log.Infof("Found keys for channel %s",
|
|
channel.ChanPoint)
|
|
|
|
continue channelLoop
|
|
}
|
|
}
|
|
}
|
|
|
|
return fmt.Errorf("didn't find matching multisig keys for "+
|
|
"channel %s", channel.ChanPoint)
|
|
}
|
|
|
|
// Let's now sum up the tally of how much of the rescued funds should
|
|
// go to which party.
|
|
var (
|
|
inputs = make([]*wire.TxIn, 0, len(keys1.Channels))
|
|
ourSum int64
|
|
theirSum int64
|
|
)
|
|
for idx, channel := range keys1.Channels {
|
|
op, err := lnd.ParseOutpoint(channel.ChanPoint)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing channel out point: %v",
|
|
err)
|
|
}
|
|
channel.txid = op.Hash.String()
|
|
channel.vout = op.Index
|
|
|
|
ourPart, theirPart, err := askAboutChannel(
|
|
channel, idx+1, len(keys1.Channels), ourPayoutAddr,
|
|
theirPayoutAddr,
|
|
)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
ourSum += ourPart
|
|
theirSum += theirPart
|
|
inputs = append(inputs, &wire.TxIn{
|
|
PreviousOutPoint: *op,
|
|
// It's not actually an old sig script but a witness
|
|
// script but we'll move that to the correct place once
|
|
// we create the PSBT.
|
|
SignatureScript: channel.witnessScript,
|
|
})
|
|
}
|
|
|
|
// Let's create a fee estimator now to give an overview over the
|
|
// deducted fees.
|
|
estimator := input.TxWeightEstimator{}
|
|
|
|
// Only add output for us if we should receive something.
|
|
if ourSum > 0 {
|
|
estimator.AddP2WKHOutput()
|
|
}
|
|
if theirSum > 0 {
|
|
estimator.AddP2WKHOutput()
|
|
}
|
|
for range inputs {
|
|
estimator.AddWitnessInput(input.MultiSigWitnessSize)
|
|
}
|
|
feeRateKWeight := chainfee.SatPerKVByte(1000 * c.FeeRate).FeePerKWeight()
|
|
totalFee := int64(feeRateKWeight.FeeForWeight(int64(estimator.Weight())))
|
|
|
|
fmt.Printf("Current tally (before fees):\n\t"+
|
|
"To our address (%s): %d sats\n\t"+
|
|
"To their address (%s): %d sats\n\t"+
|
|
"Estimated fees (at rate %d sat/vByte): %d sats\n",
|
|
ourPayoutAddr, ourSum, theirPayoutAddr, theirSum, c.FeeRate,
|
|
totalFee)
|
|
|
|
// Distribute the fees.
|
|
halfFee := totalFee / 2
|
|
switch {
|
|
case ourSum-halfFee > 0 && theirSum-halfFee > 0:
|
|
ourSum -= halfFee
|
|
theirSum -= halfFee
|
|
|
|
case ourSum-totalFee > 0:
|
|
ourSum -= totalFee
|
|
|
|
case theirSum-totalFee > 0:
|
|
theirSum -= totalFee
|
|
|
|
default:
|
|
return fmt.Errorf("error distributing fees, unhandled case")
|
|
}
|
|
|
|
// Our output.
|
|
pkScript, err := lnd.GetP2WPKHScript(ourPayoutAddr, chainParams)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing our payout address: %v", err)
|
|
}
|
|
ourTxOut := &wire.TxOut{
|
|
PkScript: pkScript,
|
|
Value: ourSum,
|
|
}
|
|
|
|
// Their output
|
|
pkScript, err = lnd.GetP2WPKHScript(theirPayoutAddr, chainParams)
|
|
if err != nil {
|
|
return fmt.Errorf("error parsing their payout address: %v", err)
|
|
}
|
|
theirTxOut := &wire.TxOut{
|
|
PkScript: pkScript,
|
|
Value: theirSum,
|
|
}
|
|
|
|
// Don't create dust.
|
|
if txrules.IsDustOutput(ourTxOut, txrules.DefaultRelayFeePerKb) {
|
|
ourSum = 0
|
|
}
|
|
if txrules.IsDustOutput(theirTxOut, txrules.DefaultRelayFeePerKb) {
|
|
theirSum = 0
|
|
}
|
|
|
|
fmt.Printf("Current tally (after fees):\n\t"+
|
|
"To our address (%s): %d sats\n\t"+
|
|
"To their address (%s): %d sats\n",
|
|
ourPayoutAddr, ourSum, theirPayoutAddr, theirSum)
|
|
|
|
// And now create the PSBT.
|
|
tx := wire.NewMsgTx(2)
|
|
if ourSum > 0 {
|
|
tx.TxOut = append(tx.TxOut, ourTxOut)
|
|
}
|
|
if theirSum > 0 {
|
|
tx.TxOut = append(tx.TxOut, theirTxOut)
|
|
}
|
|
for _, txIn := range inputs {
|
|
tx.TxIn = append(tx.TxIn, &wire.TxIn{
|
|
PreviousOutPoint: txIn.PreviousOutPoint,
|
|
})
|
|
}
|
|
packet, err := psbt.NewFromUnsignedTx(tx)
|
|
if err != nil {
|
|
return fmt.Errorf("error creating PSBT from TX: %v", err)
|
|
}
|
|
|
|
signer := &lnd.Signer{
|
|
ExtendedKey: extendedKey,
|
|
ChainParams: chainParams,
|
|
}
|
|
for idx, txIn := range inputs {
|
|
channel := keys1.Channels[idx]
|
|
|
|
// We've mis-used this field to transport the witness script,
|
|
// let's now copy it to the correct place.
|
|
packet.Inputs[idx].WitnessScript = txIn.SignatureScript
|
|
|
|
// Let's prepare the witness UTXO.
|
|
pkScript, err := input.WitnessScriptHash(channel.witnessScript)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
packet.Inputs[idx].WitnessUtxo = &wire.TxOut{
|
|
PkScript: pkScript,
|
|
Value: channel.Capacity,
|
|
}
|
|
|
|
// We'll be signing with our key so we can just add the other
|
|
// party's pubkey as additional info so it's easy for them to
|
|
// sign as well.
|
|
packet.Inputs[idx].Unknowns = append(
|
|
packet.Inputs[idx].Unknowns, &psbt.Unknown{
|
|
Key: PsbtKeyTypeOutputMissingSigPubkey,
|
|
Value: channel.theirKey.SerializeCompressed(),
|
|
},
|
|
)
|
|
|
|
keyDesc := keychain.KeyDescriptor{
|
|
PubKey: channel.ourKey,
|
|
KeyLocator: keychain.KeyLocator{
|
|
Family: keychain.KeyFamilyMultiSig,
|
|
Index: channel.ourKeyIndex,
|
|
},
|
|
}
|
|
utxo := &wire.TxOut{
|
|
Value: channel.Capacity,
|
|
}
|
|
err = signer.AddPartialSignature(
|
|
packet, keyDesc, utxo, txIn.SignatureScript, idx,
|
|
)
|
|
if err != nil {
|
|
return fmt.Errorf("error signing input %d: %v", idx,
|
|
err)
|
|
}
|
|
}
|
|
|
|
// Looks like we're done!
|
|
base64, err := packet.B64Encode()
|
|
if err != nil {
|
|
return fmt.Errorf("error encoding PSBT: %v", err)
|
|
}
|
|
|
|
fmt.Printf("Done creating offer, please send this PSBT string to \n"+
|
|
"the other party to review and sign (if they accept): \n%s\n",
|
|
base64)
|
|
|
|
return nil
|
|
}
|
|
|
|
func matchScript(address string, key1, key2 *btcec.PublicKey,
|
|
params *chaincfg.Params) (bool, []byte, error) {
|
|
|
|
channelScript, err := lnd.GetP2WSHScript(address, params)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
witnessScript, err := input.GenMultiSigScript(
|
|
key1.SerializeCompressed(), key2.SerializeCompressed(),
|
|
)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
pkScript, err := input.WitnessScriptHash(witnessScript)
|
|
if err != nil {
|
|
return false, nil, err
|
|
}
|
|
|
|
return bytes.Equal(channelScript, pkScript), witnessScript, nil
|
|
}
|
|
|
|
func askAboutChannel(channel *channel, current, total int, ourAddr,
|
|
theirAddr string) (int64, int64, error) {
|
|
|
|
fundingTxid := strings.Split(channel.ChanPoint, ":")[0]
|
|
|
|
fmt.Printf("Channel %s (%d of %d): \n\tCapacity: %d sat\n\t"+
|
|
"Funding TXID: https://blockstream.info/tx/%v\n\t"+
|
|
"Channel info: https://1ml.com/channel/%s\n\t"+
|
|
"Channel funding address: %s\n\n"+
|
|
"How many sats should go to you (%s) before fees?: ",
|
|
channel.ChanPoint, current, total, channel.Capacity,
|
|
fundingTxid, channel.ChannelID, channel.Address, ourAddr)
|
|
reader := bufio.NewReader(os.Stdin)
|
|
ourPartStr, err := reader.ReadString('\n')
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
ourPart, err := strconv.ParseUint(strings.TrimSpace(ourPartStr), 10, 64)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
// Let the user try again if they entered something incorrect.
|
|
if int64(ourPart) > channel.Capacity {
|
|
fmt.Printf("Cannot send more than %d sats to ourself!\n",
|
|
channel.Capacity)
|
|
return askAboutChannel(
|
|
channel, current, total, ourAddr, theirAddr,
|
|
)
|
|
}
|
|
|
|
theirPart := channel.Capacity - int64(ourPart)
|
|
fmt.Printf("\nWill send: \n\t%d sats to our address (%s) and \n\t"+
|
|
"%d sats to the other peer's address (%s).\n\n", ourPart,
|
|
ourAddr, theirPart, theirAddr)
|
|
|
|
return int64(ourPart), theirPart, nil
|
|
}
|