zombierecovery: add new commands for zombie channel recovery

pull/20/head v0.8.4
Oliver Gugger 3 years ago
parent 7a3c9a3f0b
commit af356685c1
No known key found for this signature in database
GPG Key ID: 8E4256593F177720

@ -185,7 +185,7 @@ compacting the DB).
don't have a `channel.db` file or because `chantools` couldn't rescue all your
node's channels. There are a few things you can try manually that have some
chance of working:
- Make sure you can connect to all nodes when restoring from SCB: It happens
- Make sure you can connect to all nodes when restoring from SCB: It happens
all the time that nodes change their IP addresses. When restoring from a
static channel backup, your node tries to connect to the node using the IP
address encoded in the backup file. If the address changed, the SCB restore
@ -194,13 +194,20 @@ compacting the DB).
`lncli connect <node-pubkey>@<updated-ip-address>:<port>` in the recovered
`lnd` node from step 3 and wait a few hours to see if the channel is now
being force closed by the remote node.
- Find out who the node belongs to: Maybe you opened the channel with someone
- Find out who the node belongs to: Maybe you opened the channel with someone
you know. Or maybe their node alias contains some information about who the
node belongs to. If you can find out who operates the remote node, you can
ask them to force-close the channel from their end. If the channel was opened
with the `option_static_remote_key`, (`lnd v0.8.0` and later), the funds can
be swept by your node.
12. **Use Zombie Channel Recovery Matcher**: As a final, last resort, you can
go to [node-recovery.com](https://www.node-recovery.com/) and register your
node's ID for being matched up against other nodes with the same problem.
<br/><br/>
Once you were contacted with a match, follow the instructions on the
[Zombie Channel Recovery Guide](doc/zombierecovery.md) page.
## Seed and passphrase input
All commands that require the seed (and, if set, the seed's passphrase) offer

@ -6,6 +6,7 @@ import (
"errors"
"fmt"
"net/http"
"strconv"
"strings"
)
@ -89,6 +90,30 @@ func (a *ExplorerAPI) Outpoint(addr string) (*TX, int, error) {
return nil, 0, fmt.Errorf("no tx found")
}
func (a *ExplorerAPI) Address(outpoint string) (string, error) {
parts := strings.Split(outpoint, ":")
if len(parts) != 2 {
return "", fmt.Errorf("invalid outpoint: %v", outpoint)
}
tx, err := a.Transaction(parts[0])
if err != nil {
return "", err
}
vout, err := strconv.Atoi(parts[1])
if err != nil {
return "", err
}
if len(tx.Vout) <= vout {
return "", fmt.Errorf("invalid output index: %d", vout)
}
return tx.Vout[vout].ScriptPubkeyAddr, nil
}
func (a *ExplorerAPI) PublishTx(rawTxHex string) (string, error) {
url := fmt.Sprintf("%s/tx", a.BaseURL)
resp, err := http.Post(url, "text/plain", strings.NewReader(rawTxHex))

@ -102,7 +102,7 @@ func reportOutspend(api *ExplorerAPI,
entry.ClosingTX.ToRemoteAddr = o.ScriptPubkeyAddr
}
}
if couldBeOurs(entry, utxo) {
summaryFile.ChannelsWithPotential++
summaryFile.FundsForceClose += utxo[0].Value

@ -67,6 +67,6 @@ func (c *dropChannelGraphCommand) Execute(_ *cobra.Command, _ []string) error {
if err := rwTx.DeleteTopLevelBucket(graphMetaBucket); err != nil {
return err
}
return rwTx.Commit()
}

@ -4,7 +4,6 @@ import (
"bytes"
"encoding/hex"
"fmt"
"github.com/lightningnetwork/lnd/tor"
"io/ioutil"
"net"
"strconv"
@ -21,6 +20,7 @@ import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tor"
"github.com/spf13/cobra"
)
@ -166,21 +166,21 @@ func (c *fakeChanBackupCommand) Execute(_ *cobra.Command, _ []string) error {
}
// Parse the short channel ID.
splitChanId := strings.Split(c.ShortChanID, "x")
if len(splitChanId) != 3 {
splitChanID := strings.Split(c.ShortChanID, "x")
if len(splitChanID) != 3 {
return fmt.Errorf("--short_channel_id expected in format: " +
"<blockheight>x<transactionindex>x<outputindex>",
)
}
blockHeight, err := strconv.ParseInt(splitChanId[0], 10, 32)
blockHeight, err := strconv.ParseInt(splitChanID[0], 10, 32)
if err != nil {
return fmt.Errorf("could not parse block height: %s", err)
}
txIndex, err := strconv.ParseInt(splitChanId[1], 10, 32)
txIndex, err := strconv.ParseInt(splitChanID[1], 10, 32)
if err != nil {
return fmt.Errorf("could not parse transaction index: %s", err)
}
chanOutputIdx, err := strconv.ParseInt(splitChanId[2], 10, 32)
chanOutputIdx, err := strconv.ParseInt(splitChanID[2], 10, 32)
if err != nil {
return fmt.Errorf("could not parse output index: %s", err)
}

@ -225,7 +225,7 @@ func commitPointsFromLogFile(lndLog string) ([]*btcec.PublicKey, error) {
dedupMap[groups[1]] = commitPoint
}
var result []*btcec.PublicKey
result := make([]*btcec.PublicKey, 0, len(dedupMap))
for _, commitPoint := range dedupMap {
result = append(result, commitPoint)
}

@ -26,7 +26,7 @@ import (
const (
defaultAPIURL = "https://blockstream.info/api"
version = "0.8.3"
version = "0.8.4"
na = "n/a"
Commit = ""
@ -102,6 +102,7 @@ func main() {
newSweepTimeLockManualCommand(),
newVanityGenCommand(),
newWalletInfoCommand(),
newZombieRecoveryCommand(),
)
if err := rootCmd.Execute(); err != nil {

@ -19,7 +19,7 @@ import (
)
const (
defaultFeeSatPerVByte = 2
defaultFeeSatPerVByte = 30
defaultCsvLimit = 2016
)

@ -0,0 +1,200 @@
package main
import (
"encoding/json"
"fmt"
"io/ioutil"
"regexp"
"time"
"github.com/btcsuite/btcd/btcec"
"github.com/gogo/protobuf/jsonpb"
"github.com/guggero/chantools/btc"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/lnrpc"
"github.com/spf13/cobra"
)
var (
patternRegistration = regexp.MustCompile(
"(?m)(?s)ID: ([0-9a-f]{66})\nContact: (.*?)\n" +
"Time: ")
)
type nodeInfo struct {
PubKey string `json:"identity_pubkey"`
Contact string `json:"contact"`
PayoutAddr string `json:"payout_addr,omitempty"`
MultisigKeys []string `json:"multisig_keys,omitempty"`
}
type channel struct {
ChannelID string `json:"short_channel_id"`
ChanPoint string `json:"chan_point"`
Address string `json:"address"`
Capacity int64 `json:"capacity"`
txid string
vout uint32
ourKeyIndex uint32
ourKey *btcec.PublicKey
theirKey *btcec.PublicKey
witnessScript []byte
}
type match struct {
Node1 *nodeInfo `json:"node1"`
Node2 *nodeInfo `json:"node2"`
Channels []*channel `json:"channels"`
}
type zombieRecoveryFindMatchesCommand struct {
APIURL string
Registrations string
ChannelGraph string
cmd *cobra.Command
}
func newZombieRecoveryFindMatchesCommand() *cobra.Command {
cc := &zombieRecoveryFindMatchesCommand{}
cc.cmd = &cobra.Command{
Use: "findmatches",
Short: "[0/3] Match maker only: Find matches between " +
"registered nodes",
Long: `Match maker only: Runs through all the nodes that have
registered their ID on https://www.node-recovery.com and checks whether there
are any matches of channels between them by looking at the whole channel graph.
This command will be run by guggero and the result will be sent to the
registered nodes.`,
Example: `chantools zombierecovery findmatches \
--registrations data.txt \
--channel_graph lncli_describegraph.json`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.APIURL, "apiurl", defaultAPIURL, "API URL to use (must "+
"be esplora compatible)",
)
cc.cmd.Flags().StringVar(
&cc.Registrations, "registrations", "", "the raw data.txt "+
"where the registrations are stored in",
)
cc.cmd.Flags().StringVar(
&cc.ChannelGraph, "channel_graph", "", "the full LN channel "+
"graph in the JSON format that the "+
"'lncli describegraph' returns",
)
return cc.cmd
}
func (c *zombieRecoveryFindMatchesCommand) Execute(_ *cobra.Command,
_ []string) error {
api := &btc.ExplorerAPI{BaseURL: c.APIURL}
logFileBytes, err := ioutil.ReadFile(c.Registrations)
if err != nil {
return fmt.Errorf("error reading registrations file %s: %v",
c.Registrations, err)
}
allMatches := patternRegistration.FindAllStringSubmatch(
string(logFileBytes), -1,
)
registrations := make(map[string]string, len(allMatches))
for _, groups := range allMatches {
if _, err := pubKeyFromHex(groups[1]); err != nil {
return fmt.Errorf("error parsing node ID: %v", err)
}
registrations[groups[1]] = groups[2]
log.Infof("%s: %s", groups[1], groups[2])
}
graphBytes, err := ioutil.ReadFile(c.ChannelGraph)
if err != nil {
return fmt.Errorf("error reading graph JSON file %s: "+
"%v", c.ChannelGraph, err)
}
graph := &lnrpc.ChannelGraph{}
err = jsonpb.UnmarshalString(string(graphBytes), graph)
if err != nil {
return fmt.Errorf("error parsing graph JSON: %v", err)
}
// Loop through all nodes now.
matches := make(map[string]map[string]*match)
for node1, contact1 := range registrations {
matches[node1] = make(map[string]*match)
for node2, contact2 := range registrations {
if node1 == node2 {
continue
}
// We've already looked at this pair.
if matches[node2][node1] != nil {
continue
}
edges := lnd.FindCommonEdges(graph, node1, node2)
if len(edges) > 0 {
matches[node1][node2] = &match{
Node1: &nodeInfo{
PubKey: node1,
Contact: contact1,
},
Node2: &nodeInfo{
PubKey: node2,
Contact: contact2,
},
Channels: make([]*channel, len(edges)),
}
for idx, edge := range edges {
cid := fmt.Sprintf("%d", edge.ChannelId)
c := &channel{
ChannelID: cid,
ChanPoint: edge.ChanPoint,
Capacity: edge.Capacity,
}
addr, err := api.Address(c.ChanPoint)
if err == nil {
c.Address = addr
}
matches[node1][node2].Channels[idx] = c
}
}
}
}
// Write the matches to files.
for node1, node1map := range matches {
for node2, match := range node1map {
if match == nil {
continue
}
matchBytes, err := json.MarshalIndent(match, "", " ")
if err != nil {
return err
}
fileName := fmt.Sprintf("results/match-%s-%s-%s.json",
time.Now().Format("2006-01-02"),
node1, node2)
log.Infof("Writing result to %s", fileName)
err = ioutil.WriteFile(fileName, matchBytes, 0644)
if err != nil {
return err
}
}
}
return nil
}

@ -0,0 +1,505 @@
package main
import (
"bufio"
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"strconv"
"strings"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/wire"
"github.com/btcsuite/btcutil/psbt"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet"
"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")
}
// Don't create dust.
if ourSum <= int64(lnwallet.DefaultDustLimit()) {
ourSum = 0
}
if theirSum <= int64(lnwallet.DefaultDustLimit()) {
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 {
pkScript, err := lnd.GetP2WPKHScript(ourPayoutAddr, chainParams)
if err != nil {
return fmt.Errorf("error parsing our payout address: "+
"%v", err)
}
tx.TxOut = append(tx.TxOut, &wire.TxOut{
PkScript: pkScript,
Value: ourSum,
})
}
if theirSum > 0 {
pkScript, err := lnd.GetP2WPKHScript(
theirPayoutAddr, chainParams,
)
if err != nil {
return fmt.Errorf("error parsing their payout "+
"address: %v", err)
}
tx.TxOut = append(tx.TxOut, &wire.TxOut{
PkScript: pkScript,
Value: theirSum,
})
}
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
}

@ -0,0 +1,138 @@
package main
import (
"bytes"
"encoding/hex"
"encoding/json"
"fmt"
"io/ioutil"
"time"
"github.com/guggero/chantools/lnd"
"github.com/spf13/cobra"
)
const (
numMultisigKeys = 2500
)
type zombieRecoveryPrepareKeysCommand struct {
MatchFile string
PayoutAddr string
rootKey *rootKey
cmd *cobra.Command
}
func newZombieRecoveryPrepareKeysCommand() *cobra.Command {
cc := &zombieRecoveryPrepareKeysCommand{}
cc.cmd = &cobra.Command{
Use: "preparekeys",
Short: "[1/3] Prepare all public keys for a recovery attempt",
Long: `Takes a match file, validates it against the seed and
then adds the first 2500 multisig pubkeys to it.
This must be run by both parties of a channel for a successful recovery. The
next step (makeoffer) takes two such key enriched files and tries to find the
correct ones for the matched channels.`,
Example: `chantools zombierecovery preparekeys \
--match_file match-xxxx-xx-xx-<pubkey1>-<pubkey2>.json \
--payout_addr bc1q...`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.MatchFile, "match_file", "", "the match JSON file that "+
"was sent to both nodes by the match maker",
)
cc.cmd.Flags().StringVar(
&cc.PayoutAddr, "payout_addr", "", "the address where this "+
"node's rescued funds should be sent to, must be a "+
"P2WPKH (native SegWit) address")
cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys")
return cc.cmd
}
func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command,
_ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %v", err)
}
_, err = lnd.GetP2WPKHScript(c.PayoutAddr, chainParams)
if err != nil {
return fmt.Errorf("invalid payout address, must be P2WPKH")
}
matchFileBytes, err := ioutil.ReadFile(c.MatchFile)
if err != nil {
return fmt.Errorf("error reading match file %s: %v",
c.MatchFile, err)
}
decoder := json.NewDecoder(bytes.NewReader(matchFileBytes))
match := &match{}
if err := decoder.Decode(&match); err != nil {
return fmt.Errorf("error decoding match file %s: %v",
c.MatchFile, err)
}
// Make sure the match file was filled correctly.
if match.Node1 == nil || match.Node2 == nil {
return fmt.Errorf("invalid match file, node info missing")
}
_, 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())
var nodeInfo *nodeInfo
switch {
case match.Node1.PubKey != pubKeyStr && match.Node2.PubKey != pubKeyStr:
return fmt.Errorf("derived pubkey %s from seed but that key "+
"was not found in the match file %s", pubKeyStr,
c.MatchFile)
case match.Node1.PubKey == pubKeyStr:
nodeInfo = match.Node1
default:
nodeInfo = match.Node2
}
// Derive all 2500 keys now, this might take a while.
for index := 0; index < numMultisigKeys; index++ {
_, pubKey, _, err := lnd.DeriveKey(
extendedKey, lnd.MultisigPath(chainParams, index),
chainParams,
)
if err != nil {
return fmt.Errorf("error deriving multisig pubkey: %v",
err)
}
nodeInfo.MultisigKeys = append(
nodeInfo.MultisigKeys,
hex.EncodeToString(pubKey.SerializeCompressed()),
)
}
nodeInfo.PayoutAddr = c.PayoutAddr
// Write the result back into a new file.
matchBytes, err := json.MarshalIndent(match, "", " ")
if err != nil {
return err
}
fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json",
time.Now().Format("2006-01-02"), pubKeyStr)
log.Infof("Writing result to %s", fileName)
return ioutil.WriteFile(fileName, matchBytes, 0644)
}

@ -0,0 +1,42 @@
package main
import (
"os"
"github.com/spf13/cobra"
)
type zombieRecoveryCommand struct {
cmd *cobra.Command
}
func newZombieRecoveryCommand() *cobra.Command {
cc := &zombieRecoveryCommand{}
cc.cmd = &cobra.Command{
Use: "zombierecovery",
Short: "Try rescuing funds stuck in channels with zombie nodes",
Long: `A sub command that hosts a set of further sub commands
to help with recovering funds tuck in zombie channels.
Please visit https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
for more information on how to use these commands.`,
Run: func(cmd *cobra.Command, args []string) {
if len(args) == 0 {
_ = cmd.Help()
os.Exit(0)
}
},
}
cobra.EnableCommandSorting = false
cc.cmd.AddCommand(
// Here the order matters, we don't want them to be
// alphabetically sorted but by step number.
newZombieRecoveryFindMatchesCommand(),
newZombieRecoveryPrepareKeysCommand(),
newZombieRecoveryMakeOfferCommand(),
newZombieRecoverySignOfferCommand(),
)
return cc.cmd
}

@ -0,0 +1,193 @@
package main
import (
"bufio"
"bytes"
"fmt"
"github.com/btcsuite/btcd/txscript"
"os"
"github.com/btcsuite/btcd/btcec"
"github.com/btcsuite/btcutil/hdkeychain"
"github.com/btcsuite/btcutil/psbt"
"github.com/guggero/chantools/lnd"
"github.com/lightningnetwork/lnd/keychain"
"github.com/spf13/cobra"
)
type zombieRecoverySignOfferCommand struct {
Psbt string
rootKey *rootKey
cmd *cobra.Command
}
func newZombieRecoverySignOfferCommand() *cobra.Command {
cc := &zombieRecoverySignOfferCommand{}
cc.cmd = &cobra.Command{
Use: "signoffer",
Short: "[3/3] Sign an offer sent by the remote peer to " +
"recover funds",
Long: `Inspect and sign an offer that was sent by the remote
peer to recover funds from one or more channels.`,
Example: `chantools zombierecovery signoffer \
--psbt <offered_psbt_base64>`,
RunE: cc.Execute,
}
cc.cmd.Flags().StringVar(
&cc.Psbt, "psbt", "", "the base64 encoded PSBT that the other "+
"party sent as an offer to rescue funds",
)
cc.rootKey = newRootKey(cc.cmd, "signing the offer")
return cc.cmd
}
func (c *zombieRecoverySignOfferCommand) Execute(_ *cobra.Command,
_ []string) error {
extendedKey, err := c.rootKey.read()
if err != nil {
return fmt.Errorf("error reading root key: %v", err)
}
signer := &lnd.Signer{
ExtendedKey: extendedKey,
ChainParams: chainParams,
}
// Decode the PSBT.
packet, err := psbt.NewFromRawBytes(
bytes.NewReader([]byte(c.Psbt)), true,
)
if err != nil {
return fmt.Errorf("error decoding PSBT: %v", err)
}
return signOffer(extendedKey, packet, signer)
}
func signOffer(rootKey *hdkeychain.ExtendedKey,
packet *psbt.Packet, signer *lnd.Signer) error {
// 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: %v",
err)
}
// Now let's check that the packet has the expected proprietary key with
// our pubkey that we need to sign with.
if len(packet.Inputs) == 0 {
return fmt.Errorf("invalid PSBT, expected at least 1 input, "+
"got %d", len(packet.Inputs))
}
for idx := range packet.Inputs {
if len(packet.Inputs[idx].Unknowns) != 1 {
return fmt.Errorf("invalid PSBT, expected 1 unknown "+
"in input %d, got %d", idx,
len(packet.Inputs[idx].Unknowns))
}
}
fmt.Printf("The PSBT contains the following proposal:\n\n\t"+
"Close %d channels: \n", len(packet.Inputs))
var totalInput int64
for idx, txIn := range packet.UnsignedTx.TxIn {
value := packet.Inputs[idx].WitnessUtxo.Value
totalInput += value
fmt.Printf("\tChannel %d (%s:%d), capacity %d sats\n",
idx, txIn.PreviousOutPoint.Hash.String(),
txIn.PreviousOutPoint.Index, value)
}
fmt.Println()
var totalOutput int64
for _, txOut := range packet.UnsignedTx.TxOut {
totalOutput += txOut.Value
pkScript, err := txscript.ParsePkScript(txOut.PkScript)
if err != nil {
return fmt.Errorf("error parsing pk script: %v", err)
}
addr, err := pkScript.Address(chainParams)
if err != nil {
return fmt.Errorf("error parsing address: %v", err)
}
fmt.Printf("\tSend %d sats to address %s\n", txOut.Value, addr)
}
fmt.Printf("\n\tTotal fees: %d sats\n\nDo you want to continue?\n",
totalInput-totalOutput)
fmt.Printf("Press <enter> to continue and sign the transaction or " +
"<ctrl+c> to abort: ")
_, _ = bufio.NewReader(os.Stdin).ReadString('\n')
for idx := range packet.Inputs {
unknown := packet.Inputs[idx].Unknowns[0]
if !bytes.Equal(unknown.Key, PsbtKeyTypeOutputMissingSigPubkey) {
return fmt.Errorf("invalid PSBT, unknown has invalid "+
"key %x, expected %x", unknown.Key,
PsbtKeyTypeOutputMissingSigPubkey)
}
targetKey, err := btcec.ParsePubKey(unknown.Value, btcec.S256())
if err != nil {
return fmt.Errorf("invalid PSBT, proprietary key has "+
"invalid pubkey: %v", err)
}
// Now we can look up the local key and check the PSBT further,
// then add our signature.
localKeyDesc, err := findLocalMultisigKey(
localMultisig, targetKey,
)
if err != nil {
return fmt.Errorf("could not find local multisig key: "+
"%v", err)
}
if len(packet.Inputs[idx].WitnessScript) == 0 {
return fmt.Errorf("invalid PSBT, missing witness " +
"script")
}
witnessScript := packet.Inputs[idx].WitnessScript
if packet.Inputs[idx].WitnessUtxo == nil {
return fmt.Errorf("invalid PSBT, witness UTXO missing")
}
utxo := packet.Inputs[idx].WitnessUtxo
err = signer.AddPartialSignature(
packet, *localKeyDesc, utxo, witnessScript, idx,
)
if err != nil {
return fmt.Errorf("error adding partial signature: %v",
err)
}
}
// We're almost done. Now we just need to make sure we can finalize and
// extract the final TX.
err = psbt.MaybeFinalizeAll(packet)
if err != nil {
return fmt.Errorf("error finalizing PSBT: %v", err)
}
finalTx, err := psbt.Extract(packet)
if err != nil {
return fmt.Errorf("unable to extract final TX: %v", err)
}
var buf bytes.Buffer
err = finalTx.Serialize(&buf)
if err != nil {
return fmt.Errorf("unable to serialize final TX: %v", err)
}
fmt.Printf("Success, we counter signed the PSBT and extracted the "+
"final\ntransaction. Please publish this using any bitcoin "+
"node:\n\n%x\n\n", buf.Bytes())
return nil
}

