diff --git a/cln/signer.go b/cln/signer.go index 5685cde..0fa7c60 100644 --- a/cln/signer.go +++ b/cln/signer.go @@ -95,6 +95,45 @@ func (s *Signer) FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey, return nil, errors.New("no matching pubkeys found") } +func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet, + signDesc *input.SignDescriptor) error { + + ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc) + if err != nil { + return fmt.Errorf("error signing with our key: %w", err) + } + ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType)) + + // Because of the way we derive keys in CLN, the public key in the key + // descriptor is the peer's public key, not our own. So we need to + // derive our own public key from the private key. + ourPrivKey, err := s.FetchPrivateKey(&signDesc.KeyDesc) + if err != nil { + return fmt.Errorf("error fetching private key for descriptor "+ + "%v: %w", signDesc.KeyDesc, err) + } + ourPubKey := ourPrivKey.PubKey() + + // Great, we were able to create our sig, let's add it to the PSBT. + updater, err := psbt.NewUpdater(packet) + if err != nil { + return fmt.Errorf("error creating PSBT updater: %w", err) + } + status, err := updater.Sign( + signDesc.InputIndex, ourSig, ourPubKey.SerializeCompressed(), + nil, signDesc.WitnessScript, + ) + if err != nil { + return fmt.Errorf("error adding signature to PSBT: %w", err) + } + if status != 0 { + return fmt.Errorf("unexpected status for signature update, "+ + "got %d wanted 0", status) + } + + return nil +} + func (s *Signer) AddPartialSignature(packet *psbt.Packet, keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte, inputIndex int) error { @@ -112,40 +151,8 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet, packet.UnsignedTx, prevOutFetcher, ), } - ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc) - if err != nil { - return fmt.Errorf("error signing with our key: %w", err) - } - ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll)) - // Because of the way we derive keys in CLN, the public key in the key - // descriptor is the peer's public key, not our own. So we need to - // derive our own public key from the private key. - ourPrivKey, err := s.FetchPrivateKey(&keyDesc) - if err != nil { - return fmt.Errorf("error fetching private key for descriptor "+ - "%v: %w", keyDesc, err) - } - ourPubKey := ourPrivKey.PubKey() - - // Great, we were able to create our sig, let's add it to the PSBT. - updater, err := psbt.NewUpdater(packet) - if err != nil { - return fmt.Errorf("error creating PSBT updater: %w", err) - } - status, err := updater.Sign( - inputIndex, ourSig, ourPubKey.SerializeCompressed(), nil, - witnessScript, - ) - if err != nil { - return fmt.Errorf("error adding signature to PSBT: %w", err) - } - if status != 0 { - return fmt.Errorf("unexpected status for signature update, "+ - "got %d wanted 0", status) - } - - return nil + return s.AddPartialSignatureWithDesc(packet, signDesc) } var _ lnd.ChannelSigner = (*Signer)(nil) diff --git a/cmd/chantools/zombierecovery_makeoffer.go b/cmd/chantools/zombierecovery_makeoffer.go index fcf2220..f4738f8 100644 --- a/cmd/chantools/zombierecovery_makeoffer.go +++ b/cmd/chantools/zombierecovery_makeoffer.go @@ -24,6 +24,7 @@ import ( "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/wallet" + "github.com/lightninglabs/chantools/cln" "github.com/lightninglabs/chantools/lnd" "github.com/lightningnetwork/lnd/fn/v2" "github.com/lightningnetwork/lnd/input" @@ -40,6 +41,8 @@ type zombieRecoveryMakeOfferCommand struct { MatchOnly bool + HsmSecret string + rootKey *rootKey cmd *cobra.Command } @@ -80,6 +83,12 @@ a counter offer.`, &cc.MatchOnly, "matchonly", false, "only match the keys, "+ "don't create an offer", ) + 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, "signing the offer") @@ -89,11 +98,6 @@ a counter offer.`, func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, _ []string) error { - extendedKey, err := c.rootKey.read() - if err != nil { - return fmt.Errorf("error reading root key: %w", err) - } - if c.FeeRate == 0 { c.FeeRate = defaultFeeSatPerVByte } @@ -183,20 +187,72 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, } } - // Make sure one of the nodes is ours. - _, pubKey, _, err := lnd.DeriveKey( - extendedKey, lnd.IdentityPath(chainParams), chainParams, + var ( + signer lnd.ChannelSigner + ourNode *btcec.PublicKey ) - if err != nil { - return fmt.Errorf("error deriving identity pubkey: %w", err) + switch { + case c.HsmSecret != "": + secretBytes, err := hex.DecodeString(c.HsmSecret) + if err != nil { + return fmt.Errorf("error decoding HSM secret: %w", err) + } + + var hsmSecret [32]byte + copy(hsmSecret[:], secretBytes) + + ourNode, _, err = cln.NodeKey(hsmSecret) + if err != nil { + return fmt.Errorf("error deriving CLN node pubkey: %w", + err) + } + + signer = &cln.Signer{ + HsmSecret: hsmSecret, + } + + default: + extendedKey, err := c.rootKey.read() + if err != nil { + return fmt.Errorf("error reading root key: %w", err) + } + + _, ourNode, _, err = lnd.DeriveKey( + extendedKey, lnd.IdentityPath(chainParams), chainParams, + ) + if err != nil { + return fmt.Errorf("error deriving identity pubkey: %w", + err) + } + + signer = &lnd.Signer{ + ExtendedKey: extendedKey, + ChainParams: chainParams, + } } - pubKeyStr := hex.EncodeToString(pubKey.SerializeCompressed()) + // Make sure one of the nodes is ours. + pubKeyStr := hex.EncodeToString(ourNode.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) } + // We need to have the peer pubkey ready, in case we're using a CLN + // signer. + peerPubKeyStr := keys1.Node1.PubKey + if keys1.Node1.PubKey == pubKeyStr { + peerPubKeyStr = keys1.Node2.PubKey + } + peerPubKeyBytes, err := hex.DecodeString(peerPubKeyStr) + if err != nil { + return fmt.Errorf("error decoding peer pubkey: %w", err) + } + peerPubKey, err := btcec.ParsePubKey(peerPubKeyBytes) + if err != nil { + return fmt.Errorf("error parsing peer pubkey: %w", err) + } + // Pick the correct list of keys. There are 4 possibilities, given 2 // files with 2 node slots each. var ( @@ -344,6 +400,12 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, PrevOutputFetcher: prevOutFetcher, } + // For CLN, we also need to set the peer's public key in the + // key descriptor. + if _, ok := signer.(*cln.Signer); ok { + signDesc.KeyDesc.PubKey = peerPubKey + } + switch a := channelAddr.(type) { case *btcutil.AddressWitnessScriptHash: estimator.AddWitnessInput(input.MultiSigWitnessSize) @@ -359,9 +421,15 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, signDesc.HashType = txscript.SigHashDefault signDesc.SignMethod = input.TaprootKeySpendSignMethod + lndSigner, ok := signer.(*lnd.Signer) + if !ok { + return errors.New("taproot channels not " + + "supported for CLN") + } + err := addMuSig2Data( - extendedKey, &pIn, channel, theirChannels[idx], - op, a.WitnessProgram(), + lndSigner.ExtendedKey, &pIn, channel, + theirChannels[idx], op, a.WitnessProgram(), ) if err != nil { return fmt.Errorf("error adding MuSig2 data: "+ @@ -487,18 +555,20 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, // Loop a second time through the inputs and sign each input. We now // have all the witness/non-witness data filled in the psbt package. - signer := &lnd.Signer{ - ExtendedKey: extendedKey, - ChainParams: chainParams, - } for idx := range packet.UnsignedTx.TxIn { signDesc := signDescs[idx] // If we're dealing with a taproot channel, we'll need to // create a MuSig2 partial signature. if signDesc.SignMethod == input.TaprootKeySpendSignMethod { + lndSigner, ok := signer.(*lnd.Signer) + if !ok { + return errors.New("taproot channels not yet " + + "supported for CLN") + } + err := muSig2PartialSign( - signer, &signDesc.KeyDesc, packet, idx, + lndSigner, &signDesc.KeyDesc, packet, idx, ) if err != nil { return fmt.Errorf("error creating MuSig2 "+ @@ -508,34 +578,11 @@ func (c *zombieRecoveryMakeOfferCommand) Execute(_ *cobra.Command, continue } - ourSigRaw, err := signer.SignOutputRaw( - packet.UnsignedTx, signDesc, - ) + err = signer.AddPartialSignatureWithDesc(packet, signDesc) if err != nil { - return fmt.Errorf("error signing with our key: %w", err) - } - ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType)) - - // Great, we were able to create our sig, let's add it to the - // PSBT. - updater, err := psbt.NewUpdater(packet) - if err != nil { - return fmt.Errorf("error creating PSBT updater: %w", + return fmt.Errorf("error adding partial signature: %w", err) } - status, err := updater.Sign( - idx, ourSig, - signDesc.KeyDesc.PubKey.SerializeCompressed(), nil, - signDesc.WitnessScript, - ) - if err != nil { - return fmt.Errorf("error adding signature to PSBT: %w", - err) - } - if status != 0 { - return fmt.Errorf("unexpected status for signature "+ - "update, got %d wanted 0", status) - } } // Looks like we're done! diff --git a/cmd/chantools/zombierecovery_signoffer.go b/cmd/chantools/zombierecovery_signoffer.go index 224e7ec..7fcf636 100644 --- a/cmd/chantools/zombierecovery_signoffer.go +++ b/cmd/chantools/zombierecovery_signoffer.go @@ -11,6 +11,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil/psbt" "github.com/btcsuite/btcd/txscript" + "github.com/lightninglabs/chantools/btc" "github.com/lightninglabs/chantools/cln" "github.com/lightninglabs/chantools/lnd" "github.com/spf13/cobra" @@ -22,6 +23,9 @@ type zombieRecoverySignOfferCommand struct { HsmSecret string RemotePeer string + APIURL string + Publish bool + rootKey *rootKey cmd *cobra.Command } @@ -54,6 +58,16 @@ peer to recover funds from one or more channels.`, "peer node identity key, only required when running "+ "'signoffer' on the CLN side", ) + cc.cmd.Flags().StringVar( + &cc.APIURL, "apiurl", defaultAPIURL, "API URL to use for "+ + "publishing the final transaction (must be esplora "+ + "compatible)", + ) + cc.cmd.Flags().BoolVar( + &cc.Publish, "publish", false, "if set, the final PSBT "+ + "will be published to the network after signing, "+ + "otherwise it will just be printed to stdout", + ) cc.rootKey = newRootKey(cc.cmd, "signing the offer") @@ -115,11 +129,13 @@ func (c *zombieRecoverySignOfferCommand) Execute(_ *cobra.Command, } } - return signOffer(packet, signer, remoteNode) + return signOffer( + packet, signer, remoteNode, newExplorerAPI(c.APIURL), c.Publish, + ) } func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner, - peerPubKey *btcec.PublicKey) error { + peerPubKey *btcec.PublicKey, api *btc.ExplorerAPI, publish bool) error { // Now let's check that the packet has the expected proprietary key with // our pubkey that we need to sign with. @@ -243,9 +259,19 @@ func signOffer(packet *psbt.Packet, signer lnd.ChannelSigner, return fmt.Errorf("unable to serialize final TX: %w", 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()) + // Publish TX. + if publish { + response, err := api.PublishTx(hex.EncodeToString(buf.Bytes())) + if err != nil { + return err + } + log.Infof("Published TX %s, response: %s", + finalTx.TxHash().String(), response) + } else { + 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 } diff --git a/lnd/signer.go b/lnd/signer.go index 3d142ed..f78588a 100644 --- a/lnd/signer.go +++ b/lnd/signer.go @@ -35,6 +35,9 @@ type ChannelSigner interface { FindMultisigKey(targetPubkey, peerPubKey *btcec.PublicKey, maxNumKeys uint32) (*keychain.KeyDescriptor, error) + AddPartialSignatureWithDesc(packet *psbt.Packet, + signDesc *input.SignDescriptor) error + AddPartialSignature(packet *psbt.Packet, keyDesc keychain.KeyDescriptor, utxo *wire.TxOut, witnessScript []byte, inputIndex int) error @@ -223,11 +226,18 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet, packet.UnsignedTx, prevOutFetcher, ), } + + return s.AddPartialSignatureWithDesc(packet, signDesc) +} + +func (s *Signer) AddPartialSignatureWithDesc(packet *psbt.Packet, + signDesc *input.SignDescriptor) error { + ourSigRaw, err := s.SignOutputRaw(packet.UnsignedTx, signDesc) if err != nil { return fmt.Errorf("error signing with our key: %w", err) } - ourSig := append(ourSigRaw.Serialize(), byte(txscript.SigHashAll)) + ourSig := append(ourSigRaw.Serialize(), byte(signDesc.HashType)) // Great, we were able to create our sig, let's add it to the PSBT. updater, err := psbt.NewUpdater(packet) @@ -235,8 +245,9 @@ func (s *Signer) AddPartialSignature(packet *psbt.Packet, return fmt.Errorf("error creating PSBT updater: %w", err) } status, err := updater.Sign( - inputIndex, ourSig, keyDesc.PubKey.SerializeCompressed(), nil, - witnessScript, + signDesc.InputIndex, ourSig, + signDesc.KeyDesc.PubKey.SerializeCompressed(), nil, + signDesc.WitnessScript, ) if err != nil { return fmt.Errorf("error adding signature to PSBT: %w", err)