From 79f65bb1a11723186da93e4052db5938de636ba7 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 23 Jan 2024 07:56:30 +0200 Subject: [PATCH 1/3] zombierecovery: add --matchonly flag to makeoffer With this commit we make it possible to just check whether two lists of public keys can match the given channels and derive the 2-of-2 multisig channel funding address. --- cmd/chantools/zombierecovery_makeoffer.go | 138 ++++++++++++++-------- 1 file changed, 92 insertions(+), 46 deletions(-) diff --git a/cmd/chantools/zombierecovery_makeoffer.go b/cmd/chantools/zombierecovery_makeoffer.go index 0683af3..63f3f8a 100644 --- a/cmd/chantools/zombierecovery_makeoffer.go +++ b/cmd/chantools/zombierecovery_makeoffer.go @@ -6,7 +6,6 @@ import ( "encoding/hex" "encoding/json" "fmt" - "io/ioutil" "os" "strconv" "strings" @@ -28,6 +27,8 @@ type zombieRecoveryMakeOfferCommand struct { Node2 string FeeRate uint32 + MatchOnly bool + rootKey *rootKey cmd *cobra.Command } @@ -64,6 +65,10 @@ a counter offer.`, &cc.FeeRate, "feerate", defaultFeeSatPerVByte, "fee rate to "+ "use for the sweep transaction in sat/vByte", ) + cc.cmd.Flags().BoolVar( + &cc.MatchOnly, "matchonly", false, "only match the keys, "+ + "don't create an offer", + ) cc.rootKey = newRootKey(cc.cmd, "signing the offer") @@ -82,12 +87,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, c.FeeRate = defaultFeeSatPerVByte } - node1Bytes, err := ioutil.ReadFile(c.Node1) + node1Bytes, err := os.ReadFile(c.Node1) if err != nil { return fmt.Errorf("error reading node1 key file %s: %w", c.Node1, err) } - node2Bytes, err := ioutil.ReadFile(c.Node2) + node2Bytes, err := os.ReadFile(c.Node2) if err != nil { return fmt.Errorf("error reading node2 key file %s: %w", c.Node2, err) @@ -153,6 +158,22 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, } } + // If we're only matching, we can stop here. + if c.MatchOnly { + ourPubKeys, err := parseKeys(keys1.Node1.MultisigKeys) + if err != nil { + return fmt.Errorf("error parsing their keys: %w", err) + } + + theirPubKeys, err := parseKeys(keys2.Node2.MultisigKeys) + if err != nil { + return fmt.Errorf("error parsing our keys: %w", err) + } + return matchKeys( + keys1.Channels, ourPubKeys, theirPubKeys, chainParams, + ) + } + // Make sure one of the nodes is ours. _, pubKey, _, err := lnd.DeriveKey( extendedKey, lnd.IdentityPath(chainParams), chainParams, @@ -206,52 +227,19 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, 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: %w", err) - } - } - for idx, pubKeyHex := range theirKeys { - theirPubKeys[idx], err = pubKeyFromHex(pubKeyHex) - if err != nil { - return fmt.Errorf("error parsing their pubKey: %w", err) - } + ourPubKeys, err := parseKeys(ourKeys) + if err != nil { + return fmt.Errorf("error parsing their keys: %w", 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: %w", 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 - } - } - } + theirPubKeys, err := parseKeys(theirKeys) + if err != nil { + return fmt.Errorf("error parsing our keys: %w", err) + } - return fmt.Errorf("didn't find matching multisig keys for "+ - "channel %s", channel.ChanPoint) + err = matchKeys(keys1.Channels, ourPubKeys, theirPubKeys, chainParams) + if err != nil { + return err } // Let's now sum up the tally of how much of the rescued funds should @@ -444,6 +432,64 @@ channelLoop: return nil } +// parseKeys parses a list of string keys into public keys. +func parseKeys(keys []string) ([]*btcec.PublicKey, error) { + pubKeys := make([]*btcec.PublicKey, 0, len(keys)) + for _, key := range keys { + pubKey, err := pubKeyFromHex(key) + if err != nil { + return nil, err + } + pubKeys = append(pubKeys, pubKey) + } + + return pubKeys, nil +} + +// matchKeys tries to match the keys from the two nodes. It updates the channels +// with the correct keys and witness scripts. +func matchKeys(channels []*channel, ourPubKeys, theirPubKeys []*btcec.PublicKey, + chainParams *chaincfg.Params) error { + + // Loop through all channels and all keys now, this will definitely take + // a while. +channelLoop: + for _, channel := range 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: %w", err) + } + + if match { + channel.ourKeyIndex = uint32(ourKeyIndex) + channel.ourKey = ourKey + channel.theirKey = theirKey + channel.witnessScript = witnessScript + + log.Infof("Found keys for channel %s: "+ + "our key %x, their key %x", + channel.ChanPoint, + ourKey.SerializeCompressed(), + theirKey.SerializeCompressed()) + + continue channelLoop + } + } + } + + return fmt.Errorf("didn't find matching multisig keys for "+ + "channel %s", channel.ChanPoint) + } + + return nil +} + func matchScript(address string, key1, key2 *btcec.PublicKey, params *chaincfg.Params) (bool, []byte, error) { From 65cc3fdf6e372556620f7f3d8eb36c5be351a163 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 23 Jan 2024 07:59:59 +0200 Subject: [PATCH 2/3] zombierecovery add --numkeys to preparekeys With this new flag it will be possible to specify the number of keys to add to the file when running the preparekeys command. --- cmd/chantools/zombierecovery_preparekeys.go | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cmd/chantools/zombierecovery_preparekeys.go b/cmd/chantools/zombierecovery_preparekeys.go index 6485fff..ad4b48f 100644 --- a/cmd/chantools/zombierecovery_preparekeys.go +++ b/cmd/chantools/zombierecovery_preparekeys.go @@ -6,6 +6,7 @@ import ( "encoding/json" "fmt" "io/ioutil" + "os" "time" "github.com/lightninglabs/chantools/lnd" @@ -20,6 +21,8 @@ type zombieRecoveryPrepareKeysCommand struct { MatchFile string PayoutAddr string + NumKeys uint32 + rootKey *rootKey cmd *cobra.Command } @@ -47,7 +50,12 @@ correct ones for the matched channels.`, 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") + "P2WPKH (native SegWit) address", + ) + cc.cmd.Flags().Uint32Var( + &cc.NumKeys, "num_keys", numMultisigKeys, "the number of "+ + "multisig keys to derive", + ) cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys") @@ -108,9 +116,9 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, } // Derive all 2500 keys now, this might take a while. - for index := 0; index < numMultisigKeys; index++ { + for index := uint32(0); index < c.NumKeys; index++ { _, pubKey, _, err := lnd.DeriveKey( - extendedKey, lnd.MultisigPath(chainParams, index), + extendedKey, lnd.MultisigPath(chainParams, int(index)), chainParams, ) if err != nil { @@ -134,5 +142,5 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, 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) + return os.WriteFile(fileName, matchBytes, 0644) } From 82a03a65efe61a93d6d13a1874f7b9dce4aa0c69 Mon Sep 17 00:00:00 2001 From: Oliver Gugger Date: Tue, 23 Jan 2024 08:02:52 +0200 Subject: [PATCH 3/3] README+root: bump version to v0.12.2 --- README.md | 2 +- cmd/chantools/root.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 433988c..22e11ef 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ Example (make sure you always use the latest version!): ```shell $ cd /tmp -$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.0/chantools-linux-amd64-v0.12.0.tar.gz +$ wget -O chantools.tar.gz https://github.com/lightninglabs/chantools/releases/download/v0.12.2/chantools-linux-amd64-v0.12.2.tar.gz $ tar -zxvf chantools.tar.gz $ sudo mv chantools-*/chantools /usr/local/bin/ ``` diff --git a/cmd/chantools/root.go b/cmd/chantools/root.go index c8aaf6f..18dc036 100644 --- a/cmd/chantools/root.go +++ b/cmd/chantools/root.go @@ -33,7 +33,7 @@ const ( // version is the current version of the tool. It is set during build. // NOTE: When changing this, please also update the version in the // download link shown in the README. - version = "0.12.1" + version = "0.12.2" na = "n/a" // lndVersion is the current version of lnd that we support. This is