@ -40,4 +40,5 @@ Complete documentation is available at https://github.com/guggero/chantools/.
* [chantools sweeptimelockmanual](chantools_sweeptimelockmanual.md) - Sweep the force-closed state of a single channel manually if only a channel backup file is available
* [chantools vanitygen](chantools_vanitygen.md) - Generate a seed with a custom lnd node identity public key that starts with the given prefix
* [chantools walletinfo](chantools_walletinfo.md) - Shows info about an lnd wallet.db file and optionally extracts the BIP32 HD root key
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -15,7 +15,7 @@ chantools derivekey [flags]
```
chantools derivekey --path "m/1017'/0'/5'/0/0'" \
--neuter
--neuter
chantools derivekey --identity
```

@ -36,7 +36,7 @@ chantools rescuefunding \
--channeldb string lnd channel.db file to rescue a channel from; must contain the pending channel specified with --channelpoint
--channelpoint string funding transaction outpoint of the channel to rescue (<txid>:<txindex>) as it is recorded in the DB
--confirmedchannelpoint string 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 ifthis is left empty
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 2)
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for rescuefunding
--rootkey string BIP32 HD root key of the wallet to use for deriving keys; leave empty to prompt for lnd 24 word aezeed
--sweepaddr string address to sweep the funds to

@ -30,7 +30,7 @@ chantools sweeptimelock \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 2)
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--fromchanneldb string channel input is in the format of an lnd channel.db file
--fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin
-h, --help help for sweeptimelock

