diff --git a/cmd/chantools/zombierecovery_makeoffer.go b/cmd/chantools/zombierecovery_makeoffer.go index a59cd11..07ab36e 100644 --- a/cmd/chantools/zombierecovery_makeoffer.go +++ b/cmd/chantools/zombierecovery_makeoffer.go @@ -181,22 +181,6 @@ 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, @@ -275,6 +259,11 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, return err } + // If we're only matching, we can stop here. + if c.MatchOnly { + return nil + } + // Let's prepare the PSBT. packet, err := psbt.NewFromUnsignedTx(wire.NewMsgTx(2)) if err != nil { diff --git a/cmd/chantools/zombierecovery_makeoffer_test.go b/cmd/chantools/zombierecovery_makeoffer_test.go index 1c250ae..9a95871 100644 --- a/cmd/chantools/zombierecovery_makeoffer_test.go +++ b/cmd/chantools/zombierecovery_makeoffer_test.go @@ -9,23 +9,37 @@ import ( "github.com/stretchr/testify/require" ) -var ( - key1Bytes, _ = hex.DecodeString( - "0201943d78d61c8ad50ba57164830f536c156d8d89d979448bef3e67f564" + - "ea0ab6", - ) - key1, _ = btcec.ParsePubKey(key1Bytes) - key2Bytes, _ = hex.DecodeString( - "038b88de18064024e9da4dfc9c804283b3077a265dcd73ad3615b50badcb" + - "debd5b", - ) - key2, _ = btcec.ParsePubKey(key2Bytes) - addr = "bc1qp5jnhnavt32fjwhnf5ttpvvym7e0syp79q5l9skz545q62d8u2uq05" + - "ul63" -) - func TestMatchScript(t *testing.T) { - ok, _, _, err := matchScript(addr, key1, key2, &chaincfg.MainNetParams) - require.NoError(t, err) - require.True(t, ok) + testCases := []struct { + key1 string + key2 string + addr string + params *chaincfg.Params + }{{ + key1: "0201943d78d61c8ad50ba57164830f536c156d8d89d979448bef3e67f564ea0ab6", + key2: "038b88de18064024e9da4dfc9c804283b3077a265dcd73ad3615b50badcbdebd5b", + addr: "bc1qp5jnhnavt32fjwhnf5ttpvvym7e0syp79q5l9skz545q62d8u2uq05ul63", + params: &chaincfg.MainNetParams, + }, { + key1: "03585d8e760bd0925da67d9c22a69dcad9f51f90a39f9a681971268555975ea30d", + key2: "0326a2171c97673cc8cd7a04a043f0224c59591fc8c9de320a48f7c9b68ab0ae2b", + addr: "bcrt1qhcn39q6jc0krkh9va230y2z6q96zadt8fhxw3erv92fzlrw83cyq40nwek", + params: &chaincfg.RegressionNetParams, + }} + + for _, tc := range testCases { + key1Bytes, err := hex.DecodeString(tc.key1) + require.NoError(t, err) + key1, err := btcec.ParsePubKey(key1Bytes) + require.NoError(t, err) + + key2Bytes, err := hex.DecodeString(tc.key2) + require.NoError(t, err) + key2, err := btcec.ParsePubKey(key2Bytes) + require.NoError(t, err) + + ok, _, err := matchScript(tc.addr, key1, key2, tc.params) + require.NoError(t, err) + require.True(t, ok) + } } diff --git a/cmd/chantools/zombierecovery_preparekeys.go b/cmd/chantools/zombierecovery_preparekeys.go index fc5c058..56cdd00 100644 --- a/cmd/chantools/zombierecovery_preparekeys.go +++ b/cmd/chantools/zombierecovery_preparekeys.go @@ -9,8 +9,10 @@ import ( "os" "time" + "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/chantools/cln" "github.com/lightninglabs/chantools/lnd" "github.com/spf13/cobra" ) @@ -25,6 +27,8 @@ type zombieRecoveryPrepareKeysCommand struct { NumKeys uint32 + HsmSecret string + rootKey *rootKey cmd *cobra.Command } @@ -58,6 +62,12 @@ correct ones for the matched channels.`, &cc.NumKeys, "num_keys", numMultisigKeys, "the number of "+ "multisig keys to derive", ) + cc.cmd.Flags().StringVar( + &cc.HsmSecret, "hsm_secret", "", "the hex encoded HSM secret "+ + "to use for deriving the multisig keys for a CLN "+ + "node; obtain by running 'xxd -p -c32 "+ + "~/.lightning/bitcoin/hsm_secret'", + ) cc.rootKey = newRootKey(cc.cmd, "deriving the multisig keys") @@ -67,12 +77,7 @@ correct ones for the matched channels.`, func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, _ []string) error { - extendedKey, err := c.rootKey.read() - if err != nil { - return fmt.Errorf("error reading root key: %w", err) - } - - err = lnd.CheckAddress( + err := lnd.CheckAddress( c.PayoutAddr, chainParams, false, "payout", lnd.AddrTypeP2WKH, lnd.AddrTypeP2TR, ) @@ -98,19 +103,51 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, return errors.New("invalid match file, node info missing") } + // Derive the keys for the node type, depending on the input flags. + var pubKeyStr string + switch { + case c.HsmSecret != "": + pubKeyStr, err = c.clnDeriveKeys(&match) + default: + pubKeyStr, err = c.lndDeriveKeys(&match) + } + if err != nil { + return err + } + + // 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 os.WriteFile(fileName, matchBytes, 0644) +} + +func (c *zombieRecoveryPrepareKeysCommand) lndDeriveKeys(match *match) (string, + error) { + + extendedKey, err := c.rootKey.read() + if err != nil { + return "", fmt.Errorf("error reading root key: %w", err) + } + _, pubKey, _, err := lnd.DeriveKey( extendedKey, lnd.IdentityPath(chainParams), chainParams, ) if err != nil { - return fmt.Errorf("error deriving identity pubkey: %w", err) + return "", fmt.Errorf("error deriving identity pubkey: %w", 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, + 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: @@ -126,7 +163,7 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, matchChannel := match.Channels[idx] addr, err := lnd.ParseAddress(matchChannel.Address, chainParams) if err != nil { - return fmt.Errorf("error parsing channel funding "+ + return "", fmt.Errorf("error parsing channel funding "+ "address '%s': %w", matchChannel.Address, err) } @@ -136,14 +173,14 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, matchChannel.ChanPoint, ) if err != nil { - return fmt.Errorf("error parsing channel "+ + return "", fmt.Errorf("error parsing channel "+ "point %s: %w", matchChannel.ChanPoint, err) } var randomness [32]byte if _, err := rand.Read(randomness[:]); err != nil { - return err + return "", err } nonces, err := lnd.GenerateMuSig2Nonces( @@ -151,8 +188,8 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, nil, ) if err != nil { - return fmt.Errorf("error generating MuSig2 "+ - "nonces: %w", err) + return "", fmt.Errorf("error generating "+ + "MuSig2 nonces: %w", err) } matchChannel.MuSig2NonceRandomness = hex.EncodeToString( @@ -171,8 +208,8 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, chainParams, ) if err != nil { - return fmt.Errorf("error deriving multisig pubkey: %w", - err) + return "", fmt.Errorf("error deriving multisig "+ + "pubkey: %w", err) } nodeInfo.MultisigKeys = append( @@ -182,14 +219,67 @@ func (c *zombieRecoveryPrepareKeysCommand) Execute(_ *cobra.Command, } nodeInfo.PayoutAddr = c.PayoutAddr - // Write the result back into a new file. - matchBytes, err := json.MarshalIndent(match, "", " ") + return pubKeyStr, nil +} + +func (c *zombieRecoveryPrepareKeysCommand) clnDeriveKeys(match *match) (string, + error) { + + secretBytes, err := hex.DecodeString(c.HsmSecret) if err != nil { - return err + return "", fmt.Errorf("error decoding HSM secret: %w", err) } - fileName := fmt.Sprintf("results/preparedkeys-%s-%s.json", - time.Now().Format("2006-01-02"), pubKeyStr) - log.Infof("Writing result to %s", fileName) - return os.WriteFile(fileName, matchBytes, 0644) + var hsmSecret [32]byte + copy(hsmSecret[:], secretBytes) + + nodePubKey, err := cln.NodeKey(hsmSecret) + if err != nil { + return "", fmt.Errorf("error deriving node pubkey: %w", err) + } + + pubKeyStr := hex.EncodeToString(nodePubKey.SerializeCompressed()) + var ourNodeInfo, theirNodeInfo *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: + ourNodeInfo = match.Node1 + theirNodeInfo = match.Node2 + + default: + ourNodeInfo = match.Node2 + theirNodeInfo = match.Node1 + } + + theirNodeKeyBytes, err := hex.DecodeString(theirNodeInfo.PubKey) + if err != nil { + return "", fmt.Errorf("error decoding peer pubkey: %w", err) + } + theirNodeKey, err := btcec.ParsePubKey(theirNodeKeyBytes) + if err != nil { + return "", fmt.Errorf("error parsing peer pubkey: %w", err) + } + + // Derive all 2500 keys now, this might take a while. + for index := range c.NumKeys { + pubKey, err := cln.FundingKey( + hsmSecret, theirNodeKey, uint64(index), + ) + if err != nil { + return "", fmt.Errorf("error deriving multisig "+ + "pubkey: %w", err) + } + + ourNodeInfo.MultisigKeys = append( + ourNodeInfo.MultisigKeys, + hex.EncodeToString(pubKey.SerializeCompressed()), + ) + } + ourNodeInfo.PayoutAddr = c.PayoutAddr + + return pubKeyStr, nil }