@ -36,7 +36,7 @@ chantools sweeptimelockmanual \
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 2)
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
--fromchanneldb string channel input is in the format of an lnd channel.db file
--fromsummary string channel input is in the format of chantool's channel summary; specify '-' to read from stdin
-h, --help help for sweeptimelockmanual

@ -0,0 +1,37 @@
## chantools zombierecovery
Try rescuing funds stuck in channels with zombie nodes
### Synopsis
A sub command that hosts a set of further sub commands
to help with recovering funds tuck in zombie channels.
Please visit https://github.com/guggero/chantools/blob/master/doc/zombierecovery.md
for more information on how to use these commands.
```
chantools zombierecovery [flags]
```
### Options
```
-h, --help help for zombierecovery
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools](chantools.md) - Chantools helps recover funds from lightning channels
* [chantools zombierecovery findmatches](chantools_zombierecovery_findmatches.md) - [0/3] Match maker only: Find matches between registered nodes
* [chantools zombierecovery makeoffer](chantools_zombierecovery_makeoffer.md) - [2/3] Make an offer on how to split the funds to recover
* [chantools zombierecovery preparekeys](chantools_zombierecovery_preparekeys.md) - [1/3] Prepare all public keys for a recovery attempt
* [chantools zombierecovery signoffer](chantools_zombierecovery_signoffer.md) - [3/3] Sign an offer sent by the remote peer to recover funds

@ -0,0 +1,45 @@
## chantools zombierecovery findmatches
[0/3] Match maker only: Find matches between registered nodes
### Synopsis
Match maker only: Runs through all the nodes that have
registered their ID on https://www.node-recovery.com and checks whether there
are any matches of channels between them by looking at the whole channel graph.
This command will be run by guggero and the result will be sent to the
registered nodes.
```
chantools zombierecovery findmatches [flags]
```
### Examples
```
chantools zombierecovery findmatches \
--registrations data.txt \
--channel_graph lncli_describegraph.json
```
### Options
```
--apiurl string API URL to use (must be esplora compatible) (default "https://blockstream.info/api")
--channel_graph string the full LN channel graph in the JSON format that the 'lncli describegraph' returns
-h, --help help for findmatches
--registrations string the raw data.txt where the registrations are stored in
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -0,0 +1,49 @@
## chantools zombierecovery makeoffer
[2/3] Make an offer on how to split the funds to recover
### Synopsis
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.
```
chantools zombierecovery makeoffer [flags]
```
### Examples
```
chantools zombierecovery makeoffer \
--node1_keys preparedkeys-xxxx-xx-xx-<pubkey1>.json \
--node2_keys preparedkeys-xxxx-xx-xx-<pubkey2>.json \
--feerate 15
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
--feerate uint16 fee rate to use for the sweep transaction in sat/vByte (default 30)
-h, --help help for makeoffer
--node1_keys string the JSON file generated in theprevious step ('preparekeys') command of node 1
--node2_keys string the JSON file generated in theprevious step ('preparekeys') command of node 2
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -0,0 +1,45 @@
## chantools zombierecovery preparekeys
[1/3] Prepare all public keys for a recovery attempt
### Synopsis
Takes a match file, validates it against the seed and
then adds the first 2500 multisig pubkeys to it.
This must be run by both parties of a channel for a successful recovery. The
next step (makeoffer) takes two such key enriched files and tries to find the
correct ones for the matched channels.
```
chantools zombierecovery preparekeys [flags]
```
### Examples
```
chantools zombierecovery preparekeys \
--match_file match-xxxx-xx-xx-<pubkey1>-<pubkey2>.json \
--payout_addr bc1q...
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for preparekeys
--match_file string the match JSON file that was sent to both nodes by the match maker
--payout_addr string the address where this node's rescued funds should be sent to, must be a P2WPKH (native SegWit) address
--rootkey string BIP32 HD root key of the wallet to use for deriving the multisig keys; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -0,0 +1,35 @@
## chantools zombierecovery signoffer
[3/3] Sign an offer sent by the remote peer to recover funds
```
chantools zombierecovery signoffer [flags]
```
### Examples
```
chantools zombierecovery signoffer \
--psbt <offered_psbt_base64>
```
### Options
```
--bip39 read a classic BIP39 seed and passphrase from the terminal instead of asking for lnd seed format or providing the --rootkey flag
-h, --help help for signoffer
--psbt string the base64 encoded PSBT that the other party sent as an offer to rescue funds
--rootkey string BIP32 HD root key of the wallet to use for signing the offer; leave empty to prompt for lnd 24 word aezeed
```
### Options inherited from parent commands
```
-r, --regtest Indicates if regtest parameters should be used
-t, --testnet Indicates if testnet parameters should be used
```
### SEE ALSO
* [chantools zombierecovery](chantools_zombierecovery.md) - Try rescuing funds stuck in channels with zombie nodes

@ -24,6 +24,7 @@ if "Pending/Open\nchannels left?" then
else
-->[no] ===MANUAL===
--> "<b>11:</b> Manual intervention necessary"
--> "<b>12:</b> Use Zombie Channel Recovery Matcher"
--> (*)
endif
else

Binary file not shown.

Before

Width:  |  Height:  |  Size: 68 KiB

After

Width:  |  Height:  |  Size: 74 KiB

@ -0,0 +1,6 @@
# Zombie Channel Recovery
TODO, more detailed documentation is being worked on.
Please look at the help output of `chantools zombierecovery --help` or the
generated [documentation for it](chantools_zombierecovery.md) for now.

@ -8,27 +8,46 @@ import (
func AllNodeChannels(graph *lnrpc.ChannelGraph,
nodePubKey string) []*lnrpc.ChannelEdge {
var result []*lnrpc.ChannelEdge
var result []*lnrpc.ChannelEdge // nolint:prealloc
for _, edge := range graph.Edges {
if edge.Node1Pub != nodePubKey && edge.Node2Pub != nodePubKey {
continue
}
result = append(result, edge)
}
return result
}
func FindCommonEdges(graph *lnrpc.ChannelGraph, node1,
node2 string) []*lnrpc.ChannelEdge {
var result []*lnrpc.ChannelEdge // nolint:prealloc
for _, edge := range graph.Edges {
if edge.Node1Pub != node1 && edge.Node2Pub != node1 {
continue
}
if edge.Node1Pub != node2 && edge.Node2Pub != node2 {
continue
}
result = append(result, edge)
}
return result
}
func FindNode(graph *lnrpc.ChannelGraph,
nodePubKey string) (*lnrpc.LightningNode, error) {
for _, node := range graph.Nodes {
if node.PubKey == nodePubKey {
return node, nil
}
}
return nil, fmt.Errorf("node %s not found in graph", nodePubKey)
}
}

@ -140,6 +140,13 @@ func IdentityPath(params *chaincfg.Params) string {
)
}
func MultisigPath(params *chaincfg.Params, index int) string {
return fmt.Sprintf(
LndDerivationPath+"/0/%d", params.HDCoinType,
keychain.KeyFamilyMultiSig, index,
)
}
func AllDerivationPaths(params *chaincfg.Params) ([]string, [][]uint32, error) {
mkPath := func(f keychain.KeyFamily) string {
return fmt.Sprintf(

@ -90,7 +90,7 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet,
return fmt.Errorf("error creating PSBT updater: %v", err)
}
status, err := updater.Sign(
0, ourSig, keyDesc.PubKey.SerializeCompressed(), nil,
inputIndex, ourSig, keyDesc.PubKey.SerializeCompressed(), nil,
witnessScript,
)
if err != nil {

Loading…
Cancel
Save